线性表是一种最基本的数据结构,它是一个一维的有序元素序列。主要具有以下特征:
- 元素具有相同类型。线性表中存放的所有数据元素具有相同的数据类型。
- 元素具有线性顺序。线性表中的数据元素以线性的顺序依次排列,每个元素至多只有前驱和后继,不存在分支。
- 可以通过索引访问每个元素。线性表中每个元素都有其在表中的唯一位置,可以通过这个位置索引访问到该元素。
- 支持插入和删除操作。可以在任意位置插入和删除元素,且插入和删除不影响其他元素的相对位置。
- 大小可变。线性表的大小是动态变化的,没有固定大小的限制。
常见的线性表有:数组、字符串、链表、队列、栈等,下面我们来挨个介绍一下:
数组
数组主要特点如下:
- 存储元素连续:数组的每个元素都有相对定的位置,并且都是相邻存储的。这个特点使得数组支持随机访问,通过索引可以快速访问每个元素。
int[] arr = new int[5];
arr[0] = 1;
arr[1] = 2;
// arr[0] 和 arr[1] 存储在连续的内存空间
- 大小固定:数组一旦创建,大小就固定了。如果要添加更多元素,必须创建一个更大的新数组并复制原数组内容。这会导致内存占用变大和数据移动的时间损耗。
int[] arr = new int[5];
// 若要扩容至10,需要:
int[] newArr = new int[10];
for (int i = 0; i < 5; i++) {
newArr[i] = arr[i];
}
arr = newArr;
- 插入和删除慢:由于数组的大小固定,插入和删除元素需要移动大量的数据,效率较低。要在中间插入或删除,需要移动所有后续元素。
int[] arr = {1, 2, 3, 4, 5};
// 在索引2插入6,需要:
for (int i = 4; i >= 2; i--) {
arr[i] = arr[i-1];
}
arr[2] = 6;
- 查找快:通过固定的索引可以快速获取任意位置的元素,时间复杂度为O(1)。
int index = 3;
int elem = arr[index]; // 直接访问索引为3的元素
- 优化内存占用:数组的空间是连续的,数组可以最大限度地利用CPU缓存,这有利于空间局部性原理,可以加速访问速度。
// 快速排序实现
quicksort(arr, 0, arr.length - 1);
- 支持快速排序:数组的随机访问特性,使得快速排序等需要随机访问的排序算法,更适合在数组上实现。
总之,数组具有存储密集和随机访问的特点,适合用于需要快速查找与排序的场景。但 Insert 和 Remove 等操作会因为数据移动而变慢。
字符串
字符串具有以下主要特点:
- 由字符序列构成:字符串是一串字符的有限序列。字符可以是字母、数字或其他符号。
char[] chars = {'h', 'e', 'l', 'l', 'o'};
String str = new String(chars);
- 具有长度:字符串有固定的字符数量,我们称为字符串的长度或字符串的大小。
String str = "hello";
int len = str.length(); // len为5
- 支持索引访问:可以通过索引访问字符串中的每个字符。
String str = "hello";
char c = str.charAt(2); // c为'l'
- 部分可变:字符串的长度一旦确定就不可变,但可以对字符串中的部分字符进行插入、删除和替换操作。
String str = "hello";
String sub = str.substring(1, 3); // sub为"el"
String newStr = str.replace('l', 'x'); // newStr为"hexxo"
- 无类型:字符串通常被实现为字符数组,而不是一种特定的数据类型。不同语言中字符串的实现细节不同。
- 支持遍历:可以从头到尾遍历字符串中的每个字符。
String str = "hello";
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// do something
}
- 支持子串:可以获取字符串的一部分,形成子串。子串也是字符串。
- 支持比较:可以比较两个字符串的字典序或长度,判断字符串是否相等。
String a = "hello";
String b = "Hello";
boolean isEqual = a.equals(b); // isEqual为false,忽略大小写
- 支持拼接:可以将多个字符串拼接成一个字符串。
String c = "hello" + "world";
String d = "hello".concat("world");
字符串是一种非常基本和常用的数据结构,作为一名程序员,需要熟练掌握字符串的概念、特性和操作。
链表
链表具有以下主要特点:
- 元素无序:链表中的元素没有顺序,元素的逻辑顺序与物理顺序无关。
ListNode node1 = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
node1.next = node2;
node2.next = node3;
- 元素个数可变:链表的大小是动态变化的,没有固定大小限制。
while (true) {
ListNode node = new ListNode(i++);
node.next = head.next;
head.next = node;
}
- 支持高效插入和删除:插入和删除操作只需要更改指针,时间复杂度为O(1),效率高。
// 插入节点
ListNode node = new ListNode(4);
node.next = head.next;
head.next = node;
// 删除节点
head.next = head.next.next;
- 没有索引:链表中的元素没有索引,无法通过索引快速访问元素。只能从头节点或尾节点开始逐个访问。
ListNode cur = head;
while (cur != null) {
// 访问cur节点
cur = cur.next;
}
- 占用空间不连续:链表中的元素在内存中可以是不连续的,只需要存储元素本身和指针域。
ListNode node1 = new ListNode(1);
ListNode node2 = new ListNode(2);
node1.next = node2; // 节点空间不连续,只通过指针连接
- 链表本身无限长度:链表的长度仅受内存大小的限制,没有理论上的限制。
- 支持增删改操作:支持对链表执行插入、删除和修改元素等操作。
// 增加节点
ListNode newNode = new ListNode(4);
newNode.next = head.next;
head.next = newNode;
// 删除节点
head.next = head.next.next;
// 修改节点
head.next.val = 5;
链表常见的分类有:
- 单链表:每个节点只包含指向后继节点的指针。
- 双向链表:每个节点包含指向前驱和后继节点的指针。
- 循环链表:尾节点的指针指向头节点,形成循环。
注意:链表的应用场景主要是插入和删除操作较频繁,且无序的场景。相比数组,链表的插入和删除操作更高效,但查找和访问元素较低效。
队列
队列(Queue)是一种遵循先入先出(FIFO)原则的有序集合。主要有以下特点:
- 只允许在一端插入元素,在另一端删除元素:队列有 head 和 tail 两个指针,新元素总是添加在 tail 所指向的位置,删除元素总是从 head 所指向的位置开始。
Queue<Integer> queue = new LinkedList<>();
queue.add(1); // 尾部添加元素
queue.add(2);
queue.remove(); // 头部删除元素
- 先入先出:最早添加的元素最先删除,采用先进先出的顺序。
queue.add(1);
queue.add(2);
queue.remove(); // 1被删除
queue.remove(); // 2被删除
- 大小可变:队列的大小是动态的,没有固定大小的限制。
// 一开始队列为空
queue.add(1);
queue.add(2);
queue.remove();
queue.add(3);
- 实现简单:队列可以用数组或链表简单实现。使用数组实现时,需处理循环队列的情况。
public class Queue<E> {
private LinkedList<E> list;
public void add(E element) {
list.addLast(element);
}
public E remove() {
return list.removeFirst();
}
}
- 适用于广度优先搜索:队列的FIFO特性很适合在广度优先搜索等层次遍历算法中使用。每一层的节点全部拓展完后才进行下一层的拓展。
Queue<Integer> queue = new LinkedList<>();
queue.add(source); // 添加起点source
while (!queue.isEmpty()) {
int vertex = queue.remove();
for (int neighbor : graph[vertex]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.add(neighbor); // 新增节点添加至队尾
}
}
}
-
适用于缓存:队列可以用来实现LRU(最近最少使用)缓存淘汰算法。Queue中元素时间久的排在前面,新添加元素排在后面。超出容量时,删除最前面的元素。
-
发展出更为抽象的数据结构:队列是一种较为简单的数据结构,但它衍生出更复杂的循环队列,优先队列等结构。
栈
栈(Stack)是一种遵循后入先出(LIFO)原则的有序集合。主要有以下特点:
- 只允许在一端插入和删除元素:栈只有一个入口,元素只能从入口插入和删除,这使得栈的插入和删除操作非常高效,时间复杂度为O(1)。
Stack<Integer> stack = new Stack<>();
stack.push(1); // 入栈
stack.push(2);
stack.pop(); // 出栈,2被弹出
- 后入先出:最近添加的元素最先删除,采用后进先出的顺序。这种特性也被称为FILO(First In Last Out)。
stack.push(1);
stack.push(2);
stack.pop(); // 2被弹出
stack.pop(); // 1被弹出
- 大小可变:栈的大小是动态的,没有固定大小的限制。
// 一开始栈为空
stack.push(1);
stack.push(2); // 大小变为2
stack.pop();
stack.push(3); // 大小变为2
- 实现简单:栈可以用数组或链表简单实现。使用数组实现时,维护一个指向栈顶的指针,插入和删除只操作数组的一端。
public class Stack {
private int[] arr;
private int size;
public void push(int value) {
arr[size++] = value;
}
public int pop() {
return arr[--size];
}
}
- 适用于深度优先搜索:栈的FILO特性很适合在深度优先搜索等递归算法中使用。每次深入都将节点压入栈,回溯时从栈中弹出节点。
void dfs(int vertex) {
visited.add(vertex);
for (int neighbor : graph[vertex]) {
if (!visited.contains(neighbor))
dfs(neighbor); // 递归调用,相当于将节点压入栈
}
}
- 发展出更为抽象的数据结构:栈是一种较为简单的数据结构,但它衍生出很多其他的数据结构,比如队列、递归栈、平衡二叉树等。