Skip to content

数组 / 链表 / 栈 / 队列 深度指南

面向 5 年经验前端/全栈工程师,覆盖底层原理、经典算法题完整实现与面试高频追问。

每个数据结构均包含:原理讲解 → ASCII 图解 → 完整可运行代码 → 复杂度分析


一、数组(Array)

1.1 JS 数组的底层实现 —— V8 的快慢数组

V8 引擎中的数组并非传统意义上的连续内存数组,而是根据使用方式在两种内部表示之间动态切换。

快数组(Fast Elements):底层使用 C++ 数组(FixedArray),元素在内存中连续存放,通过下标直接寻址,访问速度快。适用于索引连续、密集的场景。

慢数组(Dictionary Elements):底层使用哈希表(HashTable / NumberDictionary),以键值对存储元素。适用于稀疏数组,避免浪费大量内存。

快数组 (Fast Elements)
┌─────────────────────────────────────────┐
│ FixedArray (连续内存)                    │
│ [0] [1] [2] [3] [4] [5] [6] [7]        │
│  10  20  30  40  50  60  70  80         │
└─────────────────────────────────────────┘
  → 下标访问 O(1),缓存友好

慢数组 (Dictionary Elements)
┌─────────────────────────────────────────┐
│ HashTable                               │
│ { 0: 10, 100: 20, 99999: 30 }          │
└─────────────────────────────────────────┘
  → 稀疏场景节省内存,访问需要哈希计算

切换时机

方向条件
快 → 慢新增元素导致需要的 FixedArray 容量 ≥ 3 × 实际元素数量(即空洞率过高)
快 → 慢数组索引超过 1024 且空洞超过 capacity - length - 1024
慢 → 快补全所有空洞后所需的 FixedArray 空间 ≤ 50KB
js
const fast = [1, 2, 3, 4, 5]

fast[10000] = 1

const slow = new Array(100000)
slow[0] = 1
slow[99999] = 2

1.2 常用方法时间复杂度

方法时间复杂度原因
push(x)O(1) 均摊在尾部追加,偶尔触发扩容(2倍增长)需 O(n) 拷贝
pop()O(1)移除尾部元素
unshift(x)O(n)在头部插入,所有元素后移
shift()O(n)移除头部元素,所有元素前移
splice(i, k)O(n)从索引 i 处删除/插入,后续元素需要移动
indexOf(x)O(n)线性扫描
arr[i]O(1)快数组直接下标寻址
push(6):                   unshift(0):
[1, 2, 3, 4, 5, _]       [_, 1, 2, 3, 4, 5]
               ↑ 直接放    ↑ 所有元素右移一位
          → O(1)              → O(n)

1.3 双指针技巧

1.3.1 原地去重(LeetCode 26)

题目描述:给定一个升序排列的数组 nums,请原地删除重复元素,使每个元素只出现一次,返回删除后数组的新长度。

思路分析:使用快慢指针。慢指针 slow 指向当前不重复序列的末尾,快指针 fast 向前扫描,遇到与 slow 不同的值就将其放到 slow + 1 的位置。

初始:  [1, 1, 2, 2, 3]
        s
        f

步骤1: nums[f]=1 === nums[s]=1 → f++
       [1, 1, 2, 2, 3]
        s  f

步骤2: nums[f]=2 !== nums[s]=1 → s++, nums[s]=nums[f]
       [1, 2, 2, 2, 3]
           s  f

步骤3: nums[f]=2 === nums[s]=2 → f++
       [1, 2, 2, 2, 3]
           s     f

步骤4: nums[f]=3 !== nums[s]=2 → s++, nums[s]=nums[f]
       [1, 2, 3, 2, 3]
              s     f

结果: 返回 s+1 = 3,前3个元素 [1, 2, 3]
typescript
function removeDuplicates(nums: number[]): number {
  if (nums.length === 0) return 0
  let slow = 0
  for (let fast = 1; fast < nums.length; fast++) {
    if (nums[fast] !== nums[slow]) {
      slow++
      nums[slow] = nums[fast]
    }
  }
  return slow + 1
}

复杂度:时间 O(n),空间 O(1)

1.3.2 三数之和(LeetCode 15)

题目描述:给定一个整数数组 nums,找出所有和为 0 的不重复三元组 [nums[i], nums[j], nums[k]](i ≠ j ≠ k)。

思路分析:先排序,然后固定一个数 nums[i],用双指针在 [i+1, n-1] 区间找两数之和为 -nums[i] 的组合。跳过重复值以去重。

排序后: [-4, -1, -1, 0, 1, 2]

固定 i=0, nums[i]=-4, target=4
  L=1, R=5: -1+2=1 < 4 → L++
  L=2, R=5: -1+2=1 < 4 → L++
  L=3, R=5:  0+2=2 < 4 → L++
  L=4, R=5:  1+2=3 < 4 → L++
  L=5 >= R → 结束

