前端面试专栏-算法篇:21. 链表、栈、队列的实现与应用
🔥 欢迎来到前端面试通关指南专栏!从js精讲到框架到实战,渐进系统化学习,坚持解锁新技能,祝你轻松拿下心仪offer。
前端面试通关指南专栏主页
前端面试专栏规划详情
链表、栈、队列的实现与应用
在计算机科学中,数据结构是组织和存储数据的方式,直接影响算法的效率和程序的性能。链表、栈和队列作为三种基础且重要的数据结构,广泛应用于各种软件系统中。本文将深入探讨它们的实现原理、操作方法及典型应用场景,帮助读者建立清晰的概念体系和实践能力。
一、链表(Linked List)
1.1 基本概念
链表是一种线性数据结构,由一系列节点(Node)组成。与数组不同,链表在内存中的存储是非连续的。每个节点包含两部分:
- 数据域(Data):存储实际的数据元素
- 指针域(Next):存储指向下一个节点的地址
链表的主要特点是:
- 动态存储:无需预先分配固定空间,可以实时申请内存
- 非连续内存:节点可以分散存储在内存各处
- 高效插入/删除:时间复杂度为O(1)(已知前驱节点时)
链表类型详解:
-
单链表(Singly Linked List)
- 结构:每个节点包含数据和指向下一个节点的指针
- 示例:
节点A(data=5, next)→节点B(data=10, next)→节点C(data=15, next=null)
- 应用:实现栈、队列等基础数据结构
-
双向链表(Doubly Linked List)
- 结构:每个节点包含两个指针,分别指向直接前驱和直接后继
- 示例:
null←节点A(data=5)→←节点B(data=10)→←节点C(data=15)→null
- 优势:可以双向遍历,删除操作更高效
- 应用:浏览器历史记录、撤销操作功能
-
循环链表(Circular Linked List)
- 单循环链表:尾节点指向头节点
- 双循环链表:头节点的prev指向尾节点,尾节点的next指向头节点
- 示例:
A→B→C→A
(循环) - 应用:轮播图、循环队列实现
复杂度对比:
操作 | 单链表 | 双向链表 |
---|---|---|
访问 | O(n) | O(n) |
插入(头部) | O(1) | O(1) |
删除(尾部) | O(n) | O(1) |
内存占用 | 较小 | 较大 |
典型应用场景:
- 操作系统中的进程调度(使用双向链表)
- LRU缓存淘汰算法(双向链表+哈希表)
- 多项式运算(使用链表存储系数和指数)
1.2 单链表的实现
以下是单链表的Python实现,包含节点类和链表类:
class Node:def __init__(self, data=None):self.data = dataself.next = Noneclass LinkedList:def __init__(self):self.head = None # 头指针,指向第一个节点def is_empty(self):"""判断链表是否为空"""return self.head is Nonedef append(self, data):"""在链表尾部添加新节点"""new_node = Node(data)if self.is_empty():self.head = new_nodereturncurrent = self.headwhile current.next:current = current.nextcurrent.next = new_nodedef prepend(self, data):"""在链表头部插入新节点"""new_node = Node(data)new_node.next = self.headself.head = new_nodedef delete(self, key):"""删除第一个值为key的节点"""current = self.headprev = Nonewhile current and current.data != key:prev = currentcurrent = current.nextif current is None: # 未找到keyreturnif prev is None: # 删除的是头节点self.head = current.nextelse:prev.next = current.nextdef search(self, key):"""查找值为key的节点是否存在"""current = self.headwhile current and current.data != key:current = current.nextreturn current is not None # 找到返回True,否则Falsedef display(self):"""显示链表中的所有元素"""elements = []current = self.headwhile current:elements.append(str(current.data))current = current.nextprint(" -> ".join(elements))# 使用示例
my_list = LinkedList()
my_list.append(1)
my_list.append(2)
my_list.append(3)
my_list.prepend(0)
my_list.display() # 输出: 0 -> 1 -> 2 -> 3
my_list.delete(2)
my_list.display() # 输出: 0 -> 1 -> 3
print(my_list.search(1)) # 输出: True
1.3 链表的应用场景
-
动态内存分配
- 在操作系统的内存管理中,空闲内存块通常采用链表结构进行维护
- 示例:Linux内核使用buddy allocator算法,将空闲内存块组织成多个不同大小的链表
- 优势:可以灵活地分配和回收不同大小的内存块,减少内存碎片
-
实现其他数据结构
- 链表可以作为基础结构实现多种高级数据结构:
- 栈:通过单链表实现,只需维护头指针
- 队列:通过双向链表实现,维护头尾指针
- 哈希表:解决哈希冲突时常用链表法
- 典型应用:Java的LinkedList类同时实现了List和Deque接口
- 链表可以作为基础结构实现多种高级数据结构:
-
文件系统
- 文件目录结构常采用链表形式组织:
- Unix文件系统中,目录项通过链表连接
- FAT文件系统使用链表追踪文件簇
- 优势:可以方便地进行文件的创建、删除和移动操作
- 文件目录结构常采用链表形式组织:
-
浏览器历史记录
- 浏览器历史记录通常采用双向链表实现:
- 每个节点存储访问的URL和时间戳
- 通过前驱和后继指针实现前进/后退功能
- 实现细节:Chromium浏览器使用HistoryEntry链表管理导航记录
- 浏览器历史记录通常采用双向链表实现:
-
LRU缓存淘汰算法
- LRU(Least Recently Used)算法典型实现:
- 双向链表维护访问顺序
- 哈希表提供O(1)时间访问
- 工作流程:
- 访问数据时移动到链表头部
- 缓存满时淘汰链表尾部数据
- 应用实例:Redis、Memcached等缓存系统
- LRU(Least Recently Used)算法典型实现:
二、栈(Stack)
2.1 基本概念
栈是一种后进先出(Last In First Out, LIFO)的线性数据结构,支持两种基本操作:
- 入栈(Push):将元素添加到栈顶
- 出栈(Pop):移除并返回栈顶元素
栈的特点是只能在一端进行操作,这一端称为栈顶,另一端称为栈底。栈的行为类似于现实生活中的一叠盘子,只能从顶部添加或移除盘子。
主要特性
- 后进先出原则:最后入栈的元素最先被移除
- 单端操作限制:所有操作都只能在栈顶进行
- 动态大小:栈的大小会随着元素的入栈和出栈而改变
常见操作
除了基本的push和pop操作外,栈通常还支持:
- peek/top:查看栈顶元素但不移除
- isEmpty:检查栈是否为空
- size:获取栈中元素数量
实现方式
栈可以通过多种方式实现:
- 数组实现:使用固定大小的数组
- 动态数组实现:可以自动扩容的数组
- 链表实现:使用单向链表,将头结点作为栈顶
应用场景
- 函数调用栈:程序执行时保存函数调用信息
- 括号匹配:检查表达式中的括号是否匹配
- 撤销操作:软件中的撤销功能通常使用栈实现
- 表达式求值:中缀表达式转后缀表达式
- 浏览器历史记录:前进后退功能使用两个栈实现
示例代码(伪代码)
stack = []
stack.push(1) # 栈:[1]
stack.push(2) # 栈:[1, 2]
top = stack.pop() # 返回2,栈:[1]
2.2 栈的实现
栈可以用数组或链表实现,以下是基于Python列表的实现:
class Stack:def __init__(self):self.items = [] # 使用列表存储栈元素def is_empty(self):"""判断栈是否为空"""return len(self.items) == 0def push(self, item):"""入栈操作"""self.items.append(item)def pop(self):"""出栈操作,返回栈顶元素"""if self.is_empty():raise Exception("栈为空,无法执行出栈操作")return self.items.pop()def peek(self):"""查看栈顶元素,但不移除"""if self.is_empty():raise Exception("栈为空")return self.items[-1]def size(self):"""返回栈的大小"""return len(self.items)# 使用示例
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.pop()) # 输出: 3
print(stack.peek()) # 输出: 2
print(stack.size()) # 输出: 2
2.3 栈的应用场景
-
函数调用栈:
- 程序运行时,每次函数调用都会在调用栈中创建一个栈帧(stack frame),包含函数参数、局部变量和返回地址
- 当函数返回时,对应的栈帧会被弹出
- 示例:main()调用A(),A()调用B()时,调用顺序为main→A→B,返回顺序为B→A→main
-
表达式求值:
- 中缀表达式转后缀表达式(逆波兰表达式)时使用运算符栈处理优先级
- 后缀表达式求值时使用操作数栈存储中间结果
- 示例:中缀表达式"3+45"转为后缀表达式"345+",求值过程为:压入3→压入4→压入5→弹出4和5计算4*5=20→弹出3和20计算3+20=23
-
括号匹配检查:
- 遍历代码时遇到开括号就入栈
- 遇到闭括号就出栈并检查是否匹配
- 特别适用于编译器/解释器的语法检查
- 可扩展检查多种括号类型:圆括号()、方括号[]、花括号{}
-
浏览器后退功能:
- 每访问新页面就将URL压入历史栈
- 点击后退按钮时弹出栈顶URL
- 前进功能通常需要辅助栈实现
- 实际应用中可能采用更复杂的数据结构优化性能
-
递归算法实现:
- 每次递归调用相当于将当前状态压栈
- 返回时从栈中恢复状态
- 系统自动维护的调用栈可能限制递归深度
- 深度过大时可能需人工用显式栈替代递归
示例:括号匹配检查
def is_matching(open_char, close_char):"""检查括号是否匹配的辅助函数"""pairs = { '(': ')', '[': ']', '{': '}' } # 定义匹配规则return pairs.get(open_char) == close_char # 查找对应闭括号def check_brackets(expression):"""检查表达式中的括号是否匹配参数:expression: 待检查的字符串表达式返回:bool: 所有括号是否匹配"""stack = Stack() # 初始化空栈for char in expression: # 遍历每个字符if char in '([{': # 遇到开括号stack.push(char) # 压栈elif char in ')]}': # 遇到闭括号if stack.is_empty(): # 栈为空说明闭括号无匹配return Falsetop = stack.pop() # 弹出栈顶开括号if not is_matching(top, char): # 检查括号类型return Falsereturn stack.is_empty() # 最终栈为空才表示完全匹配# 测试用例
test_cases = [("(a + [b * c])", True), # 正确嵌套("(a + [b * c)", False), # 缺少闭括号("a + b)", False), # 多余闭括号("{[()]}", True), # 多层嵌套("{[(])}", False), # 交叉嵌套("", True) # 空字符串
]for expr, expected in test_cases:assert check_brackets(expr) == expected, f"测试失败: {expr}"
print("所有测试通过!")
扩展说明:
- 该算法时间复杂度为O(n),只需遍历字符串一次
- 可以扩展支持更多符号对,如HTML标签匹配
- 实际编译器使用时会结合其他语法分析技术
- 错误处理可以改进为返回具体错误位置
三、队列(Queue)
3.1 基本概念
队列是一种先进先出(First In First Out, FIFO)的线性数据结构,支持两种基本操作:
- 入队(Enqueue):将元素添加到队列尾部
- 示例:在银行排队时,新来的客户会排到队伍末尾
- 实现方式:通常通过维护一个尾指针(rear)来记录最后一个元素的位置
- 出队(Dequeue):移除并返回队列头部元素
- 示例:银行柜台叫号时,总是服务排在队伍最前面的客户
- 实现方式:通常通过维护一个头指针(front)来记录第一个元素的位置
队列的特点是元素从一端进入(称为rear/rear end),从另一端离开(称为front/front end),类似现实生活中的排队。这种特性使得队列特别适合需要按顺序处理的场景。
典型应用场景:
- 操作系统进程调度:CPU按进程到达顺序分配资源
- 打印机任务队列:打印任务按提交顺序依次执行
- 消息队列系统:如RabbitMQ等消息中间件
- 广度优先搜索:算法中需要按层次遍历节点
常见实现方式:
- 数组实现(循环队列)
- 链表实现
- 标准库实现(如Java的Queue接口,C++的queue容器)
3.2 队列的实现
队列可以用数组或链表实现,以下是基于Python列表的实现:
class Queue:def __init__(self):self.items = [] # 使用列表存储队列元素def is_empty(self):"""判断队列是否为空"""return len(self.items) == 0def enqueue(self, item):"""入队操作,将元素添加到队列尾部"""self.items.append(item)def dequeue(self):"""出队操作,移除并返回队列头部元素"""if self.is_empty():raise Exception("队列为空,无法执行出队操作")return self.items.pop(0)def peek(self):"""查看队列头部元素,但不移除"""if self.is_empty():raise Exception("队列为空")return self.items[0]def size(self):"""返回队列的大小"""return len(self.items)# 使用示例
queue = Queue()
queue.enqueue(1)
queue.enqueue(2)
queue.enqueue(3)
print(queue.dequeue()) # 输出: 1
print(queue.peek()) # 输出: 2
print(queue.size()) # 输出: 2
3.3 队列的应用场景
- 任务调度:操作系统中的任务调度,按队列顺序执行任务
- 消息队列:分布式系统中,消息的生产和消费通常使用队列
- 广度优先搜索(BFS):图算法中,BFS使用队列来遍历节点
- 网络缓冲区:网络数据包的接收和处理按队列顺序进行
- 打印任务管理:打印机的任务队列管理多个打印请求
示例:广度优先搜索(BFS)
from collections import deque # 使用Python内置的双端队列def bfs(graph, start):"""广度优先搜索算法"""visited = set() # 记录已访问的节点queue = deque([start]) # 初始化队列visited.add(start)while queue:vertex = queue.popleft() # 出队print(vertex, end=' ') # 处理当前节点# 将所有未访问的邻居节点入队for neighbor in graph[vertex]:if neighbor not in visited:visited.add(neighbor)queue.append(neighbor)# 测试
graph = {'A': ['B', 'C'],'B': ['A', 'D', 'E'],'C': ['A', 'F'],'D': ['B'],'E': ['B', 'F'],'F': ['C', 'E']
}print("BFS traversal starting from 'A':")
bfs(graph, 'A') # 输出: A B C D E F
四、链表、栈、队列的对比
数据结构 | 访问方式 | 插入/删除效率 | 典型应用场景 |
---|---|---|---|
链表 | 顺序访问(需从头遍历) | O(1)(指定位置,如双向链表首尾) | 1. 动态内存管理(操作系统内存分配) 2. LRU缓存淘汰算法 3. 文件系统目录结构 |
栈 | LIFO(后进先出) | O(1)(仅限栈顶操作) | 1. 函数调用栈(保存返回地址和局部变量) 2. 括号匹配检测 3. 逆波兰表达式求值 |
队列 | FIFO(先进先出) | O(1)(队尾插入,队首删除) | 1. 操作系统任务调度(打印队列) 2. 广度优先搜索(BFS) 3. 消息队列系统 |
补充说明:
- 链表插入效率示例:双向链表在已知位置插入节点只需修改相邻节点的4个指针
- 栈操作示例:函数调用时依次压栈(参数→返回地址→局部变量),返回时反向弹出
- 队列应用场景:BFS算法中队列保存待访问节点,保证按层次遍历图的节点
五、总结
链表、栈和队列作为基础数据结构,各自具有独特的特点和适用场景:
-
链表(Linked List)
- 特点:节点通过指针相连,内存不连续
- 优势:动态插入/删除效率高(时间复杂度O(1)),内存利用率好
- 适用场景:音乐播放列表、浏览器历史记录、内存管理等
- 实现方式:单链表、双链表、循环链表等
-
栈(Stack)
- 特点:后进先出(LIFO)的数据结构
- 核心操作:push(入栈)、pop(出栈),时间复杂度均为O(1)
- 常见应用:
- 函数调用栈(递归算法实现)
- 括号匹配检查
- 浏览器后退功能
- 表达式求值(逆波兰表达式)
-
队列(Queue)
- 特点:先进先出(FIFO)的数据结构
- 核心操作:enqueue(入队)、dequeue(出队),时间复杂度均为O(1)
- 常见应用:
- 任务调度(如打印队列)
- 消息队列系统
- 广度优先搜索算法
- 多线程资源共享
- 变种:优先队列、循环队列、双端队列等
在实际开发中,应根据问题的特性选择合适的数据结构:
-
数据结构选择原则
- 考虑数据访问模式(随机访问/顺序访问)
- 评估操作频率(插入/删除/查询哪个更频繁)
- 考虑内存使用效率
- 评估算法复杂度需求
-
典型组合应用
- 栈+队列:实现某些复杂算法(如二叉树的锯齿形遍历)
- 链表+栈:实现撤销/重做功能
- 链表+队列:实现LRU缓存淘汰算法
-
性能优化技巧
- 对于频繁插入删除:优先考虑链表
- 对于需要快速访问最新/最旧元素:考虑栈或队列
- 对于需要随机访问:可能需要结合数组使用
通过合理应用这些数据结构,可以显著提高代码的执行效率(通常能降低1-2个数量级的时间复杂度)和可维护性(更清晰的逻辑表达)。建议在实际使用时,结合具体语言的标准库实现(如Java的LinkedList、Python的deque等)来获得最佳性能。
本文详细介绍了链表、栈和队列的实现方法及典型应用,通过代码示例和应用场景分析,帮助读者建立对这三种数据结构的全面理解。
📌 下期预告:树结构(二叉树、B树、红黑树)
❤️❤️❤️:如果你觉得这篇文章对你有帮助,欢迎点赞、关注本专栏!后续解锁更多功能,敬请期待!👍🏻 👍🏻 👍🏻
更多专栏汇总:
前端面试专栏
Node.js 实训专栏
数码产品严选