❤️前言
大家好啊!今天的文章咱们一起在C语言的基础上学习数据结构中栈和队列的相关知识,我和你们一起努力,共同进步。
正文
1.栈
栈是一种特殊的线性表,它只允许在固定的一端进行进出数据,满足后进先出的性质。其中,进出端被我们称为栈顶,另一端被称为栈底。将数据由栈顶导入栈称为进栈/压栈/入栈,将数据从栈顶导出称为出栈。
我们可以把它看作一个像桶一样的东西,在不改变其他东西的情况下,我们放进去的东西一定在所有东西的顶端,而我们直接拿出桶中的东西也是从上面拿取。
我们可以选择使用数组或者链表去实现栈,但是相较于链表而言,数组尾插的代价更小,因此我们主要用数组来实现栈。
当然,我们用的数组最好是动态内存数组而不是静态的数组,因为动态存储的数组可以更合理地分配内存空间,不至于发生内存过于浪费或者不够的情况。
//下面是数组栈的实现
//定义栈的结构
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
//栈的初始化
void STInit(ST* ps)
{
assert(ps);
ps->a = (STDataType*)malloc(sizeof(STDataType) * 4);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
else
{
ps->capacity = 4;
ps->top = 0;//top表示栈顶元素的下一个元素的下标
}
}
//栈的销毁
void STDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->capacity = 0;
ps->top = 0;
}
//元素入栈
void STPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a,sizeof(STDataType) * (ps->capacity + 4));
if (tmp == NULL)
{
perror("STPush");
return;
}
else
{
ps->a = tmp;
ps->capacity += 4;
}
}
ps->a[ps->top] = x;
ps->top++;
}
//栈顶元素出栈
int STPop(ST* ps)
{
assert(ps);
if (!STEmpty(ps))
{
ps->top--;
return ps->a[ps->top];
}
else
{
printf("栈是空的\n");
return 0;
}
}
//栈的大小
int STSize(ST* ps)
{
assert(ps);
return ps->top;
}
//判断栈是否为空
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
//返回栈顶元素
STDataType STTop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
return ps->a[ps->top - 1];
}
2.队列
队列也是一个线性的数据结构,数据的进出遵循先进先出的规则,从队尾进数据,队头出数据。大致的结构就是这样:
队列的实现我们也可以使用数组或者链表,但是我们由队列的特殊性质来分析,数组的出队和入队不管是怎么选择方向,效率都比链表实现的队列更低,因此我们选择链表去实现队列。
//队列代码实现
//建立队列的结构
typedef int QDataType;
typedef struct QueueType
{
QDataType val;
struct QueueType* next;
}QueueType;
//我们在传参时如果需要传特别多的参数就可以定义一个新的结构
//这样就可以减少传参的个数了
//我们在实现顺序表时也做了相同的动作
typedef struct Queue
{
QueueType* head;
QueueType* tail;
int size;
}Queue;
//队列的初始化
void QueueInit(Queue* pq)
{
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
//队列的销毁
void QueueDestroy(Queue* pq)
{
assert(pq);
while (pq->head)
{
QueueType* tmp = pq->head;
pq->head = pq->head->next;
free(tmp);
}
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
//元素入队
//可以看作尾插
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QueueType* newnode = (QueueType*)malloc(sizeof(QueueType));
if (!newnode)
{
perror("malloc fail");
return;
}
newnode->val = x;
newnode->next = NULL;
if (pq->head == NULL)
{
pq->head = newnode;
}
else
{
pq->tail->next = newnode;
}
pq->tail = newnode;
pq->size++;
}
//元素出队
可以看作头删
QDataType QueuePop(Queue* pq)
{
assert(pq);
assert(pq->head);
QDataType ret = pq->head->val;
QueueType* tmp = pq->head;
pq->head = pq->head->next;
free(tmp);
if (pq->head == NULL)
{
pq->tail = NULL;
}
pq->size--;
return ret;
}
//返回队头元素
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->head);
return pq->head->val;
}
//判断队列是否为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
//返回队列大小
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
3.队列和栈的相互实现
(1)队列实现栈
栈和队列这两个线性的数据结构可以互相实现,现在我们来看用队列实现栈:
我们先思考栈和队列的两种不同特性,栈的元素先进后出(栈顶进出),队列的元素是先进先出(队尾进队头出),那么我们进行模拟出栈时就可以先将队尾之前的元素先转到另一个队列中,然后在模拟出栈最后的队尾元素,这样另一个队列就按原来的顺序排列了模拟栈的元素,而模拟入栈则直接在非空队列的队尾插入就可以了,具体步骤我按照图示的方式来为大家演示。
如图,我建立了两个队列,并向队列1中加入了四个数据1、2、3、4,这个过程就是模拟入栈的过程,但是根据队列的进出规则,我们不能直接在队列1中出队来达成模拟出栈的过程,那么我们现在根据队列先进先出、栈先进后出的规则进行队列模拟一次出栈,则先将队列1中的前三个元素直接导入(出队列1进队列2)队列2中,然后再进行4的出栈(出队)。这样,我们就完成了一次模拟出栈的过程。
需要注意的是,当我们完成一次模拟出栈之后,原本被我们放于队列1的未出栈数据完整地以原来的顺序转入到了队列2中,这样我们在下一次入栈和出栈时便能将现在的队列2看作之前的队列1进行后面的操作。
那么根据我们总结出的规律,我们在数据入栈时就应该选择从两队列中的非空队列进行入栈(因为如果两队列都可以入数据的话,栈中数据的顺序性就无法保持,当然,如果两队列都为空则随便选择一个队列放入数据即可,本质上我们只需要在出栈时保证有一个空的队列即可),而在出栈时,我们将非空队列中的前n-1个数据转移到空队列中,然后将最后一个元素出队列,这样便完成了出栈。由此可知,我们在设计的函数中处理数据时,应该设置一个非空队列和一个空队列,对这两个设置出来的队列进行操作,具体操作见下面的代码。
而在完成了出入栈的操作后,其他的函数对我们来讲便不再是问题了。
//用两个队列实现一个模拟的栈
//注:这下面使用的关于队列函数都来自上面我们自己实现的队列
//定义一个临时的结构体
typedef struct {
Queue que1;
Queue que2;
} MyStack;
//创建一个模拟栈
MyStack* myStackCreate() {
MyStack* stack = (MyStack*)malloc(sizeof(MyStack));
if (!stack)
{
perror("malloc fail");
return NULL;
}
QueueInit(&stack->que1);
QueueInit(&stack->que2);
return stack;
}
//模拟入栈
//我们应该在非空队列中进行入栈操作,
//但如果两队列都为空,那么我们就可以随便选择一个队列进行入栈操作
void myStackPush(MyStack* obj, int x) {
assert(obj);
if (QueueEmpty(&obj->que1))
{
QueuePush(&obj->que2, x);
}
else
{
QueuePush(&obj->que1, x);
}
}
//模拟出栈
//利用空队列和非空队列配合进行操作
int myStackPop(MyStack* obj) {
assert(obj);
Queue* emque = &obj->que1;
Queue* que = &obj->que2;
QDataType tmp = 0;
if (QueueEmpty(&obj->que2))
{
emque = &obj->que2;
que = &obj->que1;
}
while (QueueSize(que) > 1)
{
tmp = QueuePop(que);
QueuePush(emque, tmp);
}
tmp = QueuePop(que);
return tmp;
}
//返回栈顶元素
//我这里是利用模拟出栈完成返回栈顶元素,大家也可以选择利用返回栈顶元素完成出栈操作
int myStackTop(MyStack* obj) {
assert(obj);
int tmp = myStackPop(obj);
Queue* que = &obj->que1;
if (QueueEmpty(&obj->que1))
{
que = &obj->que2;
}
QueuePush(que, tmp);
return tmp;
}
//判断栈是否为空
bool myStackEmpty(MyStack* obj) {
assert(obj);
return obj->que1.size == 0 && obj->que2.size == 0;
}
//模拟栈的销毁
void myStackFree(MyStack* obj) {
assert(obj);
QueueDestroy(&obj->que1);
QueueDestroy(&obj->que2);
free(obj);
}
(2)栈实现队列
讲完了队列实现栈,我们现在再来讲用两个栈实现队列,用栈实现队列我们也需要利用栈和队列的两种不同的进出性质。那我们先在一个栈中按顺序压入1、2、3、4:
将数据压入栈中之后,我们便可以将1看作队头元素,4看作队尾元素,那么我们秉持着队列先进先出和栈先进后出的规则,必然是要先将stack1中的数据全部经过出入栈倒入stack2中,成为这样的结构:
这样,stack2的栈顶便可以被看作队头,我们可以使用stack2的出栈来模拟队列的队头出队,至此我们便完成了入队和出队的操作。而stack1的栈顶可以看作队尾,元素进栈后就作为stack2的候补元素,如果satck2中的元素出完了还需要继续出,那么我们就可以将stack1中的元素倒入stack2中,这时我们就我们完成了队列的基本功能,按照之前分析出的规律,我们在进行队列元素进出时可以将两个栈的功能进行很好的分工,一个作为入队栈,一个作为出队栈,按照上面的命名,这里就将stack1定义为入队栈pushst,stack2定义为出队栈popst,而在我们完成了队列的基本功能后,其他的函数则迎刃而解,详细的代码实现如下。
//栈模拟实现队列
//注:这下面使用的栈相关函数都来自我们自己实现的栈
//定义一个临时的结构体
typedef struct {
ST pushst;
ST popst;
} MyQueue;
//队列初始化
MyQueue* myQueueCreate() {
MyQueue* que = (MyQueue*)malloc(sizeof(MyQueue));
if(!que)
{
perror("myQueueCreate");
return NULL;
}
STInit(&que->pushst);
STInit(&que->popst);
return que;
}
//模拟元素入队列
void myQueuePush(MyQueue* obj, int x) {
assert(obj);
assert(obj->pushst.a && obj->popst.a);
STPush(&obj->pushst,x);
}
//模拟返回队列头元素
int myQueuePeek(MyQueue* obj) {
assert(obj);
if(STEmpty(&obj->popst))
{
while(!STEmpty(&obj->pushst))
{
int tmp = STPop(&obj->pushst);
STPush(&obj->popst, tmp);
}
}
return STTop(&obj->popst);
}
//模拟元素出队列
int myQueuePop(MyQueue* obj) {
assert(obj);
int ret = myQueuePeek(obj);
ret = STPop(&obj->popst);
return ret;
}
//模拟队列判空
bool myQueueEmpty(MyQueue* obj) {
assert(obj);
return STEmpty(&obj->popst) && STEmpty(&obj->pushst);
}
//模拟销毁队列
void myQueueFree(MyQueue* obj) {
assert(obj);
STDestroy(&obj->pushst);
STDestroy(&obj->popst);
free(obj);
}
4.实现循环队列
设计循环队列是leetcode上的一道队列题目,这道题目的难度评级为中等,我认为其中最大的难度并不在于如何去写代码,而是如何想到一个合适的方式去设计更为简单精妙的循环队列结构,如果只是去实现别人设计好的循环队列,我认为它只能被评为简单题。
首先,我们先前实现队列时使用的是链表,因为对于队列的元素进出,链表的消耗低于数组,那么我们可以优先先考虑使用链表去实现循环队列。而普通的链表无法实现循环队列的功能,我们需要的是限制长度的双向循环链表,以链表头做队头,链表尾做队尾,再在结构中加入一个size来统计队列中的元素个数,然后就可以很轻松地完成循环队列的所有功能。
然后我们再来看用数组实现循环队列,之前我们不使用数列去实现队列的原因是数组的头删/头插效率较低,而现在我们看循环队列的结构特征则与数组相辅相成,我们完全可以使用数组和指针相配合完成一个更好的循环队列具体思路和代码如下:
//首先定义一个临时循环队列结构体
typedef struct {
int* a;//代表队列所占空间的指针
int front;//队头指针
int rear;//队尾指针
int capacity;//队列总长
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* mcq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
mcq->a = (int*)malloc(sizeof(int)*(k+1));//分配数组空间
mcq->front = 0;//将队头和队尾指针都先初始化为0
mcq->rear = 0;
mcq->capacity = k+1;//记录数组大小
return mcq;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
//由于队列是循环的,那么rear指针的下一个位置为它加一并取模数组大小后的结果
//如果它的下一个位置为front,那么这个循环队列就是满的
return (obj->rear+1)%obj->capacity == obj->front;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(myCircularQueueIsFull(obj)) return false;//如果队列满了则返回假
obj->a[obj->rear] = value;//赋值队尾
obj->rear = (obj->rear+1)%obj->capacity;
return true;//成功插入则返回真
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
//我们令头尾指针相等时队列为空
//从这里我们就可以看到为什么要多申请一个节点的空间
//这样我们就可以很好地将队列地满状态和空状态分开
return obj->rear == obj->front;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)) return false;//队列为空删除失败
//front直接走向下一个位置,
//原来的队头数据则成为了无效数据
obj->front = (obj->front+1)%obj->capacity;
return true;
}
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)) return -1;
return obj->a[obj->front];
}
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)) return -1;
//在返回最后一个元素时,应当是返回rear之后一个位置地元素
int tmp = (obj->rear + obj->capacity - 1) % obj->capacity;
return obj->a[tmp];
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
🍀结语
栈和队列是基于顺序表和链表的延申,相信大家在学习了之前的知识以后再学栈和队列,以上就是我的栈和队列的博客内容啦,希望看博客的大家都能够天天开心,天天进步!咱们下次见啦!