目录
1.定义
栈是一种后进先出的结构,也就是我们俗称的Last in first out结构(LIFO)
它就像我们平时洗盘子,洗好的盘子会被放在最下面,那取盘子放进消毒柜里的时候,取的反而是
上面的盘子.
2.实现
我们前面已经学习过数组和链表两种存储数据的方式.
我们要采用哪种方式,去实现我们的栈呢?
链表的话,如果有一个top(栈顶指针),我们肯定设计栈顶为头指针,始终指向头结点,毕竟链表头
插和头删的效率才高.
数组的话,我们肯定就是采取动态开辟的方式,容量不够的时候,就直接扩容.
两者相较而言,其实都可以实现栈这种数据结构.
但相较于链表而言,数组实现在栈顶插入数据,显然要显得更加轻松和容易一些,同时,我们也知
道数组缓存利用率比链表更高,因此我们采用数组的方式去实现栈.
首先把改用的头文件包含一下
再把Stack(堆),里面的内容设计好.
可以发现,其实和我们的顺序表,栈几乎采取同样的设计,不过原本的size改变为栈顶top而已.
接下来,我们来看下栈有哪些函数操作.
1.StackInit(栈的初始化)
初始化可以采用先不动态申请一块空间,然后让我们的指针a指向它.这样在后续入栈的操作中,就
需要加入动态申请一块空间这一步操作.
这里采用另一种方式,也就是在初始化的时候,就把空间给开辟好,在入栈操作中,只要考虑扩容
问题即可.
在初始化时,我们还需要考虑一个问题,就是我们的top一开始设为多少.
如果设为0,则意味着没有元素的时候,top == 0;那top始终是指向栈顶元素的下一个位置.
如果设为-1,则意味着没有元素的时候,top == -1;那top始终是指向栈顶元素的当前位置.
这里我们实现采用top == 0的方式,top == -1也是类似操作.
2.DestroyStack(摧毁栈)
任务:1.s释放掉a指向的动态开辟的空间,把指针a置空
2.容量和top相应调整为初始值
3.StackEmpty(判断栈是否为空)
4.DestroyStack(入栈)
思考点:1.栈满扩容 2.top始终指向下一个位置,所以先赋值,再+1
5.StackPop(出栈)
思考点:栈为空的时候,无法出栈.
PS:不需要将原来栈顶元素改变,后续如果要进行入栈操作,会直接把原数据覆盖掉.
6.StackTop(取栈顶元素)
7.StackSize(栈长)
由于我们的top始终指向最后一个元素的后一个元素,所以它和我们栈的元素个数实际是相等的.
如果top == -1,则需要返回top + 1;
二.队列
队列和栈不同,是一个先进先出的数据结构.俗称First in first out.(FIFO)
它类似我们做核酸排队伍,先排队的,先做核酸.
同样我们考虑用数组还是链表去实现队列.
这次考虑就显得简单很多,假设用数组实现,那无论是头部还是尾部,只要是出列,那都要移动元
素,这个时间复杂度是O(n),显然效率不高,而如果用链表实现,则没有这种情况.
因此我们采用链表的方式去实现队列.
按照惯例,我们先包含相应的头文件.
然后完成队列的结构设计,队列要是一个链表,那首先我们就需要构造队列中的每一个结点.
我们重命名为QNode.
然后队列(结构体)包含队头,队尾指针,分别指向链表的头部和尾部.
为了便于链表长度的计算,不需要逐一结点遍历,我们在结构体Queue中加入size变量,用于记录链表的长度.
队列一般有下面函数需要实现,下面我们将逐一介绍.
1.QueueInit(队列初始化)
刚开始链表为空,则head 和 rear都指向NULL.
2.QueueDestroy(摧毁队列)
Queue是堆区开辟的,不能free掉,真正需要释放的空间是我们的队列,也就是我们的链表.
但我们不能直接free(Queue->head),链表的释放,是需要逐个结点,逐一进行释放.
3.QueuePush(入列)
入列实际就是构造链表.
那队列(链表)为空,和队列不为空,进行的操作显然不是相同的.
4.QueuePop(出列)
和链表删除元素也是相同的,链表为空,则无法出列.
那除此之外,是否还有什么细节需要注意呢?
假设我们不断出列,然后我们不断移动头指针head,那删除最后一个元素后,就会出现
尾指针不为空,头指针为空的情况.
但我们一个队列为空,头指针和尾指针就必须为空.
这会造成什么影响呢?
单就对QueuePush(入列)来说,就可能出现尾指针解引用的情况,导致程序崩溃.
因此,只剩一个结点的时候,需要单独进行判断.直接释放结点就可以.
5.QueueFront(取队头元素)
判断队列不为空后,直接返回头指针指向的元素即可.
6.QueueBack(取队尾元素)
同理,判断队列不为空后,直接返回尾指针指向的元素即可.
7.QueueEmpty(判断队列是否为空)
队头和队尾指针都为空,而且都相等,即可认定队列为空.
8.QueueSize(队列长度)
因为我们在队列中,本身就加了size变量,因此直接返回size即可.
三.用队列模拟实现栈
队列是一个先进先出的结构,而栈是一个后进先出的结构.
所以队列先进的元素,如果要出栈,反而是最后出栈的.
那这个元素我们要对它进行什么操作呢? 我们注意到题目提供了两个队列,这就给我们提供了思
路.
假设有两个队列,我们设计为两个队列中任意一个队列为空,另一个不为空.
出栈的时候,只需要将不为空的队列中的元素,全部压入空队列中,只剩下最后一个元素,也就
是要出栈的元素.
MyStack结构如下图所示
typedef struct {
Queue q1;
Queue q2;
} MyStack;
MyStack* myStackCreate() {
MyStack * obj = (MyStack *)malloc(sizeof (MyStack));
//分别对队列1和队列2初始化
QueueInit(&obj->q1);
QueueInit(&obj->q2);
return obj;
}
void myStackPush(MyStack* obj, int x) {
//先假设队列1不为空
Queue* notempty = &obj->q1;
//如果队列1为空,则往队列2里面插入元素
if (QueueEmpty(&obj->q1))
{
notempty = &obj->q2;
}
//往不为空的队列里面插入元素
QueuePush(notempty,x);
}
int myStackPop(MyStack* obj) {
//假设队列1为空,队列2不为空
Queue * emptyQ = &obj->q1;
Queue * notemptyQ = &obj->q2;
//如果q1不为空,则交换
if (!QueueEmpty(&obj->q1))
{
emptyQ = &obj->q2;
notemptyQ = &obj->q1;
}
//非空队列里的元素转移到空队列中,直到只剩下最后一个元素
while (QueueSize(notemptyQ) > 1)
{
QueuePush(emptyQ,QueueFront(notemptyQ));
QueuePop(notemptyQ);
}
int top = QueueFront(notemptyQ);
QueuePop(notemptyQ);
return top;
}
int myStackTop(MyStack* obj) {
//假设队列1为空,队列2不为空
Queue * emptyQ = &obj->q1;
Queue * notemptyQ = &obj->q2;
//如果q1不为空,则交换
if (!QueueEmpty(&obj->q1))
{
emptyQ = &obj->q2;
notemptyQ = &obj->q1;
}
return QueueBack(notemptyQ);
}
bool myStackEmpty(MyStack* obj) {
//队列1和队列2都为空,则myStack为空
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}
void myStackFree(MyStack* obj) {
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
}
四.用栈模拟实现队列
有了上面的实现经验,可能有人很激动,就直接想着同样让一个栈为空,另一个栈不为空.
然后每次需要pop出元素的时候,就相应的将非空栈中的元素,全部压入空栈中,再弹出top元素位
置的元素即可.
但是入列(队列)操作,是不改变元素顺序的;入栈(栈)操作,是会将元素顺序改变,将元素倒过来.
因此假如我们push 1 2 3 4,再pop一下(以此获得1),会有下面的情况.
此时我们再进行pop操作,并不需要再将元素压入空栈中.(直接pop操作,把2弹出即可)
但是又出现一个新的问题,假如此时进行的是Push操作呢?
我们可以将原来的元素压入空栈中,然后再将新元素压入栈中.
然后又pop操作,又要将它从非空栈压入空栈中,显然这样的效率很低,而且,有时候需要压入空
栈,有时候又不需要压入空栈,显然思路不太正确.
我们可以采取这样的思路.
规定一个栈A,专门用来出栈,Pop出元素.
另一个栈B,专门用来入栈,Push进入元素.
假如要出栈,则从栈APop出元素;假如栈A没有元素,则把栈B中的元素全部压入栈A中.
它的核心思路,其实也是利用了入栈A,再出栈到另一个栈B,栈B就相当于一个队列,不过这个
队列只有出列的功能,还需要栈A暂时存储元素,来实现入列的功能.
typedef struct {
ST popst;//出栈
ST pushst;//入栈
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue* obj = (MyQueue*)malloc(sizeof (MyQueue));
StackInit(&obj->popst);
StackInit(&obj->pushst);
return obj;
}
void myQueuePush(MyQueue* obj, int x) {
StackPush(&obj->pushst,x);
}
int myQueuePop(MyQueue* obj) {
//出栈为空,倒数据
if (StackEmpty(&obj->popst))
{
while (!StackEmpty(&obj->pushst))
{
StackPush(&obj->popst,StackTop(&obj->pushst));
StackPop(&obj->pushst);
}
}
int top = StackTop(&obj->popst);
StackPop(&obj->popst);
return top;
}
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->popst) && StackEmpty(&obj->pushst);
}
void myQueueFree(MyQueue* obj) {
DestroyStack(&obj->popst);
DestroyStack(&obj->pushst);
free(obj);
}
五.循环队列
假设当前为队列分配的空间为6,那当队列Queue->rear == 5的时候,就不不能再插入元素了.否
则,数组就会越界.(若用顺序栈实现).
但假如此时前面的位置依旧为空呢(实际可用的空间并未被占满)?那空间会被浪费.
一个解决方案,除了我们上面提到的链队列外,就是我们接下来要介绍的——循环队列.
循环队列和我们队列的设计是一样的,也是有一个front指针指向头,rear指针始终指向尾部的下一个
位置,当两者相等的时候,队列为空,当然,因为是数组,所以front和rear直接设为int类型,代表下
标即可.
那循环列表有什么需要考虑的吗?
首先第一点需要考虑的就是,假如我们要存k个元素,我们能够直接开k个空间存元素吗?
显然是不能的.
拿队列分配的空间为6来说
front和rear的插值总共有6种可能,0,1,2,3,4,5
而总共的状态有7种,列表为空,有一个元素,两个元素,三个元素,四个元素...,六个元素
这显然用6种可能,是不可能表示7种状态的.
因此通常有两种方式解决,一种是另设一个标志位以区别队列是空还是满.
另一个就是我们呢今天实现的方式,约定以“队列头指针在队列尾指针的下一个位置,作为队列满的状态.”
按上面图片来说,此时队列就是满的状态,大小为k + 1的空间,最多能存k个元素.
第二点要考虑的就是循环,顾名思义,循环,也就是rear在超过队列长度后,要回到数组下标为0
的位置.
那现在k = 5,代表总共能存5个元素.
rear此时下标为6,如何让它回到0呢?
这里需要我们采用以前提到过的取模操作
rear %= (k + 1)
就能让rear重新回到下标为0的位置,真正实现循环.
完整代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef struct {
int* a;
int front; //循环队列头部
int rear; //循环队列尾部
int k; //记录队列总共可存元素个数
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* q = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
if (q == NULL)
{
perror("malloc fail.\n");
exit(-1);
}
q->a = (int*)malloc(sizeof(int) * (k + 1));
q->front = q->rear = 0;
q->k = k;
return q;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->front == obj->rear;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return ((obj->rear + 1) % (obj->k + 1)) == (obj->front);
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
assert(obj);
//如果循环队列为满,则无法入列
if (myCircularQueueIsFull(obj))
{
return false;
}
else
{
obj->a[obj->rear++] = value;
//如果超出数组范围,及时将它调整回相应的大小
obj->rear %= (obj->k + 1);
return true;
}
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
assert(obj);
//如果循环队列为空,则出列失败
if (myCircularQueueIsEmpty(obj))
return false;
else
{
obj->front++;
//如果超出数组范围,及时将它调整回相应的大小
obj->front %= (obj->k + 1);
return true;
}
}
int myCircularQueueFront(MyCircularQueue* obj) {
assert(obj);
//如果循环队列为空,则出列失败
if (myCircularQueueIsEmpty(obj))
return -1;
else
return obj->a[obj->front];
}
int myCircularQueueRear(MyCircularQueue* obj) {
assert(obj);
//如果循环队列为空,则取最后元素失败
if (myCircularQueueIsEmpty(obj))
return -1;
else
return obj->a[(obj->rear + obj->k) % (obj->k + 1)];
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
最后再单独谈一下取循环队列尾部元素(myCircularQueueRear)的操作.
一般来说,取队尾元素,只要取下标为rear - 1的元素即可.(像下图的1一样)
但是可能会出现rear小于front的情况(像下图的2一样),这个时候,为了取到真实下标k
(rear - 1 + k + 1)%(k + 1)
也就是我们代码里面的
(rear + k)% (k + 1)