在链表中有八大形式,如下图:
- 其中,最为重要的两种结构为最简单的单向不带头不循环链表和双向带头循环链表。 单链表前面的文章已经介绍过,本篇文章重点介绍双向带头循环链表。
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。
另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
双向带头循环链表的基本结构如上图:
在单链表的基础上,新增了哨兵位的头节点,头节点不存储有效数据,还新增了一个指针prev,用于链接节点之前的节点。
节点的结构
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<assert.h> typedef int LTDateType; typedef struct ListNode { LTDateType data; struct ListNode* prev; struct ListNode* next; }LTNode;
所以每一个节点的结构如上,下面来逐一实现双向带头循环的接口函数:
1.初始化双向链表(创建哨兵位头节点)
LTNode* ListInit() { LTNode* phead = (LTNode*)malloc(sizeof(LTNode)); assert(phead); phead->data = 0; phead->next = phead; phead->prev = phead; return phead; }
也可以按照自己的需求,可以不在ListInit内部创建带哨兵位的头节点,也可以使用其他的方式来判断节点是否申请成功,不过建议初学者模仿别人优秀的代码。
2.申请新节点
LTNode* BuyLTNode(LTDateType x) { LTNode* newnode = (LTNode*)malloc(sizeof(LTNode)); assert(newnode); newnode->data = x; newnode->next = NULL; newnode->prev = NULL; return newnode; }
- 由于在头插尾插和在pos位置插入节点时需要申请节点空间,所以我们干脆把申请节点这件事情另外分装成一个函数,在头插尾插中直接调用该函数即可。
3.尾插节点
void ListPushBack(LTNode* phead, LTDateType x) { assert(phead); LTNode* newnode = BuyLTNode(x); assert(newnode); LTNode* tail = phead->prev; newnode->prev = tail; tail->next = newnode; newnode->next = phead; phead->prev = newnode; }
尾插中,只需要把phead->prev 指向尾插的新节点,phead->next在指向newnode。newnode->prev指向头,newnode->next也指向头,这样就链接成了双向带头循环链表。
4.尾删节点
void ListPopBack(LTNode* phead) { assert(phead); assert(phead->next != phead);//不能删哨兵位的头 LTNode* Pop = phead->prev; LTNode* PoptPrev = Pop->prev; PoptPrev->next = phead; phead->prev = PoptPrev; free(Pop); }
尾删的时候,记录最后一个删除的节点的前面的节点,然后让头节点与该节点建立关系即可。
5.头插节点
void ListPushFront(LTNode* phead, LTDateType x) { assert(phead); LTNode* newnode = BuyLTNode(x); assert(newnode); LTNode* cur = phead->next; cur->prev = newnode; newnode->next = cur; phead->next = newnode; newnode->prev = phead; }
头插也是如此,记录哨兵位的头节点的下一个节点,在哨兵位的头节点和该节点直接插入一个新节点,让它们形成链接关系即可。
6.头删节点
void ListPopFront(LTNode* phead) { assert(phead); assert(phead->next!=phead);//不能删哨兵位的头 LTNode* Pop = phead->next; LTNode* PopNext = Pop->next; phead->next = PopNext; PopNext->prev = phead; free(Pop); }
头删,记录头删的节点的下一个节点,让头节点与该节点建立联系。
7.寻找pos位置的节点
LTNode* ListFind(LTNode* phead,LTDateType x) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { if (cur->data == x) return cur; cur = cur->next; } return NULL; }
寻找节点很简单,遍历一遍链表即可。
8.在pos位置之前插入节点
//在pos位置之前插入 void ListInsert(LTNode* pos , LTDateType x) { assert(pos); LTNode* posprev = pos->prev; LTNode* newnode = BuyLTNode(x); assert(newnode); posprev->next = newnode; newnode->prev = posprev; pos->prev = newnode; newnode->next = pos; }
注意:插入某个位置的节点,一般是和ListFind函数一起使用,先找到链表中的某个节点,再在该节点之前插入节点。
所以,头插和尾插,均可以用该插入函数来代替。
先写头插尾插的原因是,对于初学者,建议一步一步走。
对于尾插来说,给phead即可,因为现在设计的插入函数是在pos位置之前插入,phead位置之前就是尾。
void ListPushBack(LTNode* phead, LTDateType x) { assert(phead); //phead->prev = newnode; ListInsert(phead, x);//可以复用插入函数 }
对于头插,更加简单,传phead->next即可。
void ListPushFront(LTNode* phead, LTDateType x) { assert(phead); ListInsert(phead->next, x);//头插 }
9.删除pos位置的节点
void ListErase(LTNode* pos) { assert(pos); LTNode* posprev = pos->prev; LTNode* posnext = pos->next; posprev->next = posnext; posnext->prev = posprev; free(pos); }
同理,头删尾删也可以调用该函数来实现。
建议下去自己尝试一遍。
10.打印链表出来
void ListPrint(LTNode* phead) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { printf("%d->", cur->data); cur = cur->next; } printf("\n"); }
11.销毁链表
void ListDestroy(LTNode* phead) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { LTNode* next = cur->next; free(cur); cur = next; } free(phead); phead = NULL;//这条语句没用,因为传进来的是形参phead //传二级指针才有用,但是为了接口函数的一致性,我们可以在调用完该函数再置空。 //例如: //ListDestroy(plist); //plist = NULL; }
注意:最后释放带哨兵位的头节点时,置空的那条语句在该函数内部不起作用,因为传进来的是phead的拷贝,是形参,形参的改变不影响实参,所以在调用完该函数后,需要手动置空。
为什么不设计成传实参进来呢?其实也可以这样设计,不过我们前面的所以的接口函数都是传形参,即一级指针,突然传一个二级指针,难免感到奇怪违和,给读代码的人带来疑惑,所以我们这里也跟随接口函数的一致性来设计。
实现结果如下,完美达到目的。