# 链表、栈和队列

*链表、栈和队列是更复杂数据结构的构建模块。本文件涵盖它们的运行机制，然后构建关键模式：快慢指针、单调栈和基于堆的优先队列，通过逐步增加难度的题目，并在每一步指出常见陷阱。*

- 数组提供了快速的随机访问但插入代价高。**链表**提供了快速插入但没有随机访问。**栈**和**队列**将访问限制在一端或两端，而正是这种限制使它们强大：通过限制你能做的事情，它们简化了你需要考虑的事情。

## 链表

- **单向链表**是一个节点链。每个节点存储一个值和一个指向下一个节点的指针。最后一个节点指向 `null`。

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

- **相对于数组的优势**：在已知位置插入或删除是 $O(1)$（只需重新指向指针）。无需移动元素。

- **劣势**：访问元素 $i$ 需要 $O(i)$ 次遍历（无随机访问）。缓存局部性差（节点分散在内存中）。

- **双向链表**增加了一个 `prev` 指针，支持向后遍历。用于 LRU 缓存（常数时间删除任何节点）和浏览器历史（前进/后退）。

| 操作 | 单向 | 双向 |
|-----------|--------|--------|
| 按索引访问 | $O(n)$ | $O(n)$ |
| 在头部插入 | $O(1)$ | $O(1)$ |
| 在尾部插入 | $O(n)$ 或 $O(1)$* | $O(1)$ |
| 删除给定节点 | $O(n)$** | $O(1)$ |
| 搜索 | $O(n)$ | $O(n)$ |

*有尾指针时。**需要前驱节点，需要遍历。

- **哨兵节点**（虚拟头/尾节点）简化了边界情况。没有虚拟头节点时，在头部插入或删除头部需要特殊代码。有了虚拟节点，每个真实节点都有前驱。

```python
# 无虚拟节点：头部删除需要特殊处理
def delete_head(head):
    if not head:
        return None
    return head.next

# 有虚拟节点：统一逻辑
dummy = ListNode(0)
dummy.next = head
# 现在每次删除都是：prev.next = prev.next.next
```

- **陷阱**：忘记处理空列表（`head is None`）或单元素列表。始终测试这些边界情况。

---

## 模式：快慢指针（弗洛伊德算法）

- 使用两个以不同速度移动的指针来检测链表的属性。**慢**指针一次移动一步；**快**指针一次移动两步。

### 简单：环形链表

- **问题**：判断一个链表是否有环。

- **模式**：如果有环，快指针最终会追上慢指针（它们会相遇）。如果没有环，快指针会到达 `null`。

```python
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
```

- **为什么有效**：如果环的长度为 $c$，快指针每步缩小1个节点的差距。它们必在慢指针进入环后的 $c$ 步内相遇。

- **陷阱**：检查 `fast and fast.next`（而不仅仅是 `fast.next`）。如果 `fast` 是 `None`，调用 `fast.next` 会崩溃。

### 中等：寻找链表的中间节点

- **问题**：返回中间节点。

- **模式**：当快指针到达末尾时，慢指针在中间。

```python
def find_middle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow  # slow 在中间（偶数长度时为第二个中间节点）
```

### 中等：环形链表 II（寻找环的起点）

- **问题**：返回环开始的节点。

- **模式**：在快指针和慢指针相遇后，将一个指针重置到头部。两者以速度1移动。它们在环的起点相遇。

```python
def detect_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            # 将一个指针重置到头部
            slow = head
            while slow != fast:
                slow = slow.next
                fast = fast.next
            return slow
    return None
```

- **为什么有效**：设从头到环起点的距离为 $a$，从环起点到相遇点的距离为 $b$。慢指针走了 $a + b$。快指针走了 $2(a + b)$。差值为一整圈：$a + b = c$（环长）。所以 $a = c - b$：从头到环起点的距离等于从相遇点到环起点的距离（沿环向前走）。

### 困难：K个一组反转链表

- **问题**：反转链表中每 $k$ 个连续节点。

```python
def reverse_k_group(head, k):
    # 检查是否还有 k 个节点
    node = head
    for _ in range(k):
        if not node:
            return head
        node = node.next

    # 反转 k 个节点
    prev, curr = None, head
    for _ in range(k):
        nxt = curr.next
        curr.next = prev
        prev = curr
        curr = nxt

    # 当前 head 现在是反转后的组的尾节点
    # 递归处理剩余部分
    head.next = reverse_k_group(curr, k)
    return prev  # prev 是这组的新头节点
```

- **陷阱**：原地反转模式（`prev, curr, nxt`）值得记住。画出来：每一步，你将 `curr.next` 指回 `prev`，然后推进所有三个指针。顺序搞错会破坏链表。

---

## 栈

- **栈**是 LIFO（后进先出）：最近添加的元素最先被移除。想象一堆盘子。

- 操作：`push(x)` 添加到顶部，`pop()` 从顶部移除，`peek()` 查看顶部不移除。全部 $O(1)$。

