当前位置: 首页 > 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.dtcms.com/a/331623.html

相关文章:

  • 2025年商协会新运营模式,正在破局
  • NokoPrint:安卓平台上的全能打印解决方案
  • 软件测试之接口测试,接口自动化测试, request
  • 【FreeRTOS】刨根问底4: 优先级反转是啥?咋解决?
  • 系统升级部署中的常见问题与解决方案
  • 京东比价项目开发实录:京东API接口(2025)
  • AI Agent 为什么需要记忆?
  • 我的 LeetCode 日记:Day 37 - 解锁动态规划:完全背包问题
  • 深度解析 Vue 高阶技巧:提升工程化能力的实用方案
  • 使用EvalScope对GPT-OSS-20B进行推理性能压测实战
  • Flink中的水位线
  • STL容器详解:Vector高效使用指南
  • 高效Unicode字符表示:一种创新的词表构建策略分析
  • MCP智能化问答系统实现方案
  • K8S企业级应用与DaemonSet实战解析
  • 【车联网kafka】用钟表齿轮理解 Kafka 时间轮​(第七篇)
  • Java应用快速部署Tomcat指南
  • # 2025全球AI游戏市场研究报告:行业洞察与未来趋势
  • OpenAI 的浏览器将使用 ChatGPT Agent 来控制浏览器
  • 亚马逊FCF计划:破解高单价产品转化困局的金融杠杆
  • RH134 管理基本存储知识点
  • 考研408《计算机组成原理》复习笔记,第四章(1)——指令系统概念(指令字长、N地址指令、定长和变长操作码)
  • H.264编码格式详解:Annex-B vs AVCC
  • 14、Docker Compose 安装 Redis 集群(三主三从)
  • 嵌入式学习笔记--MCU阶段--DAY12实时操作系统rt_thread1
  • Cypher注入详解:原理、类型与测试方法
  • 使用免费API开发口播数字人
  • 数智化与全球化,双轮驱动艾芬达持续引领行业变革
  • 嵌入式 - Linux软件编程:进程
  • PIDGenRc函数中lpstrRpc的由来和InitializePidVariables函数的关系