年轻是我们唯一拥有权利去编织梦想的时光!
•写在前面
Hello,大家好啊,我是vince,小白一名,撰写博客::⭐️一为更好的锻炼自己,巩固知识;:⭐️二为和大家一起在代码的海洋里探索。
如果大家感兴趣的话 —— 欢迎关注csdn博客:< vince >,我们互相学习,共同进步哈~
今天vince将和大家一起去一一吃透双向带头循环链表,接下来这一篇文章将带大家玩转双向带头循环链表。☀️
• 知识点1:线性表
• 2.1 线性表的概念及分类
•1.线性表的概念
线性表(linear_list)是n个具有相同特性的数据元素的有限列。 线性表是一种在实际中最常用且最简单的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表中的数据元素虽然具有相同特性,但是具体数据的含义根据不同情况而定。例如: 26个英文字母的字母表: (A,B,C,……,Y,Z) 就可以构成一个线性表,表中的数据元素是单个字母。
•2.线性表的分类
线性表在逻辑上是线性结构,也就说是连续的一条直线。 但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
图示结构展示:
• 知识点2:线性表之链表
• 2.1 链表的概念及分类
•1.链表的概念
线性表的链式存储结构是用一组任意的存储单元存储线性表的数据元素,物理存储结构上非连续,非顺序的存储结构。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的区域称为指针域。指针域中存储的信息称为指针或链。n个结点链接程一个链表,即为线性表的链式结构。
例如:
•2.链表的分类
实际中链表的结构非常多样,以下情况组合起来:
1、单向或者双向链表
图示结构展示:
2、带头或者不带头
图示结构展示:
3、循环或者不循环
图示结构展示:
分析:
(1)无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
(2) 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面们代码实现了就知道了。
图解链表分类组合:
• 知识点3:双向带头循环链表
• 3.1 双向带头循环链表的实现
这里我们就拿 双向带头循环链表 来做示例讲解哈~
•1.创建一个结点
链表一开始为空或创建头的时候,都需要使用到这里的创建结点的函数。这个函数利于后面链表一些列操作函数的进行。
代码展示://创建一个结点 LNode* BuyList(SLDataType x) { LNode* newnode = (LNode*)malloc(sizeof(LNode)); if (newnode == NULL) { printf("malloc fail\n"); exit(-1); } newnode->val = x; newnode->prev = NULL; newnode->next = NULL; return newnode; }
•2.初始化双向项链表
双向链表初始化实际上是创建一个哨兵位头节点,即建立一个哨兵。这个函数的形参可以用一级指针接受,也可以用二级指针接受。当然,一般有哨兵位的链表直接用一级指针接受,这样处理更加合理,若是用二级指针就有点得不偿失。
代码展示:(形参为一级指针形式)//链表初始化(传一级指针法) LNode* ListInit()//初始化实际上是建立一个哨兵位头节点 { LNode* Phead = BuyList(0); Phead->next = Phead; Phead->prev = Phead; return Phead; }
•3.双向链表尾插函数
尾插函数,这里在进行尾插的时候,已经存在哨兵位头结点,所以就算此时最初的链表为空也可以实现尾插,因此这里面不需要检查链表是否为空。
代码展示://尾插函数O(1) void ListPushBack(LNode* Phead, SLDataType x) { assert(Phead); LNode* newnode = BuyList(x); LNode* tail = Phead->prev; tail->next = newnode; newnode->prev = tail; newnode->next = Phead; Phead->prev = newnode; }
•4.双向链表尾删函数
尾删函数,在删除数据的时候需要判断链表是否为空,如果为空就不能继续操作尾删,即哨兵位头结点不能被删除。
代码展示:/尾删函数 void ListPopBack(LNode* Phead) { assert(Phead); //判断删除的时候链表是否为空,如果为空的时候就不能再删除了,不能将哨兵删除 assert(Phead->next != Phead); LNode* tail = Phead->prev; LNode* tailPrev = tail->prev; free(tail); tail = NULL; Phead->next = tailPrev; tailPrev->next = Phead; }
•5.双向链表头插函数
双向链表的头插其实在逻辑思维上和尾插类似,其实双向带头循环链表看起来名字复杂,但是操作和思维都是挺简单的。
代码展示://头插函数 void ListPushFront(LNode* Phead,SLDataType x) { assert(Phead); LNode* newnode = BuyList(x); LNode* cur = Phead->next; Phead->next = newnode; newnode->prev = Phead; newnode->next = cur; cur->prev = newnode; }
•6.双向链表头删函数
头删函数,这里依然需要和尾删函数一样注意链表是否为空,不能删除哨兵位头结点。
//头删函数 void ListPopFront(LNode* Phead) { assert(Phead); LNode* cur = Phead->next; LNode* str = cur->next; free(cur); cur = NULL; Phead->next = str; str->prev = Phead; }
•7.双向链表查找函数
查找函数,查找链表里面想要的数据,一般是从哨兵位头结点后面开始查找,找到再次遇到哨兵位头结点结束。查找函数一般也不单独使用,一般和后面的任意位置删除函数合作使用。
代码展示://查找函数 LNode* ListNodeFind(LNode* Phead, SLDataType x) { assert(Phead); LNode* cur = Phead->next; while ( cur != Phead) { if (cur->val == x) { return cur; } cur = cur->next; } return NULL; }
•8.任意位置插入数据函数
在任意位置插入函数,这里一般是在这个位置的前面插入数据,当然也可以在后面插入数据。但是我们这里举例是在前面插入数据,因为之后这种方法用的更多。
代码展示://在Pos位置插入x值 void ListInSert(LNode* Pos, SLDataType x) { assert(Pos); LNode* newnode = BuyList(x); LNode* cur = Pos->prev; newnode->prev = cur; cur->next = newnode; newnode->next = Pos; Pos->prev = newnode; }
•9.任意位置删除数据函数
删除任意位置函数一般和前面的查找函数结合使用,在删除函数使用之后,一把最好在调用该函数后面将其里面的指针free释放,防止出现内存泄漏情况。(这里 vince 在下面放一个知识点介绍介绍。)
代码展示://删除Pos位置的数 void ListErase(LNode* Pos) { assert(Pos); LNode* cur = Pos->prev; LNode* str = Pos->next; free(Pos); Pos = NULL; cur->next = str; str->prev = cur; }
•10.链表销毁释放函数
链表在使用结束后,需要对链表的空间进行释放销毁操作,实际上是将空间归还给内存。防止后面程序出现内存泄露问题。
代码展示://销毁函数 void Destory(LNode* Phead) { assert(Phead); LNode* cur = Phead->next; while (cur != Phead) { LNode* next = cur->next; //ListErase(cur)这里也可以调用Erase来操作,虽然Erase稍微慢一点, //但是现在计算机速度其实很快,所以没太大影响 free(cur); cur = next; } free(Phead); //Phead = NULL;这里仍然是形参,无法真正实现那边PList = NULL //所以这里Phead置空与否无所谓 }
• 知识点4:free函数的理解
• 4.1 free函数存在的目的
free不是真的销毁,而是将空间还给操作系统。free掉内容是防止发生内存泄漏问题。指针被使用之后如果不及时被free,可能后面会不注意再使用到这个指针,那么就会出现非法内存访问问题从而可能造成内存泄漏问题,可能会使得程序崩溃。当然,如果在这个链表里没有置空也可以,只要注意后面一直不使用这个指针就可以。
• 4.2 该链表里面的free
例如:
free(pos); pos = NULL;
这里free掉的实际上是pos所指向的结点内容,并不是pos作为指针本身,所以如果这里面处理严谨一些,就在调用该函数结束后,在主函数里面free掉pos指针,不能放在子函数里面free,因为形参是实参的临时拷贝,在子函数里面无法真正实现free(pos)。
图解分析:
• 知识点5:顺序表和链表的优缺点
• 5.1 顺序表优缺点
•1.顺序表优点
(1)物理空间是连续的,方便用下标随机访问;
(2)顺序表的 空间利用率是高于链表的 ;
因为顺序表储存一个数据就直接存4个字节,链表储存一个数据还要附带一个前后指针(有一些附带的消耗,但是其实顺序表在扩容的时候也有消耗,所以两者其实差距不大,一般对空间要求不大);
(3)(补充:了解掌握)顺序表CPU高速缓存命中率会更高。•2.顺序表缺点
(1)因为物理空间是连续的,所以空间不够时候需要扩容,扩容有一定的消耗,其次扩容机制还存在一定的空间浪费;
(2)头插或者中间插入数据,需要挪动数据,时间复杂度较高,都是O(N)。
• 5.2 链表优缺点
•1.链表优点
(1)链表可以按需申请和释放空间;
(2)任意位置插入数据效率高,O(1)。•2.链表缺点
(1)物理空间不是连续的,所以链表访问必须得挨个遍历,不支持下标随机访问。有些算法不适用,如二分查找、排序等。
•写在后面
链表这里面有简单的复用写法,将任意位置删除函数和任意位置插入函数写出来之后,对于头插头删、尾插尾删,这些函数就可以直接复用这两个函数,相对来说简单很多。
因为最近时间少所以vince就在链表这一块,就用双向带头循环链表作为典例介绍,当然前面本应该还有单链表,其实原理都是共通的,单链表一般在OJ面试体中有很多题型;双链表在日常生活中或者项目程序中用的最多,大家学习最终的目的就是拿来运用的嘛。所以大家一定要将其摸清、拿捏、吃透;✨✨✨
代码不负有心人,98加满,向前冲啊
以上代码均可运行,所用编译环境为 vs2019 ,运行时注意加上编译头文件#define _CRT_SECURE_NO_WARNINGS 1