数据结构——链表

在计算机科学中,链表(Linked List)是与数组并列的另一种基础线性数据结构。如果说数组是一排整齐划一的公寓,那么链表就是一条由绳子串起来的珍珠------每一颗珍珠(节点)独立存在,通过线索(指针)连接成链,你可以随时在任意位置插入或取下一颗珍珠,而无需移动其他珍珠的位置。

链表的这种动态非连续存储的特性,使其在频繁插入删除的场景中表现出色。本文将从链表的基本概念、常见类型、核心操作、代码实现、复杂度分析、与数组的对比以及经典应用等方面,带你深入理解链表。

一、什么是链表?

链表是一种线性数据结构,它由一系列节点(Node)组成,每个节点包含两部分:

  • 数据域:存储元素本身的值。

  • 指针域:存储指向下一个节点(或上一个节点)的引用。

节点之间通过指针链接,形成一条逻辑上的线性序列。与数组不同,链表中的节点在内存中不必连续,它们可以分散在内存的各个角落,通过指针串联在一起。

链表的核心特点:

  • 动态大小:可以根据需要随时增加或减少节点,没有容量限制(受限于内存)。

  • 非连续存储:节点在内存中任意位置,通过指针维护顺序。

  • 插入删除高效:只要知道操作位置,插入或删除节点只需修改指针,无需移动其他元素。

二、链表的常见类型

根据指针的连接方式,链表主要分为三种:

1. 单链表(Singly Linked List)

每个节点只包含一个指向下一个节点 的指针,链表的末尾节点的 next 指针指向 None(或 null)。

复制代码
head → [data|next] → [data|next] → [data|next] → None
  • 优点:结构简单,占用内存少。

  • 缺点:只能单向遍历,无法直接获取前驱节点,删除某个节点时需要知道其前驱。

2. 双链表(Doubly Linked List)

每个节点包含两个指针:prev 指向前一个节点,next 指向后一个节点。

复制代码
None ← [prev|data|next] ↔ [prev|data|next] ↔ [prev|data|next] → None
  • 优点:可以双向遍历,删除节点或插入节点时更方便(无需额外保存前驱)。

  • 缺点:每个节点多占用一个指针的内存空间,操作时需维护更多指针。

3. 循环链表(Circular Linked List)

最后一个节点的 next 指针指向头节点(或第一个节点),形成一个环。可以是单循环链表或双循环链表。

复制代码
head → [data|next] → [data|next] → ... → [data|next] ──┐
        ↑                                                │
        └────────────────────────────────────────────────┘
  • 优点:从任意节点出发都能遍历整个链表,适合需要循环访问的场景。

  • 缺点:需要小心处理循环结束条件,防止死循环。


三、链表的基本操作

下面以单链表 为例,用 Python 实现核心操作。双链表的实现类似,但需要多维护一个 prev 指针。

节点定义

php 复制代码
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

1. 创建链表与插入操作

头部插入
python 复制代码
def insert_at_head(head, value):
    new_node = Node(value)
    new_node.next = head
    return new_node   # 新节点成为新的头节点
尾部插入
python 复制代码
def insert_at_tail(head, value):
    new_node = Node(value)
    if head is None:
        return new_node
    current = head
    while current.next:
        current = current.next
    current.next = new_node
    return head
在指定位置插入
python 复制代码
def insert_at_position(head, value, position):
    new_node = Node(value)
    if position == 0:
        new_node.next = head
        return new_node
    current = head
    for _ in range(position - 1):
        if current is None:
            raise IndexError("Position out of range")
        current = current.next
    new_node.next = current.next
    current.next = new_node
    return head

2. 删除操作

删除头节点
python 复制代码
def delete_at_head(head):
    if head is None:
        return None
    return head.next
删除指定值的节点(第一次出现)
python 复制代码
def delete_by_value(head, value):
    if head is None:
        return None
    # 如果头节点就是要删除的节点
    if head.value == value:
        return head.next
    current = head
    while current.next:
        if current.next.value == value:
            current.next = current.next.next
            return head
        current = current.next
    return head  # 未找到,原链表不变

3. 查找操作

python 复制代码
def search(head, value):
    current = head
    index = 0
    while current:
        if current.value == value:
            return index
        current = current.next
        index += 1
    return -1

4. 遍历链表

python 复制代码
def traverse(head):
    current = head
    while current:
        print(current.value, end=" -> ")
        current = current.next
    print("None")

5. 获取长度

python 复制代码
def length(head):
    count = 0
    current = head
    while current:
        count += 1
        current = current.next
    return count

四、双链表的实现要点

双链表在单链表的基础上增加 prev 指针,操作时需要同时维护前后关系。以插入到尾部为例:

python 复制代码
class DoublyNode:
    def __init__(self, value):
        self.value = value
        self.prev = None
        self.next = None

def insert_at_tail_doubly(head, value):
    new_node = DoublyNode(value)
    if head is None:
        return new_node
    current = head
    while current.next:
        current = current.next
    current.next = new_node
    new_node.prev = current
    return head

删除节点时同样需要更新前后节点的指针,注意边界条件(头节点、尾节点)。

五、复杂度分析

操作 单链表 双链表 备注
访问(按索引) O(n) O(n) 需要从头部遍历到目标位置
搜索(按值) O(n) O(n) 可能需要遍历整个链表
头部插入 O(1) O(1) 只需修改头指针
头部删除 O(1) O(1) 只需移动头指针
尾部插入 O(n) O(1)* *如果有尾指针,则为 O(1)
尾部删除 O(n) O(1)* *如果有尾指针,且为双链表,则为 O(1)
中间插入(已知位置) O(1)** O(1)** **假设已经定位到插入点前驱
中间删除(已知位置) O(1)** O(1)** **假设已经定位到待删除节点或其前驱
  • 单链表尾部操作需要遍历到尾部,如果维护一个 tail 指针,则尾部插入可降为 O(1),但尾部删除仍需 O(n)(因为无法快速获取倒数第二个节点)。

  • 双链表配合 tail 指针,尾部插入和删除均可达到 O(1)。