- 栈是**递归**（调用栈）、**表达式求值**（中缀转后缀）和**撤销操作**（每个操作被入栈，撤销时弹出最后一个）背后的隐式结构。

### 简单：有效的括号

- **问题**：给定一个由括号 `()[]{}` 组成的字符串，判断它们是否平衡。

- **模式**：将左括号入栈。当看到右括号时，检查栈顶是否匹配。

```python
def is_valid(s):
    stack = []
    matching = {')': '(', ']': '[', '}': '{'}

    for char in s:
        if char in matching:
            if not stack or stack[-1] != matching[char]:
                return False
            stack.pop()
        else:
            stack.append(char)

    return len(stack) == 0
```

- **陷阱**：忘记最后检查 `len(stack) == 0`。字符串 "(((" 中没有不匹配的情况，但因为没有闭合的括号，它是无效的。

---

## 模式：单调栈

- **单调栈**维护按排序顺序排列的元素（递增或递减）。当新元素会破坏顺序时，你弹出元素直到顺序恢复。

- **何时使用**：问题要求"对每个元素，找到下一个/上一个更大/更小的元素。"栈的总时间复杂度为 $O(n)$，因为每个元素最多被入栈和出栈一次。

### 中等：每日温度

- **问题**：给定每日温度，对于每一天，找到需要等多少天才会升温。

- **模式**：使用一个索引栈。当当前温度高于栈顶时，弹出并记录距离。

```python
def daily_temperatures(temperatures):
    n = len(temperatures)
    result = [0] * n
    stack = []  # 索引栈，温度递减

    for i in range(n):
        while stack and temperatures[i] > temperatures[stack[-1]]:
            prev = stack.pop()
            result[prev] = i - prev
        stack.append(i)

    return result
```

- 每个元素被入栈一次，最多出栈一次：总计 $O(n)$。

- **陷阱**：在栈中存储索引（而非值）。你需要索引来计算距离。

### 困难：柱状图中最大的矩形

- **问题**：给定一个条形高度数组，找出最大矩形的面积。

- **模式**：对于每个条形，找出它可以向左右延伸多远（即，每侧最近的更短条形）。单调递增栈高效地追踪这个信息。

```python
def largest_rectangle(heights):
    stack = []  # 索引栈，高度递增
    max_area = 0
    heights.append(0)  # 哨兵，用于最后清空栈

    for i, h in enumerate(heights):
        start = i
        while stack and stack[-1][1] > h:
            idx, height = stack.pop()
            max_area = max(max_area, height * (i - idx))
            start = idx  # 当前条形可以延伸到被弹出条形开始的位置
        stack.append((start, h))

    heights.pop()  # 移除哨兵
    return max_area
```

- **陷阱**：`start = idx` 这行很微妙。当我们弹出一个比当前条形更高的条形时，当前条形可以向后延伸至被弹出条形开始的位置（因为中间的所有条形至少和被弹出条形一样高）。缺少这行会得到错误的面积。

- **陷阱**：哨兵 `heights.append(0)` 确保栈中所有剩余的条形被处理。没有它，那些右侧从未遇到更短条形的条形会被遗漏。

---

## 队列

- **队列**是 FIFO（先进先出）：元素从后面添加，从前面移除。想象商店里排队。

- **双端队列**（deque）支持在两端 $O(1)$ 插入和删除。Python 的 `collections.deque` 是标准实现。

- 队列是 **BFS**（广度优先搜索，第14章文件04）、**任务调度**和**消息传递**背后的结构。

### 简单：用栈实现队列

- **问题**：仅使用两个栈实现一个队列。

- **模式**：使用一个栈进行入队操作，一个栈进行出队操作。当出队栈为空时，将所有元素从入队栈转移到出队栈（反转顺序）。

```python
class MyQueue:
    def __init__(self):
        self.push_stack = []
        self.pop_stack = []

    def push(self, x):
        self.push_stack.append(x)

    def pop(self):
        if not self.pop_stack:
            while self.push_stack:
                self.pop_stack.append(self.push_stack.pop())
        return self.pop_stack.pop()

    def peek(self):
        if not self.pop_stack:
            while self.push_stack:
                self.pop_stack.append(self.push_stack.pop())
        return self.pop_stack[-1]

    def empty(self):
        return not self.push_stack and not self.pop_stack
```

- 每次操作的平摊复杂度 $O(1)$：每个元素最多在两个栈之间移动一次。

---

## 优先队列和堆

- **优先队列**总是返回最小（或最大）的元素，不论插入顺序。标准实现是**二叉堆**。

- **最小堆**是一棵完全二叉树，其中每个父节点都小于其子节点。最小值总是在根节点。以数组形式存储：节点 $i$ 的子节点在位置 $2i + 1$ 和 $2i + 2$。

| 操作 | 时间 |
|-----------|------|
| 插入 | $O(\log n)$ |
| 获取最小值 | $O(1)$ |
| 提取最小值 | $O(\log n)$ |
| 从数组构建堆 | $O(n)$ |

