数据结构是计算机专业的核心基础课,也是 408 考研的第一门专业课。这一篇从最基础的线性表开始,把顺序表和链表讲透。
一、什么是线性表
线性表(Linear List) 是最基本、最常用的数据结构。简单说就是一组数据排成一条线:
元素1 → 元素2 → 元素3 → ... → 元素N
每个元素都有唯一的前驱 (前一个元素)和唯一的后继(后一个元素),第一个元素没有前驱,最后一个没有后继。
生活中的例子:
- 排队买奶茶的队伍 → 线性表
- 高中座位表 → 线性表
- 手机通讯录 → 线性表
线性表的两种存储方式:
- 顺序表:用连续的内存空间存储(类似数组)
- 链表:用不连续的内存空间存储,靠指针连接
二、顺序表(数组)
1. 什么是顺序表
顺序表就是把元素按顺序存放在连续的内存空间 中。在 Java 中就是 ArrayList,在 C 中就是数组。
内存地址: 100 101 102 103 104 105 106
┌────┬────┬────┬────┬────┬────┬────┐
数据: │ 10 │ 20 │ 30 │ 40 │ 50 │ │ │
└────┴────┴────┴────┴────┴────┴────┘
下标: 0 1 2 3 4 5 6
顺序表的三个核心属性:
java
public class ArrayList<E> {
private Object[] data; // 存储数据的数组
private int size; // 当前元素个数
private int capacity; // 数组总容量(= data.length)
}
2. 顺序表的基本操作
查找(随机访问)------O(1)
java
// 按下标访问,直接通过地址偏移计算
public E get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
return (E) data[index];
}
为什么是 O(1)? 数组在内存中是连续存储的,data[i] 的地址 = 起始地址 + i × 每个元素大小。一步就能定位。
插入------O(n)
java
// 在指定位置插入元素
public void add(int index, E element) {
// 1. 检查下标范围
if (index < 0 || index > size) throw new IndexOutOfBoundsException();
// 2. 检查容量是否够,不够就扩容
if (size == data.length) {
grow(); // 扩容为原来的 1.5 倍
}
// 3. 将 index 及以后的元素后移一位(这是最耗时的)
for (int i = size; i > index; i--) {
data[i] = data[i - 1];
}
// 4. 插入新元素
data[index] = element;
size++;
}
初始: [10, 20, 30, 40, _]
↑
空位
在下标 1 处插入 15:
第一步:元素后移
[10, 20, 30, 40, _] → [10, 20, 30, _, 40]
[10, 20, 30, _, 40] → [10, 20, _, 30, 40]
[10, 20, _, 30, 40] → [10, _, 20, 30, 40]
第二步:插入 15
[10, 15, 20, 30, 40]
最坏情况: 在头部插入,所有元素都要后移 → O(n)
删除------O(n)
java
public E remove(int index) {
if (index < 0 || index >= size) throw new IndexOutOfBoundsException();
E oldValue = (E) data[index];
// 将 index 后面的元素前移
for (int i = index; i < size - 1; i++) {
data[i] = data[i + 1];
}
data[size - 1] = null; // 清空引用,帮助 GC
size--;
return oldValue;
}
最坏情况: 删除头部元素,所有元素前移 → O(n)
三、链表
1. 什么是链表
链表的元素不连续存储,每个元素是一个节点(Node),节点之间通过指针连接。
java
class Node {
int data; // 数据域
Node next; // 指针域:指向下一个节点
}
内存中不连续的位置:
[10 | 地址A] → [20 | 地址B] → [30 | 地址C] → [40 | null]
↑ ↑
头指针 head 最后一个节点指向 null
链表只需要记住头节点(head)的位置,就能找到所有节点。
2. 单向链表基本操作
查找------O(n)
java
public Node get(int index) {
Node current = head;
for (int i = 0; i < index; i++) {
if (current == null) throw new IndexOutOfBoundsException();
current = current.next;
}
return current;
}
为什么是 O(n)? 链表不是连续的,不能直接算地址,必须从头节点一个一个往后找。
插入------O(1)(已知位置)
java
// 在 node 节点后面插入一个新节点
public void insertAfter(Node node, int data) {
Node newNode = new Node(data);
newNode.next = node.next; // 新节点指向 node 的后继
node.next = newNode; // node 指向新节点
}
插入前: A → B → C
↑
在A后面插入
插入步骤:
1. newNode.next = A.next → new指向B
2. A.next = newNode → A指向new
结果: A → new → B → C
关键: 如果已经有了要插入位置的前驱节点,插入操作本身只需要改两次指针,和表长无关 → O(1)
删除------O(1)(已知前驱)
java
public void deleteAfter(Node node) {
if (node.next != null) {
node.next = node.next.next; // 跳过要删除的节点
}
}
四、顺序表 vs 链表的对比
| 操作 | 顺序表(ArrayList) | 链表(LinkedList) |
|---|---|---|
| 按下标访问 | O(1) 🏆 直接算地址 | O(n) 从头遍历 |
| 头部插入 | O(n) 所有元素后移 | O(1) 🏆 改头指针 |
| 尾部插入 | O(1) 🏆(不需要扩容时) | O(n) 找到尾部再插 |
| 中间插入 | O(n) 移动元素 | O(1) 🏆(已知前驱时) |
| 头部删除 | O(n) 所有元素前移 | O(1) 🏆 改头指针 |
| 尾部删除 | O(1) 🏆 直接删 | O(1) 双向链表 / O(n) 单向 |
| 内存占用 | 连续空间,省内存 🏆 | 每个节点多存一个指针 |
| 内存分配 | 一次性分配 | 动态分配,按需创建 |
选型建议
频繁按下标访问(查多) → 顺序表(ArrayList)
频繁头尾增删(改多) → 链表(LinkedList)
实际开发中:ArrayList 用得更普遍,因为查询的需求远多于插入删除。
五、408 考研常见考题
题1:顺序表插入的平均移动次数
在长度为 n 的顺序表中插入一个元素,平均需要移动多少个元素?
在位置 0 插入 → 移动 n 个
在位置 1 插入 → 移动 n-1 个
...
在位置 n 插入 → 移动 0 个
平均移动次数 = (0 + 1 + 2 + ... + n) / (n+1) = n/2
答案: 平均移动 n/2 个元素。
题2:链表插入的时间复杂度
在单链表的第 i 个位置插入一个节点,时间复杂度是多少?
注意:插入操作本身是 O(1),但找到第 i 个位置需要 O(n)
所以总的时间复杂度是 O(n)
题3:手写链表反转
java
public Node reverse(Node head) {
Node prev = null;
Node current = head;
while (current != null) {
Node next = current.next; // 先存下一个节点
current.next = prev; // 指向前一个
prev = current; // prev 后移
current = next; // current 后移
}
return prev; // prev 是新的头节点
}
过程图解:
初始: 1 → 2 → 3 → 4 → null
第1步: null ← 1 2 → 3 → 4 → null
prev current
第2步: null ← 1 ← 2 3 → 4 → null
prev current
第3步: null ← 1 ← 2 ← 3 4 → null
prev current
第4步: null ← 1 ← 2 ← 3 ← 4
prev current→null
结果: 4 → 3 → 2 → 1 → null
这道题面试和考研都很经典,建议能手写出来。
六、Java 中对应的集合类
java
// 顺序表(基于数组)
List<String> arrayList = new ArrayList<>();
arrayList.add("A"); // 尾部添加 O(1)
arrayList.get(0); // 按下标查 O(1)
arrayList.add(0, "B"); // 头部插入 O(n)
// 链表(基于双向链表)
List<String> linkedList = new LinkedList<>();
linkedList.add("A"); // 尾部添加 O(1)
linkedList.get(0); // 按下标查 O(n)
linkedList.add(0, "B"); // 头部插入 O(1)
总结:
线性表 = 排成一排的数据
顺序表 = 连续存放(数组),查得快,改得慢
链表 = 节点通过指针连接,改得快,查得慢
考研重点:顺序表的插入/删除平均移动次数、链表反转、顺序表和链表的复杂度对比。
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫/数据结构 实战干货,不让你白来。