固定 i=1, nums[i]=-1, target=1
  L=2, R=5: -1+2=1 === target → 记录 [-1,-1,2], L++, R--
  L=3, R=4:  0+1=1 === target → 记录 [-1,0,1], L++, R--
  L=4 >= R → 结束

固定 i=2, nums[i]=-1 === nums[i-1] → 跳过(去重)

结果: [[-1,-1,2], [-1,0,1]]
typescript
function threeSum(nums: number[]): number[][] {
  const result: number[][] = []
  nums.sort((a, b) => a - b)

  for (let i = 0; i < nums.length - 2; i++) {
    if (nums[i] > 0) break
    if (i > 0 && nums[i] === nums[i - 1]) continue

    let left = i + 1
    let right = nums.length - 1

    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right]
      if (sum === 0) {
        result.push([nums[i], nums[left], nums[right]])
        while (left < right && nums[left] === nums[left + 1]) left++
        while (left < right && nums[right] === nums[right - 1]) right--
        left++
        right--
      } else if (sum < 0) {
        left++
      } else {
        right--
      }
    }
  }
  return result
}

复杂度:时间 O(n²),空间 O(log n)(排序栈空间)

1.4 滑动窗口 —— 最长无重复子串(LeetCode 3)

题目描述:给定一个字符串 s,找出其中不含有重复字符的最长子串的长度。

思路分析:维护一个窗口 [left, right],用 Map 记录每个字符最后出现的索引。当 right 遇到窗口内已有的字符时,将 left 跳到该字符上次出现位置的下一位。

s = "abcabcbb"

步骤       窗口          Map                     maxLen
─────────────────────────────────────────────────────
right=0    [a]           {a:0}                   1
right=1    [a,b]         {a:0, b:1}              2
right=2    [a,b,c]       {a:0, b:1, c:2}         3
right=3    a重复!
           left=0+1=1
           [b,c,a]       {a:3, b:1, c:2}         3
right=4    b重复!
           left=1+1=2
           [c,a,b]       {a:3, b:4, c:2}         3
right=5    c重复!
           left=2+1=3
           [a,b,c]       {a:3, b:4, c:5}         3
right=6    b重复!
           left=4+1=5
           [c,b]         {a:3, b:6, c:5}         3
right=7    b重复!
           left=6+1=7
           [b]           {a:3, b:7, c:5}         3

结果: 3 (对应 "abc")
typescript
function lengthOfLongestSubstring(s: string): number {
  const map = new Map<string, number>()
  let maxLen = 0
  let left = 0

  for (let right = 0; right < s.length; right++) {
    if (map.has(s[right]) && map.get(s[right])! >= left) {
      left = map.get(s[right])! + 1
    }
    map.set(s[right], right)
    maxLen = Math.max(maxLen, right - left + 1)
  }
  return maxLen
}

复杂度:时间 O(n),空间 O(min(n, m)),m 为字符集大小


二、链表(Linked List)

2.1 单链表节点定义

  ┌──────┬──────┐     ┌──────┬──────┐     ┌──────┬──────┐
  │ val  │ next │────▶│ val  │ next │────▶│ val  │ next │────▶ null
  └──────┴──────┘     └──────┴──────┘     └──────┴──────┘
    Node 1               Node 2               Node 3
typescript
class ListNode {
  val: number
  next: ListNode | null
  constructor(val: number = 0, next: ListNode | null = null) {
    this.val = val
    this.next = next
  }
}

2.2 基本操作

插入

在节点 A 后插入节点 X:

  Before:  A ──────────▶ B
  After:   A ──▶ X ──▶ B

  X.next = A.next    // X 先指向 B
  A.next = X         // A 再指向 X

删除

删除节点 B (已知前驱 A):

  Before:  A ──▶ B ──▶ C
  After:   A ──────────▶ C

  A.next = B.next    // A 直接指向 C

查找

typescript
function findNode(head: ListNode | null, target: number): ListNode | null {
  let curr = head
  while (curr !== null) {
    if (curr.val === target) return curr
    curr = curr.next
  }
  return null
}
操作时间复杂度说明
按索引访问O(n)需要从头遍历
头部插入/删除O(1)直接操作头指针
尾部插入(无尾指针)O(n)需遍历到尾部
已知前驱节点的插入/删除O(1)修改指针

2.3 反转链表(LeetCode 206)

题目描述:给定单链表的头节点 head,将链表反转并返回新的头节点。

思路分析(迭代):使用三个指针 prevcurrnext。逐个翻转指针方向。

初始:  null ← prev   curr → 1 → 2 → 3 → null

步骤1: next = curr.next
       null ← prev   curr → 1   next → 2 → 3 → null

步骤2: curr.next = prev
       null ← 1 ← curr   next → 2 → 3 → null

