关于栈和队列的总结
1.栈:
1.什么是栈
栈是一种对于数据进行管理的数据结构,对于数据,我们常见的操作就是删除和添加,而栈只有一个接口负责数据的管理,不论是删除还是添加都要通过这个口去处理,所以,栈就自然而然的满足先进后出的特点,先添加到栈中的数据就会被放到最底下,在删除数据的时候就会被最后删除,反之,最后添加到栈中的数据就会被放到最顶上,删除时也第一个删除。
2.栈顶:
栈顶是栈最顶层的元素,一般是最后一个添加的元素,同时在删除时也会满足第一个出栈,栈顶是栈很特殊的一个位置,对于栈的访问和销毁都有重要的作用:
3.代码实现:
我下面是对栈的一个程序构建,在这里,考虑到栈的删除和添加都是从一个口出入,那么我们就可以联想到尾删和尾插,对于只能尾删和尾插的数据结构,数组顺序表在这一方面很便捷,所以我这里使用数组顺序表去构建一个栈:
1.头文件test.h
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h>//判断空的头文件 typedef int STDataType; typedef struct Stack { STDataType* a;//数组指针 int top;//定义栈顶位置,就相当于顺序表里的元素个数状态量 int capacity;//判断容量 }ST; void STInit(ST* ps);//重定义栈 void STDestroy(ST* ps);//销毁数组栈 void STPush(ST* ps, STDataType x);//栈顶插 void STPop(ST* ps);//栈顶删 STDataType STTop(ST* ps);//获取栈顶元素 int STSize(ST* ps);//判断元素大小 bool STEmpty(ST* ps);//判断空
对于一个项目,我们一般构建一个头文件负责对函数和结构体等元素进行声明,这一点我在扫雷游戏专栏说过,这里不多赘述。
首先我们分析一下我们头文件的内容,既然是顺序表,我们就需要利用一个结构体去管理它,为了方便管理,我们首先创建一个数组指针,其实就是构建一个数组STDataType* a,方便数据进行操作,同时我说过栈顶元素很重要,所以我们同时也要时刻记录栈顶的位置int top,为了更方便管理数据,我们对容量也要时刻掌控,故创建一个int capacity管理容量。
既然是数据结构,我们自然对数据的操作方式无非增删查改 ,后面的函数也可以看出我们的实现目的,看注释即可,我不再过度强调。
2.函数文件list.c
#include"test.h" void STInit(ST* ps)//重定义栈 { assert(ps); ps->a = NULL; ps->top = 0; ps->capacity = 0; } void STDestroy(ST* ps)//销毁数组栈 { assert(ps); free(ps->a); ps->a = NULL; ps->top = 0; ps->capacity = 0; } void STPush(ST* ps, STDataType x)//栈顶插 { assert(ps); if (ps->top == ps->capacity) { STDataType newCapacity=ps->capacity ==0 ? 4 :ps->capacity*2;//注意这里的思路,我们重新设置一个整型变量负责存放扩容的capacity,利用后续的capacity是否为0,三目操作符,是0就扩大为4,不是零就扩大原来的2倍 STDataType* tmp = (STDataType*)realloc(ps->a,newCapacity*sizeof(STDataType));//这里直接使用realloc利用realloc的特性,当指示空间为空时,realloc的作用变成malloc。 if (tmp == NULL)//注意这里要使用newCapacity,因为首先capacity本身就是不稳定的的赋值,你也不知道它赋值多少,所以我们用更稳定的newCapacity去处理 { perror("realloc fail"); exit(-1); } ps->a = tmp;//后续把新空间的指针重新赋给a ps->capacity =newCapacity;//重新赋给capacity即可 } ps->a[ps->top] = x; ps->top++; } void STPop(ST* ps)//栈顶删 { assert(ps); assert(ps->top>0);//防止为空,进行断言 --ps->top; } STDataType STTop(ST* ps)//获取栈顶元素 { assert(ps); assert(ps->top > 0); return ps->a[ps->top - 1]; } int STSize(ST* ps)//判断元素大小size { assert(ps); return ps->top; } bool STEmpty(ST* ps)//判断空 { assert(ps); return ps->top == 0; }
函数分析:我们从简单的函数逻辑开始,判断空和取元素大小以及获取栈顶元素,我们在结构体中构建了top来管理栈顶,用capacity来管理元素个数,当capacity为0时就说明整个栈是空的,所以我们返回的就是ps->top这个表达式的真假,来判断是否为空,方便对于栈的遍历操作,其次是判断元素个数,由于我们创建了一个top,初始化函数中我们让top的初始值为0,加一个元素top相应加一,故top等于多少,则对应有几个元素,所以对于元素大小,我们直接返回top即可。
对于获取栈顶元素,我们知道,数组下标是从0开始的,也就是说,top作为我们可以掌控的量,它一旦作为下标的话,每一次是都要比栈顶元素多一位的,故我们返回的栈顶元素的下标应当是数组a[top-1],这个要注意,易错。
初始化栈的函数:我们创建了一个结构体就要对其初始化,初始化标准的逻辑就是动态开辟空间,让数组指针确确实实指向一个数组,同时对于动态增长的数组,我们方便对其管理的时候,要将元素个数变量和栈顶元素变量都设置为0(如果是固定长度的栈或者队列,我们只需要在最开始对其固定好长度即可,比如后面会提到的循环队列)。但在这里我使用了一个新方法,我在这里先将a设置为空,这个目的我后续会讲到,这里不多赘述。
销毁栈的函数:其原理与初始化函数差不多,我们首先要释放掉我们开辟的栈,也就是我们的数组a,对于free库函数,它是不会自动置空指针的,所以我们要手动置空,a=NULL。对于整型的成员,我们只需要将其设置为0表示空即可。
栈顶插:
void STPush(ST* ps, STDataType x)//栈顶插 { assert(ps); if (ps->top == ps->capacity) { STDataType newCapacity=ps->capacity ==0 ? 4 :ps->capacity*2;//注意这里的思路,我们重新设置一个整型变量负责存放扩容的capacity,利用后续的capacity是否为0,三目操作符,是0就扩大为4,不是零就扩大原来的2倍 STDataType* tmp = (STDataType*)realloc(ps->a,newCapacity*sizeof(STDataType));//这里直接使用realloc利用realloc的特性,当指示空间为空时,realloc的作用变成malloc。 if (tmp == NULL)//注意这里要使用newCapacity,因为首先capacity本身就是不稳定的的赋值,你也不知道它赋值多少,所以我们用更稳定的newCapacity去处理 { perror("realloc fail"); exit(-1); } ps->a = tmp;//后续把新空间的指针重新赋给a ps->capacity =newCapacity;//重新赋给capacity即可 } ps->a[ps->top] = x; ps->top++; }
这是整个数组栈的难点部分,我单独拿出来说:
由于我们开辟的是数组栈,不是链表栈,所以显而易见我们需要在需要的时候对其展开扩容,这一点和顺序表是相同的道理,但注意,常规的顺序表是在定义函数位置开辟出来的,而我在定义位置将a设为NULL,目的就是在这里进行扩容,首先动态内存开辟一段数组,但这个时候我们突然遇到一个问题,初始的数组是空的,而我使用realloc函数(注意,realloc函数在遇到本身为空的情况是作用等同于malloc),我里面的值都是0,0*任何数都是0,没法扩容,所以我们这里还需要一个NewCapacity函数来对其容量进行一个最开始的扩容解决无法扩容的问题,我的逻辑是:ps->capacity是空么?倘若是,那么我们令容量函数值变为4并开辟4个空间,倘若不是,我们就将其扩大两倍,利用三目操作符即可实现(这里提一下三目操作符,三目操作符的三个式子可以是表达式也可以是单独的量,比如这里,ps->capacity==0就是单独的表达式,不要将其拆开看,这个整体的三目操作符是要整体赋给newCapacity的),这样,我们就完成了扩容和元素状态量定义。(别忘了扩容后要进行空的判断),不要忘了把新开辟的指针和量要赋给结构体里面的成员,同时对于栈顶元素要插入我们的x,并更新我们的栈顶元素进行top量的更新。
栈顶删:
栈顶删是很简单的,如同顺序表一样,我们没必要删除元素,只需要操控top,令其自减一,不再控制旧的第一个元素即可,后续我们加新元素的时候,这个位置不管元素是什么,都会被覆盖,不会残留。
如此,我们便完成了栈的构建,现在创建一个主函数程序来运行一下:
3.主文件test.c
#include"test.h" void test1() { ST st; STInit(&st); STPush(&st, 1); STPush(&st, 2); STPush(&st, 3); STPush(&st, 4); STPush(&st, 5); while(!STEmpty(&st))//循环的条件是数组栈不为空 { printf("%d ", STTop(&st)); STPop(&st); } printf("\n"); STDestroy(&st); }//注意,数组栈的遍历的从栈顶往前进行的,所以每次遍历都要删除一个元素接着遍历,这就是栈的特点,先进后出,后进先出 int main() { test1(); return 0; }
主函数的构建,我这里推荐一种方法,如同我上文所写,就是利用函数来实现,而且是不传参的函数,因为这样是很方便测试函数的功能,当你不使用的时候,只需要注释到函数的声明,就可以去调试其他函数的功能,对于复杂的大型工程进行测试时很有用。
让我们再一次强调一下栈的特点吧:只有一个接口,先进后出,后进先出。如果我们想要遍历整个栈,我们只能看一个删一个(利用我这个程序是如此,但假如你重新定义一个变量赋值给它top的值,然后利用它去自减似乎不用看一个删一个,例如你定义一个cur=top,你去自减cur访问,top的值是不变的,依旧指向栈顶位置,而且我们也没有删除元素,不过你需要在结构体内部创建,操作起来很麻烦,不推荐)我们循环的条件就是数组栈不为空,目的就是达到遍历的效果,在循环里,我们只需要每次都打印出当前的栈顶元素(利用构建的获取栈顶元素的函数),然后依次删除即可。最后,由于栈已经为空,我们也不进行操作,一定要销毁栈,这样是防止内存泄露。
如此,便是利用数组构建栈的全部,同样,链表也可以构建栈,但相对麻烦,根据实际情况可以去尝试。
补充细节:无论是栈抑或是下面的队列,我们都要对传入函数的指针判空,当对于删除或者添加的函数进行实现前,我们都要对栈或者队列内部的内容是否为空进行判断后再操作,否则对于整型控制的变量,我们很容易让其赋值为负数,这样后续的添加就不合理了,我这里使用assert暴力的方法进行检查,出问题会直接报错,比较直接,你也可以用较为温柔的方法,比如当其为空时直接将整型赋值为0,这样也可以避免出现负数的问题。
1.队列:
1.什么是队列:
队列,顾名思义,如同我们小时候排队,一般都是最矮的站在队尾或者队头,有时候是最高的,然后按照身高一次排列,假如我们按照从高到低的顺序排队从一个走廊里走进去,然后顺着一个方向走出来,我们会发现,最高的人是第一个进入走廊的,出来的时候也是最高的人先出来,而最矮的人是最后一个进入走廊的,但反方向出来的时候最矮的人却是第一个走出来的,这就是队列的大致特点。队列一共有头和尾两个接口,我们添加元素的时候只能从尾部去添加,而从队列中删除出元素的时候只能从队列的头部去出。
2.队列的头和尾:
队列的头可以理解为第一个进入队列的元素,而尾可以理解为最后一个进入队列的元素,头可以控制队列的删除,而尾可以控制队列的添加,这是两个很关键的队列数据结构的操控指针。
3.代码实现:
在明确队列特点的前提下,与栈不同,队列需要对头和尾部都进行操作,而且涉及到头删,这对于数组顺序表是不友好的,空间复杂度会很高,所以我们这里使用链表来实现,我这里使用的是无头不循环单向链表。
1.头文件test.h
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> typedef int QDataType; typedef struct QueueNode { struct QueueNode* next; QDataType data; }QNode; typedef struct Queue//对于头指针和尾指针,我们可以建立一个结构体指针的结构体,这样既不用哨兵位,也不用二级指针,同时也避免的函数只能传回一个值的问题,而且在这个结构体内部,我们还能统计队列的实时状态量 { QNode* head; QNode* tail; int size; }Que; void QueueInit(Que* pq);//重定义 void QueueDestroy(Que* pq);//销毁队列 void QueuePush(Que*pq, QDataType x);//尾插 对于队列来说,其特性:头出尾进,所以我们如果传入头和尾指针可以更好的操控整个队列,同时省去了遍历的时间复杂度。 void QueuePop(Que* pq);//头删 QDataType QueueBack(Que* pq);//寻找队尾元素 QDataType QueueFront(Que* pq);//寻找队头元素 bool QueueEmpty(Que* pq);//判断空 int QueueSize(Que* pq);//记录队列的状态量
首先,我们常规创建结构体节点负责管理数据和指向下一个指针的next,考虑到队列的特点,我们需要对头和尾进行管理,我这里想到,倘若直接在结构体内部管理头和尾,我们的操作不是普遍的,(其实也可以,不过我使用起来会很乱)。我这里是又建立了一个结构体,它负责控制头和尾,同时统计元素的个数,故我创建了head,tail,size。这样两个结构体就创建好了,结构体是一个很方便去管理和控制数据的结构,对于需要在一个函数内部同时控制多个变量的情况,结构体很好用。
函数的功能如同我上文所写。
2.函数文件math.c
#include"test.h" void QueueInit(Que* pq)//重定义 { assert(pq); pq->head = pq->tail = NULL; pq->size = 0; } void QueueDestroy(Que* pq)//销毁队列 { assert(pq); QNode* cur = pq->head; while (cur) { QNode* next = cur->next; free(cur); cur = next; } pq->head = pq->tail = NULL; pq->size = 0; } void QueuePush(Que* 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(Que* pq)//头删 { assert(pq); assert(!QueueEmpty(pq)); if (pq->head->next == NULL) { free(pq->head); pq->head = pq->tail = NULL; } else { QNode* next = pq->head->next; free(pq->head); pq->head = next; } pq->size--; } QDataType QueueBack(Que* pq)//寻找队尾元素 { assert(pq); assert(!QueueEmpty(pq)); return pq->tail->data; } QDataType QueueFront(Que* pq)//寻找队头元素 { assert(pq); assert(!QueueEmpty(pq)); return pq->head->data; } bool QueueEmpty(Que* pq)//判断空 { assert(pq); return pq->head == NULL; } int QueueSize(Que* pq)//记录队列的状态量 { assert(pq); return pq->size; }
对于队列,我们要控制其头和尾,在尾插和头删的位置对数据的头和尾进行处理,所以,我们要传入的数据是我们定义的 Que结构体。
重定义函数:相同的操作,对指针置空,对整型变量处理为0或者其他要求的值。
尾插函数:首先要开辟一个节点,对这个节点进行数据存储和指针置空处理。然后我们要思考如何定义头和尾呢?这里分两种情况,首先自然而然,我们的头和尾指针开始时是指向一个位置的,我们放入一个数据,尾指针向后走一步,头指针不变,不断循环,最后尾指针就会指向尾部,而头指针指向头部,所以我们只需要考虑两个情况即可,即队列为空时,我们尾插怎么处理,以及队列不为空时,我们尾插怎么处理指针。首先是为空的情况,我们插入一个数据后,倘若tail->next==NULL,证明数据为空,那么我们就让头和尾同时指向开辟的新节点,倘若!=NULL,证明数据不为空,则尾部指针向后移动,首先让pq->tail->next指向newnode,将newnode链接起来,然后对newnode也要向后移动一位,我们用pa->tail=newnode处理即可,别忘了size++.
头删函数:首先要检验队列是否为空,后续我们写出判断空函数的时候,直接利用assert去判断一下即可,对于头删,我们也要分情况,我们先考虑常规情况,如果对于一个4节点的链表,我们要头删使其变为3节点,那我我们的思路就是首先定义一个next,令其存储头指针下一个节点的地址,然后free掉当前的head,手动将head赋值为next,这样就处理干净了常规的头,不过倘若是只有一个节点呢,我们只处理了头,这个时候尾也需要处理,我们就释放掉这个节点,同时将head和tail都置空,最后别忘了size–。
寻找队头和队尾元素:这是很简单的,因为我们首先记录了头节点和尾节点,只需要返回对应的data即可。
判空:同样是利用bool变量,我只需要返回头指针是不是为NULL的真假即可。
记录节点实时状态量:返回size即可。
3.主文件test.c
#include"test.h" void test1() { Que q; QueueInit(&q); QueuePush(&q, 1); QueuePush(&q, 2); QueuePush(&q, 3); QueuePush(&q, 4); QueuePush(&q, 5); while (!QueueEmpty(&q))//循环进行的条件就是一直不是空的,空了直接停止循环 { printf("%d ", QueueFront(&q)); QueuePop(&q); } QueueDestroy(&q);//队列的遍历方式和栈基本相同,都是从尾部依次遍历,但遍历一遍后就删除,队列对于数据的出和入的逻辑很清晰 } int main() { test1(); return 0; }
主文件没太多需要强调的,主要是队列的访问方式和栈差不多,都是遍历一个删一个,只不过从尾删变成了头删而已。
以上就是队列的全部内容,需要注意的也是需要对其判空处理,以及对指针的判空处理。
3.总结:
总的来说,队列的实用性要远大于栈,比如医院摇号系统的逻辑就类似队列,方便实时管理,但栈也有其一定的作用,我后续会讲解如何利用栈实现队列以及如何利用队列实现栈,那时我们对栈和队列会有更深的理解。