🍦 雪糕、食堂、火车厢:三幅漫画吃透栈、队列与链表

🍦 从冰柜雪糕到火车车厢:栈、队列、链表,真的没那么难

很多同学初学"栈和队列"时,大脑里只有两个字:"受限"。

------ 这不就是个阉割版的数组吗?有什么好学的?

直到后来才明白,这种"操作受限"正是它们最高贵的血统。它能帮助我们明确数据的"使用场景",让程序逻辑清晰如画,bug无处遁形。

今天,我们就从最熟悉的数组出发,把栈、队列、链表这三座大山,用雪糕、排队和火车厢彻底讲透。

阅读收获

  • 用雪糕吃透"栈"
  • 用食堂排队理解"队列"
  • 用火车车厢可视化解构"链表"
  • JS 数组的隐藏彩蛋(它居然不一定是数组?)
  • 海量代码示例与性能思考

一、那个"灵活到肥胖"的数组

在 JavaScript 里,数组是我们最好的朋友。它能吃能睡------哦不,能增删改查,样样精通。

js 复制代码
const arr = [1, 2, 3];

// 尾部增加
arr.push(4);         // [1,2,3,4]

// 头部增加
arr.unshift(0);      // [0,1,2,3,4]

// 任意位置删除/增加 splice
arr.splice(2, 1, 'x'); // 从索引2删1个,插入'x'  => [0,1,'x',3,4]

// 头部删除
arr.shift();         // [1,'x',3,4]

看起来很美好,但问题藏在水下

push / pop 只在尾巴上折腾,还好;但 unshift / shift 一旦操作头部,后面的所有元素都必须集体搬家------复杂度 O(n)。splice 更是删了又补,底层相当于 slice + replace,纯粹的内存搬运工。

这就像在火车站排队改签:如果你在队伍最前面加塞一个人,后面所有人都得往后退一步;但若你只是在队尾加个人,那就轻松愉快。

结论:数组在频繁的非尾部增删时,会暴露出 O(n) 的移动成本。这时候,我们就需要请出今天的两位主角------栈和队列。


二、栈(Stack)------ 冰柜里的雪糕

栈是一种 后进先出(LIFO, Last In First Out) 的数据结构。

它就像一个只允许从顶部拿放的冰柜。你最后放进去的那根巧乐兹,总是第一个被你摸出来吃掉。最先放进去的东北大板,却被压在底下无人问津。

!图片提示词:一个透明的冰柜,里面垂直叠放着四个雪糕,从下到上分别是东北大板、可爱多、梦龙、巧乐兹。一只手从顶部伸入,正要拿走最上方的巧乐兹,突出"后进先出"

在 JS 中,用数组模拟栈实在太轻松了------只用 pushpop

js 复制代码
const stack = []; // 空栈

stack.push("东北大板");
stack.push("可爱多");
stack.push("梦龙");
stack.push("巧乐兹");

// 现在栈顶元素是 "巧乐兹"
console.log(stack[stack.length - 1]); // "巧乐兹"

// 把雪糕吃完(清空栈)
while (stack.length) {
  const top = stack[stack.length - 1]; // 看一眼栈顶(peek)
  console.log('正在吃:', top);
  stack.pop(); // 真正的出栈
}
// 输出顺序:巧乐兹 梦龙 可爱多 东北大板

栈的经典应用

  • 函数调用栈:JS 引擎就是用栈来追踪函数执行的(你调用的最后一个函数最先执行完)。
  • 撤销操作:Ctrl+Z 就是不断把栈顶的历史状态弹出。
  • 括号匹配:左括号入栈,右括号与栈顶匹配。

提示:栈的 peek(看一眼栈顶但不取出)并不是标准操作,但我们可以用 stack[stack.length - 1] 变相实现。


三、队列(Queue)------ 食堂排队打饭

队列是 先进先出(FIFO, First In First Out) 的代表。

谁先排队,谁先投篮。新人必须从队尾开始排。

我们用 pushshift 来模拟:

js 复制代码
const queue = [];

queue.push("詹姆斯");   // 詹姆斯入队
queue.push("杜兰特");
queue.push("库里");
queue.push("伦纳德");

while (queue.length) {
  const front = queue[0];      // 查看队头
  console.log('正在投篮:', front);
  queue.shift();               // 队头出队
}
// 输出顺序:詹姆斯 杜兰特 库里 伦纳德

但请注意一个性能陷阱shift() 同样会引起所有剩余元素前移一位,时间复杂度 O(n)。

如果队列很长,每次出队都让所有人往前挪一步,食堂阿姨不累吗?

解决方案

  • 双指针数组 (配合前端 head 索引和 tail 索引),避免移动元素。
  • 或者干脆用链表来实现队列,增删全在 O(1) 搞定。

