搞定面试必问的链表/栈/队列:从原理到LeetCode实战通关指南

搞定面试必问的链表/栈/队列:从原理到LeetCode实战通关指南

【免费下载链接】leetcode-notes 🐳 LeetCode 算法笔记:面试、刷题、学算法。在线阅读地址:https://datawhalechina.github.io/leetcode-notes/ 【免费下载链接】leetcode-notes 项目地址: https://gitcode.com/datawhalechina/leetcode-notes

你是否在面试中遇到过这样的场景:被问到「链表反转」磕磕巴巴,解释「栈的应用」时只能说出括号匹配,面对「队列与BFS」的关联一脸茫然?作为算法面试的三大基础数据结构,链表、栈与队列的掌握程度直接决定了能否通过初轮技术面。本文将用7000字深度解析+20张图解+5道LeetCode真题,帮你从底层原理到实战应用全面突破,文末更有面试高频考点清单,让你一周内从"概念模糊"到"手撕算法"!

读完本文你将获得

  • 链表/栈/队列的内存模型与核心操作(含动图演示)
  • 区分顺序存储vs链式存储的实战决策指南
  • 解决链表环检测/单调栈优化/循环队列设计的通解套路
  • 5道LeetCode高频题的多解法对比时空复杂度分析
  • 应对面试官追问的原理深挖(如:为什么栈适合递归?)

一、链表(Linked List):动态内存的艺术

1.1 数据结构的"变形金刚"

mermaid

链表是由节点(Node) 组成的线性集合,每个节点包含数据域指针域。与数组的连续内存不同,链表节点可以分散存储在内存中,通过指针建立逻辑连接。这种特性使它成为实现动态数据结构的基础组件。

