前置知识: 算法与数据结构

链表

7 minIntermediate

单链表、双链表与环形链表的原理、操作复杂度分析与多语言实现,涵盖常见面试题型。

1. 链表概述

1.1 链表 vs 数组

链表和数组是两种最基本的线性数据结构,它们在内存模型上有根本差异:

维度数组链表
内存布局连续离散(通过指针连接)
随机访问O(1)O(n)
头部插入O(n)O(1)
尾部插入O(1) amortizedO(n)/O(1)(有尾指针)
任意位置插入O(n)O(1)(已知前驱)
缓存局部性
空间开销无额外每节点多一个指针

1.2 缓存局部性分析

数组在内存中连续存储,CPU缓存行(通常64字节)可以预取相邻元素,缓存命中率高。链表节点分散在堆内存各处,每次访问可能触发缓存未命中。

实际性能差异:遍历100万个int元素,数组约1ms,链表约5-10ms(取决于内存分配器)。

模块引用:链表在哈希表冲突处理中的应用参见 哈希表。C++ STL list的实现参见 C++基础


2. 单链表

2.1 节点定义与基本操作

单链表每个节点包含数据域和指向下一个节点的指针域。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class SinglyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None
        self.size = 0

    def add_at_head(self, val):
        node = ListNode(val, self.head)
        self.head = node
        if self.tail is None:
            self.tail = node
        self.size += 1

    def add_at_tail(self, val):
        node = ListNode(val)
        if self.tail is None:
            self.head = self.tail = node
        else:
            self.tail.next = node
            self.tail = node
        self.size += 1

    def delete_at_head(self):
        if self.head is None:
            return None
        val = self.head.val
        self.head = self.head.next
        if self.head is None:
            self.tail = None
        self.size -= 1
        return val

    def find(self, val):
        curr = self.head
        while curr:
            if curr.val == val:
                return curr
            curr = curr.next
        return None

    def to_list(self):
        result = []
        curr = self.head
        while curr:
            result.append(curr.val)
            curr = curr.next
        return result
struct ListNode {
    int val;
    ListNode* next;
    ListNode(int v) : val(v), next(nullptr) {}
};

class SinglyLinkedList {
    ListNode* head;
    ListNode* tail;
    int sz;
public:
    SinglyLinkedList() : head(nullptr), tail(nullptr), sz(0) {}

    void addAtHead(int val) {
        ListNode* node = new ListNode(val);
        node->next = head;
        head = node;
        if (!tail) tail = node;
        sz++;
    }

    void addAtTail(int val) {
        ListNode* node = new ListNode(val);
        if (!tail) head = tail = node;
        else { tail->next = node; tail = node; }
        sz++;
    }

    int deleteAtHead() {
        if (!head) return -1;
        int val = head->val;
        ListNode* tmp = head;
        head = head->next;
        delete tmp;
        if (!head) tail = nullptr;
        sz--;
        return val;
    }

    ListNode* find(int val) {
        ListNode* curr = head;
        while (curr) {
            if (curr->val == val) return curr;
            curr = curr->next;
        }
        return nullptr;
    }
};

2.2 哨兵节点(Dummy Head)

哨兵节点是一个不存储实际数据的头节点,用于简化边界处理:

def remove_elements(head, val):
    dummy = ListNode(0, head)
    prev = dummy
    while prev.next:
        if prev.next.val == val:
            prev.next = prev.next.next
        else:
            prev = prev.next
    return dummy.next

不使用哨兵时,删除头节点需要特殊处理;使用哨兵后,所有删除操作统一为”删除prev.next”。

2.3 复杂度分析

操作时间空间
头部插入O(1)O(1)
尾部插入(有尾指针)O(1)O(1)
尾部插入(无尾指针)O(n)O(1)
查找O(n)O(1)
删除(已知前驱)O(1)O(1)
删除(已知节点指针)O(n)O(1)

3. 双链表

3.1 节点定义与基本操作

