一文彻底搞懂 JavaScript 栈和队列(建议收藏)

📖 一文彻底搞懂 JavaScript 栈和队列(建议收藏)

从基础概念到面试实战,看完这篇就够了!


前言

大家好,我是ReBound 。今天来聊聊前端面试中出场率极高 的两个数据结构------栈(Stack)队列(Queue)

很多同学觉得数据结构很难,其实不然。栈和队列本质上就是对数组操作的封装和限制,理解了它们的特性,刷题时就能事半功倍。

本文将按照 由浅入深 的顺序,带你彻底搞懂栈和队列:

复制代码
基础铺垫 → 栈入门 → 队列进阶 → 链表对比 → 面试实战

一、基础铺垫:先搞懂 JS 数组

📌 为什么要先学数组? 栈和队列都是基于数组实现的,理解数组的特性是学习它们的前提。

1.1 JS 数组是"假数组"

和其他语言不同,JS 数组不要求元素类型一致,也不要求内存连续

javascript 复制代码
const arr = [1, 2, 3, 4];
// 每个元素的类型不一样,不连续,连续也没有意义
const arr2 = ['haha', 1, { a: 1 }]; // 不那么数组了

// arr2[2] 仍然可以通过下标访问
console.log(arr2[2]); // { a: 1 }
console.log(typeof arr2[2], typeof arr[1]); // object number

💡 面试考点 :V8 引擎会根据数组内容选择存储方式------当元素类型一致且连续时,使用快速模式 (连续内存);否则降级为字典模式(哈希表)。所以 JS 数组才能存储不同类型的数据。

1.2 sort() 的经典陷阱

这是新手最容易踩的坑之一:

javascript 复制代码
let arr = [10, 2, 5];
arr.sort();
console.log(arr); // [10, 2, 5] ← 出错!

// 因为 sort 默认按 Unicode 码点排序(字符串比较),"10" < "2" < "5"

正确做法:传入比较函数

javascript 复制代码
let arr1 = [10, 2, 5];
arr1.sort((a, b) => a - b);
console.log(arr1); // [2, 5, 10] ✓

🔥 记住sort() 不传参 = 按 Unicode 码点(字符串)排序 = 数字排序必翻车

1.3 splice() 详解

splice() 是数组操作中最强大的方法之一,但也最容易搞混:

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

// 在索引 1 的位置,删除 0 个元素,插入 3
console.log(arr.splice(1, 0, 3)); // [] (返回被删除的元素)
console.log(arr); // [1, 3, 2]

// 删除索引 1 的 1 个元素
arr.splice(1, 1);
console.log(arr); // [1, 2]

💡 参数速记splice(起始索引, 删除个数, ...要插入的元素)


二、栈(Stack):后进先出

2.1 什么是栈?

📌 一句话理解:栈就像羽毛球筒------只能从一端放入和取出。

复制代码
┌─────────────┐
│   巧乐兹     │  ← 栈顶(最后放入,最先取出)
├─────────────┤
│   冰工厂     │
├─────────────┤
│   可爱多     │
├─────────────┤
│   东北大板   │  ← 栈底(最先放入,最后取出)
└─────────────┘

🔥 核心原则LIFO(Last In, First Out)后进先出

2.2 用 JS 实现栈

用数组模拟栈非常简单,只需要两个操作:

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

// 入栈(push)
stack.push("东北大板");
stack.push("可爱多");
stack.push("冰工厂");
stack.push("巧乐兹");

// 出栈(pop)
while (stack.length) {
    const top = stack[stack.length - 1]; // 查看栈顶元素(peek)
    console.log(`取出来的是:`, top);
    stack.pop(); // 移除栈顶元素
}
// 输出顺序:巧乐兹 → 冰工厂 → 可爱多 → 东北大板

2.3 栈的常用操作

操作 方法 时间复杂度 说明
入栈 push() O(1) 添加到栈顶
出栈 pop() O(1) 移除栈顶
查看栈顶 stack[stack.length - 1] O(1) 不移除,只查看
判空 stack.length === 0 O(1) 检查是否为空栈

💡 面试考点 :栈的所有操作都是 O(1),这是它最大的优势!

2.4 栈的实战应用

栈在前端中无处不在:

javascript 复制代码
// 1. 函数调用栈 ------ JavaScript 执行机制的基础
function a() { b(); }
function b() { c(); }
function c() { console.log('done'); }
a(); // 调用栈:a → b → c → 输出 → c出栈 → b出栈 → a出栈

// 2. 撤销/重做(Undo/Redo)------ 编辑器必备功能
const undoStack = [];
const redoStack = [];

function doAction(action) {
    undoStack.push(action);
    redoStack.length = 0; // 清空重做栈
}

function undo() {
    if (undoStack.length) {
        const action = undoStack.pop();
        redoStack.push(action);
    }
}