- Python 的 `heapq` 模块提供了最小堆。对于最大堆，将值取反。

```python
import heapq

# 最小堆
h = []
heapq.heappush(h, 5)
heapq.heappush(h, 2)
heapq.heappush(h, 8)
print(heapq.heappop(h))  # 2（最小）

# 最大堆技巧：取反
heapq.heappush(h, -10)
print(-heapq.heappop(h))  # 10（最大）
```

### 中等：数组中的第 K 个最大元素

- **问题**：找到第 k 个最大的元素。

- **模式**：维护一个大小为 $k$ 的最小堆。堆的根节点就是第 k 大的元素。如果堆有 $k$ 个元素且新元素大于根节点，则替换根节点。

```python
import heapq

def find_kth_largest(nums, k):
    heap = nums[:k]
    heapq.heapify(heap)  # O(k)

    for num in nums[k:]:
        if num > heap[0]:
            heapq.heapreplace(heap, num)  # 弹出最小值，推入 num：O(log k)

    return heap[0]
```

- $O(n \log k)$ 时间，$O(k)$ 空间。当 $k \ll n$ 时，这比排序（$O(n \log n)$）好得多。

- **陷阱**：使用大小为 $n$ 的最大堆并弹出 $k$ 次也可行但较慢：$O(n + k \log n)$。大小为 $k$ 的最小堆是最优方法。

### 困难：合并 K 个排序链表

- **问题**：合并 $k$ 个已排序链表为一个排序链表。

- **模式**：使用一个包含每个链表头节点的最小堆。弹出最小的节点，将其添加到结果中，并将其下一个节点推入堆中。

```python
import heapq

def merge_k_lists(lists):
    heap = []
    for i, lst in enumerate(lists):
        if lst:
            heapq.heappush(heap, (lst.val, i, lst))

    dummy = ListNode(0)
    curr = dummy

    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
```

- $O(n \log k)$，其中 $n$ 是总节点数。堆中最多有 $k$ 个元素。

- **陷阱**：堆元组中的 `i`（索引）是用于打破平局的。没有它，当值相等时 Python 会尝试比较 `ListNode` 对象，这会崩溃因为 `ListNode` 不支持 `<`。索确保了一有效的比较。

---

## 常见陷阱总结

| 陷阱 | 示例 | 修复 |
|---------|---------|-----|
| `fast.next` 上的空指针 | 循环检测中使用 `while fast.next` | 检查 `fast and fast.next` |
| 未处理空链表 | 反转 `None` | 添加 `if not head` 守卫 |
| 栈下溢 | 从空栈弹出 | 检查 `len(stack) > 0` 或 `if stack` |
| 忘记哨兵 | 直方图遗漏了最后的条形 | 追加 0 来清空栈 |
| 堆中缺少平局打破 | 比较不可比较的对象 | 向堆元组添加索引 |
| 遍历时修改链表 | 遍历时删除节点 | 使用 prev/curr 模式或虚拟头节点 |

---

## 课后练习题（NeetCode）

### 链表
- [反转链表](https://neetcode.io/problems/reverse-a-linked-list) — 基础的原地反转
- [合并两个有序链表](https://neetcode.io/problems/merge-two-sorted-linked-lists) — 双指针合并
- [环形链表](https://neetcode.io/problems/linked-list-cycle-detection) — 快慢指针
- [重排链表](https://neetcode.io/problems/reorder-linked-list) — 找中间 + 反转 + 合并
- [删除链表的倒数第 N 个节点](https://neetcode.io/problems/remove-node-from-end-of-linked-list) — 间距为 $n$ 的双指针
- [LRU 缓存](https://neetcode.io/problems/lru-cache) — 哈希表 + 双向链表

### 栈
- [有效的括号](https://neetcode.io/problems/validate-parentheses) — 括号匹配
- [最小栈](https://neetcode.io/problems/minimum-stack) — 在每层跟踪最小值
- [逆波兰表达式求值](https://neetcode.io/problems/evaluate-reverse-polish-notation) — 基于栈的求值
- [每日温度](https://neetcode.io/problems/daily-temperatures) — 单调递减栈
- [柱状图中最大的矩形](https://neetcode.io/problems/largest-rectangle-in-histogram) — 单调递增栈
- [车队](https://neetcode.io/problems/car-fleet) — 带到达时间的栈

### 堆 / 优先队列
- [数据流中的第 K 大元素](https://neetcode.io/problems/kth-largest-integer-in-a-stream) — 大小为 $k$ 的最小堆
- [最后一块石头的重量](https://neetcode.io/problems/last-stone-weight) — 最大堆模拟
- [最接近原点的 K 个点](https://neetcode.io/problems/k-closest-points-to-origin) — 按距离排序的最小堆
- [任务调度器](https://neetcode.io/problems/task-scheduler) — 贪心 + 最大堆 + 冷却时间
- [数据流的中位数](https://neetcode.io/problems/find-median-in-a-data-stream) — 双堆（下半部分用最大堆，上半部分用最小堆）