双链表每个节点额外包含指向前驱节点的指针,支持双向遍历。

class DoublyListNode:
    def __init__(self, val=0, prev=None, next=None):
        self.val = val
        self.prev = prev
        self.next = next

class DoublyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    def add_at_head(self, val):
        node = DoublyListNode(val, None, self.head)
        if self.head:
            self.head.prev = node
        else:
            self.tail = node
        self.head = node

    def add_at_tail(self, val):
        node = DoublyListNode(val, self.tail, None)
        if self.tail:
            self.tail.next = node
        else:
            self.head = node
        self.tail = node

    def remove_node(self, node):
        if node.prev:
            node.prev.next = node.next
        else:
            self.head = node.next
        if node.next:
            node.next.prev = node.prev
        else:
            self.tail = node.prev
struct DoublyListNode {
    int val;
    DoublyListNode* prev;
    DoublyListNode* next;
    DoublyListNode(int v) : val(v), prev(nullptr), next(nullptr) {}
};

class DoublyLinkedList {
    DoublyListNode* head;
    DoublyListNode* tail;
public:
    DoublyLinkedList() : head(nullptr), tail(nullptr) {}

    void addAtHead(int val) {
        auto node = new DoublyListNode(val);
        node->next = head;
        if (head) head->prev = node;
        else tail = node;
        head = node;
    }

    void addAtTail(int val) {
        auto node = new DoublyListNode(val);
        node->prev = tail;
        if (tail) tail->next = node;
        else head = node;
        tail = node;
    }

    void removeNode(DoublyListNode* node) {
        if (node->prev) node->prev->next = node->next;
        else head = node->next;
        if (node->next) node->next->prev = node->prev;
        else tail = node->prev;
        delete node;
    }
};

3.2 LRU缓存中的双链表应用

LRU(Least Recently Used)缓存使用哈希表+双链表实现O(1)的get和put操作:

class LRUCache:
    def __init__(self, capacity):
        self.cap = capacity
        self.cache = {}
        self.head = DoublyListNode()
        self.tail = DoublyListNode()
        self.head.next = self.tail
        self.tail.prev = self.head

    def _remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _add_to_front(self, node):
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node

    def get(self, key):
        if key not in self.cache:
            return -1
        node = self.cache[key]
        self._remove(node)
        self._add_to_front(node)
        return node.val

    def put(self, key, value):
        if key in self.cache:
            self._remove(self.cache[key])
            del self.cache[key]
        node = DoublyListNode(value)
        node.key = key
        self._add_to_front(node)
        self.cache[key] = node
        if len(self.cache) > self.cap:
            lru = self.tail.prev
            self._remove(lru)
            del self.cache[lru.key]

模块引用:LRU缓存的完整分析参见 哈希表


4. 环形链表

4.1 循环链表结构

循环链表的尾节点指向头节点,形成环。常用于操作系统进程调度(轮转调度)、约瑟夫问题等。

4.2 约瑟夫问题

n个人围成一圈,从第1个人开始报数,报到m的人出列,求最后剩下的人。

数学解法:f(n,m) = (f(n-1,m) + m) % n,f(1,m) = 0

def josephus_math(n, m):
    result = 0
    for i in range(2, n + 1):
        result = (result + m) % i
    return result + 1

def josephus_simulate(n, m):
    people = list(range(1, n + 1))
    idx = 0
    while len(people) > 1:
        idx = (idx + m - 1) % len(people)
        people.pop(idx)
    return people[0]
int josephusMath(int n, int m) {
    int result = 0;
    for (int i = 2; i <= n; i++) {
        result = (result + m) % i;
    }
    return result + 1;
}

复杂度:数学解O(n),模拟解O(nm)。


5. 经典操作与技巧

5.1 快慢指针

快慢指针是链表最核心的技巧,两个指针以不同速度前进。

找中点:快指针走两步,慢指针走一步,快指针到末尾时慢指针在中点。

判环:快慢指针相遇则存在环。

