>> >> >> Reference << << << <<<<<<Ref>>>>>>
Linked List
Modified: 2025-12-31 | Author:ljf12825

链表是一种线性数据结构,其中的元素(节点)不是通过物理顺序存储,而是通过指针(或引用)连接在一起

[data | next] -> [data | next] -> [data | next]

核心特点

缺点

操作时间复杂度
遍历O(n)
查找O(n)
插入O(1)*
删除O(1)*

*前提是已知节点

Single LinkedList

结构

节点结构

[data|next]
[data|next] -> [data|next] -> [data|next] -> null

适用场景

  1. 需要频繁插入/删除的场景
  2. 内存受限或数据规模不确定的场景
  3. 只需要单向遍历
  4. 实现栈/队列

不适用场景

  1. 频繁随机访问
  2. 需要双向遍历
  3. 数据量大:指针占用空间
  4. 需要快速查找
  5. 缓存性能重要

应用案例

LRU缓存淘汰算法

实现

遍历

时间复杂度O(n)

void traverse(ListNode* head) {
    ListNode* current = head;
    while (current != nullptr) {
        cout << current->val << " ";
        current = current->next;
    }
}

查找

ListNode* search(ListNode* head, int target) {
    ListNode* current = head;
    while (current != nullptr) {
        if (current->val == target) {
            return current;
        }
        current = current->next;
    }
    return nullptr;
}

插入

1.头插(O(1))

ListNode* insertAtHead(ListNode* head, int val) {
    ListNode* newNode = new ListNode(val);
    newNode->next = head;
    return newNode;  // 新节点成为新的头节点
}

2.在指定位置后插入(O(1))

void insertAfter(ListNode* prevNode, int val) {
    if (prevNode == nullptr) return;
    
    ListNode* newNode = new ListNode(val);
    newNode->next = prevNode->next;
    prevNode->next = newNode;
}

删除

1.删除指定值的节点

ListNode* deleteNode(ListNode* head, int val) {
    // 处理头节点就是要删除的节点
    while (head != nullptr && head->val == val) {
        ListNode* temp = head;
        head = head->next;
        delete temp;
    }
    
    // 处理中间节点
    ListNode* current = head;
    while (current != nullptr && current->next != nullptr) {
        if (current->next->val == val) {
            ListNode* temp = current->next;
            current->next = current->next->next;
            delete temp;
        } else {
            current = current->next;
        }
    }
    return head;
}

2.删除指定位置的节点

void deleteNode(ListNode* node) {
    // 假设node不是尾节点
    node->val = node->next->val;
    ListNode* temp = node->next;
    node->next = node->next->next;
    delete temp;
}

哨兵节点(sentinel/dummy node)

if (head == nullptr) { ... }
if (pos == head) { ... }
if (head->data = val) { ... }

这些判断有几个共同点

因此哨兵节点的目标只有一个:让头节点变成普通节点

哨兵节点 = 永远存在的“假节点”,不存放有效数据

[sentinel] -> [data1] -> [data2] -> nullptr

产生变化

结构变化
head -> [data|next] -> [data|next] -> [data|next] -> null

传统单链表head指向第一个有效节点或nullptr,若有哨兵节点,永远指向哨兵节点

初始化

head = new Node<T>(T{});
head->next = nullptr;

判空

bool isEmpty() const {
    return head->next == nullptr;
}

头节点特判不复存在,退化为普通节点处理

哨兵节点在工程实践中是一种强烈推荐的做法,广泛应用于Linux内核/STL/各种容器中

Double LinkedList

节点结构

[prev|data|next]

链表结构

NULL <-> [data|next] <-> [data|next] <-> [data|next] <-> NULL

双向的好处

单向链表有一个致命缺陷:无法O(1)从当前节点回到前一个节点
只能沿着next的方向遍历,想要倒退需要重新遍历
双向链表解决了这个问题,任意节点都能O(1)前进/后退
即用空间换时间
这直接带来了几个重要能力

核心操作

插入节点(中间插入)

假设结构

