JavaScript栈和队列:从“冰柜里的雪糕”到“排队打饭”

JavaScript栈和队列:从"冰柜里的雪糕"到"排队打饭"

摘要:栈和队列是两种操作受限的线性数据结构。本文从数组出发,用 push/pop 实现 LIFO 的栈,用 push/shift 实现 FIFO 的队列,并对比了链表与数组的增删性能差异。还顺手整理了 JS 数组的 splice、sort 陷阱以及内存模型。

📑 目录

  • 线性数据结构:数组、链表、栈、队列
  • 栈:后进先出,就像冰柜里的雪糕
  • 队列:先进先出,就像排队打饭
  • 灵活增删的数组:splice 与 API 副作用
  • sort 的坑:默认按字符串排序
  • 链表:用对象嵌套模拟节点
  • 数组内存真的连续吗?
  • 一点总结
  • 互动讨论

线性数据结构:数组、链表、栈、队列

数据结构可以分为两类:

  • 线性结构:每个元素只有一个前驱和一个后继。包括数组、链表、栈、队列。
  • 非线性结构:树、图等。

数组和链表是"底层容器",栈和队列可以看作操作受限的数组 ------它们只允许在特定位置增删元素。由于 JS 数组提供了 pushpopshift 等 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(pushpopshiftunshiftsplice)都会直接修改原数组,不是纯函数。

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)

  • 返回值:被删除的元素组成的数组。
  • 如果没有删除元素,返回空数组 []

注意:unshiftshift 操作会导致数组所有元素在内存中后移或前移,开销较大。


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 数组可以装不同类型的原因------它放弃了连续内存的严格性。


一点总结

  1. 栈(LIFO) :用 push + pop 实现,典型场景:函数调用栈、撤销操作。
  2. 队列(FIFO) :用 push + shift 实现,典型场景:任务队列、打印队列。
  3. 数组增删 API 都会修改原数组,splice 功能强大但注意参数含义。
  4. sort 默认按字符串排序,数字排序必须传比较函数。
  5. 链表适合频繁增删,但查找慢;数组适合随机访问。
  6. JS 数组未必连续,类型一致时才是真正的数组,否则是哈希表。

理解这些基础数据结构,能帮你写出更高效的代码,也是面试的高频考点。


互动讨论

  1. 如果用数组实现队列,频繁 shift 会导致性能问题,有什么优化方案? (提示:用"循环队列"或两个栈模拟)
  2. splice 返回的是什么?如果只传 start_index 不传 delete_count 会怎样?
  3. 为什么链表必须有 head?如果没有头指针,你能找到链表的第一个节点吗?
  4. JS 中 arr.sort((a,b) => a-b) 的工作原理是什么? 比较函数返回负数、零、正数分别代表什么?
  5. 除了栈和队列,还有哪些"操作受限"的数据结构? 比如双端队列(deque)?

📌 一点心得:数据结构不只是面试题。在开发中理解数据结构的特性,能帮你判断用数组还是链表、用栈还是队列,写出更合适的代码。

相关推荐
槑有老呆1 小时前
JavaScript 数组,远不止 [] 那么简单
javascript
HjhIron1 小时前
从栈到队列,再到链表:前端开发者必知的线性数据结构
前端·javascript
papership1 小时前
入门级-数据结构-2、简单树:二叉树的遍历(前序、中序、后序)
数据结构·算法
WWW65261 小时前
代码随想录 打卡第五十四天
数据结构·c++·算法
happymaker06261 小时前
LeetCodeHot100——15.三数之和
数据结构·算法
阿猫的故乡1 小时前
Vue自定义指令从入门到实用:自动聚焦、权限控制、防抖、懒加载……全案例教学
前端·javascript·vue.js
该用户已成仙2 小时前
vue3 使用 vuedraggable 报错 TypeError: isFunction2 is not a function
前端·javascript·vue.js
TPBoreas2 小时前
前端面试问题打把-场景题
开发语言·前端·javascript
J2虾虾2 小时前
C 语言 sizeof 完全用法指南
c语言·数据结构·算法