找环入口:快慢指针相遇后,一个指针从头部出发,另一个从相遇点出发,两者相遇即为环入口。

def find_middle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow

def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False

def detect_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            ptr = head
            while ptr != slow:
                ptr = ptr.next
                slow = slow.next
            return ptr
    return None
ListNode* findMiddle(ListNode* head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

bool hasCycle(ListNode* head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) return true;
    }
    return false;
}

ListNode* detectCycle(ListNode* head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            ListNode* ptr = head;
            while (ptr != slow) { ptr = ptr->next; slow = slow->next; }
            return ptr;
        }
    }
    return nullptr;
}

环入口的数学证明:设head到环入口距离为a,环入口到相遇点距离为b,环长度为c。快指针走2(a+b)步,慢指针走a+b步。快指针多走a+b = kc步。因此a = kc - b = (k-1)c + (c-b)。从head和相遇点同时出发,各走a步后必在环入口相遇。

5.2 反转链表

def reverse_list(head):
    prev = None
    curr = head
    while curr:
        next_node = curr.next
        curr.next = prev
        prev = curr
        curr = next_node
    return prev

def reverse_list_recursive(head):
    if not head or not head.next:
        return head
    new_head = reverse_list_recursive(head.next)
    head.next.next = head
    head.next = None
    return new_head

def reverse_between(head, left, right):
    dummy = ListNode(0, head)
    prev = dummy
    for _ in range(left - 1):
        prev = prev.next
    curr = prev.next
    for _ in range(right - left):
        next_node = curr.next
        curr.next = next_node.next
        next_node.next = prev.next
        prev.next = next_node
    return dummy.next
ListNode* reverseList(ListNode* head) {
    ListNode* prev = nullptr;
    ListNode* curr = head;
    while (curr) {
        ListNode* nextNode = curr->next;
        curr->next = prev;
        prev = curr;
        curr = nextNode;
    }
    return prev;
}

ListNode* reverseBetween(ListNode* head, int left, int right) {
    ListNode* dummy = new ListNode(0);
    dummy->next = head;
    ListNode* prev = dummy;
    for (int i = 0; i < left - 1; i++) prev = prev->next;
    ListNode* curr = prev->next;
    for (int i = 0; i < right - left; i++) {
        ListNode* nextNode = curr->next;
        curr->next = nextNode->next;
        nextNode->next = prev->next;
        prev->next = nextNode;
    }
    return dummy->next;
}

5.3 合并有序链表

def merge_two_lists(l1, l2):
    dummy = ListNode()
    curr = dummy
    while l1 and l2:
        if l1.val <= l2.val:
            curr.next = l1
            l1 = l1.next
        else:
            curr.next = l2
            l2 = l2.next
        curr = curr.next
    curr.next = l1 or l2
    return dummy.next

def merge_k_lists(lists):
    import heapq
    dummy = ListNode()
    curr = dummy
    heap = []
    for i, node in enumerate(lists):
        if node:
            heapq.heappush(heap, (node.val, i, node))
    while heap:
        val, i, node = heapq.heappop(heap)
        curr.next = node
        curr = curr.next
        if node.next:
            heapq.heappush(heap, (node.next.val, i, node.next))
    return dummy.next
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    ListNode dummy(0);
    ListNode* curr = &dummy;
    while (l1 && l2) {
        if (l1->val <= l2->val) { curr->next = l1; l1 = l1->next; }
        else { curr->next = l2; l2 = l2->next; }
        curr = curr->next;
    }
    curr->next = l1 ? l1 : l2;
    return dummy.next;
}

6. 常见面试题型

6.1 题型分与解题模板

题型核心技巧代表题目
反转系列迭代/递归反转LC-206/92/25
合并系列指针归并LC-21/23
环检测快慢指针LC-141/142
相交链表指针交叉遍历LC-160
回文链表快慢指针+反转LC-234
删除节点哨兵+双指针LC-19/203/83
排序链表归并排序LC-148
重排链表找中点+反转+合并LC-143

