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

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

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

相关推荐
计算机小白一个2 小时前
蓝桥杯 Java B 组之设计 LRU 缓存
java·算法·蓝桥杯
万事可爱^2 小时前
HDBSCAN:密度自适应的层次聚类算法解析与实践
算法·机器学习·数据挖掘·聚类·hdbscan
大数据追光猿4 小时前
Python应用算法之贪心算法理解和实践
大数据·开发语言·人工智能·python·深度学习·算法·贪心算法
Dream it possible!5 小时前
LeetCode 热题 100_在排序数组中查找元素的第一个和最后一个位置(65_34_中等_C++)(二分查找)(一次二分查找+挨个搜索;两次二分查找)
c++·算法·leetcode
夏末秋也凉5 小时前
力扣-回溯-46 全排列
数据结构·算法·leetcode
南宫生5 小时前
力扣每日一题【算法学习day.132】
java·学习·算法·leetcode
柠石榴5 小时前
【练习】【回溯No.1】力扣 77. 组合
c++·算法·leetcode·回溯
Leuanghing5 小时前
【Leetcode】11. 盛最多水的容器
python·算法·leetcode
qy发大财5 小时前
加油站(力扣134)
算法·leetcode·职场和发展
王老师青少年编程5 小时前
【GESP C++八级考试考点详细解读】
数据结构·c++·算法·gesp·csp·信奥赛