栈的世界:让我们“FILO”起来!

你以为栈只是个数学概念?错!它就在你身边!

大家好,欢迎来到我的博客!今天咱们来聊一聊栈(Stack)。栈听起来像是某个数学课上的抽象概念,但其实它无处不在,特别是在我们日常开发中,真的是让人又爱又恨的好伙伴。别担心,我不会让你听一堆枯燥的理论,而是通过实际的例子,让你明白栈到底有多酷。

栈的最经典特性就是 FILO ,也就是 First In, Last Out(先进后出)。你可以想象自己去超市买东西,拿了购物车放了一堆东西进去。结果到结账时,你发现最早放进去的那些货物反而被放到了最底下,只有最后拿的那瓶牛奶可以最先结账。所以,如果你想要买那个最早放进购物车的苹果,得先拿到牛奶。明白了吗?栈就是这么个顺序------最早放进去的最后拿出来。

好啦,开个玩笑,接下来我们用一些实际的 JavaScript 示例,带你真正理解栈是什么。


栈与类:一场面向对象的邂逅

在 JavaScript 的世界里,栈可以用数组(Array)或者链表(LinkedList)来实现。今天我们先聊聊 数组实现栈 ,然后再看看 链表实现栈 的优缺点。

数组版栈:简单、直接、快速

栈的实现不复杂,最简单的方式就是用 JavaScript 的数组。数组本身就提供了 pushpop 方法,所以你可以很方便地模拟栈的行为。看看下面的实现:

javascript 复制代码
const MinStack = function () {
  this.stack = [];  // 主栈
  this.stack2 = []  // 辅助栈,用来保存最小值
};

MinStack.prototype.push = function (x) {
  this.stack.push(x);
  // 如果栈2为空,或者当前值小于等于栈2顶端的最小值,压入栈2
  if (this.stack2.length === 0 || this.stack2[this.stack2.length - 1] >= x) {
    this.stack2.push(x);
  }
};

MinStack.prototype.pop = function () {
  if (this.stack.pop() === this.stack2[this.stack2.length - 1]) {
    this.stack2.pop();
  }
};

MinStack.prototype.top = function () {
  return this.stack[this.stack.length - 1];
};

MinStack.prototype.getMin = function () {
  return this.stack2[this.stack2.length - 1];
};

如何理解上面的代码呢?

  1. push(x) :每当我们将一个新元素压入栈时,我们不仅会将其加入主栈(stack),还会检查它是否应该成为当前的最小值。如果是,它就被压入辅助栈(stack2)中。这样,辅助栈就始终保持了最小元素。
  2. pop() :当我们弹出元素时,如果弹出的元素是当前的最小值(即和辅助栈的顶端元素一样),我们也从辅助栈中弹出该元素。
  3. top() :返回主栈的顶部元素,简单吧。
  4. getMin() :返回当前栈中的最小值,它就是辅助栈的顶部元素。这样,我们就可以在 O(1) 的时间内获取栈中的最小值!

数组实现栈的优缺点:

  • 优点:

    • O(1) 时间复杂度的 push()pop() 操作;
    • 可以直接通过 stackstack2 访问栈顶元素和最小值。
  • 缺点:

    • 数组会在达到一定大小时进行扩容,但扩容操作是 O(n) 的,因此在大规模数据的情况下可能会变慢。不过,这种情况出现得比较少。

链表版栈:动态的魔法!

不过,你可能会想,数组的实现虽然方便,但如果栈的大小一直增长怎么办?会不会浪费很多内存呢?这时候链表就显得格外有用。链表是一种动态的数据结构,内存不会浪费,而且每次只需分配存储当前节点所需的内存。

这里有一个基于链表实现的栈的例子:

javascript 复制代码
class LinkedListStack {
  #stackPeek; // 将头节点作为栈顶
  #stkSize = 0; // 栈的长度

  constructor() {
    this.#stackPeek = null;
  }

  get size() {
    return this.#stkSize;
  }

  isEmpty() {
    return this.size === 0;
  }

