在数据结构的世界里,栈和队列就像一对性格迥异的双胞胎。栈遵循LIFO(后进先出)原则,而队列则遵循FIFO(先进先出)原则。虽然它们的操作方式不同,但通过巧妙的算法设计,我们可以用栈实现队列的功能,也可以用队列模拟栈的行为。这种转换不仅是数据结构学习中的经典练习,在实际开发中也有其应用场景。
提示:理解这两种数据结构的互转,能帮助你更深入地掌握它们的本质特性,同时锻炼算法设计能力。
在实际开发中,我们可能会遇到一些特殊场景:
用栈实现队列的核心挑战在于:栈是后进先出,而队列是先进先出。我们需要找到一种方法,让最先进入的元素能够最先被取出。
解决方案是使用两个栈:
当需要出队时,如果pop栈为空,就将push栈的所有元素依次弹出并压入pop栈。这样,pop栈的栈顶元素就是最早进入push栈的元素。
c复制typedef int STDataType;
typedef struct stack {
STDataType* a;
int capacity;
int top;
} ST;
// 栈的初始化
void STInit(ST* pst) {
assert(pst);
pst->a = NULL;
pst->top = pst->capacity = 0;
}
// 栈的销毁
void STDestroy(ST* pst) {
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = 0;
pst->capacity = 0;
}
// 入栈操作
void STPush(ST* pst, STDataType x) {
assert(pst);
// 动态扩容处理
if (pst->capacity == pst->top) {
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("malloc fail!");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
// 出栈操作
void STPop(ST* pst) {
assert(pst);
assert(pst->top > 0);
pst->top--;
}
// 获取栈顶元素
STDataType STTop(ST* pst) {
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
// 判断栈是否为空
bool STEmpty(ST* pst) {
assert(pst);
return pst->top == 0;
}
// 获取栈中元素数量
int STSize(ST* pst) {
assert(pst);
return pst->top;
}
// 队列结构定义(用两个栈实现)
typedef struct {
ST pushst; // 用于入队操作的栈
ST popst; // 用于出队操作的栈
} MyQueue;
// 创建队列
MyQueue* myQueueCreate() {
MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
STInit(&(obj->pushst));
STInit(&(obj->popst));
return obj;
}
// 入队操作
void myQueuePush(MyQueue* obj, int x) {
STPush(&(obj->pushst), x);
}
// 获取队首元素
int myQueuePeek(MyQueue* obj) {
// 如果pop栈为空,将push栈的所有元素转移到pop栈
if (STEmpty(&(obj->popst))) {
while (!STEmpty(&(obj->pushst))) {
int top = STTop(&(obj->pushst));
STPush(&(obj->popst), top);
STPop(&(obj->pushst));
}
}
return STTop(&(obj->popst));
}
// 出队操作
int myQueuePop(MyQueue* obj) {
int front = myQueuePeek(obj); // 确保pop栈有元素
STPop(&(obj->popst));
return front;
}
// 判断队列是否为空
bool myQueueEmpty(MyQueue* obj) {
return STEmpty(&(obj->pushst)) && STEmpty(&(obj->popst));
}
// 释放队列
void myQueueFree(MyQueue* obj) {
STDestroy(&(obj->pushst));
STDestroy(&(obj->popst));
free(obj);
}
注意:虽然最坏情况下出队操作可能需要O(n)时间(当pop栈为空时),但每个元素最多被压入和弹出各一次,所以平均下来时间复杂度是O(1)。
用队列实现栈的核心思想是:通过两个队列的配合,使得最后进入的元素能够最先被取出。我们有两种主要实现方式:
双队列法:
单队列法:
本文展示的是双队列法的实现。
c复制typedef int QDatatype;
typedef struct QueueNode {
struct QueueNode* next;
QDatatype val;
} QNode;
typedef struct Queue {
QNode* phead;
QNode* ptail;
int size;
} Queue;
// 队列初始化
void QueueInit(Queue* pq) {
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
// 创建新节点
QNode* Buynode(QDatatype x) {
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL) {
perror("malloc fail");
return NULL;
}
newnode->val = x;
newnode->next = NULL;
return newnode;
}
// 获取队首元素
QDatatype QueueFront(Queue* pq) {
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
// 获取队尾元素
QDatatype QueueBack(Queue* pq) {
assert(pq);
assert(pq->ptail);
return pq->ptail->val;
}
// 入队操作
void QueuePush(Queue* pq, QDatatype x) {
assert(pq);
if (pq->ptail == NULL) {
pq->phead = pq->ptail = Buynode(x);
} else {
pq->ptail->next = Buynode(x);
pq->ptail = pq->ptail->next;
}
pq->size++;
}
// 出队操作
void QueuePop(Queue* pq) {
assert(pq);
assert(pq->size != 0);
if (pq->phead->next == NULL) {
free(pq->phead);
pq->ptail = pq->phead = NULL;
pq->size--;
} else {
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
pq->size--;
}
}
// 获取队列大小
int QueueSize(Queue* pq) {
assert(pq);
return pq->size;
}
// 判断队列是否为空
bool QueueEmpty(Queue* pq) {
assert(pq);
return pq->size == 0;
}
// 销毁队列
void QueueDestroy(Queue* pq) {
assert(pq);
QNode* cur = pq->phead;
while (cur) {
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
// 栈结构定义(用两个队列实现)
typedef struct {
Queue q1;
Queue q2;
} MyStack;
// 创建栈
MyStack* myStackCreate() {
MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&(pst->q1));
QueueInit(&(pst->q2));
return pst;
}
// 入栈操作
void myStackPush(MyStack* obj, int x) {
// 将元素压入非空队列
if (!QueueEmpty(&(obj->q1))) {
QueuePush(&obj->q1, x);
} else {
QueuePush(&obj->q2, x);
}
}
// 出栈操作
int myStackPop(MyStack* obj) {
// 把非空队列的前n-1个元素转移到空队列,然后弹出最后一个元素
assert(obj);
Queue* empty = &obj->q1;
Queue* nonempty = &obj->q2;
if (!QueueEmpty(&obj->q1)) {
empty = &obj->q2;
nonempty = &obj->q1;
}
// 转移元素
while (QueueSize(nonempty) > 1) {
QueuePush(empty, QueueFront(nonempty));
QueuePop(nonempty);
}
// 获取并弹出栈顶元素
int top = QueueFront(nonempty);
QueuePop(nonempty);
return top;
}
// 获取栈顶元素
int myStackTop(MyStack* obj) {
// 栈顶元素就是非空队列的队尾元素
if (!QueueEmpty(&obj->q1)) {
return QueueBack(&obj->q1);
} else {
return QueueBack(&obj->q2);
}
}
// 判断栈是否为空
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}
// 释放栈
void myStackFree(MyStack* obj) {
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
}
c复制void myStackPush_SingleQueue(MyStack* obj, int x) {
Queue* q = &obj->q1; // 只使用一个队列
int size = QueueSize(q);
QueuePush(q, x);
// 将前面的元素依次出队再入队,使新元素成为队首
for (int i = 0; i < size; i++) {
QueuePush(q, QueueFront(q));
QueuePop(q);
}
}
int myStackPop_SingleQueue(MyStack* obj) {
Queue* q = &obj->q1;
int top = QueueFront(q);
QueuePop(q);
return top;
}
| 操作 | 栈实现队列 | 队列实现栈 |
|---|---|---|
| 入队/入栈 | O(1) | O(1) |
| 出队/出栈 | 摊还O(1) | O(n) |
| 查看首元素 | 摊还O(1) | O(1) |
| 空间复杂度 | O(n) | O(n) |
用栈实现队列的适用场景:
用队列实现栈的适用场景:
内存泄漏:
空结构访问:
逻辑错误:
c复制void debugPrintQueue(MyQueue* obj) {
printf("Push Stack: ");
for (int i = 0; i < obj->pushst.top; i++) {
printf("%d ", obj->pushst.a[i]);
}
printf("\nPop Stack: ");
for (int i = 0; i < obj->popst.top; i++) {
printf("%d ", obj->popst.a[i]);
}
printf("\n");
}
c复制void testQueue() {
MyQueue* q = myQueueCreate();
myQueuePush(q, 1);
myQueuePush(q, 2);
assert(myQueuePeek(q) == 1); // 检查队首
assert(myQueuePop(q) == 1); // 检查出队
myQueuePush(q, 3);
assert(myQueuePop(q) == 2); // 检查第二次出队
assert(!myQueueEmpty(q)); // 检查非空
myQueueFree(q);
}
在实际编码中,理解这些数据结构转换的本质,能够帮助开发者更灵活地解决问题。特别是在资源受限的环境下,这种能力显得尤为重要。