作者主页
lovewold少个r博客主页
人生的每一个选择,都是得到一些,失去一些。
前言
双向循环链表是什么
双向带头循环链表的实现
开辟节点空间
创建返回链表的头结点
双向链表打印
双向链表在pos的前面进行插入
双向链表尾插
双向链表头插
双向链表删除pos位置的节点
双向链表尾删
双向链表头删
双向链表查找
双向链表销毁
源文件
test.c
List.h
List.c
总结
在单链表的实现过程中,单链表展示了对碎片化内存空间的有效利用,并且在实现的过程中我们也发现在一些操作过程中,单链表也表现出了他的劣势。主要对插入的情况需要判断的太繁琐,并且查找上一个元素还比较麻烦,那有没有什么能解决单链表存在的这种劣势,实现对查找删除的更加优化的结构呢。答案就是双向带头循环列表。
双向循环链表和单链表类似,我们把一个节点定义为两个指针域和一个数据域,数据域用来存放值,两个指针域一个指向下一个节点,一个指向上一个节点。所以相对于单链表,他查找上一个节点就非常的方便快捷。
在头文件中包含函数声明。
#pragma once #include
#include #include #include // 带头+双向+循环链表增删查改实现 typedef int LTDataType; typedef struct ListNode { LTDataType data; struct ListNode* next; struct ListNode* prev; }ListNode; //开辟节点空间 ListNode* BuyLTnewNode(LTDataType x); // 创建返回链表的头结点. ListNode* ListCreate(); // 双向链表销毁 void ListDestory(ListNode* pHead); // 双向链表打印 void ListPrint(ListNode* pHead); // 双向链表尾插 void ListPushBack(ListNode* pHead, LTDataType x); // 双向链表尾删 void ListPopBack(ListNode* pHead); // 双向链表头插 void ListPushFront(ListNode* pHead, LTDataType x); // 双向链表头删 void ListPopFront(ListNode* pHead); // 双向链表查找 ListNode* ListFind(ListNode* pHead, LTDataType x); // 双向链表在pos的前面进行插入 void ListInsert(ListNode* pos, LTDataType x); // 双向链表删除pos位置的节点 void ListErase(ListNode* pos); 开辟节点空间
ListNode* BuyLTnewNode(LTDataType x);
节点包含两个域,一个指向节点的直接前驱,一个指向节点的直接后继,在数据域我们存储我们的值。ListNode* BuyLTnewNode(LTDataType x) { ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));//申请空间 if (newNode == NULL)//开辟成功判断 { perror("malloc fail\n"); exit(-1); } //初始化 newNode->data = x; newNode->prev = NULL; newNode->next = NULL; return newNode; }
创建返回链表的头结点
ListNode* ListCreate();
这里我们要确定头节点的状态情况,当只有一个头节点的时候,他的直接前驱和后继指向应该是指向直接本身,达到循环的状态。ListNode* ListCreate() { ListNode* phead = BuyLTnewNode(-1); phead->prev = phead; phead->next = phead; return phead; }
双向链表打印
void ListPrint(ListNode* pHead);
双向链表的打印操作依旧是基于对链表的遍历过程,主要是理解遍历的过程和遍历的起止条件。在有头节点的遍历过程中,起始遍历位置应该是pHead->next;终止条件会最后回到头节点,此时应该终止。即cur==pHead时。
void ListPrint(ListNode* pHead) { assert(pHead); printf("pHead<=>"); ListNode* cur = pHead->next; while (cur != pHead) { printf("%d<=>",cur->data); cur = cur->next; } printf("\n"); }
双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
在一个已有链表中插入一个节点,要注意先链接后面节点在断开,避免先链接新节点把后面的节点地址忘记。同时对于一个节点,我们是不是也能在pos(指头节点)前插入呢,答案是可以的,因为头节点本身就是循环的一个结构,其前驱和后继都是其本身,插入新节点,无外乎就是在pHead前面插入一个,其头节点的下一个指向依旧是新节点也就等同于在pHead后面放置一个新元素。即只有一个节点时,pos为头节点时,插入新节点依旧不需要额外考虑,等同于在pHead后尾插。
void ListInsert(ListNode* pos, LTDataType x) { assert(pos); ListNode* newNode = BuyLTnewNode(x); ListNode* tail = pos->prev; newNode->next = pos; pos->prev=newNode; tail->next = newNode; newNode->prev = tail; }
双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
尾插的在单链表的操作过程中首先需要找到尾部,而对于带头双向循环链表,他的优势就展现的淋漓尽致了。对于找尾操作过程,因为头节点和尾节点本身就有前后关系,即只需要pHead->prev即可访问到尾节点,完全避免了单链表的尾插时间复杂度。更关键点是我们在双向链表尾插等同于在pHead节点(pos位)前插入,也就可以复用void ListInsert(ListNode* pos, LTDataType x)函数。void ListPushBack(ListNode* pHead, LTDataType x) { assert(pHead); ListInsert(pHead, x); }
双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
同理,对于头插,等同于在头节点的下一个元素进行插入,而如果只有一个头节点,其下一个节点为本身,依旧不需要额外考虑。pHead->next即可访问到下一节点(pos位),并在前面进行插入。
void ListPushFront(ListNode* pHead, LTDataType x) { assert(pHead->next); ListInsert(pHead->next, x); }
双向链表删除pos位置的节点
void ListErase(ListNode* pos);
删除pos位置的时候,只需要改变pos位置的上一个节点和下一个节点的连接关系,并free pos位节点,对于链表的任何一个位置,因为整体为循环结构,边界情况也一样,完全不需要额外考虑。也就是说当pos位分别位于pHead->next和pHead->prev就可以实现头删和尾删。
void ListErase(ListNode* pos) { assert(pos); ListNode* first = pos->prev; ListNode* second = pos->next; first->next = second; second->prev = first; free(pos); }
双向链表尾删
void ListPopBack(ListNode* pHead);
void ListPopBack(ListNode* pHead) { assert(pHead); ListErase(pHead->prev); }
双向链表头删
void ListPopFront(ListNode* pHead);
void ListPopFront(ListNode* pHead) { assert(pHead); ListErase(pHead->next); }
双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
实现过程就是对双向链表的遍历和比对查找,找到返回值的pos位。后续可以对地址,值的更改,前后插入进行操作。
ListNode* ListFind(ListNode* pHead, LTDataType x) { ListNode* cur = pHead->next; while (cur != pHead) { if (cur->data == x) { return cur; } cur = cur->next; } return NULL; }
双向链表销毁
void ListDestory(ListNode* pHead);
销毁操作可以理解遍历头删或者尾删,当最后的遍历到头节点本身时候即销毁完毕,释放整个链表即可。
void ListDestory(ListNode* pHead) { assert(pHead); while (pHead->next != pHead) { ListPopBack(pHead); } free(pHead); }
测试环境我们通过建立菜单的方式进行操作,以下为全部源码。
test.c
#define _CRT_SECURE_NO_WARNINGS 1 #include"List.h" void menu() { printf("*********************************************\n"); printf("*********** 1.头插 *****************\n"); printf("*********** 2.尾插 *****************\n"); printf("*********** 3.头删 *****************\n"); printf("*********** 4.尾删 *****************\n"); printf("*********** 5.查找 *****************\n"); printf("*********** 6.删除pos位 *****************\n"); printf("*********** 7.销毁链表 *****************\n"); printf("*********** 0.exit *****************\n"); printf("*********************************************\n"); printf("*********************************************\n"); } int main() { ListNode* List = ListCreate(); int x = 0; int input = 1; menu(); while (input) { printf("请选择你要执行的操作->\n"); scanf("%d", &input); switch (input) { case 1: printf("请输入你要头插的值\n"); scanf("%d", &x); ListPushFront(List,x); ListPrint(List); break; case 2: printf("请输入你要尾插的值\n"); scanf("%d", &x); ListPushBack(List, x); ListPrint(List); break; case 3: ListPopFront(List); ListPrint(List); break; case 4: ListPopBack(List); ListPrint(List); break; case 5: printf("请输入你要查找的值\n"); scanf("%d", &x); ListNode* Node = ListFind(List, x); if (Node != NULL) { printf("这个值在内存中的地址是->%p", &Node); } break; case 6: printf("请输入你要删除的值\n"); scanf("%d", &x); ListNode* Node1 = ListFind(List, x); if (Node1 != NULL) { ListErase(Node1); ListPrint(List); } else { printf("链表没有'%d'这个值\n",x); } break; case 7: ListDestory(List); ListPrint(List); break; default: printf("输入错误,重新输入\n"); break; } } return 0; }
List.h
#pragma once #include
#include #include #include // 带头+双向+循环链表增删查改实现 typedef int LTDataType; typedef struct ListNode { LTDataType data; struct ListNode* next; struct ListNode* prev; }ListNode; //开辟节点空间 ListNode* BuyLTnewNode(LTDataType x); // 创建返回链表的头结点. ListNode* ListCreate(); // 双向链表销毁 void ListDestory(ListNode* pHead); // 双向链表打印 void ListPrint(ListNode* pHead); // 双向链表尾插 void ListPushBack(ListNode* pHead, LTDataType x); // 双向链表尾删 void ListPopBack(ListNode* pHead); // 双向链表头插 void ListPushFront(ListNode* pHead, LTDataType x); // 双向链表头删 void ListPopFront(ListNode* pHead); // 双向链表查找 ListNode* ListFind(ListNode* pHead, LTDataType x); // 双向链表在pos的前面进行插入 void ListInsert(ListNode* pos, LTDataType x); // 双向链表删除pos位置的节点 void ListErase(ListNode* pos);
List.c
#define _CRT_SECURE_NO_WARNINGS 1 #include"List.h" ListNode* BuyLTnewNode(LTDataType x) { ListNode* newNode = (ListNode*)malloc(sizeof(ListNode)); if (newNode == NULL) { perror("malloc fail\n"); exit(-1); } newNode->data = x; newNode->prev = NULL; newNode->next = NULL; return newNode; } ListNode* ListCreate() { ListNode* phead = BuyLTnewNode(-1); phead->prev = phead; phead->next = phead; return phead; } void ListPrint(ListNode* pHead) { assert(pHead); printf("pHead<=>"); ListNode* cur = pHead->next; while (cur != pHead) { printf("%d<=>",cur->data); cur = cur->next; } printf("\n"); } void ListInsert(ListNode* pos, LTDataType x) { assert(pos); ListNode* newNode = BuyLTnewNode(x); ListNode* tail = pos->prev; newNode->next = pos; pos->prev=newNode; tail->next = newNode; newNode->prev = tail; } void ListPushFront(ListNode* pHead, LTDataType x) { assert(pHead->next); ListInsert(pHead->next, x); } void ListPushBack(ListNode* pHead, LTDataType x) { assert(pHead); ListInsert(pHead, x); } void ListPopFront(ListNode* pHead) { assert(pHead); ListErase(pHead->next); } void ListPopBack(ListNode* pHead) { assert(pHead); ListErase(pHead->prev); } void ListErase(ListNode* pos) { assert(pos); ListNode* first = pos->prev; ListNode* seconed = pos->next; first->next = seconed; seconed->prev = first; free(pos); } ListNode* ListFind(ListNode* pHead, LTDataType x) { ListNode* cur = pHead->next; while (cur != pHead) { if (cur->data == x) { return cur; } cur = cur->next; } return NULL; } void ListDestory(ListNode* pHead) { assert(pHead); while (pHead->next != pHead) { ListPopBack(pHead); } free(pHead); }
整个实现过程中,我们发现我们并不需要像单链表一样,对不同的情况进行分别考虑,在删除和插入的实现可以通用删除和插入函数。只是传递的pos节点不同即可实现一样的效果。双向链表看起来实现需要两个指针,需要额外小心删除和插入的操作,但是当这个问题解决后,他的实现效率大大提高。通过建立两个指针在空间上是比单链表和顺序表消耗了更多的空间,这种空间换时间的方式也大大提高了销量,毕竟目前空间的消耗成本往往低于时间。
结构特点:
- 双向性:每个节点都有指向前一个节点和后一个节点的指针,这使得在链表中可以双向遍历元素。
- 带头节点:通常情况下,双向带头循环链表会包含一个头节点(dummy node),该节点不包含数据,但用于简化链表的操作,例如在插入和删除元素时无需特殊处理边界情况。
- 循环性:链表的尾节点指向头节点,形成一个循环。
基本操作:
- 插入元素:在链表中插入元素通常涉及修改前后节点的指针,以保持链表的连续性。
- 删除元素:从链表中删除元素需要同样修改前后节点的指针,并释放被删除节点的内存。
- 遍历:可以从头节点开始,沿着指针依次遍历链表的元素。由于是双向链表,可以实现正向和逆向遍历。
优点:
- 插入和删除操作效率较高:与数组相比,在链表中插入和删除元素的开销较小,因为不需要移动其他元素。
- 循环性质:适用于需要循环访问数据的场景,例如循环队列、循环缓冲区等。
缺点:
- 随机访问低效:与数组不同,链表需要从头节点开始遍历,才能访问到特定位置的元素,因此随机访问的性能较差。
- 额外空间开销:每个节点都需要额外的指针空间,占用额外的内存。
双向带头循环链表是一种灵活的数据结构,适用于特定的应用场景,其中插入和删除操作是频繁进行的情况下表现出色。然而,需要权衡其随机访问性能较差和额外的空间开销。在选择数据结构时,应根据具体需求和操作的频率来考虑是否使用双向带头循环链表。
文章由于个人水平有限,如果有错误欢迎读者批评指正!感激不尽!