JavaScript栈和队列:从"冰柜里的雪糕"到"排队打饭"
摘要:栈和队列是两种操作受限的线性数据结构。本文从数组出发,用 push/pop 实现 LIFO 的栈,用 push/shift 实现 FIFO 的队列,并对比了链表与数组的增删性能差异。还顺手整理了 JS 数组的 splice、sort 陷阱以及内存模型。
📑 目录
- 线性数据结构:数组、链表、栈、队列
- 栈:后进先出,就像冰柜里的雪糕
- 队列:先进先出,就像排队打饭
- 灵活增删的数组:splice 与 API 副作用
- sort 的坑:默认按字符串排序
- 链表:用对象嵌套模拟节点
- 数组内存真的连续吗?
- 一点总结
- 互动讨论
线性数据结构:数组、链表、栈、队列
数据结构可以分为两类:
- 线性结构:每个元素只有一个前驱和一个后继。包括数组、链表、栈、队列。
- 非线性结构:树、图等。
数组和链表是"底层容器",栈和队列可以看作操作受限的数组 ------它们只允许在特定位置增删元素。由于 JS 数组提供了 push、pop、shift 等 API,用数组实现栈和队列非常方便,开箱即用。
栈:后进先出,就像冰柜里的雪糕
栈(Stack)的特点是 LIFO(Last In First Out)。最后放进去的,最先拿出来。
想象一下冰柜里卖雪糕:老板把新进的雪糕放在最上面,顾客买的时候也从最上面拿。这就是栈。
javascript
arduino
const stack = []; // 空栈
stack.push("东北大板");
stack.push("可爱多");
stack.push("冰工厂");
stack.push("巧乐兹");
// 出栈(后进先出)
while (stack.length) {
const top = stack[stack.length - 1];
console.log("取出的是", top);
stack.pop();
}
console.log(stack); // []
输出顺序:巧乐兹 → 冰工厂 → 可爱多 → 东北大板。
栈的关键操作:
push:入栈(尾部添加)pop:出栈(尾部删除)peek:查看栈顶元素(stack[stack.length-1])
队列:先进先出,就像排队打饭
队列(Queue)的特点是 FIFO(First In First Out)。先来的先服务。
就像食堂排队打饭,先排队的先打到饭。
javascript
arduino
const queue = [];
queue.push('许');
queue.push('叶');
queue.push('戴');
while (queue.length) {
const front = queue[0];
console.log(front);
queue.shift(); // 队头出队
}
console.log(queue); // []
输出顺序:许 → 叶 → 戴。
队列的关键操作:
push:入队(尾部添加)shift:出队(头部删除)
灵活增删的数组:splice 与 API 副作用
数组的增删 API(push、pop、shift、unshift、splice)都会直接修改原数组,不是纯函数。
splice 是一个多功能方法:可以同时完成删除和插入。
javascript
scss
const arr = [1, 2];
arr.splice(1, 0, 3); // 在下标1处,删除0个元素,插入3
console.log(arr); // [1, 3, 2]
console.log(arr.splice(1, 2)); // 删除从下标1开始的2个元素,返回 [3, 2]
console.log(arr); // [1]
splice 语法:(start_index, delete_count, ...items_to_add)
- 返回值:被删除的元素组成的数组。
- 如果没有删除元素,返回空数组
[]。
注意:unshift 和 shift 操作会导致数组所有元素在内存中后移或前移,开销较大。
sort 的坑:默认按字符串排序
JS 的 sort() 默认将元素转为字符串,按 UTF-16 码点排序。这会导致数字排序不符合直觉:
javascript
ini
let arr = [10, 5, 2];
arr.sort();
console.log(arr); // [10, 2, 5] ------ 因为 "10" < "2" < "5"
正确做法:传入比较函数。
javascript
css
arr.sort((a, b) => a - b); // 升序 [2, 5, 10]
arr.sort((a, b) => b - a); // 降序 [10, 5, 2]
链表:用对象嵌套模拟节点
链表是线性结构,但元素在内存中不连续,通过指针(引用)链接。
JS 中可以用对象字面量模拟节点:
javascript
ini
function ListNode(val) {
this.val = val;
this.next = null;
}
const node = new ListNode(1);
node.next = new ListNode(2);
console.log(node);
// { val: 1, next: { val: 2, next: null } }
链表必须有 head 指针记录起始位置。访问任意节点必须从头开始遍历。增删元素时,只需要修改相邻节点的 next 指针,复杂度 O(1)(前提是已经找到目标位置)。
数组与链表对比:
| 特性 | 数组 | 链表 |
|---|---|---|
| 内存 | 连续(纯数组)或不连续(非纯数组) | 不连续,离散 |
| 访问 | O(1) 按索引 | O(n) 需遍历 |
| 插入/删除 | O(n) 需移动元素 | O(1) 只需改指针 |
| 扩容 | 需要整段转移 | 每次新增节点申请内存 |
小规模数据用数组,大规模频繁增删用链表。
数组内存真的连续吗?
在 JS 中,如果数组所有元素类型一致(如全是 number),引擎会分配连续内存,这是真正的数组。
但如果元素类型不同(如 [1, '2', {a:3}]),就无法通过索引计算偏移量。此时引擎用哈希表(对象)模拟数组,下标作为 key,值作为 value,内存不连续。这就是为什么 JS 数组可以装不同类型的原因------它放弃了连续内存的严格性。
一点总结
- 栈(LIFO) :用
push+pop实现,典型场景:函数调用栈、撤销操作。 - 队列(FIFO) :用
push+shift实现,典型场景:任务队列、打印队列。 - 数组增删 API 都会修改原数组,
splice功能强大但注意参数含义。 sort默认按字符串排序,数字排序必须传比较函数。- 链表适合频繁增删,但查找慢;数组适合随机访问。
- JS 数组未必连续,类型一致时才是真正的数组,否则是哈希表。
理解这些基础数据结构,能帮你写出更高效的代码,也是面试的高频考点。
互动讨论
- 如果用数组实现队列,频繁
shift会导致性能问题,有什么优化方案? (提示:用"循环队列"或两个栈模拟) splice返回的是什么?如果只传start_index不传delete_count会怎样?- 为什么链表必须有
head?如果没有头指针,你能找到链表的第一个节点吗? - JS 中
arr.sort((a,b) => a-b)的工作原理是什么? 比较函数返回负数、零、正数分别代表什么? - 除了栈和队列,还有哪些"操作受限"的数据结构? 比如双端队列(deque)?
📌 一点心得:数据结构不只是面试题。在开发中理解数据结构的特性,能帮你判断用数组还是链表、用栈还是队列,写出更合适的代码。