栈:那个“先进后出”的小可爱,其实超好用!

大家好!今天咱们来聊一个数据结构界的小明星------栈(Stack) 。它不像链表那样复杂,也不像树那样高深,但它在算法和工程实践中却无处不在。从浏览器的"返回"按钮,到表达式求值、括号匹配,甚至函数调用本身,都离不开栈的身影。

如果你刚入门数据结构,或者正在刷 LeetCode,那这篇轻松愉快的小文,或许能帮你把"栈"这个概念彻底搞明白!


什么是栈?

简单来说,栈是一种"先进后出"(FILO, First In Last Out)的线性数据结构。你可以把它想象成一摞盘子:

  • 你只能从最上面放盘子(入栈)
  • 也只能从最上面拿盘子(出栈)
  • 想拿中间的?不好意思,得先把上面的全拿走!

这种"只在一端操作"的特性,让栈成为一种非常简洁但强大的工具。


JavaScript 里,栈怎么实现?

JavaScript 虽然没有内置"栈"类型,但它的 数组(Array)天生就是个开箱即用的栈

ini 复制代码
const arr = [1, 2, 3];
arr.push(4);   // 入栈 → [1,2,3,4]
arr.pop();     // 出栈 → 返回 4,arr 变成 [1,2,3]

你看,pushpop 就是栈的核心操作!再加上 arr[arr.length - 1] 就能"偷看"栈顶元素(这叫 peek),一个简易栈就完成了。

不过,如果想更规范一点,我们可以用 ES6 的 class 来封装一个真正的栈类:

✅ 用数组实现栈(推荐!)

kotlin 复制代码
class ArrayStack {
  #stack = []; // 私有属性,外部无法直接访问

//定义一个getter 外部可以通过stack.size获取栈的大小 其实也就是数组的长度
  get size() {
    return this.#stack.length;
  }

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

  push(num) {
    this.#stack.push(num);
  }

  pop() {
    if (this.isEmpty()) throw new Error('栈为空');
    return this.#stack.pop();
    //移除数组最后一个元素 (即栈顶)
  }

  peek() {
    if (this.isEmpty()) throw new Error('栈为空');
    return this.#stack[this.size - 1];
  }
}

是不是很清爽?而且 JS 引擎对数组做了大量优化,日常开发中,用数组实现栈是最高效、最简洁的选择


那......链表也能实现栈?

当然可以!虽然不常用,但作为学习,用链表实现栈能帮你更深入理解"指针"和"动态内存"的概念。

我们先定义一个链表节点:

kotlin 复制代码
class ListNode {
  constructor(val) {
    this.val = val;
    this.next = null; // 指向下一个节点
  }
}

然后用它构建一个栈:

kotlin 复制代码
class LinkedListStack {
  #stackPeek = null; // 栈顶(即链表头)
  #size = 0;

  push(num) {
    const node = new ListNode(num);
    node.next = this.#stackPeek;
    this.#stackPeek = node;
    this.#size++;
  }

  pop() {
    const num = this.peek();
    this.#stackPeek = this.#stackPeek.next;
    this.#size--;
    return num;
  }

  peek() {
    if (!this.#stackPeek) throw new Error('栈为空');
    return this.#stackPeek.val;
  }

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

这里的关键是:每次入栈都在链表头部插入节点,这样就能保证"后进先出"。


数组 vs 链表:谁更适合做栈?

维度 数组实现 链表实现
时间效率 大部分 O(1),扩容时 O(n) 稳定 O(1)
空间效率 连续内存,无额外开销 每个节点需存 next 指针,略占空间
实际使用 ✅ 推荐!JS 引擎高度优化 学习用,生产少见

所以结论很明确:在 JavaScript 中,优先用数组实现栈


实战:LeetCode 经典题 ------ 有效的括号

说到栈的应用,不得不提这道面试高频题:20. 有效的括号

题目要求判断字符串中的括号是否匹配,比如 "([{}])" 是有效的,而 "([)]" 不是。

思路超简单

  1. 遇到左括号 ([{,就把对应的右括号压入栈;
  2. 遇到右括号,就检查是否和栈顶一致;
  3. 最后栈必须为空才算有效。

代码如下:

arduino 复制代码
const leftToRight = {
  '(': ')',
  '[': ']',
  '{': '}'
};

const isValid = function(s) {
  if (!s) return true;
  //如果s是null undefined 或者"" 则被认为是有效的 返回true
  
  const stack = [];
  for (let ch of s) {
    if (ch in leftToRight) {
      stack.push(leftToRight[ch]); // 压入期待的右括号
    } else {
         //否则  说明当前字符是右括号
      if (!stack.length || stack.pop() !== ch) {
      //此时还会分两种情况返回false
      //1.!stack.length 表示 栈为空的情况下 却来了一个右符号(单单一个右括号) 说明没有匹配的左括号 那么返回false
      //2.stack.pop() !== ch 弹出栈顶(期待的右括号) 与当前字符不一致时 也就是类型不匹配 返回false
        return false; // 不匹配 or 多余右括号
      }
    }
  }
  return !stack.length
  //如果栈为空 说明所有的左括号都被正确匹配 返回true
  //如果栈非空 说明有多余的左括号未闭合 返回false
};

是不是一气呵成?这就是栈的魅力------用最简单的规则,解决看似复杂的问题


小结:栈虽小,作用大

  • 栈是 FILO 的线性结构,操作只在"顶端"进行。
  • JavaScript 中,数组 + push/pop 就是最天然的栈
  • class 封装可以让代码更清晰、安全(私有字段 # + get 访问器)。
  • 链表实现虽可行,但日常开发没必要"杀鸡用牛刀"。
  • 栈的经典应用:括号匹配、表达式求值、撤销操作、DFS 遍历等。

下次当你看到"后进先出"的场景,不妨想想:嘿,这不就是栈该上场的时候吗?


希望这篇轻松的小文能帮你把"栈"这个知识点稳稳拿下!如果你觉得有用,欢迎点赞、收藏,也欢迎在评论区聊聊你用栈解决过哪些有趣的问题~ 🚀

相关推荐
心随雨下2 小时前
typescript中Triple-Slash Directives如何使用
前端·javascript·typescript
自在极意功。2 小时前
AJAX 深度详解:从基础原理到项目实战
前端·ajax·okhttp
s***4532 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端
海上彼尚2 小时前
[逆向] 1.本地登录爆破
前端·安全
什么时候吃饭2 小时前
vue2、vue3父子组件嵌套生命周期执行顺序
前端·vue.js
2501_940943912 小时前
体系课\ Python Web全栈工程师
开发语言·前端·python
q***06472 小时前
SpringSecurity相关jar包的介绍
android·前端·后端
低保和光头哪个先来2 小时前
场景2:Vue Router 中 query 与 params 的区别
前端·javascript·vue.js·前端框架
q***95222 小时前
SpringMVC 请求参数接收
前端·javascript·算法