  push(num) {
    const node = new ListNode(num);
    node.next = this.#stackPeek;
    this.#stackPeek = node;
    this.#stkSize++;
  }

  pop() {
    const num = this.peek();
    this.#stackPeek = this.#stackPeek.next;
    this.#stkSize--;
    return num;
  }

  peek() {
    if (!this.#stackPeek) throw new Error('栈为空');
    return this.#stackPeek.val;
  }

  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;
  }
}

链表实现栈的优势:

  • 链表的 动态内存分配 让我们避免了数组扩容时可能遇到的性能瓶颈。
  • 每次 push()pop() 只需操作栈顶节点,不会像数组那样发生大规模数据的移动。

不过,链表实现栈的效率稍低一些,因为每次入栈都需要为节点分配内存,且操作的指针有一些额外开销。


栈的神奇应用:括号匹配问题

说到栈,你可能会好奇它到底能做什么有趣的事情。其实,栈在解决一些经典的算法问题中非常有用,譬如 括号匹配问题

想象一下,你有一个字符串,里面包含了各种括号:"()""[]""{}"。你需要判断这个字符串中的括号是否匹配,栈正是解决这个问题的利器。

括号匹配问题的解法:

javascript 复制代码
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;
};

这个算法怎么用栈解决呢?

  1. 我们先定义一个 leftToRight 映射,用来记录左括号和右括号的对应关系。

  2. 遍历字符串:

    • 如果遇到左括号("(""{""["),就把它对应的右括号压入栈中;
    • 如果遇到右括号,栈顶元素应该是它对应的左括号,如果不匹配,就返回 false
  3. 如果所有括号都匹配完了,栈应该是空的,如果栈里还有剩余的括号,说明匹配失败。

栈在括号匹配中的优势:

  • O(n) 时间复杂度:只需要遍历字符串一次,栈的操作是常数时间的;
  • O(n) 空间复杂度:最坏情况下,所有字符都是左括号,栈的大小为 n

总结:栈,这个看似简单却又极具魔力的数据结构

栈,不仅仅是一个基础的数据结构,它在很多经典的算法问题中发挥着巨大的作用。无论是通过数组实现栈,还是链表实现栈,栈都能帮助我们高效地解决问题。而且,它的 FILO 特性使得栈在括号匹配、递归调用、内存管理等领域都能大显身手。

所以,下次你遇到需要 "后进先出" 的问题时,记得召唤栈来帮忙!是不是感觉栈突然就变得高大上了?😎

如果你对栈有任何疑问,或者想了解更多的栈应用,欢迎留言,我会第一时间为你解答!

相关推荐
Navigator_Z22 分钟前
LeetCode //C - 1089. Duplicate Zeros
c语言·算法·leetcode
JAVA96524 分钟前
JAVA面试-并发篇 03-使用synchronized doublecheck实现单例有什么坑
java·单例模式·面试
小江的记录本2 小时前
【JVM虚拟机】堆内存分代模型:年轻代(Eden+Survivor)、老年代、元空间Metaspace(附《思维导图》+《面试高频考点清单》)
java·前端·jvm·后端·python·spring·面试
云泽8083 小时前
C++ 可调用对象通关指南:深度解析 Lambda 表达式、function 包装器与 bind 绑定器
开发语言·c++·算法
wlsh153 小时前
Go 迭代器
算法
语戚4 小时前
力扣 3161. 块放置查询:线段树解法(Java 实现)
java·算法·leetcode·面试·线段树·力扣·
天天进步20154 小时前
Python全栈项目实战:从零构建校园心理健康咨询平台
面试·职场和发展
CS创新实验室4 小时前
从顺序表到动态数组:数据结构的永恒基石与现代语言的优雅封装
数据结构·算法
Black蜡笔小新5 小时前
自动化AI算法训练服务器DLTM训推一体化平台助力农业生产管理实现安全智能化
人工智能·算法·自动化
JAVA社区6 小时前
Java高级全套教程(十一)—— Kubernetes 超详细企业级实战详解
java·运维·微服务·容器·面试·kubernetes