当前位置: 首页 > news >正文

数据结构:用两个栈模拟队列(Queue Using 2 Stacks)

目录

第一步:分析本质矛盾——LIFO vs. FIFO

第二步:设计两个栈的角色分工

第三步:推导核心操作的实现逻辑

1. 入队 (Enqueue)

2. 出队 (Dequeue)

性能分析(摊还分析)

第四步:一步步实现代码


这是一个非常经典且有趣的问题。我们同样从第一性原理出发,推导如何只用两个“后进先出”的栈,来模拟一个“先进先出”的队列。

第一步:分析本质矛盾——LIFO vs. FIFO

我们手上有什么工具?

  • 两个栈 (Stack)

它们的行为是 LIFO (Last-In, First-Out),像一个死胡同,后进去的必须先出来。

放入顺序 1, 2, 3,取出顺序 3, 2, 1

我们的目标是什么?

  • 一个队列 (Queue):它的行为是 FIFO (First-In, First-Out),像一个通道,先进去的先出来。

放入顺序 1, 2, 3,取出顺序 1, 2, 3

核心矛盾:栈会颠倒元素的顺序,而队列需要维持元素的顺序。

如何解决这个矛盾?让我们从最基本的思想实验开始。

一个栈会把 1, 2, 3 变成 3, 2, 1。 如果我们能把 3, 2, 1 这个序列再颠倒一次,会得到什么?

3, 2, 1 颠倒后就是 1, 2, 3。这正是我们想要的队列顺序!

第一性推论:对一个序列进行两次颠倒,其结果等于维持原序列。

而我们手上有两个栈,一个栈可以实现一次颠倒。那么,用两个栈就能实现两次颠倒。

LIFO( LIFO(序列) ) = FIFO(序列)

这个推论,就是我们整个算法设计的灵魂。


第二步:设计两个栈的角色分工

既然要进行两次颠倒,我们可以给两个栈分配不同的角色:

所有 enqueue 操作都直接进入这个栈。它扮演了第一次颠倒的角色。

  1. inStack (输入栈):负责接收所有新加入的元素。

  2. outStack (输出栈):负责所有弹出的元素。

所有 dequeue 操作都从这个栈获取。它必须含有我们期望的、正确顺序的元素。


第三步:推导核心操作的实现逻辑

1. 入队 (Enqueue)

当一个新元素,比如 4,要加入队列时,我们应该怎么做?

为了将来能进行颠倒,我们必须先把它存起来。最直接的地方就是 inStack

Enqueue 操作逻辑:

  • 将新元素 pushinStack 中。

这个操作非常简单,时间复杂度永远是 O(1)。

【操作模拟 1:连续入队】

  1. enqueue(1) -> inStack 中压入 1inStack: [1]

  2. enqueue(2) -> inStack 中压入 2inStack: [2, 1]

  3. enqueue(3) -> inStack 中压入 3inStack: [3, 2, 1] (3在栈顶)

outStack 此时还是空的。


2. 出队 (Dequeue)

这是整个设计的精髓所在。根据 FIFO 原则,我们现在 dequeue,应该得到元素 1

1 在哪里?它在 inStack 的最底部。我们无法直接拿到它。

但是,根据我们的推论,outStack 应该是负责“出”的栈。所以我们应该尝试从 outStackpop 元素。

情况一:outStack 不为空。

如果 outStack 里已经有按正确顺序排列的元素,那么我们直接从 outStackpop 一个出来就行了。这是最理想的情况。

情况二:outStack 为空。

如果 outStack 是空的,我们不能直接返回“队列为空”,因为 inStack 里可能还有很多元素。

这个时候,就到了执行第二次颠倒的时刻了! 我们需要把 inStack 中的所有元素,全部“倒入” outStack

“倾倒”过程

  1. inStack 弹出一个元素 (例如 3)。

  2. 将这个弹出的元素压入 outStack

  3. 重复此过程,直到 inStack 为空。

【操作模拟 2:第一次出队】

当前状态: inStack: [3, 2, 1],  outStack: [] 我们需要 dequeue()

  1. 检查 outStack,发现是空的。

  2. 执行“倾倒”操作:

    • pop 3 from inStack, push 3 to outStack. inStack: [2, 1], outStack: [3]

    • pop 2 from inStack, push 2 to outStack. inStack: [1], outStack: [2, 3]

    • pop 1 from inStack, push 1 to outStack. inStack: [], outStack: [1, 2, 3] (1在栈顶)

  3. 倾倒完成。outStack 里的元素顺序已经是 1, 2, 3 (从顶到底),完全符合队列的顺序。

  4. 现在可以从 outStackpop 了。pop1。返回 1

Dequeue 操作逻辑总结

  1. 检查 outStack 是否为空。

  2. 如果不为空,直接从 outStackpop 并返回。

  3. 如果为空,则先将 inStack 的所有元素依次 poppushoutStack 中。

  4. 然后再从 outStackpop 并返回。

  5. 如果两个栈都为空,则说明整个队列为空。


