栈的全面解析:ADT、实现与应用

栈的全面解析:ADT、实现与应用

一、栈的基础概念与ADT

栈是一种先进后出(FILO, First In Last Out)的线性数据结构,它就像现实生活中堆叠的盘子,只能从顶部添加或移除元素。在计算机科学中,栈的"栈顶"是唯一可操作的位置,而"栈底"固定不动。

作为抽象数据类型(ADT),栈提供了一组标准操作:

  • push:向栈顶添加元素
  • pop:移除并返回栈顶元素
  • peek:查看栈顶元素但不移除
  • isEmpty:判断栈是否为空
  • size:获取栈中元素数量

这些操作确保了栈的"后进先出"特性,使其在程序执行、表达式求值、括号匹配等场景中发挥着关键作用。

二、数组实现栈

数组是开箱即用的线性数据结构。在JavaScript中数组的相关操作:

复制代码
// 在JavaScript数组的相关操作
const arr = [1,2,3];
arr.push(4); // 尾部添加
arr.unshift(0); // 头部添加
console.log(arr); // [0,1,2,3,4]
arr.pop(); // 弹出尾部元素
arr.shift(); // 弹出头部元素
console.log(arr); // [1,2,3]

在用数组实现栈之前我们先了解以下知识:

ES6引入的class语法为实现栈提供了优雅的解决方案。通过class,我们可以创建一个类模板,封装栈的所有属性和方法。

es6引入的constructor(构造方法)------ 它是 ES6 class唯一用于初始化实例的特殊方法 ,当通过 new 关键字创建类的实例时,constructor自动执行 ,核心作用是初始化实例的属性(尤其是私有属性,如下面代码中的#stack)。

es6 中,getset 是用于定义访问器属性(Accessor Property) 的语法,访问器属性不直接存储值,而是通过函数拦截对属性的读取(get)赋值(set) 操作,隐藏内部状态(封装性),从而实现对属性访问的精细化控制。

数组实现栈代码

复制代码
 // 数组来实现stack
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;
    }
}

代码相关解释

  1. 该类基于数组实现了栈的核心功能:入栈(push)、出栈(pop)、查看栈顶(peek)、获取大小(size)、判断为空(isEmpty);
  2. #stack:ES6 新增的私有字段(Private Field) ,前缀#表示该属性只能在类内部访问 / 修改,外部无法直接操作(比如stack.#stack会报错),保证了栈的封装性(避免外部随意修改栈的底层数据);
  3. constructor():类的构造函数,创建ArrayStack实例时自动执行。初始化私有属性#stack为一个空数组,作为栈的底层存储结构 (数组的 "末尾" 对应栈的 "栈顶",因为数组的push/pop操作时间复杂度为 O (1),效率最高)。

数组实现的栈非常直观,利用了JavaScript数组的pushpop方法,实现了栈的核心功能。

三、链表实现栈

链表实现栈需要自定义节点和栈结构。

链表实现栈代码

复制代码
// 链表来实现栈
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++;
    }
    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;
    }
}

代码相关解释:

  1. 采用单向链表实现栈,核心逻辑是将链表头节点作为栈顶 ------ 链表头增删节点仅需修改指针(时间复杂度 O (1)),完美适配栈 "后进先出(LIFO)" 的核心操作要求。

  2. 节点类(ListNode)作用:定义无序列表的最小存储单元,val存储栈元素值,next仅用于关联后继节点(离散存储),无任何排序相关逻辑,是实现无序列表的基础。

  3. 入栈方法(push):创建新节点→新节点next指向原栈顶→更新栈顶指针为新节点→栈大小 + 1,全程仅修改指针,无遍历操作,效率 O (1)。

  4. 出栈方法(pop):复用peek完成空栈校验和栈顶值获取→栈顶指针后移(原栈顶节点被垃圾回收)→栈大小 - 1→返回栈顶值,效率 O (1)。

  5. 栈顶查看方法(peek):先校验栈是否为空(空栈抛错),再返回栈顶节点的val值(仅读取,不修改栈结构),对外屏蔽无序列表的底层细节。

  6. 辅助属性 / 方法:

    size:通过 ES6 访问器属性(get)定义为只读属性,外部可获取但无法赋值篡改;

    isEmpty:基于size判断栈是否为空,语义清晰且无需重复编写空栈校验逻辑。

