数据结构入门——线性表:顺序表与链表

数据结构是计算机专业的核心基础课,也是 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/爬虫/数据结构 实战干货,不让你白来。