性能分析(摊还分析)

enqueue 永远是 O(1)。 dequeue 有时是 O(1),有时是 O(N)(在需要“倾倒”时)。

但从长远来看,每个元素一生中最多只会:

  • pushinStack 一次。

  • popinStack 一次。

  • pushoutStack 一次。

  • popoutStack 一次。

对于 N 个元素,总的操作次数是 4N。

平均到每个元素的入队和出队操作上,其摊还时间复杂度 (Amortized Time Complexity) 是 O(1)。这是一个非常高效的设计。


第四步:一步步实现代码

首先,我们需要一个可用的栈。这里我们快速给出一个基础实现。

【代码实现 1:基础栈的实现】

#include <stdio.h>
#include <stdlib.h>#define STACK_CAPACITY 100typedef struct {int data[STACK_CAPACITY];int top; // 指向栈顶元素的下标
} Stack;Stack* createStack() {Stack* s = (Stack*)malloc(sizeof(Stack));s->top = -1; // -1 表示栈空return s;
}int isStackEmpty(Stack* s) {return s->top == -1;
}void push(Stack* s, int value) {if (s->top >= STACK_CAPACITY - 1) return; // 栈满s->data[++s->top] = value;
}int pop(Stack* s) {if (isStackEmpty(s)) return -1; // 错误值return s->data[s->top--];
}

现在,我们用这个栈来构建我们的队列。

【代码实现 2:队列结构体与创建】

// 队列的蓝图:由两个栈构成
typedef struct {Stack* inStack;Stack* outStack;
} QueueWithStacks;// 创建队列
QueueWithStacks* createQueue() {QueueWithStacks* q = (QueueWithStacks*)malloc(sizeof(QueueWithStacks));q->inStack = createStack();q->outStack = createStack();return q;
}

【代码实现 3:入队操作】

// 入队操作
void enqueue(QueueWithStacks* q, int value) {// 逻辑非常简单:直接压入 inStackpush(q->inStack, value);
}

【代码实现 4:出队操作与“倾倒”辅助函数】

// “倾倒”操作,这是一个内部辅助函数
void pourInToOut(QueueWithStacks* q) {// 仅在 outStack 为空时调用if (!isStackEmpty(q->outStack)) return;while (!isStackEmpty(q->inStack)) {push(q->outStack, pop(q->inStack));}
}// 出队操作
int dequeue(QueueWithStacks* q) {// 如果两个栈都为空,队列才是真的空if (isStackEmpty(q->inStack) && isStackEmpty(q->outStack)) {printf("出队失败:队列为空。\n");return -1; // 错误值}// 检查 outStack,如果为空,则执行倾倒pourInToOut(q);// 现在可以安全地从 outStack 弹出return pop(q->outStack);
}// 检查队列是否为空
int isQueueEmpty(QueueWithStacks* q) {return isStackEmpty(q->inStack) && isStackEmpty(q->outStack);
}

通过这个从“矛盾”到“原理”再到“实现”的推导过程,我们就用两个 LIFO 的栈,成功构建了一个功能正确且性能高效的 FIFO 队列。这个方案的巧妙之处在于,它不是在每次操作时都去维护队列的顺序,而是在必要时(当 outStack 为空时)才进行一次性的“批量”顺序调整,从而实现了整体的高效率。

http://www.lryc.cn/news/621237.html

相关文章:

  • 8.14网络编程——TCP通信基础
  • 【22-决策树】
  • 零基础-动手学深度学习-10.3. 注意力评分函数
  • 20道CSS相关前端面试题及答案
  • torch.nn中Sequential的使用
  • 【代码随想录day 20】 力扣 538.把二叉搜索树转换为累加树
  • CMake语法与Bash语法的区别
  • 扩展用例-失败的嵌套
  • 流式数据服务端怎么传给前端,前端怎么接收?
  • jenkins在windows配置sshpass
  • 设计模式笔记_行为型_状态模式
  • 【JavaEE】多线程 -- 线程状态
  • 纸箱拆垛:物流自动化中的“开箱密码”与3D视觉的智能革命
  • 面试题之项目中灰度发布是怎么做的
  • Flink on YARN启动全流程深度解析
  • 会议通信系统核心流程详解(底稿1)
  • Linux软件编程:进程和线程
  • C#面试题及详细答案120道(01-10)-- 基础语法与数据类型
  • Flink Stream API 源码走读 - socketTextStream
  • 2025H1手游市场:SLG领涨、休闲爆发,何为出海新航道?
  • 广告灯的左移右移
  • Day43 复习日
  • FPGA+护理:跨学科发展的探索(五)
  • Kotlin Data Classes 快速上手
  • 【深度学习】深度学习基础概念与初识PyTorch
  • 报数游戏(我将每文更新tips)
  • IPTV系统:开启视听与管理的全新篇章
  • 14 ABP Framework 文档管理
  • 【软考中级网络工程师】知识点之入侵防御系统:筑牢网络安全防线
  • SpringMVC(详细版从入门到精通)未完