目录
6.4.2 判空与判满函数(Is(Empty)/(Full))
一.前言
二.前文回顾
三.栈
3.1 栈的概念及结构
- 栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
- 压栈: 栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
- 出栈:栈的删除操作叫做出栈,出数据也在栈顶。
3.2 栈的实现
其实数组或者链表都是可以实现栈的,二者区别无法就是实现的方式有所不同,效率各有千秋。
3.2.1 初始化函数
3.2.2 销毁函数
3.2.3 入栈函数
3.2.4 出栈函数
3.2.5 计算大小函数
3.2.7 获取栈顶函数
3.2.8 小测试
3.3 全部代码
其实写到最后我们可以发现,栈其实就是顺序表的简化版,相当于定义了部分适用于栈特征的接口罢了~
四.栈的练习
4.1 有效的括号
链接:力扣——有效的括号
基本思路:
- 数量上最后再进行判断~
- 类型上:遇到右括号就进行匹配,看有没有对应的左括号。
- 顺序上:当遇到右括号时判断距离最进的括号是否为对应左括号。
那如何找到最近的括号呢?
- 左括号,入栈
- 右括号,出栈顶括号,进行匹配
备注:千万不要想着去用switch,千万不要,除非你想要头脑风暴。。。。
五.队列
5.1队列的概念及结构
- 队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)。
- 入队列:进行插入操作的一端称为队尾。
- 出队列:进行删除操作的一端称为队头。
5.2 队列的实现
队列也可以用数组或链表的结构实现,使用链表的结构表现更优一些,因为数组在实现出队列(头删)效率比较低~
5.2.1 初始化函数
5.2.2 入队列函数
在我们处理好空间问题后,要进行入队列(尾插)就得先找到尾节点,不过这里还有一些小问题需要注意。
如果链表是空的呢?——赋值给它们2个
5.2.3 出队列函数
5.2.4 获取头队列函数
5.2.5 获取尾队列函数
5.2.6 判空(队列)函数
5.2.7 计算队列大小函数
5.2.8 销毁函数
5.2.9 小测试
5.3 全部代码
六.队列的练习
6.1选择题
学习完队列和栈我们可以知道,入队列是1 2 3 4 5那出队列就是1 2 3 4 5,那栈呢?也入栈1 2 3 4 5,出栈就一定是5 4 3 2 1吗?
6.2用队列实现栈
链接:力扣——用队列实现栈
6.2.1 整体思路:
- 入队列:不为空的队列
- 出队列:不为空队列前N-1个出队列,插入空队列。删除剩余的数据。
代码解析:
MyStack* myStackCreate() {
MyStack st;
//......
return &st
}
这个接口的作用仅仅是返回&st吗?——这个局部作用域一出来那么里面的变量都会销毁,这时候传st的地址已经是没意思的了,它已经变成一个野指针了。为了保证MyStack的对象还在,我们可以改用malloc来创造节点,这样出作用域时,malloc出来的空间还是在的。
MyStack* myStackCreate() {
MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&pst->q1);
QueueInit(&pst->q2);
return pst;
}
6.2.2 Push函数(入队列):
由于我们不知道哪个为空,哪个不为空,那么我们直接用判空条件来判断再进行入队列
void myStackPush(MyStack* obj, int x) {
if (!QueueEmpty(&obj->q1))//如果q1为空
{
QueuePush(&obj->q1, x);
}
else
{
QueuePush(&obj->q2, x);
}
}
6.2.3 Pop函数(出队列):
还是一样的逻辑,不为空的队列往空队列导入。在这里我们假设q1为空,q2不为空。如果q1不为空(判空函数)那交换即可。
int myStackPop(MyStack* obj) {
QNode* empty = &obj->q1;//假设q1为空
QNode* nonEmpty = &obj->q2;//假设q2不为空
if (!QueueEmpty(&obj->q1))//如果qi不为空
{
empty = &obj->q2;//交换
nonEmpty = &obj->q1;
}
//前size-1个导入空队列
while (QueueSize(nonEmpty) > 1)
{
QueuePush(empty, QueueFront(nonEmpty));
//进行入队列,在空队列中存入在不为空队列中的首个数据
QueuePop(nonEmpty);
//再Pop掉该数据,使下一次循环是首数据后面的数据
}//入一次出一次,直达剩下一个
//这时候只剩下一个数据,获取栈顶元素
int top = QueueFront(nonEmpty);//存储不为空队列中的元素
QueuePop(nonEmpty);//再Pop掉该元素
return top;//返回所谓的栈顶数据
//至此,该函数完全实现在2个队列入好数据后,
//结果返回的是后入队列的数据,实现了后进后出。
}
到这里大家是不是有疑惑,为什么都是&obj,到了empty传输时反而不用取地址了呢?
我们需要把类型认识清楚,q1与q2是队列的结构体,该结构体包含了头指针(head),尾指针(tail),和size。对于我们所要实现的接口(Pop,Push,Creat)而言,它们想要改变的都是结构体的指针(tail,head等)来实现自身功能。所以这里需要&obj,而empty与nonEmpty已经是结构体指针了,就相当于&obj,也就不需要&了。
6.2.4 Top函数(取栈顶):
这个函数要实现的功能是取栈顶元素,也就是取最后入队列的数据。这里我们可以直接复用前面所写的获取队列尾部数据函数( QueueBack)来轻松实现。
int myStackTop(MyStack* obj) {
//哪个队列不为空就去取该队列的尾部数据
if (!QueueEmpty(&obj->q1))
{
return QueueBack(&obj->q1);
}
else
{
return QueueBack(&obj->q2);
}
}
6.2.5 Empty函数(判空):
在这里的判空应该是两个队列都为空,那才是空。一个为空的不算空。
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}
6.2.6 Free(释放空间)函数:
这里我们不能直接free掉obj,因为在obj里面有两个队列q1,q2而这两个队列的底层是链表,所以我们在free之前记得使用销毁函数(QueueDestroy)(销毁链表)
void myStackFree(MyStack* obj) {
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
}
6.2.7 全部代码
注:想要写题解或者测试,请自行添加上文关于Queue.c与Queue.h的代码段(下面代码能够复用的前提)
typedef struct {
Que q1;
Que q2;
} MyStack;
MyStack* myStackCreate() {
MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&pst->q1);
QueueInit(&pst->q2);
return pst;
}
void myStackPush(MyStack* obj, int x) {
if (!QueueEmpty(&obj->q1))//如果q1为空
{
QueuePush(&obj->q1, x);
}
else
{
QueuePush(&obj->q2, x);
}
}
int myStackPop(MyStack* obj)
{
QNode* empty = &obj->q1;//假设q1为空
QNode* nonEmpty = &obj->q2;//假设q2不为空
if (!QueueEmpty(&obj->q1))//如果qi不为空
{
empty = &obj->q2;//交换
nonEmpty = &obj->q1;
}
//前size-1个导入空队列
while (QueueSize(nonEmpty) > 1)
{
QueuePush(empty, QueueFront(nonEmpty));
//进行入队列,在空队列中存入在不为空队列中的首个数据
QueuePop(nonEmpty);
//再Pop掉该数据,使下一次循环是首数据后面的数据
}//入一次出一次,直达剩下一个
//这时候只剩下一个数据,获取栈顶元素
int top = QueueFront(nonEmpty);//存储不为空队列中的元素
QueuePop(nonEmpty);//再Pop掉该元素
return top;//返回所谓的栈顶数据
//至此,该函数完全实现在2个队列入好数据后,
//结果返回的是后入队列的数据,实现了后进后出。
}
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);
}
最后附上结构图,帮助大家理解~
这也是我们为什么不能直接free掉obj的原因。
6.3用栈实现队列
链接:力扣——用栈实现队列
typedef struct {
} MyQueue;
MyQueue* myQueueCreate() {
}
void myQueuePush(MyQueue* obj, int x) {
}
int myQueuePop(MyQueue* obj) {
}
int myQueuePeek(MyQueue* obj) {
}
bool myQueueEmpty(MyQueue* obj) {
}
void myQueueFree(MyQueue* obj) {
}
6.3.1 整体思路:
老规矩我们先创造两个栈,其中一个依次输入1 2 3 4,要想实现队列的先进先出,那最终要Pop的数就是1.至此,我们可以沿用与上一题差不多的思路(先入栈,再出栈导到另一个栈,最后再处理1.)。
当我们把1Pop掉时再尝试Push5 6,把2 3 4导回去再入栈5 6继续重复上述操作是没问题的,那能不能不把2 3 4导回原来的栈呢?——这时候的情况就发生了变化:可以把2 3 4全部Pop掉,这样第二个栈就空了,再把5 6导入第二个栈最终可以Pop出5.
这时候我们就可以制定规则了
- pushst栈: 这个栈只能用来入栈。
- popst栈:这个栈只能用来出栈。
跟上题一样先搞出两个栈出来;
MyQueue* myStackCreate() {
MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
STInit(&obj->pushst);
STInit(&obj->popst);
return obj;
}
6.3.2 Push(入栈)函数
void myQueuePush(MyQueue* obj, int x) {
STPush(&obj->pushst, x);
}
6.3.3 Peek(取头)函数
我们先来写Peek(获取头队列数据函数),要想获取首先得判断popst中是否为空,如果是空的我们需要从pushst中导出数据到popst里。这样才可以获取头队列数据。如果有友友对这里复用的函数命名不熟悉可以去目录对照查看。
int myQueuePeek(MyQueue* obj) {
//哪个队列不为空就去取该队列的尾部数据
if (STEmpty(&obj->popst))
{
//判断popst为空,开始导入数据
while (!STEmpty(&obj->pushst))
{
STPush(&obj->popst, STTop(&obj->pushst));//导入数据后再进行删除头队列数据
STPop(&obj->pushst);//导进一个删除一个,清空pushst
}
}
return STTop(&obj->popst);
}
6.3.4 Pop(出栈)函数
到这一步我们就可以发现先写Peek的好处了,因为Peek已经先判断popst是否为空,为空就把pushst的数据搬过来,所以我们只需要处理好popst里面的数据就好了。
int myQueuePop(MyQueue* obj)
{
int front = myQueuePeek(obj);//存储队列首个数据
STPop(&obj->popst);//pop掉队列的头数据
return front;//返回首个数据,达成用两个栈实现先进先出的队列
}
6.3.5 Empty(判空)函数
bool myQueueEmpty(MyQueue* obj) {
return QueueEmpty(&obj->pushst) && QueueEmpty(&obj->popst);
}
6.3.6 Free(释放空间)函数
void myQueueFree(MyQueue* obj) {
STDestroy(&obj->pushst);
STDestroy(&obj->popst);
free(obj);
}
6.3.7 全部代码
注:想要写题解或者测试,请自行添加上文关于Stack.h与Stack.c的代码段(下面代码能够复用的前提)
typedef struct {
ST pushst;
ST popst;
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
STInit(&obj->pushst);
STInit(&obj->popst);
return obj;
}
void myQueuePush(MyQueue* obj, int x) {
STPush(&obj->pushst, x);
}
int myQueuePeek(MyQueue* obj) {
//哪个队列不为空就去取该队列的尾部数据
if (STEmpty(&obj->popst))
{
//判断popst为空,开始导入数据
while (!STEmpty(&obj->pushst))
{
STPush(&obj->popst, STTop(&obj->pushst));//导入数据后再进行删除头队列数据
STPop(&obj->pushst);//导进一个删除一个,清空pushst
}
}
return STTop(&obj->popst);
}
int myQueuePop(MyQueue* obj)
{
int front = myQueuePeek(obj);//存储队列首个数据
STPop(&obj->popst);//pop掉队列的头数据
return front;//返回首个数据,达成用两个栈实现先进先出的队列
}
bool myQueueEmpty(MyQueue* obj) {
return STEmpty(&obj->pushst) && STEmpty(&obj->popst);
}
void myQueueFree(MyQueue* obj) {
STDestroy(&obj->pushst);
STDestroy(&obj->popst);
free(obj);
}
6.4 设计循环队列
链接:力扣——设计循环队列
typedef struct {
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
}
int myCircularQueueFront(MyCircularQueue* obj) {
}
int myCircularQueueRear(MyCircularQueue* obj) {
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
}
void myCircularQueueFree(MyCircularQueue* obj) {
}
图像示例:
这里用数组实现比较轻松,用链表反而会很复杂。
- 第一缺陷:用rear取队尾数据不好取,因为rear是每次插入数据后才往后移,如果想取队尾部那就得用到rear->pre了,这就变成了双向链表了。又或者再多给一个指针,变成3个指针。
- 第二缺陷:不好判满,链表为空rear与front指向同一处,而只有一个数据时,还是指向同一处。
- 第三缺陷:当链表满时rear又指向了与front一致的地方,与链表为空的情况一致,空与满判断不了。
所以为了解决这个问题,很多人选择多开一处空间,该空间不存储数据就为了让rear指到最后。
所以用链表始终存在一些问题,故而我们选择比链表适合一些的数组来实现。
6.4.1 整体思路:
这里判定满的条件就是rear+1,当rear的下标是4时可以想象+1就回到front的指向位置。所以最终的规律就是(rear+1)%(k+1)==front.
我们先来模拟一下增删:
当准备插入7时,只要我们的判满条件达成理论上是不会继续插入滴~我们继续删除数据~
当我们把6给删除后链表为空链表了,不能再继续删除了,而这时候我们发现front与rear相等,也满足了判空条件。
总结:实现循环链表的两个关键点
- 判空:front==rear
- 判满:(rear+1)%(k+1)==front
6.4.2 创造函数(Creat)
typedef struct {
int* a;//定义数组
int front;//起点
int rear;//终点
int k;//有效数字
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
//多开一个空间方便区分空和满
obj->a = (int*)malloc(sizeof(int) * (k + 1));
obj->front = obj->rear = 0;
obj->k = k;
return obj;
}
6.4.2 判空与判满函数(Is(Empty)/(Full))
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->front == obj->rear;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->rear + 1)%(obj->k+1) == obj->front;
}
6.4.3 EnQueue(插入)函数
在三种插入情况中我们需要注意第三种:rear循环绕到开头。
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if (myCircularQueueIsFull(obj))//如果已经满了
{
return false;
}
//没满情况下
obj->a[obj->rear] = value;
obj->rear++;
obj->rear %= (obj->k + 1);
//针对第三种情况当rear在下标4插入7时rear++变成5需要循环到头部
return true;
}
6.4.4 DeQueue(删除)函数
删除完数据front++,那front也会遇到跟上面rear一样的问题,当指向尾时再++需要循环绕到头部。
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))//如果已经是空的
{
return false;
}
obj->front++;
obj->front %= (obj->k + 1);
return true;
}
6.4.5 Front(取头)函数
int myCircularQueueFront(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))//如果已经是空的
{
return -1;
}
else
{
return obj->a[obj->front];
}
}
6.4.6 rear(取尾)函数
取队尾函数就不像上面取队头那么好取了。正常的队尾取到rear-1就行,但如果是这种情况呢?取到-1该怎么办。
虽然可以通过if条件来判断并修改,但这里我们来引用一种巧妙的方法;
int myCircularQueueRear(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))//如果已经是空的
{
return -1;
}
else
{
return obj->a[(obj->rear+obj->k)%(obj->k+1)];
}
}
6.4.7 Free(释放空间)函数
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
6.4.8 全部代码
typedef struct {
int* a;//定义数组
int front;//起点
int rear;//终点
int k;//有效数字
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
//多开一个空间方便区分空和满
obj->a = (int*)malloc(sizeof(int) * (k + 1));
obj->front = obj->rear = 0;
obj->k = k;
return obj;
}
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) {
if (myCircularQueueIsFull(obj))//如果已经满了
{
return false;
}
//没满情况下
obj->a[obj->rear] = value;
obj->rear++;
obj->rear %= (obj->k + 1);
//针对第三种情况当rear在下标4插入7时rear++变成5需要循环到头部
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))//如果已经是空的
{
return false;
}
obj->front++;
obj->front %= (obj->k + 1);
return true;
}
int myCircularQueueFront(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))//如果已经是空的
{
return -1;
}
else
{
return obj->a[obj->front];
}
}
int myCircularQueueRear(MyCircularQueue* 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);
}