JS 栈结构难理解?底层原理 + 结构图 + 2 道算法题,彻底吃透!

56 阅读4分钟

本文目标:带你从“会用栈”到“理解栈的底层逻辑”。文章不仅讲原理,还有结构图、真实调试示例,以及两道高频面试题(括号匹配 + 最小栈)完整拆解。


🎯 一、为什么要深入理解「栈」?

在前端开发、JS 引擎执行机制、深度算法题里,你会频繁看到:

  • 调用栈(Call Stack)
  • 执行上下文(Execution Context)
  • 事件循环(Event Loop)
  • DFS 深度优先
  • 括号匹配
  • 最小栈、单调栈
  • 表达式求值

这些背后都离不开同一个数据结构 —— 栈(Stack)

理解栈,等于理解 JS 的一半底层行为。今天我们就来一次真正的“底层级别的栈”解析。


🧱 二、栈是什么?一句话概括

栈(Stack)是一种后进先出(LIFO)的线性结构。

你可以把它理解成一台“只能从一端操作”的自动售货机:

  • 入口(Push)
  • 出口(Pop)

来看一个结构图:

image.png

  • push:从上面丢一个新数据上去
  • pop:只能从上面拿走一个(永远不能直接拿走中间或底部)

🏗️ 三、JS 的栈底层是什么?数组?链表?硬件?

在 JavaScript 里,栈的底层通常由“连续内存块 + 指针”实现。而在你的代码世界里,最常用的栈是:

✔️ 1. JavaScript 引擎的调用栈(Call Stack)

JS 在执行你的代码时,用栈来维护「函数执行顺序」。

结构图如下:

image.png

每次调用函数 → 入栈
函数执行完毕 → 出栈


✔️ 2. 代码实现里的“模拟栈”:直接用数组实现

JavaScript 内置没有 Stack 类型,我们一般用数组来模拟:

const stack = [];
stack.push(1);
stack.push(2);
stack.pop();

但注意:

  • push/pop 是 O(1)
  • shift/unshift 是 O(n) (不要用)

🔍 四、栈的核心操作(JS 实现 + 原理)

1. push —— 入栈

底层本质:

  • 把元素写到连续内存的下一个地址
stack.push(x);

2. pop —— 出栈

底层本质:

  • 指针往下移动一格,并返回值
stack.pop();

3. top (peek) —— 查看栈顶,不移除

stack[stack.length - 1]

这三个操作构成栈的全部核心能力。


🌈 五、可视化:栈在 JS 引擎中的真实变化

假设我们执行:

function a(){ b(); }
function b(){ c(); }
function c(){ console.log('hi') }
a();

调用栈变化如下:

Step1: a() 入栈
┌────────┐
│  a()   │
└────────┘

Step2: b() 入栈
┌────────┐
│  b()   │ ← top
├────────┤
│  a()   │
└────────┘

Step3: c() 入栈
┌────────┐
│  c()   │ ← top
├────────┤
│  b()   │
├────────┤
│  a()   │
└────────┘

Step4: c() 出栈 …

理解这一段,对理解 JS 异步、事件循环、Promise 执行顺序至关重要。


⚔️ 六、进入实战!两道最经典「栈」算法题

这两道是前端、算法面试出现率最高的“栈”题。

接下来的内容不仅讲解,还给你完整的 JS 代码拆解 + 图示。


💡 面试题 1:括号匹配(Valid Parentheses)

🧩 核心思想

用一个 map + 栈 来匹配括号。

显示步骤图

s = "{ [ ( ) ] }"

遇左括号:推入期望的右括号
遇右括号:必须与栈顶匹配

图示:

输入:"( [ { } ] )"

栈变化:
push )
push ]
push }
pop } ✔️
pop ] ✔️
pop ) ✔️
最终栈空 = 有效

✔️ 完整代码

const map = {
  '(': ')',
  '{': '}',
  '[': ']'
};

function isValid(s) {
  const stack = [];

  for (let ch of s) {
    if (map[ch]) {
      // 左括号,压入它“期望”的右括号
      stack.push(map[ch]);
    } else {
      // 遇到右括号
      if (stack.pop() !== ch) {
        return false;
      }
    }
  }

  return stack.length === 0;
}

📌 时间复杂度

  • O(n)
  • 只扫一遍

📌 空间复杂度

  • 最差 O(n)(“((((((”这种情况)

💡 面试题 2:最小栈 MiniStack(Min Stack)

面试中几乎百分百出现:“请 O(1) 时间返回最小值的栈”。

普通栈没法 O(1) 求最小值,需要遍历:

O(n)

于是我们引入:辅助栈(单调栈)


🎨 结构图:两个栈如何配合?

主栈(stack)      辅助栈(stack2:单调递增)

   5push 5       5

   5                   5
   3push 3       3

   5                   5
   3
   3push 3       3
                         3

辅助栈始终保持「从栈底 → 栈顶」递减:栈顶永远是最小值。


✔️ 完整 JS 实现

const MiniStack = function () {
  this.stack = [];
  this.minStack = []; // 单调栈
};

MiniStack.prototype.push = function (x) {
  this.stack.push(x);

  if (
    this.minStack.length === 0 ||
    x <= this.minStack[this.minStack.length - 1]
  ) {
    this.minStack.push(x);
  }
};

MiniStack.prototype.pop = function () {
  const val = this.stack.pop();

  if (val === this.minStack[this.minStack.length - 1]) {
    this.minStack.pop();
  }

  return val;
};

MiniStack.prototype.top = function () {
  return this.stack[this.stack.length - 1];
};

MiniStack.prototype.getMin = function () {
  return this.minStack[this.minStack.length - 1];
};

📌 时间复杂度(全都 O(1))

  • push:O(1)
  • pop:O(1)
  • top:O(1)
  • getMin:O(1)

这就是面试官高频问你的原因:

你必须通过栈结构设计,使得逻辑保持 O(1)!


🧭 七、栈的更多应用场景(JS 开发中的真实用例)

场景为什么需要栈?
表达式求值经典双栈算法,运算符优先级
浏览器返回/前进典型的两个栈互转
DFS递归其实就是调用栈
React fiber递归展开与任务调度
V8 执行引擎栈帧保存执行上下文

每当你看到“回溯、嵌套、递归、顺序恢复”,99% 是栈的能力。


🎉 八、全文总结

今天我们从零到一,把 JS 中的栈彻底拆开:

  • 栈底层本质:LIFO + 连续内存块
  • JS 引擎的调用栈:理解执行机制的核心
  • 用数组模拟栈:push/pop = O(1)
  • 图示 + 调用流程可视化
  • 两道经典的高频面试题:括号匹配、最小栈
  • 结构图辅助理解

你现在已经具备——从底层到实战的完整栈思维体系