// 3. 括号匹配 ------ LeetCode 第 20 题
function isValid(s) {
    const stack = [];
    const map = { ')': '(', ']': '[', '}': '{' };

    for (const char of s) {
        if ('({['.includes(char)) {
            stack.push(char); // 左括号入栈
        } else {
            if (stack.pop() !== map[char]) return false; // 右括号匹配栈顶
        }
    }
    return stack.length === 0; // 栈空则匹配成功
}

🔥 重点理解 :括号匹配是栈最经典的应用------遇到左括号入栈,遇到右括号出栈匹配


三、队列(Queue):先进先出

3.1 什么是队列?

📌 一句话理解:队列就像排队买奶茶------先来的人先买到。

scss 复制代码
入队 →  [许, 王, 张]  → 出队
         ↑         ↑
        队尾       队首
      (新来的)   (先走的)

🔥 核心原则FIFO(First In, First Out)先进先出

3.2 用 JS 实现队列

javascript 复制代码
const queue = []; // 空队列

// 入队(push)
queue.push('许');
queue.push('王');
queue.push('张');

// 出队(shift)
while (queue.length) {
    const front = queue[0]; // 查看队首元素
    console.log(front);
    queue.shift(); // 移除队首元素
}
// 输出顺序:许 → 王 → 张

3.3 ⚠️ 性能警告!

上面的实现有个致命问题shift() 的时间复杂度是 O(n)

scss 复制代码
shift() 执行过程:
[许, 王, 张]  →  [王, 张]
   ↑ 移除第一个,后面所有元素都要前移一位

🔥 面试高频考点 :为什么 shift() 是 O(n)?因为需要移动所有后续元素!

3.4 优化方案:双指针模拟队列

为了解决 shift() 的性能问题,我们用双指针实现 O(1) 的队列:

javascript 复制代码
class Queue {
    constructor() {
        this.items = {};
        this.head = 0;  // 队首指针
        this.tail = 0;  // 队尾指针
    }

    // 入队 O(1)
    enqueue(item) {
        this.items[this.tail] = item;
        this.tail++;
    }

    // 出队 O(1) ------ 关键:只移动指针,不移动元素!
    dequeue() {
        if (this.isEmpty()) return undefined;
        const item = this.items[this.head];
        delete this.items[this.head];
        this.head++;
        return item;
    }

    // 查看队首
    peek() {
        return this.items[this.head];
    }

    // 获取大小
    get size() {
        return this.tail - this.head;
    }

    isEmpty() {
        return this.size === 0;
    }
}

💡 核心思想 :用 headtail 两个指针标记队首和队尾,只移动指针,不移动元素,从而实现 O(1) 的入队出队!


四、栈 vs 队列:一张图搞懂区别

scss 复制代码
         栈 (LIFO)                    队列 (FIFO)
    ┌─────────────┐              ┌─────────────┐
    │    巧乐兹    │ ← 栈顶      │ 许 → 王 → 张 │
    │    冰工厂    │              └─────────────┘
    │    可爱多    │               ↑           ↑
    │    东北大板  │              入队        出队
    └─────────────┘
         ↑ 入/出                  先进先出
    后进先出
特性 队列
原则 LIFO 后进先出 FIFO 先进先出
入操作 push() O(1) push() O(1)
出操作 pop() O(1) shift() O(n) / 优化后 O(1)
生活类比 羽毛球筒、叠盘子 排队、打印机队列
典型应用 函数调用栈、撤销操作、括号匹配 消息队列、BFS、任务调度

五、链表:另一种线性结构

📌 为什么要学链表? 理解链表有助于深入理解数据结构的本质,也是面试常考内容。

5.1 什么是链表?

链表和数组不同,它的元素不需要连续存储,而是通过指针连接:

css 复制代码
数组:[1] [2] [3] [4]  ← 内存连续,按下标访问

链表:[1] → [2] → [3] → [4] → null  ← 通过指针连接,按顺序访问

5.2 用 JS 实现链表节点

javascript 复制代码
function ListNode(val) {
    this.val = val;
    this.next = null; // 指向下一个节点的指针
}

// 创建链表 1 → 2 → 3
const node = new ListNode(1);
node.next = new ListNode(2);
node.next.next = new ListNode(3);
console.log(node);
// ListNode {
//   val: 1,
//   next: ListNode {
//     val: 2,
//     next: ListNode { val: 3, next: null }
//   }
// }

5.3 数组 vs 链表对比

特性 数组 链表
内存 连续 分散
访问 O(1) 随机访问 O(n) 顺序访问
插入/删除 O(n) 需要移动元素 O(1) 只改指针(前提是已定位到节点)
缓存友好
适用场景 频繁读取 频繁增删

💡 面试考点 :链表插入删除是 O(1),但定位到节点需要 O(n),所以整体仍然是 O(n)!


