数组、链表、栈、队列:最基础也最容易被问倒
很多人在面试里栽跟头,不是栽在什么高深的算法上,而是栽在最基础的线性结构上。
比如被问一句”数组和链表的区别是什么”,能说出”数组连续、链表不连续”的人很多,但能讲清楚”为什么数组按下标访问是 O(1)、链表在中间插入是 O(1)”的人,就少多了。
这篇文章把这四种最基础的线性结构,从底层特性到复杂度一次讲透。
数组:连续内存,随机访问
数组的本质是一块连续的内存。
正因为连续,所以当你知道首地址和元素下标时,可以直接算出目标元素的地址:
1 | 目标地址 = 首地址 + 下标 × 元素大小 |
这个计算是常量时间的,所以数组按下标访问是 O(1),这就是”随机访问”。
但也正因为连续,数组有两个代价:
- 插入和删除要搬移数据:在中间插入一个元素,后面的元素都要往后挪一位,平均 O(n)
- 扩容成本高:数组创建时要指定大小,满了就得申请更大的新数组、整体拷贝
数组的适用场景:读多写少、大小相对固定。
链表:离散内存,靠指针连接
链表的本质是每个节点存数据和下一个节点的指针,节点之间靠指针串联,不需要连续内存。
- 优点:插入删除快(只需改指针,O(1)),天然支持动态扩容
- 缺点:不能随机访问,要找第 n 个元素,必须从头一个个遍历,O(n)
链表的适用场景:频繁增删、不依赖随机访问。
一个容易忽略的点:链表虽然”插入是 O(1)”,但这个 O(1) 有个前提——你已经找到了插入位置。如果找位置本身要遍历(O(n)),那整体还是 O(n)。所以不要笼统说”链表插入快”,要分情况。
数组 vs 链表:一张表对比
| 操作 | 数组 | 链表 |
|---|---|---|
| 按下标访问 | O(1) | O(n) |
| 头部插入 | O(n) | O(1) |
| 尾部插入 | O(1) | O(1) |
| 中间插入/删除 | O(n) | O(1)(已知位置) |
| 内存 | 连续,可能浪费 | 离散,有指针开销 |
| CPU 缓存友好度 | 高 | 低 |
面试技巧:讲到链表插入快,一定要补充”已知位置”这个前提,这会让你看起来真的懂,而不是背结论。
栈:后进先出(LIFO)
栈就像一个只能从顶部操作的盘子堆,后放进去的,先拿出来。
push:入栈pop:出栈peek:查看栈顶
栈的应用场景非常多:
- 函数调用栈:递归的本质就是系统在帮你维护一个栈
- 括号匹配:左括号入栈,右括号出栈配对
- 浏览器的前进后退:两个栈实现
- 表达式求值:中缀转后缀
栈的意义在于:它能天然地”记住”之前的状态,并且按照”最近优先”的顺序回溯。
队列:先进先出(FIFO)
队列就像排队,先来的先处理。
enqueue:入队dequeue:出队
队列的应用场景:
- 消息队列:生产者消费者模型
- BFS 广度优先遍历:就是靠队列实现的
- 线程池的任务队列:任务按提交顺序执行
队列的意义在于:它保证处理顺序,天然适合”缓冲”和”公平调度”。
栈和队列的关系
栈和队列其实是一对”镜面”:
- 栈是”后进先出”,队列是”先进先出”
- 它们都可以用数组或链表来实现
- 用两个栈可以实现一个队列,用两个队列也可以实现一个栈
理解了”线性结构 + 出入规则”,就理解了这四种结构的本质:它们都是”存储元素的容器”,区别只在于”怎么取出来”。
写在最后
这四种结构看起来简单,但它们是所有复杂数据结构的基石:
- 哈希表是”数组 + 链表”的组合
- 图是”顶点 + 边”,边可以用邻接表(链表)或邻接矩阵(二维数组)表示
- 堆是”完全二叉树 + 数组存储”
把地基打牢,后面盖楼才稳。下一篇讲哈希表——它就是从”数组 + 链表”长出来的。
