大家好!今天咱们来聊一个数据结构界的小明星------栈(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]
你看,push 和 pop 就是栈的核心操作!再加上 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. 有效的括号。
题目要求判断字符串中的括号是否匹配,比如 "([{}])" 是有效的,而 "([)]" 不是。
思路超简单:
- 遇到左括号
(、[、{,就把对应的右括号压入栈; - 遇到右括号,就检查是否和栈顶一致;
- 最后栈必须为空才算有效。
代码如下:
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 遍历等。
下次当你看到"后进先出"的场景,不妨想想:嘿,这不就是栈该上场的时候吗?
希望这篇轻松的小文能帮你把"栈"这个知识点稳稳拿下!如果你觉得有用,欢迎点赞、收藏,也欢迎在评论区聊聊你用栈解决过哪些有趣的问题~ 🚀