从零手写栈:用链表 vs 数组实现,谁更胜一筹?

63 阅读3分钟

🔍 什么是栈?

栈(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 数组栈

现在我们来系统比较这两种实现方式在 时间效率空间效率 上的表现。

🕒 时间效率对比

操作链表栈数组栈
pushO(1)(稳定)O(1) 平均,O(n) 最坏(扩容时)
popO(1)O(1)
peekO(1)O(1)
toArrayO(n)O(n)

关键差异:扩容行为

  • 数组栈依赖动态数组,当容量不足时会触发扩容(如从 8 → 16),此时需复制所有元素,导致单次 push 耗时突增。
  • 链表栈每次只创建一个新节点,无扩容开销,性能更平稳。

💡 虽然数组栈最坏情况是 O(n),但由于扩容频率低,其摊销时间复杂度仍为 O(1) ,日常使用完全够用。


💾 空间效率对比

维度链表栈数组栈
内存开销较高(每个节点含额外指针)较低(仅存储数据)
内存布局离散(堆上分配)连续(缓存友好)
空间浪费无(按需分配)可能有(预分配未用满)
  • 链表每个节点需额外存储 next 指针(通常 8 字节),在大量数据下空间开销显著。
  • 数组虽可能预留空位,但整体内存利用率更高,且连续内存对 CPU 缓存更友好。