步骤3: prev = curr, curr = next
       null ← 1 ← prev   curr → 2 → 3 → null

步骤4: 重复...
       null ← 1 ← 2 ← prev   curr → 3 → null

步骤5: 重复...
       null ← 1 ← 2 ← 3 ← prev   curr → null

curr === null,返回 prev
结果:  3 → 2 → 1 → null

迭代实现

typescript
function reverseList(head: ListNode | null): ListNode | null {
  let prev: ListNode | null = null
  let curr = head

  while (curr !== null) {
    const next = curr.next
    curr.next = prev
    prev = curr
    curr = next
  }
  return prev
}

递归实现

递归展开:
  reverseList(1→2→3→null)
    reverseList(2→3→null)
      reverseList(3→null)
        return 3              ← 递归到底,3 是新头节点

      回溯: head=2, head.next=3
        3.next = 2            ← 让 3 指向 2
        2.next = null         ← 断开 2 的原指向
        return 3

    回溯: head=1, head.next=2
      2.next = 1              ← 让 2 指向 1
      1.next = null           ← 断开 1 的原指向
      return 3

结果: 3→2→1→null
typescript
function reverseListRecursive(head: ListNode | null): ListNode | null {
  if (head === null || head.next === null) return head
  const newHead = reverseListRecursive(head.next)
  head.next.next = head
  head.next = null
  return newHead
}

复杂度:迭代 — 时间 O(n),空间 O(1);递归 — 时间 O(n),空间 O(n)

2.4 合并两个有序链表(LeetCode 21)

题目描述:将两个升序链表合并为一个新的升序链表并返回。

思路分析:使用哨兵节点 dummy 简化边界处理,双指针逐一比较取较小值。

l1: 1 → 3 → 5
l2: 2 → 4 → 6

dummy → ?

比较 1 vs 2 → 取 1:  dummy → 1
比较 3 vs 2 → 取 2:  dummy → 1 → 2
比较 3 vs 4 → 取 3:  dummy → 1 → 2 → 3
比较 5 vs 4 → 取 4:  dummy → 1 → 2 → 3 → 4
比较 5 vs 6 → 取 5:  dummy → 1 → 2 → 3 → 4 → 5
l1 为空 → 接上 l2:   dummy → 1 → 2 → 3 → 4 → 5 → 6

返回 dummy.next
typescript
function mergeTwoLists(
  l1: ListNode | null,
  l2: ListNode | null
): ListNode | null {
  const dummy = new ListNode(-1)
  let curr = dummy

  while (l1 !== null && l2 !== null) {
    if (l1.val <= l2.val) {
      curr.next = l1
      l1 = l1.next
    } else {
      curr.next = l2
      l2 = l2.next
    }
    curr = curr.next
  }
  curr.next = l1 ?? l2
  return dummy.next
}

复杂度:时间 O(n + m),空间 O(1)

2.5 环形链表检测(LeetCode 141 / 142)

题目描述

  • 141:判断链表中是否有环。
  • 142:返回环的入口节点。

思路分析:Floyd 判圈算法(快慢指针)。快指针每次走 2 步,慢指针每次走 1 步。若存在环,两者必定在环内相遇。

        ┌───────────────────────┐
        ▼                       │
  1 → 2 → 3 → 4 → 5 → 6 → 7 ──┘
        ↑ 入口 (index=1)

快慢指针运动:
step  slow  fast
  0     1     1
  1     2     3
  2     3     5
  3     4     7
  4     5     4     (环内)
  5     6     6     ← 相遇!

找入口 (142):
  设 head 到入口距离 = a
  入口到相遇点距离 = b
  相遇点到入口距离 = c (环剩余)

  慢走了: a + b
  快走了: a + b + n(b + c)   (n 为快指针多绕的圈数)
  快 = 2 × 慢: a + b + n(b+c) = 2(a + b)
  化简: a = (n-1)(b+c) + c
  即: 从 head 走 a 步 = 从相遇点走 c + 若干整圈

  → 一个指针从 head 出发,一个从相遇点出发,
    每次各走 1 步,相遇处即为入口

141 — 判断是否有环

typescript
function hasCycle(head: ListNode | null): boolean {
  let slow = head
  let fast = head

  while (fast !== null && fast.next !== null) {
    slow = slow!.next
    fast = fast.next.next
    if (slow === fast) return true
  }
  return false
}

142 — 找环入口

typescript
function detectCycle(head: ListNode | null): ListNode | null {
  let slow = head
  let fast = head

  while (fast !== null && fast.next !== null) {
    slow = slow!.next
    fast = fast.next.next
    if (slow === fast) {
      let ptr = head
      while (ptr !== slow) {
        ptr = ptr!.next
        slow = slow!.next
      }
      return ptr
    }
  }
  return null
}

