目录
一、线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
二、栈和堆
栈和堆这两者,我们其实也已经听过很多次了,但是我们会发现栈和堆我们也产生了一些混淆
我们在C语言中听说过内存会分为栈区堆区静态区等,有函数栈帧,malloc出来的数据都是在堆区上的
而我们在数据结构中,我们所说的栈又变成了一种数据结构,堆其实也是一种数据结构,堆是一颗完全二叉树。
可见我们又被搞混了。
事实上,数据结构中的栈和堆与操作系统中的栈的堆是两种不一样的东西,他们仅仅只是因为不同学科之间而导致的名字一样,但是含义却完全不一样。
总的来说:
在数据结构中:栈和堆是一种数据结构,栈是先进后出,堆是一颗完全二叉树
在操作系统中:栈和堆是内存的划分,栈区用来存放局部变量,函数栈帧等。堆区用来存放malloc、realloc、calloc出来的空间
三、顺序表、链表、栈和队列结构上的区别
而在前面的章节中,我们已经实现了顺序表、链表、栈和队列,但是我们发现,这些结构容易混淆,而且我们特别容易犯一种定势思维,比如说,我们习惯上顺序表是直接定义在栈区的,然后我们对栈区上的顺序表进行初始化就需要传地址了,习惯上单链表是定义在栈区,然后传二级指针,双向链表的初始化又不需要传二级指针了。队列的实现更是需要在构造一个结构体......,可见这些结构特别容易令人混淆。要注意的是,这些数据结构的实现其实并非一种固定的模式,就必须按照这个来实现,只不过这种实现是相对较优的。所以我们习惯上使用它,而我们不能犯定势思维,一定要知道,数据结构是十分灵活的!!!
1.顺序表的变化
顺序表,我们分为静态顺序表和动态顺序表。
静态顺序表的定义如下
typedef int SLDateType; #define N 10 typedef struct SeqList { SLDateType a[N]; int size; }SL;
对于静态顺序表,我们的结构是这样使用的
1>如果在主函数中直接定义SL seq
也就是说直接定义在栈区上,也就相当于直接在栈区上定义了一个变量名为seq的结构体,这个结构体里面有一个SLDateType类型的,长度为N的数组,还有一个用来记录当前数组有效元素个数size计数器
它的内存图如下所示
上面就是静态顺序表结构上内存图了,而对于静态顺序表我们定义好这个结构体以后,我们想要它的增删查改都封装为一个个函数,那么就需要传这个结构体的地址过去,然后我们才可以进行增删查改等操作。因此静态顺序表函数接口一定是结构体指针。当然对于查找函数,由于我们并不涉及对内存数据修改,所以这个接口我们可以直接将结构体传过去,但是这样的话,会使得函数栈帧中形参的开销过大,而使用指针的话可以大大节省开销。指针只栈四个字节,所以仍然建议传结构体指针,但是若非要传结构体也是没有任何问题的,只是开销比较大
2>如果在主函数中定义的是结构体指针SL* s
如果我们在主函数中定义了一个结构体指针,那么栈区中就只有一个指针变量了。那么结构体的初始化就需要发挥它的作用,它必须得使用堆区的内存了,对于这种情况就有两种办法来进行初始化,要么在堆区开辟好所有的空间并初始化,然后返回这个地址,最后接收这个地址,要么就是通过传这个指针的地址过去,直接为这个原栈区地址进行赋值,这样我们可以不用返回值,但是对应的就需要传二级指针
上图也是静态顺序表的内存图,它是通过只在栈区中定义一个指针来操控堆区的内存的。这种结构不消耗栈区的空间,但是需要使用堆区的空间,在为堆区初始化,有传二级指针和传返回值两种方案,而且还需要注意,这种方法一定要释放空间,否则内存泄漏。而前面在栈区定义的结构体是不需要释放空间的
动态顺序表的定义如下
//动态顺序表 typedef int SLDateType; typedef struct SeqList { SLDateType* a;//指向数据的指针 int size; int capacity; //容量 }SL;
对于动态顺序表,我们仍然既可以在主函数中直接定义SL seq,也可以定义结构体指针SL* seq
1>如果直接定义SL seq
那么我们不难得出它的内存图应该如下所示,这样的话,它的每一个函数都需要传入这个结构体的地址过去,也就是说都只需要一级指针即可完成任务
2>如果定义结构体指针SL* seq
对于这种情形,它的内存图仅仅就是将上面栈区的数据搬移到堆区上,然后再栈区定义一个结构体指针,去指向堆区中的数据,然后继续指向存放数据的空间,从而进行增删查改,但是显然,这样的话,会导致很多过程变得十分繁琐。没有这样的必要。
3.总结
可见,顺序表可以分为动态顺序表和静态顺序表,每种顺序表又可以根据它在内存中的分布不同而使得函数的接口有所变化。数据结构的变化是非常之多,我们不能只记住定式,而是需要灵活运用。选择最合适的结构,我们在前面的文章中,我们所选的结构就是动态顺序表,且直接定义结构体的方式去实现的,这样的效率也是相较于其他的实现方式优的
2.链表的变化
我们知道。链表可以分为八种结构,每种结构各具特色,但是我们最常用的两种结构是无头单向不循环链表和带头双向循环链表。
1>无头单向不循环链表
我们对于这种链表的定义是这样的
typedef int SLTDateType; typedef struct SListNode { SLTDateType data; struct SListNode* next; }SListNode;
对于单链表,我们在主函数中可以定义一个指针,也可以直接定义一个节点,但是要注意,如果直接定义成一个节点,那么在栈区的数据,想要在其他的函数栈帧中去使用主函数栈帧中的数据是比较麻烦的。确实,我们可以这样做,但是这样做纯属自找麻烦。因此我们定义一个链表的指针才是最好的方案
对于单链表,由于我们是需要将栈区的数据直接和堆区上的数据链接起来的,所以我们需要传二级指针来解决这个问题,也就是说,它的增删查改都需要使用二级指针了,但是有时候也会使用一级指针来进行操作,如果使用一级指针的话,那么就必须要通过返回头节点的方式来进行处理。
可见单链表的实现也具有十分大的灵活性,而非定式
而且除此之外,为了使得计算链表中每一个节点的个数更加方便,我们可以再次封装一个结构体
这样的变化之后,有一个新的好处就在于,我们可以不需要传二级指针了,我们只需要传SL这个结构体的指针就好了。而且还使得计算节点的方式更快了。
可见链表的变化之多,数据结构是十分灵活的,而非定式
2>带头双向循环链表
对于带头双向循环链表,它的定义是这样的
typedef int LTDateType; typedef struct ListNode { struct ListNode* prev; struct ListNode* next; LTDateType data; }ListNode;
对于这种链表,我们和单链表类似,尽量将数据集中在一个区域内,所以这种链表也需要定义一个指针较优,它的内存分布图与单链表是一样的
而对于这个链表它有一个麻烦之处在于初始化操作,我们可以直接将这个链表的地址传过去,也就是二级指针来进行初始化,也可以使用不传参,通过返回头节点的方式来进行操作。对于其他的操作,由于哨兵位的存在,都可以传一级指针就可以了
它也可以向单链表一样,在使用一个size来与链表指针进行封装成一个新的结构体,这样我们就可以对于任何一个接口传的都是一级指针,实现了大统一。
可见链表的变化是十分灵活的
3.栈的变化
对于栈这种数据结构,我们可以使用链表来完成,也可以使用顺序表来完成。如果使用链表的话,会使得尾删不好处理。时间复杂度较高,所以最优的解法就是通过顺序表来实现,而使用顺序表来实现的话,它的内存分布图就与顺序表一模一样了
typedef int STDateType; typedef struct Stack { STDateType* a; int top; int capacity; }Stack;
因此栈的变化也是十分之多,当然具体使用哪一种方式都是可以的,只不过存在一些效率上的不同。
4.队列的变化
对于队列这种数据结构,我们可以使用链表来完成,也可以使用顺序表来完成。由于队列只涉及头删和尾插,所以相对而言使用链表较优。但是如果非要使用顺序表,也是可以的
对于队列,它的定义如下所示
typedef int QDateType; typedef struct QNode { QDateType data; struct QNode* next; }QNode; typedef struct Queue { QNode* head; QNode* tail; int size; }Queue;
我们可以看出来,我们是使用了单链表中的一种变化,为了使得尾删方便,计算节点个数效率高,我们使用两个指针和一个size来封装为一个结构体,这样的好处就在于,我们在栈区可以只定义一个队列,然后直接传队列的地址就可以了,不需要使用二级指针。这其实就是单链表变化中最优的一种变化。当然我们也可以直接在主函数中使用队列的地址,这就需要在堆区中为队列申请空间了,其实是自找麻烦。
由此可见,队列也有十分多的变化,我们无需关注它的实现是如何的,我们只需要将接口的功能给实现出来就可以了。切记数据结构不是定式,而是十分灵活的。
5.衍生的变化
上面的变化仅仅只是一些基本的变化,而且由上面的变化,可以进一步生成更多的变化,如两个队列可以完成一个栈,两个栈可以实现一个队列,以及循环队列的变化。这些数据结构的实现都是由前面基本的变化来组成的。
好了本期内容就到这里了
如果对你有帮助的话,不要忘记点赞加收藏哦!!!