栈(Stack):从实现到应用的详解

144 阅读5分钟

栈(Stack):从实现到应用的详解

栈是一种遵循后进先出(LIFO,Last In First Out)  原则的线性数据结构,它在计算机科学中有着广泛的应用,如表达式求值、括号匹配、函数调用栈等。本文将结合具体代码,详细介绍栈的两种实现方式(数组、链表)及其经典应用。

一、栈的基本概念

栈只允许在一端进行数据操作(插入 / 删除),这一端被称为 “栈顶”,另一端则称为 “栈底”。栈的核心操作包括:

  • 入栈(Push) :向栈顶添加元素
  • 出栈(Pop) :从栈顶移除元素
  • 查看栈顶(Peek) :获取栈顶元素但不移除
  • 判空(isEmpty) :判断栈是否为空
  • 获取大小(Size) :获取栈中元素的数量

二、栈的数组实现

数组是实现栈最简单的方式,因为数组的尾部操作(push/pop)时间复杂度为 O (1),非常适合作为栈的底层存储结构。

1. 基础数组操作实现栈

const stack = [];

// 入栈:向数组尾部添加元素(栈顶)
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.push(5); // 此时栈为 [1,2,3,4,5],栈顶是5

// 访问栈顶元素:数组最后一个元素
const peek = stack[stack.length - 1]; // 5
console.log(peek);

// 出栈:移除数组尾部元素(栈顶)
const pop = stack.pop(); // 移除5,返回5,此时栈为 [1,2,3,4]

// 栈的长度
const size = stack.length; // 4

// 栈是否为空
const isEmpty = stack.length === 0; // false(此时栈非空)

解析:数组的push方法直接在尾部添加元素(入栈),pop方法移除尾部元素(出栈),通过length-1可快速访问栈顶,操作高效且简洁。

2. 封装数组栈为类

为了更规范地使用栈,可将其封装为类,隐藏内部实现细节:

class ArrayStack {
    #stack; // 私有属性,存储栈元素
    constructor(){
        this.#stack = []; // 初始化空数组
    }

    // 获取栈的大小(修正笔误:原代码为this.#stack,length)
    get size(){
        return this.#stack.length;
    }

    // 判断栈是否为空
    isEmpty(){
        return this.size === 0;
    }

    // 入栈(修正:原代码缺少参数num)
    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;
    }
}

解析

  • 使用私有属性#stack隐藏内部数组,避免外部直接修改,保证栈的操作规范性。
  • 封装push/pop/peek等方法,统一栈的操作接口,同时在空栈操作时抛出错误,增强健壮性。

三、栈的链表实现

当数据量不确定或需要动态扩容时,链表实现的栈更灵活(数组可能需要预分配内存)。链表实现中,通常将头节点作为栈顶(操作更高效,避免尾部遍历)。

链表栈的实现

// 链表节点类
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++; // 大小+1
    }

    // 查看栈顶元素
    peek(){
        if(!this.#stackPeek) throw new Error('栈为空');
        return this.#stackPeek.val;
    }

    // 出栈:移除栈顶节点
    pop(){
        const num = this.peek(); // 先获取栈顶值(会检查空栈)
        this.#stackPeek = this.#stackPeek.next; // 栈顶指针后移
        this.#size--; // 大小-1
        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;
    }
}

// 使用示例
const stack = new LinkedListStack();
console.log(stack.size); // 0(初始为空)
stack.push(1);
stack.push(2);
console.log(stack.toArray()); // [1,2](栈底到栈顶)

解析

  • 链表栈通过#stackPeek指针指向栈顶(头节点),push时在头部插入新节点,pop时移除头节点,操作时间复杂度均为 O (1)。
  • toArray方法通过反向遍历链表,将栈元素从栈底到栈顶存入数组,方便查看栈的完整结构。

四、栈的经典应用:括号匹配

栈的 “后进先出” 特性非常适合解决括号匹配问题,例如判断字符串"()[]{}"是否有效。

括号匹配实现

// 存储左右括号的对应关系
const leftToRight = {
    "(": ")",
    "[": "]",
    "{": "}"
};

const isValid = function(s){
    if(!s) return true; // 空字符串视为有效
    const stack = []; // 用栈存储待匹配的右括号
    const len = s.length;

    for(let i = 0; i < len; i++){
        const ch = s[i];
        // 遇到左括号:将对应的右括号入栈
        if(ch === "(" || ch === "{" || ch === "["){
            stack.push(leftToRight[ch]);
        } else {
            // 遇到右括号:检查是否与栈顶匹配
            // 若栈为空(无匹配的左括号)或不匹配,直接返回false
            if(!stack.length || stack.pop() !== ch){
                return false;
            }
        }
    }

    // 遍历结束后,栈必须为空(所有左括号都有匹配的右括号)
    return !stack.length;
};

解析

  1. leftToRight映射左括号到对应的右括号,方便快速获取匹配目标。
  2. 遍历字符串时,遇到左括号则将其对应的右括号入栈(未来需要被匹配)。
  3. 遇到右括号时,若栈顶元素与当前右括号一致,则出栈(匹配成功);否则匹配失败。
  4. 遍历结束后,若栈为空,说明所有左括号都被匹配,字符串有效。

五、总结

栈作为一种简单而重要的数据结构,其核心是 “后进先出” 的特性。本文介绍了两种实现方式:

  • 数组实现:简洁高效,适合数据量固定的场景。
  • 链表实现:动态灵活,适合数据量不确定的场景。