三种常见形态

  • 单链表:每个节点仅包含指向下一节点的指针(next
  • 双向链表:节点同时包含nextprev指针(如Java的LinkedList)
  • 循环链表:尾节点的next指针指向头节点,形成闭合回路(如约瑟夫问题)

1.2 核心操作的实现密码

🔥 面试必写:单链表反转(递归vs迭代)

迭代法(推荐)

def reverseList(head):
    prev, curr = None, head
    while curr:
        next_temp = curr.next  # 保存下一跳
        curr.next = prev       # 反转指针
        prev = curr            # 双指针后移
        curr = next_temp
    return prev

递归法(优雅但需理解调用栈)

def reverseList(head):
    if not head or not head.next:
        return head
    p = reverseList(head.next)  # 递归到尾节点
    head.next.next = head       # 反转当前节点与下一节点
    head.next = None            # 断开原连接
    return p

可视化过程mermaid

⚠️ 避坑指南:链表操作的边界条件
  1. 空链表判断(head is None
  2. 单节点链表处理
  3. 操作尾节点时的指针处理(curr.next is None
  4. 避免断链(操作前务必保存后续节点引用)

1.3 实战痛点:环检测与交点查找

Floyd判圈算法(龟兔赛跑)是检测链表是否有环的最优解,时间复杂度O(n),空间复杂度O(1):

def hasCycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next      # 慢指针走1步
        fast = fast.next.next # 快指针走2步
        if slow == fast:      # 相遇则有环
            return True
    return False

为什么快指针速度是2?
假设环长为L,当慢指针进入环时,快指针已在环内走了k步。两者距离为(L-k),相对速度为1步/次,经过(L-k)次迭代后必定相遇。若速度比不是1:2,可能出现永远追不上的情况(如1:3在某些环长下)。

二、栈(Stack):LIFO的秩序之美

2.1 从弹匣到浏览器后退:栈的应用场景

栈遵循后进先出(LIFO) 原则,就像叠盘子——只能从顶部取放。这种特性使其成为解决嵌套结构回溯问题的理想工具。

mermaid

生活中的栈模型

  • 浏览器的"后退"按钮(访问历史栈)
  • 函数调用栈(递归深度限制)
  • 表达式求值(中缀转后缀)
  • 编辑器的"撤销"操作(操作记录栈)

2.2 两种实现方案的对决

实现方式优点缺点适用场景
数组栈随机访问快,实现简单固定大小,扩容成本高已知数据规模
链式栈动态扩容,无内存浪费指针开销,缓存不友好数据规模动态变化

Python列表实现的栈(注意:列表的pop(0)是O(n)操作,应使用pop()):

class ArrayStack:
    def __init__(self, capacity=10):
        self._data = [None] * capacity
        self._top = -1  # 栈顶指针
        
    def push(self, val):
        if self._top == len(self._data)-1:
            self._resize(2*len(self._data))  # 双倍扩容
        self._top += 1
        self._data[self._top] = val
        
    def pop(self):
        if self.is_empty():
            raise IndexError("pop from empty stack")
        val = self._data[self._top]
        self._top -= 1
        # 缩容(可选,防止内存浪费)
        if self._top < len(self._data)//4 and len(self._data)//2 > 0:
            self._resize(len(self._data)//2)
        return val
        
    def _resize(self, new_cap):
        new_data = [None] * new_cap
        for i in range(self._top+1):
            new_data[i] = self._data[i]
        self._data = new_data

2.3 算法优化神器:单调栈

单调栈是一种特殊的栈,要求入栈元素保持递增递减顺序。它能将Next Greater Element这类问题的时间复杂度从O(n²)降至O(n)。

LeetCode 496. 下一个更大元素 I

def nextGreaterElement(nums1, nums2):
    stack = []
    greater = {}  # 存储每个元素的下一个更大元素
    for num in reversed(nums2):
        # 弹出所有小于当前数的元素
        while stack and stack[-1] <= num:
            stack.pop()
        greater[num] = stack[-1] if stack else -1
        stack.append(num)
    return [greater[num] for num in nums1]

执行过程图解

nums2 = [2,1,2,4,3]
reversed: [3,4,2,1,2]

step1: num=3 → stack=[3] → greater[3]=-1
step2: num=4 → pop 3 → stack=[4] → greater[4]=-1
step3: num=2 → stack[-1]=4>2 → greater[2]=4 → stack=[4,2]
...
最终greater = {2:4, 1:2, 4:-1, 3:4}

三、队列(Queue):FIFO的公平哲学

3.1 从打印机队列到消息中间件

队列遵循先进先出(FIFO) 原则,就像排队买票——先到先服务。这种特性使其成为缓冲调度的核心组件。

计算机中的队列应用

  • 操作系统的进程调度(就绪队列)
  • 网络请求处理(TCP滑动窗口)
  • 消息队列(如RabbitMQ)
  • BFS算法实现(逐层遍历)

3.2 循环队列:解决假溢出的艺术

顺序队列的致命缺陷是假溢出——当rear到达数组末尾时,即使前方有空位也无法入队。循环队列通过模运算将数组变为环形缓冲区,完美解决此问题。

mermaid

循环队列实现要点

  • 空队列条件:front == rear
  • 满队列条件:(rear + 1) % capacity == front(浪费一个空间)
  • 元素个数:(rear - front + capacity) % capacity

3.3 双端队列:Deque的强大威力

双端队列(Double-ended Queue)允许在两端进行插入和删除操作,结合了栈和队列的特性。Python的collections.deque就是高效的双端队列实现。

滑动窗口最大值问题(LeetCode 239):

def maxSlidingWindow(nums, k):
    from collections import deque
    q = deque()  # 存储索引,保持队首为最大值
    res = []
    for i, num in enumerate(nums):
        # 移除窗口外的元素
        while q and q[0] <= i - k:
            q.popleft()
        # 移除所有小于当前数的元素
        while q and nums[q[-1]] <= num:
            q.pop()
        q.append(i)
        # 窗口形成后开始记录结果
        if i >= k - 1:
            res.append(nums[q[0]])
    return res

为什么时间复杂度是O(n)?
每个元素最多入队和出队各一次,总操作次数为2n。

四、实战演练:LeetCode高频题精讲

4.1 链表经典题:反转链表 II(LeetCode 92)

题目:反转从位置m到n的链表(1 ≤ m ≤ n ≤ 链表长度)

解法对比

方法时间复杂度空间复杂度特点
迭代法O(n)O(1)需保存多个指针,易出错但效率高
递归法O(n)O(n)代码简洁,适合理解递归思想

迭代法实现

def reverseBetween(head, m, n):
    dummy = ListNode(0)
    dummy.next = head
    # 找到反转起始点的前一个节点
    prev = dummy
    for _ in range(m-1):
        prev = prev.next
    
    # 开始反转
    curr = prev.next
    for _ in range(n - m):
        next_node = curr.next
        curr.next = next_node.next
        next_node.next = prev.next
        prev.next = next_node
    return dummy.next

关键步骤图解

初始链表:1 -> 2 -> 3 -> 4 -> 5 (m=2,n=4)
prev指向1,curr指向2

第一次迭代(反转3):
next_node = 3 → curr.next = 4 → 3.next = 2 → prev.next =3
结果:1->3->2->4->5

第二次迭代(反转4):
next_node=4 → curr.next=5 →4.next=3 → prev.next=4
结果:1->4->3->2->5

4.2 栈的经典应用:最小栈设计(LeetCode 155)

题目:设计一个支持push、pop、top和getMin的栈,getMin需在O(1)时间内完成。

最佳解法:辅助栈法

class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []  # 存储当前最小值
        
    def push(self, val: int) -> None:
        self.stack.append(val)
        # 只保存小于等于当前最小值的元素
        if not self.min_stack or val <= self.min_stack[-1]:
            self.min_stack.append(val)
            
    def pop(self) -> None:
        val = self.stack.pop()
        if val == self.min_stack[-1]:
            self.min_stack.pop()
            
    def top(self) -> int:
        return self.stack[-1]
        
    def getMin(self) -> int:
        return self.min_stack[-1]

空间优化技巧
不使用额外栈,而是存储"当前值与最小值的差值"。当差值为负时表示出现新的最小值。

4.3 队列+栈:用栈实现队列(LeetCode 232)

题目:使用两个栈实现先入先出队列。

核心思想:用一个栈(in_stack)接收push操作,另一个栈(out_stack)处理pop/peek操作。当out_stack为空时,将in_stack所有元素弹出并压入out_stack。

class MyQueue:
    def __init__(self):
        self.in_stack = []
        self.out_stack = []
        
    def push(self, x: int) -> None:
        self.in_stack.append(x)
        
    def pop(self) -> int:
        self._transfer()
        return self.out_stack.pop()
        
    def peek(self) -> int:
        self._transfer()
        return self.out_stack[-1]
        
    def empty(self) -> bool:
        return not self.in_stack and not self.out_stack
        
    def _transfer(self):
        if not self.out_stack:
            while self.in_stack:
                self.out_stack.append(self.in_stack.pop())

操作复杂度分析

  • 摊还时间复杂度:每个元素最多入栈2次、出栈2次,故每个操作的摊还复杂度为O(1)
  • 空间复杂度:O(n),需要存储所有元素

五、面试高频考点与解题套路

5.1 链表解题套路总结

问题类型核心解法典型题目
反转问题双指针迭代 / 递归206.反转链表 / 92.反转链表II
环检测Floyd算法 / 哈希集141.环形链表 / 142.环形链表II
交点查找双指针法(长度差消除)160.相交链表
删除节点哑节点技巧 / 递归删除203.移除链表元素 / 19.删除链表的倒数第N个节点

5.2 栈与队列的选择策略

优先用栈的场景

  • 需要后入先出处理数据(如括号匹配)
  • 实现深度优先搜索(DFS)
  • 需要回溯的算法(如迷宫问题)
  • 表达式求值与转换

优先用队列的场景

  • 需要先进先出处理数据(如任务调度)
  • 实现广度优先搜索(BFS)
  • 需要缓冲的场景(如限流)
  • 实现生产者-消费者模型

5.3 必须掌握的5个手写算法

  1. LRU缓存(哈希表+双向链表)
  2. 用队列实现栈(双队列法)
  3. 有效的括号(栈+哈希映射)
  4. 滑动窗口最大值(单调队列)
  5. 合并K个有序链表(优先级队列)

六、总结与升华

链表、栈与队列作为最基础的数据结构,是理解更复杂结构(如树、图)的基石。它们的本质是对内存的不同组织方式

  • 链表:动态内存的灵活管理
  • 栈:函数调用的天然模型
  • 队列:并发编程的基础组件

技术面试的黄金法则:能用O(1)空间解决的问题,就不要用O(n);能一遍遍历解决的,就不要用两遍。掌握本文的双指针技巧单调栈/队列优化环检测算法,能让你在80%的基础算法题中脱颖而出。

持续学习路径

  1. 用Python实现各数据结构的基础操作(插入/删除/遍历)
  2. 完成LeetCode上的探索卡片(Linked List/Stack/Queue)
  3. 研究Java集合框架C++ STL的源码实现
  4. 尝试并发环境下的数据结构设计(如ConcurrentLinkedQueue)

最后送大家一句算法学习的至理名言:"不是因为难才不去做,而是因为不去做才难"。收藏本文,从今天开始,每天刷1道题,30天后你会感谢现在的自己!

附录:面试自查清单

链表

  •  能手写单链表的插入、删除操作
  •  理解双向链表相比单链表的优势
  •  掌握判断链表是否有环的两种方法
  •  能解释头节点(dummy node)的作用

  •  能区分顺序栈和链式栈的实现差异
  •  理解单调栈的"单调性"如何维护
  •  能用栈实现表达式求值
  •  知道栈溢出(Stack Overflow)的原因

队列

  •  能设计循环队列并处理边界条件
  •  理解阻塞队列和非阻塞队列的区别
  •  能用队列实现BFS算法
  •  知道优先级队列的底层实现(堆)

【免费下载链接】leetcode-notes 🐳 LeetCode 算法笔记:面试、刷题、学算法。在线阅读地址:https://datawhalechina.github.io/leetcode-notes/ 【免费下载链接】leetcode-notes 项目地址: https://gitcode.com/datawhalechina/leetcode-notes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值