数据结构:线性表 —— 顺序表与链表完整总结

摘要:线性表是最基础的数据结构,分为顺序存储(顺序表)链式存储(链表)。本文梳理线性表核心概念、顺序表、单链表、双向循环链表、高频 OJ/408 真题、顺序表与链表对比、CPU 缓存原理,适合考研 408、校招笔试面试复习。

一、线性表基础概念

  1. 定义:相同类型 n 个数据元素的有限序列 \(L=(a_1,a_2...a_n)\),\(n=0\)为空表。
  • 位序从 1 开始数组下标从 0 开始,做题务必区分。
  • 逻辑特征:表头无前驱、表尾无后继;其余结点拥有唯一前驱、唯一后继。
  1. 两种存储实现

表格

存储方式 特点 产物
顺序存储 逻辑相邻,物理内存也连续 顺序表
链式存储 逻辑相邻,物理地址随意,依靠指针维系关系 链表
  1. ADT 抽象数据类型:包含初始化、销毁、按位获取、按值查找、插入、删除、判空、求长度等基础操作。

补充:严蔚敏教材出现&LC++ 引用,引用是变量别名,不会开辟内存;C 语言中使用指针替代引用完成修改实参。

二、顺序表(数组实现线性表)

2.1 分类:静态顺序表、动态顺序表

  1. 静态顺序表:使用固定大小数组。优点实现简单;缺点容量写死,容易出现空间浪费或者空间不足。

  2. 动态顺序表(重点):在堆上 malloc 申请数组,结构体包含 3 个成员

    typedef struct{
    SqDataType* arr; //动态数组指针
    int size; //有效元素个数
    int capacity;//总容量
    }SqList;

2.2 核心接口

  1. 初始化:malloc 申请默认空间,size 置 0,记录 capacity。
  2. 销毁:free 释放堆内存,指针置 NULL,size、capacity 清零,避免内存泄漏。
  3. 插入SqListInsert(ps,i,x)
    • 下标合法范围:0 ≤ i ≤ size;i 等于 size 等价于尾插。
    • 数组存满调用realloc扩容(常见策略 2 倍扩容);realloc 分为原地扩容、异地扩容。
    • i 及之后元素整体向后挪动,必须从后往前挪!防止数据被覆盖
  4. 删除SqListDelete(ps,i)
    • 下标合法范围:0 ≤ i < size
    • 将 i 之后元素向前覆盖,size 自减。
  5. 查找
    • 按下标GetElem随机访问 O (1),依靠连续内存做地址偏移。
    • 按值LocateElem:逐个遍历比较,平均时间复杂度 O (n)。

2.3 时间复杂度汇总

表格

操作 最好 最坏 平均
插入 尾插 O (1) 头插 O (n) O(n)
删除 尾删 O (1) 头删 O (n) O(n)
下标访问 O(1) O(1) O(1)
按值查找 O(1) O(n) O(n)

408 真题考点:获取第 i 个元素平均复杂度 O (1);插入、删除、按值查找平均 O (n)

2.4 力扣经典顺序表习题

  1. 27. 移除元素 :双指针src/dst原地覆盖,不等于 val 就保留,时间 O (N),空间 O (1)。
  2. 26. 删除有序数组重复项:快慢双指针,保留不重复元素。

顺序表痛点:中间位置插入删除需要大量搬移元素。

三、链表

3.1 链表总览

链表结点由数据域 + 指针域 组成,按需 malloc 结点,不需要整体扩容;插入删除只修改指针,不需要移动数据

链表 8 种组合分类:

  • 方向:单向链表、双向链表
  • 是否带哨兵:带头结点、不带头结点
  • 是否循环:循环链表、非循环链表

力扣 OJ 绝大多数题目:不带头结点单链表 ;严蔚敏 408 教材:带头结点链表

3.2 带头结点单链表(重点)

结点定义:

复制代码
typedef struct ListNode{
    LDataType data;
    struct ListNode* next;
}LNode,*LinkList;
  1. BuyListNode:堆上创建新结点,禁止使用局部栈变量。
  2. 初始化:创建哨兵头结点,头结点不存储有效数据。
  3. 遍历打印:cur = L->next;循环终止条件cur != NULL
  4. 查找:
    • 按值查找:遍历链表,找到返回结点地址,找不到返回 NULL。
    • 按下标查找:循环计数遍历找第 i 个结点,不支持随机访问,O (n)
  5. 插入 ListInsert (L,i,x)

⚠️两句代码顺序绝对不能颠倒!

复制代码
newNode->next = i_1Node->next;
i_1Node->next = newNode;

先保存后继结点,再修改前驱 next,否则丢失后续整条链表。 插入前需要先找到i‑1位置结点。

  1. 删除 ListDelete (L,i)

    LNode* iNode = i_1Node->next; //保存待删结点
    i_1Node->next = iNode->next;
    free(iNode);

必须保存待删结点指针,否则 free 无法释放内存。

  1. 销毁:循环先保存 next 指针,再 free 当前结点,不能 free 之后再访问 next。