复杂度:时间 O(n),空间 O(1)

2.6 删除链表的倒数第 N 个节点(LeetCode 19)

题目描述:给定链表,删除其倒数第 n 个节点,返回链表的头节点。

思路分析:快指针先走 n 步,然后快慢指针同步走。当快指针到达末尾时,慢指针恰好在倒数第 n+1 个节点(待删除节点的前驱)。使用 dummy 节点避免删除头节点的边界问题。

链表: 1 → 2 → 3 → 4 → 5, n=2

Step 1: fast 先走 n=2 步
  dummy → 1 → 2 → 3 → 4 → 5 → null
  s              f

Step 2: 同步走直到 fast.next === null
  dummy → 1 → 2 → 3 → 4 → 5 → null
                   s              f

Step 3: 删除 slow.next
  slow.next = slow.next.next
  dummy → 1 → 2 → 3 → 5 → null

返回 dummy.next
typescript
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
  const dummy = new ListNode(0, head)
  let fast: ListNode | null = dummy
  let slow: ListNode | null = dummy

  for (let i = 0; i <= n; i++) {
    fast = fast!.next
  }

  while (fast !== null) {
    fast = fast.next
    slow = slow!.next
  }

  slow!.next = slow!.next!.next
  return dummy.next
}

复杂度:时间 O(n),空间 O(1)

2.7 相交链表(LeetCode 160)

题目描述:给定两个链表的头节点 headAheadB,找出两个链表相交的起始节点。如果不相交返回 null

思路分析:指针 A 遍历完链表 A 后接到链表 B 头部继续走,指针 B 同理。两者走过的总路程相同(lenA + lenB),若相交则必在交点相遇,否则同时到达 null。

A:  a1 → a2 ↘
                c1 → c2 → c3
B:  b1 → b2 → b3 ↗

A 的路径: a1 → a2 → c1 → c2 → c3 → b1 → b2 → b3 → [c1]
B 的路径: b1 → b2 → b3 → c1 → c2 → c3 → a1 → a2 → [c1]
                                                       ↑ 相遇!

路程: A走了 (2+3) + 3 = 8 步
      B走了 (3+3) + 2 = 8 步  → 同步到达 c1
typescript
function getIntersectionNode(
  headA: ListNode | null,
  headB: ListNode | null
): ListNode | null {
  let pA = headA
  let pB = headB

  while (pA !== pB) {
    pA = pA === null ? headB : pA.next
    pB = pB === null ? headA : pB.next
  }
  return pA
}

复杂度:时间 O(n + m),空间 O(1)


三、栈(Stack)

3.1 用数组实现栈

栈的操作示意 (LIFO — Last In, First Out):

  push(1)  push(2)  push(3)   pop()    peek()
  ┌───┐   ┌───┐   ┌───┐     ┌───┐    ┌───┐
  │   │   │   │   │ 3 │ ←   │   │    │   │
  │   │   │ 2 │   │ 2 │     │ 2 │ ←  │ 2 │ ← 返回2
  │ 1 │   │ 1 │   │ 1 │     │ 1 │    │ 1 │   (不弹出)
  └───┘   └───┘   └───┘     └───┘    └───┘
typescript
class Stack<T> {
  private items: T[] = []

  push(val: T): void {
    this.items.push(val)
  }

  pop(): T | undefined {
    return this.items.pop()
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1]
  }

  isEmpty(): boolean {
    return this.items.length === 0
  }

  size(): number {
    return this.items.length
  }
}

所有操作均为 O(1)

3.2 有效括号(LeetCode 20)

题目描述:给定一个只包含 (){}[] 的字符串,判断字符串是否有效。有效条件:左括号必须用相同类型的右括号闭合,且闭合顺序正确。

思路分析:遍历字符串,遇到左括号入栈,遇到右括号弹出栈顶检查是否匹配。

s = "{[()]}"

字符   操作          栈
──────────────────────────
 {     push '}'     ['}']
 [     push ']'     ['}', ']']
 (     push ')'     ['}', ']', ')']
 )     pop → ')'    ['}', ']']        ✓ 匹配
 ]     pop → ']'    ['}']             ✓ 匹配
 }     pop → '}'    []                ✓ 匹配

栈为空 → 返回 true
typescript
function isValid(s: string): boolean {
  const stack: string[] = []
  const map: Record<string, string> = {
    '(': ')',
    '[': ']',
    '{': '}'
  }

  for (const ch of s) {
    if (map[ch]) {
      stack.push(map[ch])
    } else {
      if (stack.pop() !== ch) return false
    }
  }
  return stack.length === 0
}

复杂度:时间 O(n),空间 O(n)

3.3 最小栈(LeetCode 155)

题目描述:设计一个支持 pushpoptopgetMin 操作的栈,且 getMin 在 O(1) 时间内返回栈内最小元素。

