栈与队列综合实战(一):用栈实现队列、用队列实现栈

各位同学,今天咱们来点硬核的。前面几章我们把栈和队列的底层原理、基本操作都捋了一遍。但说实话,光会写个入栈出栈,那只是入门水平。真正的嵌入式开发里,你经常会遇到一个尴尬的情况:手头只有栈的库,但项目里需要一个队列;或者只有队列的库,却要模拟栈的行为。

怎么办?自己重写一个?别急,我教你一招——用栈实现队列,用队列实现栈。这可不是炫技,这是实打实的工程技巧。

一、为什么需要这种“套娃”操作?

我在做一个小型RTOS项目时,遇到过这么个事:芯片的硬件FIFO只支持栈操作(后进先出),但我的任务调度器需要队列(先进先出)。重新写一个队列?资源不够。于是我就用两个栈,硬生生模拟出了一个队列。嗯,效果还不错。

说白了,这种技巧的核心思想就是:用两个相同的数据结构,通过“倒腾”数据,改变数据的进出顺序

核心结论:

  • 两个栈 → 一个队列(先进先出)
  • 两个队列 → 一个栈(后进先出)

二、用栈实现队列

2.1 思路解析

栈是后进先出,队列是先进先出。怎么用栈模拟队列?你想想看,如果我们把数据先压入栈A,再逐个弹出压入栈B,那么栈B的栈顶元素,不就是最早进入栈A的那个元素吗?

举个例子:

  • 入队顺序:1 → 2 → 3
  • 全部压入栈A:栈底[1,2,3]栈顶
  • 从栈A弹出,压入栈B:栈底[3,2,1]栈顶
  • 从栈B弹出:得到1(最早入队的元素)

完美实现了先进先出。

2.2 代码实现

#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 100

// 栈结构体
typedef struct {
    int data[MAX_SIZE];
    int top;
} Stack;

// 队列结构体(内部用两个栈)
typedef struct {
    Stack *stack_in;   // 入队栈
    Stack *stack_out;  // 出队栈
} Queue;

// 栈操作
void stack_init(Stack *s) {
    s->top = -1;
}

int stack_push(Stack *s, int val) {
    if (s->top >= MAX_SIZE - 1) return -1;
    s->data[++s->top] = val;
    return 0;
}

int stack_pop(Stack *s, int *val) {
    if (s->top < 0) return -1;
    *val = s->data[s->top--];
    return 0;
}

int stack_is_empty(Stack *s) {
    return s->top == -1;
}

// 队列操作
Queue* queue_create() {
    Queue *q = (Queue*)malloc(sizeof(Queue));
    q->stack_in = (Stack*)malloc(sizeof(Stack));
    q->stack_out = (Stack*)malloc(sizeof(Stack));
    stack_init(q->stack_in);
    stack_init(q->stack_out);
    return q;
}

void queue_enqueue(Queue *q, int val) {
    stack_push(q->stack_in, val);
}

int queue_dequeue(Queue *q, int *val) {
    // 如果出队栈为空,把入队栈的所有元素倒过来
    if (stack_is_empty(q->stack_out)) {
        int tmp;
        while (!stack_is_empty(q->stack_in)) {
            stack_pop(q->stack_in, &tmp);
            stack_push(q->stack_out, tmp);
        }
    }
    return stack_pop(q->stack_out, val);
}

int queue_is_empty(Queue *q) {
    return stack_is_empty(q->stack_in) && stack_is_empty(q->stack_out);
}

避坑指南:我曾经在项目中犯过一个低级错误——每次出队都把入队栈的数据全部倒腾到出队栈。其实只有在出队栈为空时才需要倒腾。否则会打乱顺序,而且浪费CPU时间。

三、用队列实现栈

3.1 思路解析

队列是先进先出,栈是后进先出。怎么用队列模拟栈?这里有个小技巧:每次入“栈”时,把新元素放到队列尾部,然后把前面的所有元素依次出队再入队。这样新元素就跑到队列头部了,出队时自然就是它先出来。

举个例子:

  • 当前队列:[1,2](1在队头,2在队尾)
  • 要压入3:先把3入队 → [1,2,3]
  • 然后把1和2依次出队再入队 → [2,3,1] → [3,1,2]
  • 现在队头是3,出队得到3(后进先出)

