提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
本文选择题目的链接(均来源于leetcode):
用栈实现队列
用队列实现栈
设计循环队列
一、用栈实现队列
1.栈的简介
栈的特点是LIFO,即先进后出,删除元素和增添元素的操作只在一端进行,我们进行操作的位置被称为栈顶
在“栈”中增加元素的过程一般被称为“压栈”,删除元素的操作被称为“出栈”;
如果把栈类比为步枪的单价、元素比作子弹的话,那么“压栈”就是在弹夹里放入子弹,“出栈”则是把弹夹中的子弹打出;
一般用数组实现栈,这种栈的结构和顺序表很像
还不太熟悉栈的读者可以先了解一下后文的思路,有兴趣的话可以看一下下面的函数代码:
typedef int STDatatype;
typedef struct Stack
{
STDatatype* a;
int capacity;
int top;
}ST;
void StackInit(ST* ps)
{
ps->a = (STDatatype*)malloc(sizeof(STDatatype) * 4);
if (ps->a == NULL)
{
perror("malloc fail");
exit(-1);
}
ps->top = 0;
ps->capacity = 4;
}
void StackDestroy(ST* ps)
{
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
void StackPush(ST* ps, STDatatype x)
{
if (ps->top == ps->capacity)
{
STDatatype* tmp = (STDatatype*)realloc(ps->a, ps->capacity * 2 * sizeof(STDatatype));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->top] = x;
ps->top++;
}
void StackPop(ST* ps)
{
ps->top--;
}
STDatatype StackTop(ST* ps)
{
return ps->a[ps->top - 1];
}
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
2.实现队列的思路
题目链接:用栈实现队列
题目要求用两个栈实现队列,但并没有直接提供给我们栈的函数代码,所以首先要把前文的函数代码粘贴进去/doge
队列也因此要这样写:
typedef struct {
ST spush;//不可能期望只创建一个指针 指针就给你把指向的空间也创建好,所以这里不能用指针
ST spop;
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
StackInit(&obj->spush);
StackInit(&obj->spop);
return obj;
}
重点是出/入队列的函数改造怎么写:
假设我先在一个栈中依次插入1,2,3,4:
这时如果我要删除的话,按理来说因为我实现的是队列,所以“出队列”的元素应该是1,但正常出栈的话将会删除4,这时就需要另一个栈参与进来了:
首先存储这个栈的栈顶元素并插入进另一个队列当中,然后将栈顶元素删除:
再重复3次上述操作:
然后再对另一个元素进行出栈操作,就可以成功删除先进入“队列”的元素了,但是如果想插入新元素的话,只需要在左侧的栈插入元素就可以了
左侧的栈专门用于插入元素,代码中用spush表示;右侧的栈专门用于删除元素,,用spop表示
如果在进行“删除/插入”元素的过程中发现当前队列空了,那么就将另一栈的全部元素转移到当前队列当中
所以入队列的函数相对比较简单:
void myQueuePush(MyQueue* obj, int x) {
StackPush(&obj->spush, x);
}
考虑到有一个名为myQueuePeek的函数功能为返回队列开头的元素,我们可以先写出这个函数,把“转移元素”的操作写道这个函数里面,然后在出队列函数中进行复用:
int myQueuePeek(MyQueue* obj) {
if (StackEmpty(&obj->spop))//spop为空时,将spush的元素转移到spop中
{
while (!StackEmpty(&obj->spush))
{
StackPush(&obj->spop, StackTop(&obj->spush));
StackPop(&obj->spush);
}
}
return StackTop(&obj->spop);
}
出队列函数就可以简洁一点:
int myQueuePop(MyQueue* obj) {
int ret = 0;
ret = myQueuePeek(obj);
StackPop(&obj->spop);
return ret;//题目要求返回已删除的元素
}
3.其他函数代码
就剩两个函数了:
bool myQueueEmpty(MyQueue* obj) {
return (StackEmpty(&obj->spush) && StackEmpty(&obj->spop));
}//两个栈都为空时,队列为空
void myQueueFree(MyQueue* obj) {
StackDestroy(&obj->spush);
StackDestroy(&obj->spop);
free(obj);
}//依次释放两个栈以后,释放队列obj
二、用队列实现栈
1.队列的简介
和栈的“先进先出”不同,队列的特点是“先进先出”
其中插入元素的一段被称为“队尾”,删除元素的一段被称为“队头”,由于在使用数组时“出队列”的效率会低一点,所以队列一般用链表实现;
单链表的讲解可以看一下我的这篇博客,全文9000多字(包括代码),希望大家能救一下这个位数的阅读量
传送门
函数代码:
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* del = cur;
cur = cur->next;
free(del);
}
pq->head = pq->tail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
if (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(!QueueEmpty(pq));
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* del = pq->head;
pq->head = pq->head->next;
free(del);
}
pq->size--;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL && pq->tail == NULL;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
2.实现栈的思路
题目链接:用队列实现栈
题目要求使用两个队列,故技重施:
typedef struct
{
Queue p1;
Queue p2;
} MyStack;
先向其中一个队列依次插入元素1、2、3:
看起来一切正常,但如果想像栈一样删除元素3的话,也要把p1中的前几个元素全部转移到p2中,p1中剩余的最后一个元素直接删除,不进入p2:
注意队列是从队头删除,从队尾插入,所以转移到p2以后元素顺序不变
插入元素时,直接插入有元素的那个队列;删除元素时,先把现有队列除最后一个元素M以外的所有元素转移到另一个队列,然后删除元素M
体感上要比上一题的过程简单一点,代码:
void myStackPush(MyStack* obj, int x)
{
if(!QueueEmpty(&obj->p1))
{
QueuePush(&obj->p1,x);
}
else
{
QueuePush(&obj->p2,x);
}
}
int myStackPop(MyStack* obj)
{
int ret=0;
if(!QueueEmpty(&obj->p1))
{
while(QueueSize(&obj->p1)!=1)
{
QueuePush(&obj->p2,QueueFront(&obj->p1));
QueuePop(&obj->p1);
}
ret=QueueFront(&obj->p1);
QueuePop(&obj->p1);
}
else
{
while(QueueSize(&obj->p2)!=1)
{
QueuePush(&obj->p1,QueueFront(&obj->p2));
QueuePop(&obj->p2);
}
ret=QueueFront(&obj->p2);
QueuePop(&obj->p2);
}
return ret;
}
这次就不复用函数“StackTop”,感兴趣的话建议自己试一下
int myStackTop(MyStack* obj)
{
int ret=0;
if(!QueueEmpty(&obj->p1))
{
while(QueueSize(&obj->p1)!=1)
{
QueuePush(&obj->p2,QueueFront(&obj->p1));
QueuePop(&obj->p1);
}
ret=QueueFront(&obj->p1);
QueuePush(&obj->p2,QueueFront(&obj->p1));
QueuePop(&obj->p1);
}
else
{
while(QueueSize(&obj->p2)!=1)
{
QueuePush(&obj->p1,QueueFront(&obj->p2));
QueuePop(&obj->p2);
}
ret=QueueFront(&obj->p2);
QueuePush(&obj->p1,QueueFront(&obj->p2));
QueuePop(&obj->p2);
}
return ret;
}
3.其他函数代码
和上一题的函数长得“一模一样”
bool myStackEmpty(MyStack* obj) {
return (QueueEmpty(&obj->p1)&&QueueEmpty(&obj->p2));
}
void myStackFree(MyStack* obj) {
QueueDestroy(&obj->p1);
QueueDestroy(&obj->p2);
free(obj);
}
三、设计循环队列
题目链接:设计循环队列
“循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。”
这里我们需要设置两个指针:
其中下标为4的空间不存储元素
在插入元素时,先存储到下标为rear的部分中,然后rear向后移动一位;
删除元素时front向后移动一位
有一种方法是用环形链表做,但是这种方法不容易获取队尾元素,但可以通过增加新变量提前存储rear的前一个值来解决
除了直接写一个唤醒链表以外,我们还可以手动控制指针走到队列尽头时回到起点,达到类似于“环形”的效果,当然这种写法需要格外注意边界条件;
既然只是一个直线型的结构的话,也没必要坚持用链表写了,毕竟数组在处理数据的时候要更灵活一些;
唤醒列表的结构:
ypedef struct {
int *a;
int front;
int rear;
int k;
} MyCircularQueue;
题目要求创建一个长度为k的环形队列,按照我们前面的推理,一共需要创建(k+1)份空间:
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
obj->k=k;
obj->a=(int*)malloc(sizeof(int)*(k+1));
obj->front=obj->rear=0;
return obj;
}
入队列的过程很简单:首先判断队列是否为空,然后将a[rear]置为想要的值,然后rear++,但边界条件不太好想:
如果重复上面的操作,当rear走到队列尽头时,rear的值增加到5;
在这时我们需要把它置为0,也就是在rear++之后将rear%=(k+1),这样就可以保证rear在下标范围内变化时不变,超过时则返回起点,实现循环;
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(myCircularQueueIsFull(obj))
return false;
obj->a[obj->rear++]=value;
obj->rear%=(obj->k+1);
return true;
}
在进行出队列操作时,front向后移动时也要进行同样的操作:
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return false;
obj->front++;
obj->front%=(obj->k+1);
return true;
}
当rear等于front时,队列为空:
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return (obj->rear==obj->front);
}
当“(rear+k)%(k+1)等于front”时,队列为满,函数代码:
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return ( (obj->rear+1)%(obj->k+1)==obj->front);
}
返回队头队尾的函数非常简单,如果用循环链表写的话会非常麻烦:
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
return obj->a[obj->front];
}
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
return obj->a[(obj->rear+obj->k)%(obj->k+1)];
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
这里需要注意,如果你按顺序把上面的函数粘贴到题目里面,一定会出现报错的,原因是在前面的函数中我们就使用了“ 判空”和“判满”两个函数,但这两个函数却位于“增删”函数之后,记得把“ 判空”和“判满”两个函数剪切到最前面就好了