3.3 单链表高频力扣算法题

  1. 876. 链表中间结点:快慢指针,slow 走一步、fast 走两步。奇数结点 slow 指向正中间;偶数结点 slow 指向靠后结点。
  2. 面试题 02.02 倒数第 k 个结点:fast 先走 k 步;fast、slow 同步走,fast 走到 NULL,slow 即为倒数第 k 结点。
  3. 203 移除链表元素 :使用虚拟哨兵头结点,规避删除头结点的特殊判断。
  4. 206 反转链表:头插法,新建空链表,依次将原链表结点头插到新链表。
  5. 21 合并两个有序链表:哨兵头结点做归并,规避处理第一个结点的特殊判断。
  6. 160 相交链表:求取两个链表长度,长链表先走长度差值步,之后同步遍历找相等结点;尾结点不相等则链表不相交。
  7. 141 环形链表:快慢指针,fast 两步 slow 一步;相遇代表有环,fast 走到 NULL 代表无环。
  8. 142 环形链表 II:快慢指针相遇后,一个指针从 head 出发、一个从相遇点出发,每次走一步,相遇点就是环入口。

拓展:fast 一次走 3 步不一定会相遇,取决于环周长与追击距离。

四、带头双向循环链表(STL::list 底层实现)

每个结点:prev(前驱指针) + data + next(后继指针)

  • 空链表状态:头结点L->next = LL->prev = L自己指向自己。
  • 优势:
    1. 通过L->prev直接拿到尾结点;头插、头删、尾插、尾删全部 O (1)
    2. 给定 pos 结点,可以 O (1) 完成 pos 之后插入、删除 pos 结点,不需要从头遍历寻找前驱。

核心操作

  1. pos 后插入新结点 newNode(四句顺序)

    newNode->next = pos->next;
    newNode->prev = pos;
    pos->next->prev = newNode;
    pos->next = newNode;

  2. 删除 pos 结点

    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;
    free(pos);

408 高频选择题:双向链表插入删除指针改写顺序,做题一定要画图

五、顺序表 vs 链表对比

表格

对比维度 顺序表(动态数组) 单链表 双向循环链表
物理内存 连续 内存分散 内存分散
随机访问 ✅O(1) ❌O(n) ❌O(n)
头部插入删除 O (n) 大量搬移 O(1) O(1)
尾部插入删除 O (1)(不计扩容) O (n) 需要找尾 ✅O(1)
已知位置 pos 中间插入删除 O (n) 搬移 O (n) 需要先查找 pos O(1)
CPU 缓存友好 ✅空间局部性,Cache 命中率高 ❌内存离散,缓存失效 ❌缓存不友好
内存碎片 几乎无碎片 频繁 malloc/free 产生碎片 产生碎片
额外开销 少量 capacity 预留空间 每个结点存储 next 指针 每个结点存储 prev+next 两个指针

使用场景建议:

  1. 大量随机访问、二分查找:优先选顺序表
  2. 频繁头尾增删、已知结点位置频繁插入删除:优先双向循环链表
  3. 工程开发很少直接使用普通单链表。

六、补充:CPU 缓存与局部性原理

  1. Cache Line:CPU 读取内存最小单位,一般 64 字节。读取一个数据会把相邻一批数据载入缓存。
  2. 空间局部性:访问某块内存,它相邻内存大概率马上访问。顺序表连续内存,遍历缓存命中率极高;链表结点内存散乱,每次都要访问内存,性能差很多。
  3. 时间局部性:访问过的数据短时间可能再次访问。

面试考点:为什么数组遍历比链表快?核心就是 CPU 缓存。

七、考研 / 面试踩坑提醒

  1. 位序 vs 下标:教材位序从 1;代码下标从 0,读题先确认描述。
  2. 链表做题优先画图,指针改写顺序写错直接内存崩溃。
  3. 链表注意内存泄漏:malloc 对应 free;OJ 平台自动回收内存,实际工程必须手动释放。
  4. realloc 扩容分为原地 / 异地扩容,异地会拷贝旧数据,旧地址直接失效。
  5. 哨兵头结点(虚拟头):减少大量 if 特殊分支判断,做题高频技巧。
相关推荐
鹿角片ljp1 小时前
LeetCode 142:环形链表 II |HashSet 保底解 + Floyd 快慢指针找环入口
算法·leetcode·链表
鹿角片ljp1 小时前
LeetCode 19:删除链表的倒数第 N 个节点复盘| dummy + 快慢指针
算法·leetcode·链表
白狐_7981 小时前
408 数据结构|散列表构造题:开放定址法做题套路
数据结构·散列表
runningshark2 小时前
CH-6图论基础-有向树
数据结构
旖旎夜光2 小时前
LeetCode 525:连续数组(前缀和) —— 题解
数据结构·c++·算法·leetcode·前缀和
白狐_7982 小时前
408 数据结构|双散列与探测覆盖率:互质判断法
数据结构
shehuiyuelaiyuehao12 小时前
算法31,前缀和,可被k整除的子数组
数据结构·python·算法
203号居民13 小时前
LeetCode hot 100 — 141. 环形链表2
算法·leetcode·链表
LuminousCPP14 小时前
数据结构-二叉树(六):BFS层序遍历与完全二叉树判断|复用链式队列 + (N_0=N_2+1) 性质证明
c语言·数据结构·笔记·算法·二叉树·宽度优先