前言:
当今信息化时代,数据结构作为计算机科学的基础学科之一,无疑具有重要的地位和作用。在日常开发和算法设计中,我们经常需要运用各种数据结构来解决问题。而其中最常用的两种数据结构就是栈和队列。本篇博客将介绍如何用栈实现队列以及用队列实现栈,希望能够帮助读者深入理解栈和队列的概念,掌握它们之间的相互转换方法,从而更加灵活地运用它们来解决实际问题。
![](https://i-blog.csdnimg.cn/blog_migrate/4717000acbf9b16f80f7f1c0c3c75d51.png)
一,栈:
定义
栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
图示:
根据栈后入先出(LIFO)的特点,我们可以将它看作一个桶,我们只能再最顶层添加元素,也只能移走最顶层的元素
![](https://i-blog.csdnimg.cn/blog_migrate/d26a2bf4a771df05e98bfb92227ee277.jpeg)
“栈溢出”:需要注意的是栈是计算机暂时存储数据的地方,空间是有限的,当栈满时再有元素入栈就会发生上溢。当栈空时再出栈就会导致下溢。
实现:
我们可以用链表或数组来实现栈的结构,两者的优劣如下:
数组能做到随机的快速(通过下标)的访问元素,查询速度快,但当涉及到元素的增删时需要在数组空间中移动大量的数据而导致效率低下。
链表只支持顺序访问,由于需要遍历,导致查找元素效率低,但是增删元素快速。
由于我们模拟栈主要涉及元素的插入和删除,下文我将使用链表来实现栈。
头文件的引用:
#include <stdio.h>
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
节点的定义:
typedef int StackDataType;
typedef struct Stack
{
int* a;
int top;
int capacity;
}Stack;
各接口的声明:
void StackInit(Stack* ps);//栈的初始化
void StackDestroy(Stack* ps);//栈的销毁
void StackPush(Stack* ps, StackDataType x);//入栈
void StackPop(Stack* ps);//出栈
int StackTop(Stack* ps);//返回栈顶元素
bool StackEmpty(Stack* ps);//判断栈是否为空
void StackPrint(Stack* ps);//栈元素的打印
栈的初始化
void StackInit(Stack* ps)
{
assert(ps);//assert的作用是判断ps是否不为空,下文作用相同
ps->a = (StackDataType*)malloc(sizeof(StackDataType) * 4);
if (ps->a == NULL)//当ps->a == NULL时即为扩容失败
{
perror("malloc fail");
return;
}
ps->capacity = 4;//初始化容量为4
ps->top = 0;
}
栈的销毁
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->a);//释放ps->a的空间
ps->a = NULL;//指针置空
ps->top = 0;
ps->capacity = 0;
}
3.入栈
入栈分两种情况,一种是栈没满,这时只需要加入栈顶元素然后ps->top++即可;另一种是栈满了,此时需要先扩容。
void StackPush(Stack* ps, StackDataType x)
{
assert(ps);
if (ps->top == ps->capacity)//扩容过程
{
StackDataType* tmp = (StackDataType*)realloc(ps->a, sizeof(StackDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->top] = x;//top是从0开始的
ps->top++;
}
![](https://i-blog.csdnimg.cn/blog_migrate/5baf989254b1bcd8aa0008d9bd916fb7.jpeg)
4.出栈
出栈的过程可以通过缩小栈的大小来完成,因为size减小时无法访问原栈顶元素,类似于将栈顶元素移出栈。
void StackPop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));//这里需要判断栈是否已空防止栈溢出
ps->top--;
}
5.返回栈顶元素
int StackTop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top - 1];//之所以是ps->top-1前面在入栈时已经说明,下文相同
}
6.判断栈是否为空
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
7.栈元素的打印
void StackPrint(Stack* ps)
{
assert(ps);
int i = ps->top-1;
while (i>=0)
{
printf("%d\n", ps->a[i]);
i--;
}
}
效果演示:
![](https://i-blog.csdnimg.cn/blog_migrate/8fd90ee6c8e65f9c6bcf6c033d0a6adf.png)
二,队列:
定义:
队列(Queue)是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
图示:
根据队列先入先出(FIFO)的特点,我们可以将它看作高速公路上的收费站,各元素需要排好队,先入队列的先出队列。
![](https://i-blog.csdnimg.cn/blog_migrate/88b2018f6bfbc41b740c5b11dbe32b15.jpeg)
实现:
我们同样可以使用数组或链表来实现队列,究竟选哪种,先让我们来看看两者的优缺点。
数组实现队列的优点:
数组实现队列的代码相对简单易懂,不需要涉及指针等概念。
数组的存储方式是连续的,因此访问效率较高。
数组实现队列的缺点:
数组需要预先定义好大小,且不能动态调整大小,如果队列需要存储的元素数量超过了数组大小,就会发生溢出错误。
在删除队列元素时,需要移动整个数组中的元素,效率较低。
链表实现队列的优点:
链表可以动态分配内存,可以适应不同大小的队列。
在删除队列元素时,只需要改变链表指针的指向,不需要移动整个链表,效率较高。
链表实现队列的缺点:
链表的存储方式是不连续的,访问效率较低。
链表实现队列的代码相对较复杂,需要涉及指针等概念,容易出现指针问题导致程序出错。
综上所述,如果队列元素数量不大且固定,且需要访问效率高,可以选择使用数组实现队列;如果队列元素数量不确定,且需要删除操作频繁,可以选择使用链表实现队列。当然,在实际情况中,也可以根据具体场景进行综合考虑和权衡,选择最合适的实现方式。
由于我们的重点仍然是增删操作,下文我仍然使用链表来实现队列。
头文件的引用:
#include <stdio.h>
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
节点的定义:
typedef int QueueDataType;
// 定义队列结点
typedef struct Node
{
QueueDataType data;
struct Node *next;
} Node;
// 定义队列结构体
typedef struct Queue
{
Node *front; // 队头指针
Node *rear; // 队尾指针
} Queue;
各接口的声明:
Queue* QueueCreate();//队列的初始化
void QueueDestroy(Queue *q);//队列的销毁
void QueuePush(Queue *q, QueueDataType data) ;//入对
QueueDataType QueuePop(Queue *q);//出对
bool QueueIsEmpty(Queue *q);//判断队列是否为空
void QueuePrint(Queue *q);//队列元素的打印
队列的初始化
Queue* QueueCreate()
{
Queue *q = (Queue*)malloc(sizeof(Queue));
if (q == NULL)
{
printf("Memory allocation failed.\n");
return NULL;
}
q->front = q->rear = NULL;
return q;
}
2.队列的销毁
void QueueDestroy(Queue *q)
{
assert(q);
Node *temp;
while (q->front != NULL)
{
temp = q->front;
q->front = q->front->next;
free(temp);
temp=NULL;
}
free(q);
q=NULL;
}
3.入队
void QueuePush(Queue *q, QueueDataType data)
{
Node *node = (Node*)malloc(sizeof(Node));
if (node == NULL)
{
printf("Memory allocation failed");
return;
}
node->data = data;
node->next = NULL;
if (q->front == NULL)
{
q->front = node;
q->rear = node;
}
else
{
q->rear->next = node;
q->rear = node;
}
}
4.出队
出队时需要判断对头是否为空,以及出队后队列是否为空(若出对后为空队列则队尾要置空)
int QueuePop(Queue *q)
{
assert(q);
if (q->front == NULL)
{
printf("Queue is empty.\n");
return -1;
}
int data = q->front->data;
Node *temp = q->front;
q->front = q->front->next;
free(temp);
temp = NULL;
if (q->front == NULL)
{
q->rear = NULL;
}
return data;
}
5.判断队列是否为空
bool QueueIsEmpty(Queue *q)
{
assert(q);
return q->front == NULL;
}
6.队列元素的打印
void QueuePrint(Queue *q)
{
assert(q);
if (QueueIsEmpty(q))
{
printf("Queue is empty.\n");
return;
}
Node *node = q->front;
while (node != NULL)
{
printf("%d ", node->data);
node = node->next;
}
printf("\n");
}
效果演示:
![](https://i-blog.csdnimg.cn/blog_migrate/f0bd2888d6b6401e97cd3786108ea38d.png)
三,用栈实现队列:
题目参考:力扣232. 用栈实现队列
由于栈只能从顶部入栈和出栈,想用栈模拟队列就需要两个栈,一个用来模拟队列尾部入队列,一个用来模拟队列头部出队列。如下图所示,入队顺序为1-2-3,出队顺序也为1-2-3。
![](https://i-blog.csdnimg.cn/blog_migrate/c55e53aa7506279f94a6f4f6043c22dd.jpeg)
代码:
为了避免代码太多造成阅读困难,下面我将代码分为4个文件,需要使用时可通过头文件"myQueue.h"调用,代码如下:
myQueue.h
#pragma once
#include<stdbool.h>
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include"Stack.h"
typedef struct {
Stack pushst;
Stack popst;
} MyQueue;
MyQueue* myQueueCreate();//队列的创建(由入入栈和出出栈两个栈组成)
void myQueuePush(MyQueue* obj, int x);//将元素X推到队列的末尾
int myQueuePop(MyQueue* obj);//从队列的开头移除并返回元素
int myQueuePeek(MyQueue* obj);//返回队列开头的元素
bool myQueueEmpty(MyQueue* obj);//判断队列是否为空
void myQueueFree(MyQueue* obj);//释放队列
2. Stack.h
#pragma once
#include"myQueue.h"
typedef int StackDataType;
typedef struct Stack
{
int* a;
int top;
int capacity;
}Stack;
void StackInit(Stack* ps);
void StackDestroy(Stack* ps);
void StackPush(Stack* ps, StackDataType x);
void StackPop(Stack* ps);
int StackTop(Stack* ps);
bool StackEmpty(Stack* ps);
3. Stack.c
该文件下各函数都时上文再栈的模拟实现中已经完成了的,我们可以直接用,下面的代码仅仅是将各函数总结到一起而已
#include "myQueue.h"
void StackInit(Stack* ps)
{
assert(ps);
ps->a = (StackDataType*)malloc(sizeof(StackDataType) * 4);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->capacity = 4;
ps->top = 0;
}
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
void StackPush(Stack* ps, StackDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
StackDataType* tmp = (StackDataType*)realloc(ps->a, sizeof(StackDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->top] = x;
ps->top++;
}
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
void StackPop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
ps->top--;
}
int StackTop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top - 1];
}
4. myQueue.c
#include"myQueue.h"
MyQueue* myQueueCreate()
{
MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
if (obj == NULL)
{
perror("malloc fail");
return NULL;
}
StackInit(&obj->pushst);//入入栈初始化
StackInit(&obj->popst);//出出栈初始化
return obj;
}
void myQueuePush(MyQueue* obj, int x)
{
StackPush(&obj->pushst, x);
}
int myQueuePeek(MyQueue* obj)
{
if (StackEmpty(&obj->popst))
{
//倒数据
while (!StackEmpty(&obj->pushst))
{
StackPush(&obj->popst, StackTop(&obj->pushst));
StackPop(&obj->pushst);
}
}
return StackTop(&obj->popst);
}
bool myQueueEmpty(MyQueue* obj)
{
return StackEmpty(&obj->pushst) && StackEmpty(&obj->popst);
}
int myQueuePop(MyQueue* obj)
{
int front = myQueuePeek(obj);
StackPop(&obj->popst);
return front;
}
void myQueueFree(MyQueue* obj)
{
StackDestroy(&obj->pushst);
StackDestroy(&obj->popst);
free(obj);
}
效果演示:
![](https://i-blog.csdnimg.cn/blog_migrate/332bba1b39298fe6160154de53a9c2c0.png)
四,用队列实现栈:
题目参考:力扣225. 用队列实现栈
用队列实现栈,关键是如何实现栈”后入先出“的特点,这里我们额外借助一个空队列用于移出先入的元素,那么在原队列中剩下的一个元素即为栈顶元素,移除栈顶元素后我们又得到了一个空队列,重复上面的操作就可以不断的移出栈顶元素,这样就可以实现用队列实现栈
![](https://i-blog.csdnimg.cn/blog_migrate/b2e5eb6771be5489a142981e45c73f63.jpeg)
代码:
myStack.h
#include<stdio.h>
#include<stdbool.h>
typedef int QDatatype;
typedef struct QueueNode {
struct QueueNodef* next;
QDatatype data;
}QNode;
typedef struct Quenue {
QNode* head;
QNode* tail;
int size;
}Queue;
typedef struct {
Queue q1;
Queue q2;
} MyStack;
MyStack* myStackCreate();//栈的创建
void myStackPush(MyStack* obj, int x);//将元素X压入栈顶
int myStackPop(MyStack* obj);//移除并返回栈顶元素
int myStackTop(MyStack* obj);//返回栈顶元素
bool myStackEmpty(MyStack* obj);//判断栈是否为空,空返回true
void myStackFree(MyStack* obj);//释放栈
Queue.h
#include<stdio.h>
#include<assert.h>
#include"myStack.h"
void QueueInit(Queue* pq);//队列的初始化
bool QueueEmpty(Queue* pq);//判断队列是否为空
void QueuePush(Queue* pq, QDatatype x);//进入队列
void QueuePop(Queue* pq);//删除
QDatatype QueueFront(Queue* pq);//先入元素出队列(仅复制)(队列头先出)
QDatatype QueueBack(Queue* pq);//后入元素出队列
void QueueDestroy(Queue* pq);//销毁队列
Queue.c
nclude "Queue.h"
#include "myStack.h"
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
pq->size = 0;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
void QueuePush(Queue* pq, QDatatype x)
{
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
if (pq->head == NULL)
{
assert(pq->tail == NULL);
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->head != NULL);
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
pq->size--;
}
QDatatype QueueFront(Queue* pq)
{
assert(pq);
assert(pq->head != NULL);
return pq->head->data;
}
QDatatype QueueBack(Queue* pq)
{
assert(pq);
assert(pq->tail != NULL);
return pq->tail->data;
}
QDatatype QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
pq->size = 0;
}
myStack.c
#include"myStack.h"
#include"Queue.h"
MyStack* myStackCreate() {
MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
if (pst == NULL)
{
perror("malloc fail");
return NULL;
}
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) {
Queue* emptyQ = &obj->q1;
Queue* nonemptyQ = &obj->q2;
if (!QueueEmpty(&obj->q1))
{
emptyQ = &obj->q2;
nonemptyQ = &obj->q1;
}
while (QueueSize(nonemptyQ) > 1)
{
QueuePush(emptyQ, QueueFront(nonemptyQ));//非空队列前(n-1)元素先入先出进入空队列。相当于拷贝,实际元素没出队列
QueuePop(nonemptyQ);
}
int top = QueueFront(nonemptyQ);
QueuePop(nonemptyQ);
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);
}
效果演示:
![](https://i-blog.csdnimg.cn/blog_migrate/8945fafc22f65f73354dc1b6f417942e.png)
小结:
本篇博客介绍了如何用栈实现队列以及用队列实现栈。首先,我们简单介绍了栈和队列的概念,特点和如何实现他们。然后,我们阐述了两种数据结构之间的转换方法。
在用栈实现队列的部分,我们通过两个栈的互相倒腾来实现队列的“先进先出”特点。在队列中,元素总是从队列的前端出队,而在栈中,元素总是从栈顶出栈。因此,我们可以用一个栈来作为入队列的栈,用另一个栈作为出队列的栈,通过将入队列的元素逐个压入入队列的栈中,然后再将入队列的栈中的所有元素逐个弹出并压入出队列的栈中,最后再从出队列的栈中弹出元素实现队列的“先进先出”。
在用队列实现栈的部分,我们使用两个队列的互相倒腾来实现栈的“后进先出”特点。在栈中,最后压入栈的元素最先出栈,而在队列中,最先进入队列的元素最先出队列。因此,我们可以用一个空队列来作为先入元素的中转站,通过移出原队列先入的元素,即可实现栈顶元素先出栈,然后原队列又变为了空队列,支持我们重复操作,实现了栈的”后入先出“。
总的来说,这两种数据结构的相互转换离不开对原结构的理解。通过对两种结构的相互转换,我们不仅可以更深入地理解栈和队列的概念,而且也可以更加灵活地运用它们来解决实际问题。