🧱 深入理解栈(Stack):原理、实现与实战应用

一、什么是栈?🤔

(Stack)是一种经典的线性数据结构 ,其核心特性是 "先进后出" (Last In First Out, LIFO)。

你可以把它想象成一摞盘子🍽️:每次只能从顶部放入或取出盘子,最晚放进去的盘子最先被拿出来。

在计算机科学中,栈广泛应用于以下场景:

  • 🔁 函数调用(调用栈)
  • 🧮 表达式求值与转换(如中缀转后缀)
  • ✅ 括号匹配验证
  • 🖥️ 浏览器前进/后退历史
  • 🔄 撤销(Undo)操作

💡 栈的核心思想:只操作一端(栈顶),另一端封闭


二、栈的抽象数据类型(ADT)🧩

一个标准的栈应具备以下属性和方法

方法 / 属性 说明
push(item) ➕ 入栈:将元素压入栈顶
pop() ➖ 出栈:移除并返回栈顶元素
peek() / top() 👀 查看栈顶元素但不移除
isEmpty() ❓ 判断栈是否为空
size 🔢 获取栈中元素数量
toArray()(可选) 📤 将栈内容转为数组(用于调试或展示)

⚠️ 注意 :栈不允许随机访问中间元素,只能操作栈顶


三、ES6 Class 与栈的封装 🛠️

ES6 引入了 class 语法,使面向对象编程更清晰。结合私有字段(#)、get/set 访问器等新特性,我们可以优雅地实现栈。

下面深入解析这些关键特性👇:


1️⃣ class:定义类的模板 📐

在 ES6 之前,JavaScript 通过构造函数 + 原型链模拟类:

javascript 复制代码
// 🕰️ ES5 风格
function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function() {
  console.log('Hello, ' + this.name);
};

ES6 的 class 是对上述模式的语法糖,但结构更清晰、更接近传统 OOP:

javascript 复制代码
// ✨ ES6 class
class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log('Hello, ' + this.name);
  }
}

关键点

  • 底层仍基于 原型(prototype)
  • 提供声明式、结构化的代码组织方式
  • 显著提升可读性与可维护性,尤其适合大型项目

2️⃣ #privateField:私有属性 🔒

传统 JS 中所有属性都是公开的,容易被外部篡改:

ini 复制代码
class Counter {
  constructor() {
    this.count = 0; // 😱 外部可随意修改!
  }
}
const c = new Counter();
c.count = 999; // 破坏封装!