思路分析:使用辅助栈同步记录每个状态下的最小值。每次 push 时,辅助栈压入 min(当前值, 辅助栈顶)

操作          主栈            辅助栈(最小值)
─────────────────────────────────────────
push(-2)     [-2]            [-2]
push(0)      [-2, 0]         [-2, -2]
push(-3)     [-2, 0, -3]     [-2, -2, -3]
getMin()                     → -3
pop()        [-2, 0]         [-2, -2]
top()                        → 0
getMin()                     → -2
typescript
class MinStack {
  private stack: number[] = []
  private minStack: number[] = []

  push(val: number): void {
    this.stack.push(val)
    const min = this.minStack.length === 0
      ? val
      : Math.min(val, this.minStack[this.minStack.length - 1])
    this.minStack.push(min)
  }

  pop(): void {
    this.stack.pop()
    this.minStack.pop()
  }

  top(): number {
    return this.stack[this.stack.length - 1]
  }

  getMin(): number {
    return this.minStack[this.minStack.length - 1]
  }
}

复杂度:所有操作时间 O(1),空间 O(n)

3.4 单调栈原理与应用

单调栈:栈内元素保持单调递增(或递减)的顺序。新元素入栈前,弹出所有破坏单调性的栈顶元素。弹出时意味着找到了这些元素的"下一个更大/更小值"。

单调递减栈示意(栈底 → 栈顶 递减):

  元素依次入栈: 5, 3, 1, 4

  push 5 → [5]
  push 3 → [5, 3]           3 < 5 ✓ 单调
  push 1 → [5, 3, 1]        1 < 3 ✓ 单调
  push 4 → 4 > 1 → pop 1    1 的下一个更大元素 = 4
           4 > 3 → pop 3    3 的下一个更大元素 = 4
           4 < 5 ✓
           [5, 4]

3.4.1 每日温度(LeetCode 739)

题目描述:给定一个温度数组 temperatures,对于每一天,求至少要等多少天才能遇到更高的温度。如果之后不会更高则为 0。

思路分析:维护一个单调递减栈(存索引)。遍历到新温度时,若比栈顶温度高,则弹出栈顶并计算天数差。

temperatures = [73, 74, 75, 71, 69, 72, 76, 73]

i=0: 73  栈=[]         push 0         栈=[0]
i=1: 74>73 pop 0       ans[0]=1-0=1   栈=[]
     push 1                           栈=[1]
i=2: 75>74 pop 1       ans[1]=2-1=1   栈=[]
     push 2                           栈=[2]
i=3: 71<75 push 3                     栈=[2,3]
i=4: 69<71 push 4                     栈=[2,3,4]
i=5: 72>69 pop 4       ans[4]=5-4=1
     72>71 pop 3       ans[3]=5-3=2
     72<75 push 5                     栈=[2,5]
i=6: 76>72 pop 5       ans[5]=6-5=1
     76>75 pop 2       ans[2]=6-2=4
     push 6                           栈=[6]
i=7: 73<76 push 7                     栈=[6,7]

结果: [1, 1, 4, 2, 1, 1, 0, 0]
typescript
function dailyTemperatures(temperatures: number[]): number[] {
  const n = temperatures.length
  const answer = new Array(n).fill(0)
  const stack: number[] = []

  for (let i = 0; i < n; i++) {
    while (
      stack.length > 0 &&
      temperatures[i] > temperatures[stack[stack.length - 1]]
    ) {
      const j = stack.pop()!
      answer[j] = i - j
    }
    stack.push(i)
  }
  return answer
}

复杂度:时间 O(n)(每个元素最多入栈出栈各一次),空间 O(n)

3.4.2 下一个更大元素 I(LeetCode 496)

题目描述:给定两个没有重复元素的数组 nums1nums2,其中 nums1nums2 的子集。对于 nums1 中的每个元素,找到它在 nums2 中的下一个更大元素。如果不存在则返回 -1。

思路分析:先用单调栈对 nums2 求出每个元素的下一个更大元素存入 Map,再对 nums1 查表。

nums1 = [4,1,2], nums2 = [1,3,4,2]

对 nums2 建立单调递减栈:
i=0: 1   栈=[]       push 1        栈=[1]
i=1: 3>1 pop 1       map[1]=3      栈=[]
     push 3                        栈=[3]
i=2: 4>3 pop 3       map[3]=4      栈=[]
     push 4                        栈=[4]
i=3: 2<4 push 2                    栈=[4,2]

栈中剩余 4,2 → map[4]=-1, map[2]=-1