六、面试真题实战

题目 1:用栈实现队列(LeetCode 232)

🔥 经典思路:两个栈"翻转"一次,就变成了队列!

javascript 复制代码
class MyQueue {
    constructor() {
        this.stackIn = [];   // 入队栈
        this.stackOut = [];  // 出队栈
    }

    push(x) {
        this.stackIn.push(x);
    }

    pop() {
        // 如果出队栈为空,把入队栈的元素全部倒入
        if (!this.stackOut.length) {
            while (this.stackIn.length) {
                this.stackOut.push(this.stackIn.pop());
            }
        }
        return this.stackOut.pop();
    }

    peek() {
        const val = this.pop();
        this.stackOut.push(val);
        return val;
    }

    empty() {
        return !this.stackIn.length && !this.stackOut.length;
    }
}

图解过程

scss 复制代码
入队:push(1), push(2), push(3)
stackIn: [1, 2, 3]
stackOut: []

出队:pop()
stackIn 为空,倒入 stackOut
stackIn: []
stackOut: [3, 2, 1]  ← 栈翻转了!
返回 stackOut.pop() = 1  ← 先进先出!

题目 2:有效的括号(LeetCode 20)

🔥 核心思路:遇到左括号入栈,遇到右括号出栈匹配!

javascript 复制代码
function isValid(s) {
    if (s.length % 2) return false; // 奇数直接 false

    const stack = [];
    const map = new Map([
        [')', '('],
        [']', '['],
        ['}', '{']
    ]);

    for (const c of s) {
        if (map.has(c)) {
            // 是右括号,检查栈顶是否匹配
            if (stack.length === 0 || stack.pop() !== map.get(c)) {
                return false;
            }
        } else {
            // 是左括号,入栈
            stack.push(c);
        }
    }

    return stack.length === 0; // 栈空则全部匹配
}

示例

arduino 复制代码
输入:"(]"
过程:
  '(' 是左括号,入栈 → stack: ['(']
  ']' 是右括号,栈顶是 '(',不匹配 → return false
输出:false

七、总结

核心知识点速查表

数据结构 核心原则 关键操作 时间复杂度
LIFO 后进先出 push / pop 都是 O(1)
队列 FIFO 先进先出 push / shift shift 是 O(n),优化后 O(1)
链表 指针连接 定位 O(n),插入删除 O(1) 整体 O(n)

必背结论

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│  1. 栈:后进先出 LIFO,push/pop 都是 O(1)                    │
│  2. 队列:先进先出 FIFO,注意 shift 的 O(n) 陷阱             │
│  3. 链表:通过指针连接,插入删除 O(1),访问 O(n)              │
│  4. sort():不传参按 Unicode 排序,数字排序必须传比较函数       │
└─────────────────────────────────────────────────────────────┘

最后送大家一句话 :数据结构不是背出来的,是画出来、写出来、用出来的。建议大家看完文章后,自己动手实现一遍栈和队列,再刷几道 LeetCode,保证印象深刻!


📌 觉得有帮助的话,点个赞👍再走吧~

📧 有问题欢迎评论区讨论,我会一一回复!

🔖 关注我,持续更新前端算法系列文章!


📚 相关资源

LeetCode 必刷题目

题目 难度 链接
20. 有效的括号 🟢 简单 leetcode.cn/problems/va...
155. 最小栈 🟡 中等 leetcode.cn/problems/mi...
232. 用栈实现队列 🟢 简单 leetcode.cn/problems/im...
225. 用队列实现栈 🟢 简单 leetcode.cn/problems/im...
239. 滑动窗口最大值 🔴 困难 leetcode.cn/problems/sl...
1047. 删除字符串中的所有相邻重复项 🟢 简单 leetcode.cn/problems/re...

MDN 官方文档

推荐学习资料

相关推荐
WL学习笔记1 小时前
通讯录(顺序表实现)
c语言·数据结构·算法
Asize1 小时前
Prompt 驱动 NLP:从 ES6 模块化到文本推理实战
javascript·人工智能·机器学习
JieE2121 小时前
树与二叉树--JS实例
javascript·数据结构
To_OC1 小时前
搞懂二叉树递归遍历,我居然是从爬楼梯开始的
前端·javascript·数据结构
用户7229134504522 小时前
数字故障美学:用 Canvas 实现 RGB 偏移、像素排序与扫描线
javascript
Jerryhut2 小时前
opencv对齐算法及其应用
人工智能·opencv·算法
小森林之主2 小时前
深入正则表达式:核心语法与实战剖析
javascript·python·正则表达式·编程技巧·字符串处理
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第113题】【并发篇】第13题:说一下乐观锁的优点和缺点?
java·开发语言·面试
Mahir082 小时前
HashMap 底层原理深度解密:从数据结构到 JDK1.7/1.8 演进全解
java·后端·面试·hashmap