6.2 相交链表(LC-160)

两个链表在某节点相交后共享后续节点。双指针交叉遍历:pA走完A后走B,pB走完B后走A,两者必在交点相遇(或同时为None)。

def get_intersection_node(headA, headB):
    if not headA or not headB:
        return None
    pA, pB = headA, headB
    while pA != pB:
        pA = pA.next if pA else headB
        pB = pB.next if pB else headA
    return pA
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
    if (!headA || !headB) return nullptr;
    ListNode *pA = headA, *pB = headB;
    while (pA != pB) {
        pA = pA ? pA->next : headB;
        pB = pB ? pB->next : headA;
    }
    return pA;
}

正确性证明:设A独有a个节点,B独有b个节点,共享c个节点。pA走a+c+b步,pB走b+c+a步,两者步数相等,必在交点相遇。

6.3 删除链表倒数第N个节点(LC-19)

指针先走n步,然后快慢指针同时前进,快指针到末尾时慢指针在倒数第n+1个位置。

def remove_nth_from_end(head, n):
    dummy = ListNode(0, head)
    fast = slow = dummy
    for _ in range(n):
        fast = fast.next
    while fast.next:
        fast = fast.next
        slow = slow.next
    slow.next = slow.next.next
    return dummy.next
ListNode* removeNthFromEnd(ListNode* head, int n) {
    ListNode* dummy = new ListNode(0);
    dummy->next = head;
    ListNode *fast = dummy, *slow = dummy;
    for (int i = 0; i < n; i++) fast = fast->next;
    while (fast->next) { fast = fast->next; slow = slow->next; }
    ListNode* toDelete = slow->next;
    slow->next = slow->next->next;
    delete toDelete;
    return dummy->next;
}

6.4 回文链表(LC-234)

找中点 -> 反转后半部分 -> 双指针比较 -> 恢复(可选)

def is_palindrome(head):
    if not head or not head.next:
        return True
    slow = fast = head
    while fast.next and fast.next.next:
        slow = slow.next
        fast = fast.next.next
    second_half = reverse_list(slow.next)
    p1, p2 = head, second_half
    result = True
    while p2:
        if p1.val != p2.val:
            result = False
            break
        p1 = p1.next
        p2 = p2.next
    slow.next = reverse_list(second_half)
    return result

6.5 K个一组翻转链表(LC-25)

def reverse_k_group(head, k):
    def get_kth(node, k):
        while node and k > 0:
            node = node.next
            k -= 1
        return node

    dummy = ListNode(0, head)
    group_prev = dummy
    while True:
        kth = get_kth(group_prev, k)
        if not kth:
            break
        group_next = kth.next
        prev, curr = kth.next, group_prev.next
        while curr != group_next:
            next_node = curr.next
            curr.next = prev
            prev = curr
            curr = next_node
        tmp = group_prev.next
        group_prev.next = kth
        group_prev = tmp
    return dummy.next

7. 链表操作速查表

操作时间空间关键技巧
头部插入O(1)O(1)直接操作head
尾部插入O(1)*O(1)维护tail指针
查找O(n)O(1)线性遍历
删除(已知前驱)O(1)O(1)prev.next = prev.next.next
反转O(n)O(1)指针迭代
找中点O(n)O(1)快慢指针
判环O(n)O(1)快慢指针
找环入口O(n)O(1)快慢指针+数学
合并两个有序O(n+m)O(1)指针
合并K个有序O(Nlogk)O(k)最小堆
删除倒数第nO(n)O(1)快慢指针间隔n
回文判断O(n)O(1)中点+反转

*有尾指针


8. 延伸阅读

  • CLRS 第 10 章(链表基础)
  • 《剑指 Offer》链表专题
  • Linked List — VisuAlgo
  • Skiena, The Algorithm Design Manual, Section 3.1

模块引用:链表在哈希表和LRU缓存中的应用参见 哈希表。刷题实践参见 LeetCode刷题指南