一.队列实现栈
队列遵循的是先入先出规则(First In First Out,FIFO),并且只能从队尾插入数据,从队头删除数据;而栈遵循的规则是(Last In First Out,LIFO),只能从栈顶入数据和出数据。这个题目要求我们使用两个队列来实现栈,这两种数据结构遵循的操作都不同,我们要怎么实现呢?
1.1栈的设计
我们先创建两个队列出来,并用一个结构体来表示,之后我们就可以利用该结构体来维护我们的栈。我们这里采用的是一个匿名结构体,将它重命名为MyStack。
typedef struct
{
Queue q1;
Queue q2;
} MyStack;
1.2栈的初始化
当我们对栈初始化时,其实也就是对这两个队列初始化而已。所以我们对栈初始化的时候,直接调用我们队列的初始化函数,对栈中的两个队列分别初始化就行。
//栈的初始化
MyStack* myStackCreate()
{
MyStack* ms = (MyStack*)malloc(sizeof(MyStack));
if(ms == NULL)
{
perror("mallco");
return NULL;
}
QueueInit(&(ms->q1));
QueueInit(&(ms->q2));
return ms;
}
1.3出栈
当我们完成了初始化之后,其实就创建好了两个空队列。
我们先假设队列1中插入了1 2 3 4四个数据,然后如果我们要出数据的话 ,根据栈的规则,应该删除的是最后插入的数据,也就是4。但是对队列来说,我们只能删除队头的数据也就是1,这怎么解决呢?
因为队列删除数据的性质,我们可以将队列1中的前size-1个数据插入到队列2中,然后队列1中剩余的那个数,就是我们需要删除的数据。
//删除数据
int myStackPop(MyStack* obj)
{
//删除数据首先要将非空队列的前size-1个元素复制到空队列中
//假设法
Queue* empty = &(obj->q1);
Queue* noempty = &(obj->q2);
if(!QueueEmpty(&(obj->q1)))
{
empty = &(obj->q2);
noempty = &(obj->q1);
}
//此时empty就是空队列,noempty就是非空队列
while(GetQueueSize(noempty)>1)
{
//将前size-1个元素复制到非空队列
QueuePush(empty,GetQueueFront(noempty));
QueuePop(noempty);
}
//此时,noempty中剩余的就是最后push的元素,即栈顶元素
int top = GetQueueFront(noempty);
QueuePop(noempty);
return top;
}
1.4压栈
当我们删除数据之后,数据所在的队列就会改变,但是队列中数据的顺序并不会改变,还是保持着先插入的在前面后插入的在后面。
那我们此时压栈的时候是往那个队列中插入呢?如果插入到队列1中,那么我们下一次删除数据的时候,怎么将一个队列的数据转移到另一个空队列中呢?
所以我们在压栈的时候要往非空队列里面插入数据,保证一直有一个空队列可以让我们进行导数据。所以我们需要用到队列的判空接口,以及插入接口。
//插入数据
void myStackPush(MyStack* obj, int x)
{
//往非空的队列里面插入
if(!QueueEmpty(&(obj->q1)))
{
QueuePush(&(obj->q1),x);
}
else
{
QueuePush(&(obj->q2),x);
}
}
1.5返回栈顶数据
首先,我们栈中的所有数据其实都储存在一个队列中。所以我们返回栈顶数据首先要找到非空队列。然后我们栈顶的数据其实就是队列的队尾数据。我们只需要在调用队列的获取队尾元素的接口,就可以拿到栈顶数据。
//返回栈顶数据
int myStackTop(MyStack* obj)
{
//栈顶数据在非空队列中
if(!QueueEmpty(&(obj->q1)))
{
return GetQueueBack(&(obj->q1));
}
else
{
return GetQueueBack(&(obj->q2));
}
}
1.6判空
判空非常简单,我们的栈是由两个队列组成的,所以只要两个队列都为空,那么栈就为空。
//判空
bool myStackEmpty(MyStack* obj)
{
return QueueEmpty(&(obj->q1)) && QueueEmpty(&(obj->q2));
}
1.7栈的销毁
我们的栈由两个队列组成,所以销毁栈之前,我们应该先销毁栈的下一级,也就是两个队列,销毁掉队列之后,再销毁掉维护栈的结构体。
//销毁栈
void myStackFree(MyStack* obj)
{
//销毁栈其实就是销毁构成栈的两个队列
QueueDestroy(&(obj->q1));
QueueDestroy(&(obj->q2));
free(obj);
}
二.栈实现队列
我们看到,这个题和上面题刚好相反。这道题用两个栈来实现队列。 栈只能从栈顶入数据栈顶出数据,并且遵循后入先出;队列从队尾进数据队头出数据,遵循先进先出。我们先分析:
所以对上面两种方法来说,只有判空是相同的,其他部分均不同。这里我们用两种方法都写一下。 为了下面好描述,我在声明一下:法一(导数据还原数据法),法二(设计插入栈和删除栈)。
2.1队列的设计、初始化及销毁
该队列由两个栈组成,所以队列我们可以用结构体来表示,用一个结构体指针来维护该队列。
//法一
typedef struct
{
Stack s1;
Stack s2;
} MyQueue;
//法二
typedef struct
{
Stack pushstack;
Stack popstack;
} MyQueue;
该队列的初始化其实就是对两个栈进行初始化而已,直接调用栈的初始化即可(法一法二初始化相同)。
//队列的初始化
MyQueue* myQueueCreate()
{
MyQueue* mq = (MyQueue*)malloc(sizeof(MyQueue));
StackInit(&(mq->s1));
StackInit(&(mq->s2));
return mq;
}
队列的销毁其实就是栈的销毁,调用栈的销毁函数,再释放掉维护队列的指针即可(法一法二销毁方法相同)。
//队列的销毁
void myQueueFree(MyQueue* obj)
{
StackDestory(&(obj->s1));
StackDestory(&(obj->s2));
free(obj);
obj = NULL;
}
2.2入队列
对于法一来说,我们入队列的方式就是向非空栈中压栈即可,我们判断哪个栈不为空,然后调用栈的插入方法
//入队尾
void myQueuePush(MyQueue* obj, int x)
{
//往非空的栈中入数据
if(!StackEmpty(&(obj->s1)))
{
//s1非空
StackPush(&(obj->s1),x);
}
else
{
//s2非空
StackPush(&(obj->s2),x);
}
}
方法二的话,我们只需要向插入栈中插入数据即可。
void myQueuePush(MyQueue* obj ,int x)
{
//直接向插入栈插入数据
StackPush(&(obj->pushstack),x);
}
2.3获取队头的数据
法一:获取队头的方式和删除队头的数据类似,还是先导数据,之后栈顶元素就是队头的元素,保存该数据,然后在将数据倒回去即可。chu
//返回队头元素
int myQueuePeek(MyQueue* obj)
{
//假设法,给出空栈和非空栈
MyQueue* empty = &(obj->s1);
MyQueue* noempty = &(obj->s2);
if(!StackEmpty(empty))
{
empty = &(obj->s2);
noempty = &(obj->s1);
}
//将数据拷贝到另一个栈中
while(GetStackSize(noempty))
{
StackPush(empty,GetStackTop(noempty));
StackPop(noempty);
}
//另一个栈中的栈顶元素就是队列的队头元素
int top = GetStackTop(empty);
//将数据重新拷贝回去
while(GetStackSize(empty))
{
StackPush(noempty,GetStackTop(empty));
StackPop(empty);
}
//返回队头元素
return top;
}
法二:如果我们想要获取队头的元素,可以先将插入栈中的元素导入到删除栈中,删除栈中的栈顶元素就是待返回的队头元素。 如果再想返回队头元素的话,直接返回删除栈栈顶的元素即可。如果删除栈为空,则再倒一次数据。
int myQueuePeek(MyQueue* obj)
{
//判断删除栈是否为空
if(StackEmpty(&(obj->poppush)))
{
//如果为空从插入栈导数据
while(!StackEmpty(&(obj->pushstack)))
{
StackPush(&(obj->popstack),GetStackTop(&(obj->pushstack)));
StackPop(&(obj->pushstack));
}
}
//删除栈不为空直接返回删除栈的栈顶元素
return GetStackTop(&(obj->popstack));
}
2.4出队列
法一:出队列的方式就是导数据,删除栈顶元素,再将剩余数据倒回去。
//从队头出数据
int myQueuePop(MyQueue* obj)
{
//假设法,给出空栈和非空栈
MyQueue* empty = &(obj->s1);
MyQueue* noempty = &(obj->s2);
if(!StackEmpty(empty))
{
empty = &(obj->s2);
noempty = &(obj->s1);
}
//将非空栈中的后size-1个数据拷贝到空栈中
while(GetStackSize(noempty)>1)
{
//1.将非空栈数据push到空栈中
StackPush(empty,GetStackTop(noempty));
//2.pop掉已经导入的数据
StackPop(noempty);
}
//记录并删除原栈中栈顶的数据
int top = GetStackTop(noempty);
StackPop(noempty);
//将数据重新复制到原栈中
while(GetStackSize(empty))
{
StackPush(noempty,GetStackTop(empty));
StackPop(empty);
}
return top;
}
法二:我们有专门的插入栈和删除栈,所以当我们需要出队列的时候,其实就是删除删除栈的栈顶元素。而我们上一个peek方法的作用就是返回队头元素的数据,所以我们只需要调用peek方法,将返回值删除即可。
int myQueuePop(MyQueue* obj)
{
int top = myQueuePeek(obj);
StackPop(&(obj->popstack));
return top;
}
2.5判空
法一法二的判空都是相同的,因为队列由两个栈组成,只要两个栈同时为空,那么队列就为空。
//法一
bool myQueueEmpty(MyQueue* obj)
{
return StackEmpty(&(obj->s1)) && StackEmpty(&(obj->s2));
}
//法二
bool myQueueEmpty(MyQueue* obj)
{
return StackEmpty(&(obj->pushstack)) && StackEmpty(&(obj->popstack));
}
三.循环队列
我们根据题目以及圈画的地方得出,循环队列是一个大小给定,队头和队尾连接, 遵循先进先出的特殊队列。题目已经给出了我们要实现的接口,我们现在分析一下如何实现循环队列。
我们普通队列大小是可以扩大的,队头和队尾没有相连接,但是也遵循先进先出的规则。
而我们循环队列的前提是固定了队列的大小,队头和队尾连接的特殊结构。
我们在实现普通队列时是借助单链表实现的,那么循环队列是否可以依托于单链表实现呢?其实是可以的,我们只需要将单链表中的尾节点和头节点连接起来了。但是用单链表的话,找尾就很麻烦。
这样的话,我们可以借助数组来实现循环链表。当我们刚创建好循环队列时,head和tail相等,此时队列为空。
当我们向里面插入数据后,使循环队列变满。
我们看到当队列变满的时候,tail此时指向了队尾的下一个元素,此时tail已经越界了,因为是循环队列,所以此时tail应该重新指向头部。
我们发现队列为空或者队列满的时候,head和tail都是相等的。那么我们在设计判空和判满接口的时候,就会出现歧义。为了解决这个问题,我们有两种方法:1.添加一个计数器size用来记录队列当前的有效数字个数,2.多开辟一个空间,避免head和tail重复。
法一很好理解,我们在这里讨论一下法二。
我们看到上面的分析,发现2式是满足1式的,所以我们2式可以用1式来表示,综上:当head == tail时,队列为空;当(tail+1) %(k+1)== head时,队列满。我们借助这两个表达式就可以完成循环队列判空和判满的接口设计了。
我们现在已经了解了循环队列的基本组成,下来我们来设计循环队列的结构。
3.1循环队列的结构以及初始化和销毁
我们上面分析要借助数组来实现循环队列,所以先要有一个数组,其次就是分表代表头和尾的指针,这里是数组,我们直接用下标,还有就是我们要知道循环队列的容量。
typedef struct
{
int *a;//维护队列的数组
int head;//指向头
int tail;//指向尾
int k;//循环队列的大小
} MyCircularQueue;
队列的初始化其实就是对组成循环队列这个结构体的成员变量的初始化。我们只需要根据接口的特征,对每个变量赋初值即可。
MyCircularQueue* myCircularQueueCreate(int k)
{
MyCircularQueue* mcq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
mcq->a = (int*)malloc(sizeof(int)*(k+1));
mcq->head = 0;
mcq->tail = 0;
mcq->k = k;
return mcq;
}
循环队列的销毁就是销毁队列中的数组,在紧接着销毁掉动态申请的结构体变量。
void myCircularQueueFree(MyCircularQueue* obj)
{
free(obj->a);
obj->a = NULL;
obj->head = 0;
obj->tail = 0;
obj->k = 0;
free(obj);
obj = NULL;
}
3.2循环队列判空和判满
我们在前面已经分析了该队列什么时候是满是什么是空,我们只需要将上面分析的过程写成代码即可。
//判空
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
return obj->head == obj->tail;
}
//判满
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
return (obj->tail+1) %(obj->k+1) == obj->head;
}
3.3循环队列入数据
我们前面说到,循环队列的大小是固定的,当循环队列满了之后,就不能再添加数据了。所以我们入数据的时候要先判断该队列是否满,满了就不能再入数据。
而且我们看到,其实我们的tail指向的是队尾元素的下一个位置,所以我们再插入的时候直接在tail位置上插入就可以了。但是为了防止tail溢出,所以我们再插入数据完成之后,对tail进行取余处理。
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
//判满,队列没满才可以插入
if(myCircularQueueIsFull(obj))
{
//队列满
return false;
}
else
{
//队列没满
//直接在tail位置上插入
obj->a[obj->tail++] = value;
}
//避免tail越界
obj->tail %= obj->k+1;
return true;
}
3.4循环队列出数据
循环队列出数据其实只需要移动head就行了,删除一个数据,head++就行。当然前提是队列不为空。同时head也有可能会越界,所以我们删除之后也要对head进行取模处理防溢出。
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
if(myCircularQueueIsEmpty(obj))
{
//如果队列为空,则无法删除,返回false
return false;
}
else
{
obj->head++;
}
obj->head %= obj->k+1;
return true;
}
3.5获取队头数据
我们循环队列在维护的时候就有一个head变量,表示队头数据的下标,所以要获取队头数据,我们直接访问该下标的元素即可。
int myCircularQueueFront(MyCircularQueue* obj)
{
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
else
{
return obj->a[obj->head];
}
}
3.6获取队尾数据
因为我们tail指向的其实队尾数据的下一个位置,所以我们要借助下标访问队尾数据时,要进行tail-1,但是如果tial此时为0,如果-1的话,就会变成-1,再访问的话,肯定会造成越界。
其实当tail为0时,此时队尾元素在下标为k的位置。
所以我们应该分别讨论tail的值,之后再来访问下标。
int myCircularQueueRear(MyCircularQueue* obj)
{
if(myCircularQueueIsEmpty(obj))
{
return -1;
}
else
{
return obj->tail == 0 ? obj->a[obj->k]:obj->a[obj->tail-1];
}
}