查询 nums1: [map[4], map[1], map[2]] = [-1, 3, -1]
typescript
function nextGreaterElement(nums1: number[], nums2: number[]): number[] {
  const map = new Map<number, number>()
  const stack: number[] = []

  for (const num of nums2) {
    while (stack.length > 0 && num > stack[stack.length - 1]) {
      map.set(stack.pop()!, num)
    }
    stack.push(num)
  }
  while (stack.length > 0) {
    map.set(stack.pop()!, -1)
  }

  return nums1.map(n => map.get(n)!)
}

复杂度:时间 O(n + m),空间 O(n)


四、队列(Queue)

4.1 用数组实现队列

队列操作示意 (FIFO — First In, First Out):

  enqueue(1)  enqueue(2)  enqueue(3)  dequeue()
  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
  │ 1       │ │ 1  2    │ │ 1  2  3 │ │ 2  3    │
  └─────────┘ └─────────┘ └─────────┘ └─────────┘
  front=1      front=1     front=1      front=2
               rear=2      rear=3       rear=3
typescript
class Queue<T> {
  private items: T[] = []

  enqueue(val: T): void {
    this.items.push(val)
  }

  dequeue(): T | undefined {
    return this.items.shift()
  }

  front(): T | undefined {
    return this.items[0]
  }

  isEmpty(): boolean {
    return this.items.length === 0
  }

  size(): number {
    return this.items.length
  }
}

注意shift() 是 O(n) 操作。生产环境如需高性能队列,应使用链表实现或环形缓冲区。

4.2 用链表实现队列(O(1) 出队)

typescript
class LinkedListQueue<T> {
  private head: { val: T; next: any } | null = null
  private tail: { val: T; next: any } | null = null
  private _size = 0

  enqueue(val: T): void {
    const node = { val, next: null }
    if (this.tail) {
      this.tail.next = node
    } else {
      this.head = node
    }
    this.tail = node
    this._size++
  }

  dequeue(): T | undefined {
    if (!this.head) return undefined
    const val = this.head.val
    this.head = this.head.next
    if (!this.head) this.tail = null
    this._size--
    return val
  }

  front(): T | undefined {
    return this.head?.val
  }

  isEmpty(): boolean {
    return this._size === 0
  }

  size(): number {
    return this._size
  }
}

4.3 双端队列(Deque)

双端队列两端都可以进行插入和删除操作:

         ┌──── addFront

  ┌──┬──┬──┬──┬──┐
  │  │  │  │  │  │
  └──┴──┴──┴──┴──┘
  ▲                 ▲
  │                 │
  removeFront       addRear / removeRear
typescript
class Deque<T> {
  private items: T[] = []

  addFront(val: T): void {
    this.items.unshift(val)
  }

  addRear(val: T): void {
    this.items.push(val)
  }

  removeFront(): T | undefined {
    return this.items.shift()
  }

  removeRear(): T | undefined {
    return this.items.pop()
  }

  peekFront(): T | undefined {
    return this.items[0]
  }

  peekRear(): T | undefined {
    return this.items[this.items.length - 1]
  }

  isEmpty(): boolean {
    return this.items.length === 0
  }

  size(): number {
    return this.items.length
  }
}

4.4 滑动窗口最大值(LeetCode 239)

题目描述:给定一个整数数组 nums 和滑动窗口大小 k,窗口从最左端滑动到最右端,每次滑动一位。返回每个窗口位置的最大值。

思路分析:使用单调递减双端队列(存索引)。队头始终是当前窗口的最大值索引。每次滑动时:

  1. 移除队头已滑出窗口的索引
  2. 从队尾移除所有小于当前元素的索引(它们不可能成为最大值)
  3. 当前索引入队
  4. 窗口形成后(i >= k - 1),队头即为当前窗口最大值
nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3

i=0: num=1  deque=[]
     push 0                    deque=[0]
i=1: num=3  3>1 → pop 0
     push 1                    deque=[1]
i=2: num=-1 -1<3
     push 2                    deque=[1,2]
     i>=2 → ans.push(nums[1])  ans=[3]

i=3: num=-3 -3<-1
     push 3                    deque=[1,2,3]
     i>=2 → ans.push(nums[1])  ans=[3,3]

i=4: num=5  检查队头: 1 >= 4-3+1=2? → 否(1<2) → pop front
     deque=[2,3]
     5>-3 → pop 3
     5>-1 → pop 2
     push 4                    deque=[4]
     i>=2 → ans.push(nums[4])  ans=[3,3,5]

i=5: num=3  3<5
     push 5                    deque=[4,5]
     i>=2 → ans.push(nums[4])  ans=[3,3,5,5]

i=6: num=6  6>3 → pop 5
     6>5 → pop 4
     push 6                    deque=[6]
     i>=2 → ans.push(nums[6])  ans=[3,3,5,5,6]

i=7: num=7  7>6 → pop 6
     push 7                    deque=[7]
     i>=2 → ans.push(nums[7])  ans=[3,3,5,5,6,7]