3.2 代码实现

#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 100

// 队列结构体
typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
    int count;
} Queue;

// 栈结构体(内部用两个队列)
typedef struct {
    Queue *q1;
    Queue *q2;
} StackByQueue;

// 队列操作
void queue_init(Queue *q) {
    q->front = 0;
    q->rear = 0;
    q->count = 0;
}

int queue_enqueue(Queue *q, int val) {
    if (q->count >= MAX_SIZE) return -1;
    q->data[q->rear] = val;
    q->rear = (q->rear + 1) % MAX_SIZE;
    q->count++;
    return 0;
}

int queue_dequeue(Queue *q, int *val) {
    if (q->count == 0) return -1;
    *val = q->data[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    q->count--;
    return 0;
}

int queue_is_empty(Queue *q) {
    return q->count == 0;
}

// 栈操作
StackByQueue* stack_create() {
    StackByQueue *s = (StackByQueue*)malloc(sizeof(StackByQueue));
    s->q1 = (Queue*)malloc(sizeof(Queue));
    s->q2 = (Queue*)malloc(sizeof(Queue));
    queue_init(s->q1);
    queue_init(s->q2);
    return s;
}

void stack_push(StackByQueue *s, int val) {
    // 先把新元素入队到q2
    queue_enqueue(s->q2, val);
    
    // 把q1的所有元素移到q2
    int tmp;
    while (!queue_is_empty(s->q1)) {
        queue_dequeue(s->q1, &tmp);
        queue_enqueue(s->q2, tmp);
    }
    
    // 交换q1和q2
    Queue *temp = s->q1;
    s->q1 = s->q2;
    s->q2 = temp;
}

int stack_pop(StackByQueue *s, int *val) {
    return queue_dequeue(s->q1, val);
}

int stack_is_empty(StackByQueue *s) {
    return queue_is_empty(s->q1);
}

注意:用队列实现栈时,每次push操作的时间复杂度是O(n),因为需要把前面的元素全部挪动一遍。如果你的应用场景是频繁push、偶尔pop,那这个方案就不太合适。我个人建议,如果push操作特别多,还是老老实实用真正的栈吧。

四、两种实现对比

对比项 栈实现队列 队列实现栈
核心操作 入队栈 + 出队栈 主队列 + 辅助队列
入队/入栈时间复杂度 O(1) O(n)
出队/出栈时间复杂度 均摊O(1) O(1)
空间复杂度 O(n) O(n)
适用场景 频繁入队,偶尔出队 频繁出栈,偶尔入栈

五、知识体系总览

下面这张图,把本章的核心逻辑串起来了。我建议你多看几遍,理解数据是怎么在两个结构之间“倒腾”的。

栈与队列互相实现 · 核心逻辑 用栈实现队列 入队:直接压入 stack_in 出队: → 如果 stack_out 为空 → 把 stack_in 全部弹出压入 stack_out → 从 stack_out 弹出 stack_in → stack_out 用队列实现栈 入栈: → 新元素入队到 q2 → 把 q1 所有元素移到 q2 → 交换 q1 和 q2 出栈:直接从 q1 出队 q1 ↔ q2 交换 核心思想:通过“倒腾”数据,改变数据进出顺序

六、实战中的坑与建议

我做了这么多年嵌入式,总结出几个经验:

  1. 不要滥用这种“套娃”实现。能用原生数据结构就用原生的,这种实现只是为了解决特定场景下的兼容问题。
  2. 注意内存管理。我见过有人用栈实现队列,结果忘记释放stack_out的内存,导致内存泄漏。嵌入式系统内存本来就紧张,这种错误很致命。
  3. 考虑多线程安全。如果你的队列或栈会被多个任务访问,记得加锁。我曾经在一个中断服务函数里直接操作队列,结果数据全乱了。
  4. 测试边界条件。空队列出队、满队列入队、只入不出、只出不入……这些情况都要覆盖到。

一句话总结:栈和队列互相实现,本质上是利用两个相同的数据结构,通过“倒腾”来改变数据的进出顺序。理解了这个核心思想,你就能灵活应对各种资源受限的嵌入式场景。


公众号:蓝海资料掘金营,微信deep3321