ES2022 引入 私有字段 (以 # 开头),仅限类内部访问:

javascript 复制代码
class Counter {
  #count = 0; // 🔒 私有属性

  increment() { this.#count++; }
  getCount() { return this.#count; } // ✅ 安全暴露
}

const c = new Counter();
c.increment();
console.log(c.getCount()); // 1

// c.#count; // ❌ SyntaxError!

优势

  • 封装性:隐藏实现细节
  • 安全性:防止外部误操作
  • 可维护性:内部逻辑变更不影响外部调用

⚠️ 私有字段必须显式声明,不能动态添加。


3️⃣ constructor():初始化实例 🧬

constructor 是类的构造函数,在 new 实例时自动调用:

kotlin 复制代码
class Stack {
  #items;
  constructor(initialItems = []) {
    this.#items = [...initialItems]; // 初始化私有数组
  }
}

作用

  • 初始化实例属性(包括私有属性)
  • 接收参数设置初始状态
  • 若未定义,JS 会提供空默认构造函数

🔁 注意 :每个类最多只能有一个 constructor


4️⃣ get size():只读属性访问器 📏

有时我们希望暴露某个值,但禁止修改 。这时可用 get 定义访问器:

arduino 复制代码
class ArrayStack {
  #stack = [];
  get size() {
    return this.#stack.length; // 📊 像读属性一样使用
  }
}

const stack = new ArrayStack();
console.log(stack.size); // 0
// stack.size = 10; // ❌ 无效(严格模式报错)

好处

  • 语义清晰:size 看似属性,实为计算值
  • 可加入校验、日志、缓存等逻辑
  • 实现只读接口,避免误写

💡 同理,set 可拦截赋值:

javascript 复制代码
set maxSize(value) {
  if (value < 0) throw new Error('maxSize 不能为负');
  this._maxSize = value;
}

5️⃣ 方法共享于原型链,节省内存 🧠

这是 class 最重要的性能优势!

所有实例方法(非静态、非箭头函数)都定义在类的原型上:

javascript 复制代码
class Stack {
  push() { /* ... */ }
  pop() { /* ... */ }
}

const s1 = new Stack();
const s2 = new Stack();

console.log(s1.push === s2.push); // ✅ true!

这意味着

  • 方法只在内存中存在一份
  • 所有实例通过原型链共享方法
  • 极大节省内存,尤其适合创建大量对象(如游戏实体、UI 组件)

❌ 对比反模式(ES5 常见陷阱):

javascript 复制代码
function BadStack() {
  this.push = function() { /* 每次 new 都新建函数!*/ };
}

📌 建议 :现代项目优先使用 ES6+ class,善用私有字段与访问器,构建高内聚、低耦合的组件。


四、两种实现方式:数组 vs 链表 ⚖️

栈可以用数组链表实现,各有优劣:


1️⃣ 基于数组的栈(ArrayStack)📦

kotlin 复制代码
class ArrayStack {
  #stack = [];
  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];
  }
  toArray() { return [...this.#stack]; }
}

✅ 优点:

  • 时间效率高push/pop 在尾部操作,平均 O(1)
  • 内存连续,缓存友好(CPU 更快访问)
  • 代码简洁,JS 数组原生支持

❌ 缺点:

  • 扩容成本高 :容量不足时需复制所有元素 → O(n)
  • 可能存在空间浪费(预分配未用完)

💡 实际中,扩容是低频事件均摊时间复杂度仍为 O(1)


2️⃣ 基于链表的栈(LinkedListStack)⛓️

kotlin 复制代码
class ListNode {
  constructor(val) {
    this.val = val;
    this.next = null;
  }
}

class LinkedListStack {
  #stackPeek = null;
  #size = 0;

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

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

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

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

  toArray() {
    const arr = new Array(this.size);
    let node = this.#stackPeek;
    let i = this.size - 1;
    while (node) {
      arr[i--] = node.val;
      node = node.next;
    }
    return arr;
  }
}

✅ 优点:

  • 动态扩容 :每次插入只需 O(1) ,无复制开销
  • 空间按需分配,无浪费

❌ 缺点:

  • 每个节点需额外存储 next 指针 → 内存开销更大
  • 节点在内存中离散分布 → 缓存局部性差
  • 实例化 ListNode 有一定性能损耗

总结

  • 🚀 日常开发、轻量场景 → 数组实现
  • 🏗️ 大数据、稳定性要求高 → 链表实现

五、实战应用:有效的括号匹配 ✅

栈的经典应用场景之一!

📌 问题描述:

给定字符串 s,仅含 '(', ')', '[', ']', '{', '}',判断是否有效:

  • 左右括号必须正确闭合
  • 顺序必须匹配(如 "([)]" ❌ 无效)

🧠 解题思路:

  1. 遇到左括号 → 将其对应的右括号压入栈
  2. 遇到右括号 → 检查是否与栈顶匹配
  3. 遍历结束 → 栈必须为空

💻 代码实现:

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

function isValid(s) {
  if (!s) return true;
  const stack = [];
  for (let ch of s) {
    if (ch in leftToRight) {
      stack.push(leftToRight[ch]); // 压入期望的右括号
    } else {
      if (!stack.length || stack.pop() !== ch) {
        return false; // 不匹配或栈空
      }
    }
  }
  return stack.length === 0; // 栈空则有效
}

🧪 测试

arduino 复制代码
console.log(isValid("()"));       // ✅ true
console.log(isValid("()[]{}"));   // ✅ true
console.log(isValid("(]"));       // ❌ false
console.log(isValid("([)]"));     // ❌ false
console.log(isValid("{[]}"));     // ✅ true

🔍 为什么压入"右括号"?

这样遇到右括号时可直接比较 stack.pop() === ch无需二次查表,逻辑更简洁高效!


六、总结对比 📊

维度 数组栈 📦 链表栈 ⛓️
时间复杂度(平均) O(1) O(1)
扩容开销 O(n)(低频) O(1)
空间效率 可能浪费 指针开销(约 +50%)
实现难度 ⭐ 简单 ⭐⭐ 中等
适用场景 通用、轻量级 大数据、稳定性要求高

🎯 结语

栈虽简单,却是理解程序运行机制(如调用栈)和解决算法问题(DFS、表达式解析、回溯)的基石数据结构

掌握其两种实现方式及典型应用,不仅能写出更高效的代码,还能在面试中展现扎实的基本功!

📌 终极建议

  • 日常开发 → 优先用 数组实现(简单高效)
  • 面试/性能敏感场景 → 主动讨论 链表方案,展现深度思考 💡

📚 延伸思考:你能用栈实现"浏览器后退"功能吗?或者用两个栈实现一个队列?欢迎动手尝试!

相关推荐
玉宇夕落1 小时前
🔁 字符串反转 × 两数之和:前端面试高频题深度拆解(附5种反转写法 + 哈希优化)
javascript
用户2965412759171 小时前
JSAPIThree UI 控件学习笔记:用内置控件提升交互
前端
明教教主张5G1 小时前
Vue响应式原理(13)-ref实现原理解析
前端·vue.js
StockPP1 小时前
印度尼西亚股票多时间框架K线数据可视化页面
前端·javascript·后端
kungggyoyoyo2 小时前
TRAE中国版SOLO模式上线!我用它从0到1开发了一款AI小说编辑器
前端·vue.js·trae
ohyeah2 小时前
栈:那个“先进后出”的小可爱,其实超好用!
前端·数据结构
心随雨下2 小时前
typescript中Triple-Slash Directives如何使用
前端·javascript·typescript
自在极意功。2 小时前
AJAX 深度详解:从基础原理到项目实战
前端·ajax·okhttp
s***4532 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端