栈的世界:让我们“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 特性使得栈在括号匹配、递归调用、内存管理等领域都能大显身手。

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

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

相关推荐
不想当程序猿_3 分钟前
【蓝桥杯每日一题】扫描游戏——线段树
c++·算法·蓝桥杯·线段树·模拟
SoraLuna42 分钟前
「Mac畅玩鸿蒙与硬件46」UI互动应用篇23 - 自定义天气预报组件
开发语言·算法·macos·ui·华为·harmonyos
KaiPeng-Nie43 分钟前
代码随想录day22 | 回溯算法理论基础 leetcode 77.组合 77.组合 加剪枝操作 216.组合总和III 17.电话号码的字母组合
java·算法·leetcode·剪枝·回溯算法·回归算法·递归函数
打不了嗝 ᥬ᭄1 小时前
P8795 [蓝桥杯 2022 国 A] 选素数
算法·leetcode·职场和发展·蓝桥杯·图论
serenity宁静1 小时前
Focal Loss损失函数理解
人工智能·算法·机器学习
窜天遁地大吗喽1 小时前
abc 384 D(子数组->前缀和) +E(bfs 扩展的时候 按照数值去扩展)
算法
Liknana1 小时前
unique_ptr 智能指针
c++·算法
诗歌难吟4641 小时前
Marquee
javascript·数据结构
Sol-itude2 小时前
【项目介绍】基于机器学习的低空小、微无人机识别技术
人工智能·算法·机器学习·matlab·无人机
友培2 小时前
工业大数据分析算法实战-day08
算法·数据挖掘·数据分析