结果: [3, 3, 5, 5, 5, 6, 7]
typescript
function maxSlidingWindow(nums: number[], k: number): number[] {
  const deque: number[] = []
  const result: number[] = []

  for (let i = 0; i < nums.length; i++) {
    while (deque.length > 0 && deque[0] < i - k + 1) {
      deque.shift()
    }

    while (deque.length > 0 && nums[deque[deque.length - 1]] <= nums[i]) {
      deque.pop()
    }

    deque.push(i)

    if (i >= k - 1) {
      result.push(nums[deque[0]])
    }
  }
  return result
}

复杂度:时间 O(n)(每个元素最多入队出队各一次),空间 O(k)


五、面试题精选(5 道,带详细解答)

面试题 1:用两个栈实现队列(LeetCode 232)

题目描述:请仅使用两个栈实现先入先出(FIFO)队列。

思路分析:使用 inStack 负责入队,outStack 负责出队。当 outStack 为空需要出队时,将 inStack 的所有元素逐个弹出压入 outStack,顺序自动翻转。

push(1), push(2), push(3):

  inStack     outStack
  ┌───┐       ┌───┐
  │ 3 │       │   │
  │ 2 │       │   │
  │ 1 │       │   │
  └───┘       └───┘

pop() → outStack 为空,倒入:

  inStack     outStack
  ┌───┐       ┌───┐
  │   │       │ 1 │ ← pop → 返回 1
  │   │       │ 2 │
  │   │       │ 3 │
  └───┘       └───┘

push(4), pop():

  inStack     outStack
  ┌───┐       ┌───┐
  │ 4 │       │ 2 │ ← pop → 返回 2
  │   │       │ 3 │
  └───┘       └───┘
typescript
class MyQueue {
  private inStack: number[] = []
  private outStack: number[] = []

  push(x: number): void {
    this.inStack.push(x)
  }

  pop(): number {
    this.move()
    return this.outStack.pop()!
  }

  peek(): number {
    this.move()
    return this.outStack[this.outStack.length - 1]
  }

  empty(): boolean {
    return this.inStack.length === 0 && this.outStack.length === 0
  }

  private move(): void {
    if (this.outStack.length === 0) {
      while (this.inStack.length > 0) {
        this.outStack.push(this.inStack.pop()!)
      }
    }
  }
}

复杂度:push O(1),pop/peek 均摊 O(1),空间 O(n)


面试题 2:用栈实现括号匹配的最大嵌套深度(LeetCode 1614)

题目描述:给定一个合法的括号字符串 s(可能包含数字和运算符),返回括号的最大嵌套深度。

思路分析:不需要真正用栈存储,只需维护一个计数器 depth。遇到 (depth++,遇到 )depth--,过程中记录最大值。

s = "(1+(2*3)+((8)/4))+1"

字符:  (  1  +  (  2  *  3  )  +  (  (  8  )  /  4  )  )  +  1
depth: 1           2           1     2  3     2     1  0

最大深度 = 3
typescript
function maxDepth(s: string): number {
  let depth = 0
  let max = 0

  for (const ch of s) {
    if (ch === '(') {
      depth++
      max = Math.max(max, depth)
    } else if (ch === ')') {
      depth--
    }
  }
  return max
}

复杂度:时间 O(n),空间 O(1)


面试题 3:旋转数组(LeetCode 189)

题目描述:给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置。

思路分析:三次翻转法。先整体翻转,再翻转前 k 个,最后翻转后 n-k 个。

nums = [1, 2, 3, 4, 5, 6, 7], k = 3

Step 1: 整体翻转       [7, 6, 5, 4, 3, 2, 1]
Step 2: 翻转前 k=3 个  [5, 6, 7, 4, 3, 2, 1]
Step 3: 翻转后 n-k 个  [5, 6, 7, 1, 2, 3, 4]
typescript
function rotate(nums: number[], k: number): void {
  k = k % nums.length

  const reverse = (arr: number[], start: number, end: number) => {
    while (start < end) {
      ;[arr[start], arr[end]] = [arr[end], arr[start]]
      start++
      end--
    }
  }

  reverse(nums, 0, nums.length - 1)
  reverse(nums, 0, k - 1)
  reverse(nums, k, nums.length - 1)
}

复杂度:时间 O(n),空间 O(1)


面试题 4:LRU 缓存(LeetCode 146)

题目描述:设计一个 LRU(最近最少使用)缓存机制,支持 getput 操作,均为 O(1) 时间复杂度。

思路分析:使用哈希表 + 双向链表。哈希表用于 O(1) 查找,双向链表用于 O(1) 插入和删除以维护访问顺序。在 JS 中可以直接利用 Map(保留插入顺序)来实现。

容量 capacity = 3