A <-> B <-> C

要在B和C之间插入X
需要修改的指针(4个)

X.prev = B
X.next = C
B.next = X
C.prev = X

顺序很重要,错一个就断链

删除节点

删除节点B

A <-> B <-> C

操作

A.next = C
C.prev = A

不需要从head找前驱,O(1), 这正是双向链表存在的核心理由之一

遍历

正向遍历

for (node = head; node != null; node = node.next)

反向遍历

for (node = tail; node != null; node = node.prev)

时间复杂度

操作时间复杂度
随机访问O(n)
头部插入O(1)
中间插入O(1)
删除节点O(1)

哨兵节点对比

哨兵节点通过引入一个(或多个)永远存在的特殊节点,将链表在头尾处的边界情况统一为普通节点,从而消除插入和删除时对nullptr的分支判断
循环哨兵节点进一步通过将头尾连接成环,把“前边界”和“后边界”合并为同一个哨兵节点,使所有节点在结构上完全等价

类型结构示意空链表状态
No Sentinelnullptr <- A <-> B <-> C -> nullptrhead = nullptr, tail = nullptr
Normal Sentinel[HEAD] <-> A <-> B <-> C <-> [TAIL]HEAD.next = TAIL, TAIL.prev = HEAD
Circular Sentinel[SENTINEL] <-> A <-> B <-> C <-> [SENTINEL]sentinel.next = sentinel.prev = sentinel

边界处理对比

操作No SentinelNormal SentinelCircular Sentinel
插入头要判断 head 是否为空直接插在 head 后,无需判断直接插在 sentinel 后,无需判断
插入尾要判断 tail 是否为空直接插在 tail 前,无需判断直接插在 sentinel 前,无需判断
删除头要判断 head 是否为空直接删除 node,无需判断直接删除 node,无需判断
删除尾要判断 tail 是否为空同上同上
空链表判断head == nullptrhead->next == tailsentinel.next == sentinel

节点数量与存储开销

类型每条链表额外节点
No Sentinel0
Normal Sentinel2(head & tail)
Circular Sentinel1(sentinel)

操作复杂度对比

操作No SentinelNormal SentinelCircular Sentinel
push_front/backO(1)O(1)O(1)
remove需要判断 prev/next 是否 nullptr不需判断不需判断
emptyO(1)O(1)O(1)

安全性对比

类型UB / 崩溃风险指针错误概率
No Sentinel边界判断少写就 UB
Normal Sentinelsentinel 管控了头尾
Circular Sentinelsentinel + 循环保证安全

循环哨兵节点是工业级链表的首选,如Linuxlist_head, STL内部_List_node_base

Circular LinkedList

循环链表是最后一个节点指向头节点的链表
从任意节点出发,都能转一圈回到自己

单向循环链表

 ┌──────────┐
 v          
[A]->[B]-> [C]
head       tail

设计理念

因为是next 单链表
tail永远是最后一个节点
tail->next永远是头节点
这是工程中常见的做法

head永远指向头节点
尾节点的next == head
如果不关心尾插,只需要从头遍历,从头取元素,可以这样写
但工程中几乎永远不会这样做

操作head basetail base
头插O(1)O(1)
尾插O(n)O(1)
删除头O(n) 要找尾O(1)
删除尾O(n)O(n)
任意插入O(n)O(n)
遍历一样一样

简而言之,tail能很快找到headheadtail需要遍历

双向循环链表

      ↖─────────────↗
head  [A]  [B]  [C]

链表相关算法

  1. 链表反转(Reverse Linked List)
  2. 快慢指针(Floyd思想)
  3. 删除倒数第N各节点
  4. 合并两个有序链表
  5. 判断链表是否回文
  6. 两两交换节点(Swap Nodes in Pairs)
  7. K个一组反转链表
  8. LRU Cache(链表 + 哈希表)
  9. 侵入式链表(Intrusive List)
  10. 空闲链表(Free List)
  11. 链表排序
  12. 链表相交判断
  13. 判断链表是否有环