链表实现栈的关键在于使用#stackPeek作为栈顶指针,通过链表的链接关系实现栈的操作。

四、数组与链表实现栈的优缺点比较

时间效率

数组实现

  • 入栈和出栈操作在尾部进行,时间复杂度为O(1)
  • 当数组容量不足时,触发扩容(复制所有元素到新数组),时间复杂度为O(n)
  • 但扩容是低频操作,平均时间复杂度仍为O(1)

链表实现

  • 入栈操作需要创建新节点,时间复杂度为O(1)
  • 无需扩容,始终能保持O(1)的时间复杂度
  • 但链表节点需要额外操作(指针设置),在入栈时略慢于数组

空间效率

数组实现

  • 可能造成空间浪费(如分配了100个元素空间,但只使用了50个)
  • 但内存是连续的,缓存命中率高,访问效率更好

链表实现

  • 每个节点需要额外空间存储指针(next引用)
  • 内存不连续,缓存命中率较低
  • 但不会造成空间浪费,空间利用率更高

五、栈的实践:括号匹配问题

栈在括号匹配问题中展现出了其独特优势。这个问题要求判断字符串中的括号是否正确闭合。

复制代码
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 {
            if(!stack.length || stack.pop() != ch) {
                return false;
            }
        }
    }
    return stack.length === 0;
}

这个实现利用了栈的FILO特性:

  1. 遇到左括号时,将对应的右括号压入栈
  2. 遇到右括号时,弹出栈顶元素进行匹配
  3. 最终栈为空表示所有括号都正确闭合

例如,对于字符串"({[]})"

  • ( → 压入)
  • { → 压入}
  • [ → 压入]
  • ] → 弹出],匹配
  • } → 弹出},匹配
  • ) → 弹出),匹配
  • 栈为空,返回true

六、总结

栈作为计算机科学中最基础、最常用的数据结构之一,其简洁性和高效性使其成为解决许多编程问题的首选工具。通过ES6的类特性,我们可以更清晰、更安全地实现栈,同时理解其背后的原理和适用场景。

在实际应用中,根据具体需求选择数组或链表实现栈:

  • 数组实现:适合数据量相对稳定、对性能要求高且内存空间有限的场景
  • 链表实现:适合数据量不确定、需要频繁动态扩展的场景

栈的实践应用远不止括号匹配问题,它在函数调用栈、表达式求值、浏览器历史记录管理等众多领域都有广泛应用。理解栈的本质------"后进先出"的特性,以及如何在不同场景下选择合适的实现方式,将使你在编程中更加得心应手。

相关推荐
DevUI团队1 小时前
Angular开发者必看:深度解析单元测试核心技巧与最佳实践
前端·javascript·angular.js
哭泣方源炼蛊2 小时前
HAUE 新生周赛(七)题解
数据结构·c++·算法
q***64972 小时前
SpringMVC 请求参数接收
前端·javascript·算法
小此方2 小时前
从零开始手搓堆:核心操作实现 + 堆排序 + TopK 算法+ 向上调整 vs 向下调整建堆的时间复杂度严密证明!
开发语言·数据结构·算法
向葭奔赴♡2 小时前
若依系统权限控制全流程解析
前端·javascript·vue.js·ruoyi·navicat
艾小码3 小时前
Vue 3 defineProps 与 defineEmits 深度解析
前端·javascript·vue.js
sulikey4 小时前
深入讲解:什么是 RAII(资源获取即初始化)——原理、实现、面试常考点与实战示例
c++·面试·智能指针·raii·shared_ptr·auto_ptr·资源获取即初始化
前端炒粉6 小时前
35.LRU 缓存
开发语言·javascript·数据结构·算法·缓存·js