put(1,A)  →  [1:A]
put(2,B)  →  [1:A, 2:B]
put(3,C)  →  [1:A, 2:B, 3:C]
get(1)    →  [2:B, 3:C, 1:A]       // 1 被访问,移到末尾
put(4,D)  →  [3:C, 1:A, 4:D]       // 容量满,淘汰最久未用的 2
get(2)    →  -1                      // 2 已被淘汰

双向链表示意:
  head ↔ [2:B] ↔ [3:C] ↔ [1:A] ↔ tail
  最久未用                    最近使用
typescript
class LRUCache {
  private capacity: number
  private cache: Map<number, number>

  constructor(capacity: number) {
    this.capacity = capacity
    this.cache = new Map()
  }

  get(key: number): number {
    if (!this.cache.has(key)) return -1
    const val = this.cache.get(key)!
    this.cache.delete(key)
    this.cache.set(key, val)
    return val
  }

  put(key: number, value: number): void {
    if (this.cache.has(key)) {
      this.cache.delete(key)
    } else if (this.cache.size >= this.capacity) {
      const oldestKey = this.cache.keys().next().value!
      this.cache.delete(oldestKey)
    }
    this.cache.set(key, value)
  }
}

复杂度:get/put 均 O(1),空间 O(capacity)


面试题 5:扁平化嵌套列表迭代器(LeetCode 341)

题目描述:给定一个嵌套的整数列表 nestedList,每个元素要么是整数,要么是一个列表(元素同样是整数或列表)。实现一个迭代器来展开它。

思路分析:利用栈进行 DFS。初始化时将列表逆序压入栈中(保证正序输出)。调用 hasNext 时不断检查栈顶:若栈顶是列表,则展开(逆序压栈);若是整数则直接可用。

输入: [[1,1], 2, [1,1]]

初始化(逆序压栈):
  栈 = [[1,1], 2, [1,1]]    (bottom → top)
  即 top = [1,1]

hasNext():
  top = [1,1] → 是列表 → 展开逆序压栈
  栈 = [[1,1], 2, 1, 1]
  top = 1 → 是整数 ✓

next() → 1
next() → 1

hasNext():
  top = 2 → 是整数 ✓
next() → 2

hasNext():
  top = [1,1] → 展开
  栈 = [1, 1]
next() → 1
next() → 1

输出: [1, 1, 2, 1, 1]
typescript
interface NestedInteger {
  isInteger(): boolean
  getInteger(): number | null
  getList(): NestedInteger[]
}

class NestedIterator {
  private stack: NestedInteger[]

  constructor(nestedList: NestedInteger[]) {
    this.stack = [...nestedList].reverse()
  }

  hasNext(): boolean {
    while (this.stack.length > 0) {
      const top = this.stack[this.stack.length - 1]
      if (top.isInteger()) return true
      this.stack.pop()
      const list = top.getList()
      for (let i = list.length - 1; i >= 0; i--) {
        this.stack.push(list[i])
      }
    }
    return false
  }

  next(): number {
    return this.stack.pop()!.getInteger()!
  }
}

复杂度next()hasNext() 均摊 O(1),空间 O(n)(n 为所有整数个数)


六、追问思考(5 道)

追问思考

  1. 为什么 JavaScript 的 Array.prototype.sort() 在 V8 中从快排改为了 TimSort? TimSort 在什么场景下优于快排?对部分有序数据的时间复杂度是多少?

  2. 链表题中的"哨兵节点(dummy node)"本质上解决了什么问题? 除了简化头节点操作外,在哪些场景下你会主动引入哨兵节点?试分析不使用哨兵节点时反转链表的边界 case 有几种。

  3. 单调栈只能解决"下一个更大元素"类问题吗? 如果要求"上一个更小元素"该如何调整?尝试用单调栈解决 LeetCode 84(柱状图中最大的矩形)并分析为什么需要在数组两端各添加一个高度为 0 的哨兵。

  4. LRU 缓存的 Map 实现依赖了 ES6 Map 的插入有序特性。 如果面试官要求你不使用 Map,而是用普通对象 + 手写双向链表实现,链表节点应包含哪些字段?如何做到 get/put 都严格 O(1)?

  5. 滑动窗口最大值(LeetCode 239)中的单调队列操作 deque.shift() 在数组实现下是 O(n) 的。 如何将其优化为严格 O(1)?比较使用链表双端队列 vs 环形缓冲区 vs 维护 head 指针偏移量这三种方案的优劣。


数据结构对比总结

数据结构随机访问头部插入/删除尾部插入/删除查找适用场景
数组O(1)O(n)O(1) 均摊O(n)随机访问频繁、已知大小
链表O(n)O(1)O(1)*O(n)频繁插入删除、大小不确定
O(1)括号匹配、DFS、撤销操作
队列O(1)**O(1)BFS、任务调度、缓冲

* 需维护尾指针    ** 链表实现下

用心学习,用代码说话 💻