六、数组与链表的对比

特性 数组 链表
内存分配 连续内存,静态或动态扩容 非连续内存,节点动态分配
随机访问 O(1) O(n)
插入/删除(头部/中间) O(n)(需移动元素) O(1)(已知位置)
插入/删除(尾部) 均摊 O(1)(动态数组) O(1)(有尾指针的双链表)或 O(n)
内存开销 低(仅元素本身) 高(额外存储指针)
缓存友好 非常友好(连续内存) 不友好(节点可能不连续)
大小 固定或可动态扩展,但可能有空间浪费 动态,按需分配,无浪费

选择哪种数据结构取决于应用场景:

  • 需要频繁随机访问 → 数组。

  • 需要频繁插入删除(尤其在头部或中间) → 链表。

  • 对内存和缓存敏感 → 数组。

  • 需要动态增长且无法预估大小 → 链表或动态数组均可,链表在频繁插入删除时更有优势。

七、链表的经典应用

链表在计算机科学中有着广泛的应用,尤其在需要动态管理和频繁修改的场景中:

1. 实现栈和队列

  • 用单链表实现栈:头部作为栈顶,入栈出栈均为 O(1)。

  • 用双链表实现队列:头部出队、尾部入队均为 O(1)。

2. 操作系统中的进程调度

  • 就绪队列、阻塞队列等常用链表管理,便于插入和删除进程。

3. 内存管理(空闲链表)

  • 操作系统中的空闲内存块可以用链表组织,便于分配和回收。

4. 哈希表的冲突解决(链地址法)

  • 每个哈希桶对应一个链表,当发生哈希冲突时,新元素存入链表。

5. 图的邻接表表示

  • 每个顶点对应一个链表,存储其所有邻接顶点,节省空间且便于遍历。

6. 音乐播放列表、浏览器历史

  • 双链表天然适合实现双向导航的列表。

7. LRU 缓存淘汰算法

  • 通常结合哈希表和双链表实现,保证 O(1) 的访问和更新。

八、链表的常见变体与进阶操作

  • 有序链表:插入时保持元素有序。

  • 跳表(Skip List):在有序链表基础上增加多级索引,实现近似 O(log n) 的查找,是 Redis 中有序集合的底层实现。

  • 静态链表:用数组模拟链表,用于没有指针的语言或特定场景。

  • 链表反转:经典面试题,迭代或递归实现。

  • 检测环:快慢指针法(Floyd 判圈算法)。

  • 合并两个有序链表找到中间节点删除倒数第 n 个节点 等常见操作。

九、链表的局限与思考

链表虽然灵活,但也有其代价:

  • 随机访问能力弱:无法直接通过下标访问,必须遍历。

  • 额外内存开销:每个节点都需要存储指针,对于小数据量,内存开销可能超过数据本身。

  • 缓存不友好:节点分散在内存中,遍历时 CPU 缓存命中率低,效率可能不如数组。

  • 实现复杂:相比数组,链表操作需要小心处理指针,容易出错(如指针丢失、内存泄漏)。

在实际开发中,许多高级语言的内置数据结构(如 Python 的 list、Java 的 ArrayList)在大多数场景下优于手动链表,因为现代 CPU 对连续内存的访问速度远高于链表。因此,链表更多出现在算法设计、底层系统、面试题中,或者当插入删除极其频繁且性能关键时才被选用。

十、总结

链表是一种基础而灵活的数据结构,它以节点和指针的方式实现了动态的线性存储。通过本文,你应该掌握了:

  • 链表的基本概念:节点、指针、动态大小。

  • 三种常见链表类型:单链表、双链表、循环链表。

  • 核心操作(插入、删除、查找、遍历)的实现与复杂度。

  • 链表与数组的对比及适用场景。

  • 链表在系统设计、算法实现中的经典应用。

理解链表不仅是为了写出正确的代码,更是为了培养对内存布局、指针操作和算法设计的深刻认识。如果你正在学习数据结构,不妨亲手实现一个双链表,并尝试用它解决几个经典问题(如反转链表、合并有序链表),相信你会对链表的"灵活连接"有更深的体会。

相关推荐
计算机安禾2 小时前
【数据结构与算法】第5篇:线性表(一):顺序表(ArrayList)的实现与应用
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
仰泳的熊猫2 小时前
题目2584:蓝桥杯2020年第十一届省赛真题-数字三角形
数据结构·c++·算法·蓝桥杯
第二只羽毛2 小时前
第三章 栈,队列和数组
大数据·数据结构·c#
cpp_25013 小时前
P8395 [CCC 2022 S1] Good Fours and Good Fives
数据结构·c++·算法·动态规划·图论·题解·洛谷
another heaven3 小时前
【计算机 字符编码类型及其应用场景详解】
数据结构·字符编码
1104.北光c°3 小时前
Leetcode21.合并两个有序链表 双指针+递归 【hot100算法个人笔记】【java写法】
java·后端·程序人生·算法·leetcode·链表·学习方法
我头发多我先学4 小时前
二叉树从入门到精通:概念、结构与核心实现全解析
数据结构
第二只羽毛4 小时前
第四章 串
大数据·数据结构·c#
浅念-4 小时前
Linux 基础命令与核心知识点
linux·数据结构·c++·经验分享·笔记·算法·ubuntu