四、链表(Linked List)------ 火车车厢,自由连接

数组和链表都是有序 的线性结构,每个元素都有唯一的前驱和后继。

但数组是大通铺 (连续内存),链表是一节节独立车厢(离散内存),通过指针连接。

在 JS 中,链表节点可以用对象字面量轻松构建:

js 复制代码
function ListNode(val) {
  this.val = val;
  this.next = null;
}

const node1 = new ListNode(1);
const node2 = new ListNode(2);
node1.next = node2; // 1 -> 2

这种离散存放带来的最大优势就是增删极其高效

  • 插入节点 :改变前驱节点的 next 指向新节点,新节点指向原来的后继节点。
  • 删除节点 :让前驱节点的 next 直接跳过目标节点,指向其后继。

上述操作都只需要修改一两个指针,时间复杂度 O(1)。

但代价是:访问元素必须从头节点开始逐个遍历,最坏 O(n)。

数组 vs 链表

操作 数组 链表
随机访问 O(1) O(n)
头部增删 O(n) O(1)
尾部增删 O(1) O(1)(有尾指针)
内存连续性 连续 离散

工程哲学

  • 数据规模小、需要频繁索引 → 用数组。
  • 数据规模大、频繁增删头部/中间 → 用链表。

五、💣 JS 数组的彩蛋:它不一定是个"真数组"

我们总以为 const arr = [1,2,3] 在内存里一定是连续存储的。错!

在 V8 引擎中:

  • 如果数组元素类型一致(如全是小整数),引擎会给它分配一块连续内存,它就是真正意义上的数组(快数组)。
  • 如果元素类型五花八门 (比如 ['a', 1, {a:1}]),连续存放毫无意义(不同类型占据内存大小不同),引擎会将其降级为以哈希表为基础的"慢数组",底层更像一个用索引做 key 的字典。
js 复制代码
const arr = [1, 2, 3];          // 快数组,连续内存
const arr2 = ['a', 1, {a:1}];  // 慢数组,哈希结构

所以你会发现,JS 数组即使有空洞(稀疏数组)也能正常访问,这得益于它的底层自动切换机制。

另一个容易踩的坑是数组的 sort() 方法:

js 复制代码
let arr = [10, 2, 5];
arr.sort(); // 默认按字符串Unicode码点排序 → [10, 2, 5]

必须传入比较函数:

js 复制代码
arr.sort((a, b) => a - b); // [2, 5, 10]

总结:数据结构是思维的形状

栈、队列、链表,它们不仅仅是"被限制的数组",而是我们为了清晰表达数据的流动逻辑而精心设计的容器。

  • 需要"历史记录回溯" → 栈(LIFO)
  • 需要"任务排队处理" → 队列(FIFO)
  • 需要"频繁中间插队,又不关心索引" → 链表

理解这些结构如何映射到 JS 的具体实现,理解内存背后的故事,能让我们在面对性能调优和复杂业务时,做出更精准的架构决策。

终极思考 :JS 里没有原生的链表和栈/队列类,我们一直用数组模拟。但在某些海量数据处理时,手写一个基于链表的队列,能彻底告别 shift 带来的 O(n) 噩梦。这就是算法基础给你的底气。


如果觉得这篇"冰柜雪糕风格"的讲解对你有帮助,请点赞收藏,让更多徘徊在数据结构门口的同学吃到这根"巧乐兹"~ 有疑问欢迎在评论区一起讨论!

相关推荐
胡志辉2 小时前
从v8源码和react深入浅出理解 JavaScript 作用域链与闭包
前端·javascript
Bolt3 小时前
TypeScript 7.0 来了:当 tsc 用 Go 重写之后
javascript·typescript·go
阳火锅4 小时前
😭测试小姐姐终于不骂我了!这个提BUG神器太香了...
前端·javascript·面试
林希_Rachel_傻希希6 小时前
js里面的proxy理解。以及vue3响应式数据设计底层
前端·javascript·面试
阿黎梨梨6 小时前
AI Loop:告别“人肉写提示词”,让代码替你“鞭策”AI
javascript·人工智能
猿人谷9 小时前
不只是 CPU 阈值:STAR 如何用 GAT + Transformer 做容器级自动扩缩容?
人工智能·算法
竹林81810 小时前
用 wagmi v2 + viem 监听链上事件,我踩了三天坑终于搞懂了实时日志与历史补全
javascript
只一10 小时前
😭从回调地狱到 async/await:一文打通 Ajax 与 JS 异步编程
javascript
复杂网络10 小时前
Stable Diffusion 视觉大模型微调技术深度调研
算法