🔍 什么是栈?
栈(Stack)是一种遵循 “后进先出”(LIFO, Last In First Out) 原则的线性数据结构。它的核心操作包括:
push(x):将元素压入栈顶pop():弹出并返回栈顶元素peek():查看栈顶元素但不移除isEmpty():判断栈是否为空size:获取当前元素个数
虽然 JavaScript 中我们可以直接用数组的 push() 和 pop() 模拟栈,但真正理解栈的本质,需要亲手实现它。
本文将带你用 链表 和 数组 两种方式从零实现栈,并深入对比它们的性能与适用场景。
✍️ 实现一:基于链表的栈(LinkedListStack)
链表天然适合实现栈——只需维护一个指向“栈顶”的指针,所有操作都在头部进行,时间复杂度均为 O(1)。
class ListNode {
constructor(val) {
this.val = val;
this.next = null; // 指向下一个节点
}
}
class LinkedListStack {
#stackPeek; // 私有属性:栈顶指针
#size = 0;
constructor() {
this.#stackPeek = null;
}
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;
}
get size() {
return this.#size;
}
isEmpty() {
return this.size === 0;
}
toArray() {
let node = this.#stackPeek;
const res = new Array(this.size);
for (let i = res.length - 1; i >= 0; i--) {
res[i] = node.val;
node = node.next;
}
return res;
}
}
✅ 特点:
- 动态分配内存,无需预设容量;
- 所有操作稳定 O(1);
- 使用私有字段
#封装内部状态,提升安全性。
✍️ 实现二:基于数组的栈(ArrayStack)
这是最直观、最常用的实现方式,利用 JS 数组的内置方法即可轻松完成。
class ArrayStack {
#stack; // 私有属性:内部数组
constructor() {
this.#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]; // 返回副本,避免外部修改
}
}
✅ 特点:
- 代码简洁,可读性强;
- 利用引擎优化,实际运行效率高;
- 支持快速调试(如直接打印数组)。
⚔️ 深度对比:链表栈 vs 数组栈
现在我们来系统比较这两种实现方式在 时间效率 和 空间效率 上的表现。
🕒 时间效率对比
| 操作 | 链表栈 | 数组栈 |
|---|---|---|
push | O(1)(稳定) | O(1) 平均,O(n) 最坏(扩容时) |
pop | O(1) | O(1) |
peek | O(1) | O(1) |
toArray | O(n) | O(n) |
关键差异:扩容行为
- 数组栈依赖动态数组,当容量不足时会触发扩容(如从 8 → 16),此时需复制所有元素,导致单次
push耗时突增。 - 链表栈每次只创建一个新节点,无扩容开销,性能更平稳。
💡 虽然数组栈最坏情况是 O(n),但由于扩容频率低,其摊销时间复杂度仍为 O(1) ,日常使用完全够用。
💾 空间效率对比
| 维度 | 链表栈 | 数组栈 |
|---|---|---|
| 内存开销 | 较高(每个节点含额外指针) | 较低(仅存储数据) |
| 内存布局 | 离散(堆上分配) | 连续(缓存友好) |
| 空间浪费 | 无(按需分配) | 可能有(预分配未用满) |
- 链表每个节点需额外存储
next指针(通常 8 字节),在大量数据下空间开销显著。 - 数组虽可能预留空位,但整体内存利用率更高,且连续内存对 CPU 缓存更友好。