上一篇博客写了链表中最简单的单链表的实现,今天一起来看看链表中最复杂的结构带头双向循环链表的实现吧!
带头双向循环链表:结构最复杂,一般用在单独存储数据,也是实际中使用最多的链表结构。结构图如下:
下面看看它的实现吧!
链表单个节点的结构
带头双向循环链表要实现双向循环,那通过一个节点必然要能找到它的上一个元素,并且也要找到它的下一个元素,还要储存这个节点的数据,所以单个节点的结构体成员有三个:1)存储上一个节点位置的指针;2)存储下一个节点位置的指针;3)存储数据的量
代码:
typedef int LNTypeData;
typedef struct ListNote
{
struct ListNote* next;//存储下一个节点地址
struct ListNote* prev;//存储上一个节点地址
LNTypeData data;//存储数据
}ListNote;
开辟新节点
链表插入数据需要开辟新的节点,因为不知一个函数里要开辟新节点,所以把开辟节点封装为函数。这里将结构体里的指针全都置为空。代码如下:
//开辟节点
ListNote* BuyListNote(LNTypeData x)
{
ListNote* NewNote = (ListNote*)malloc(sizeof(ListNote));//开辟新节点
assert(NewNote);//判断是否开辟成功
NewNote->data = x;
NewNote->next = NULL;
NewNote->prev = NULL;
return NewNote;//返回开辟节点的地址
}
创建返回链表的头结点
开辟一个节点作为链表的头,但是这个头节点不存储实际数据也就是虚设节点;因为是双向循环所以创建之初,指向前一个节点的指针和指向后一个节点的指针都指向它本身,这样做也是为了后面的实现。代码如下:
//创建返回链表的头结点
ListNote* ListNoteInit(LNTypeData x)
{
ListNote* pHead = BuyListNote(x);
pHead->prev = pHead;
pHead->next = pHead;
return pHead;
}
双链表尾插
在单链表中我们实现这个功能首先要找到为链表的尾,但是在这里不用,我们通过头结点(虚设头节点)就能找到尾,也就是头节点的前一个节点就是尾;只需要记录尾的位置,连接上新的节点,以及作为新的尾与头的连接。
代码如下:
//尾插
void ListNotePushBack(ListNote* pHead, LNTypeData x)
{
assert(pHead);
ListNote* tail = pHead->prev;//保存尾的地址
ListNote* NewNote = BuyListNote(x);//开辟新节点
tail->next = NewNote;//将原来的尾与新尾(新节点)相连
NewNote->prev = tail;
NewNote->next = pHead;//将新尾与头相连
pHead->prev = NewNote;
}
双向链表头插
头插实际上是在头(虚设头节点)的后面的插入,因为我们创建的头实际是虚设的,实际的头是虚设头节点下一个节点;
代码如下:
//头插
void ListNotePushFront(ListNote* pHead, LNTypeData x)
{
assert(pHead);
ListNote* next = pHead->next;//存储实际头的地址
ListNote* NewNote = BuyListNote(x);//开辟节点
//将新建节点插入到链表中并连接
pHead->next = NewNote;
NewNote->next = next;
next->prev = NewNote;
NewNote->prev = pHead;
}
双向链表尾删
尾删需要记录尾的前一个节点,以防止释放尾后找不到它的前一个节点,那这样也就没办法与头(虚设头结点)连接起来;代码如下:
//尾删
void ListNotePopBack(ListNote* pHead)
{
assert(pHead);
assert(pHead->next != pHead);//判断链表中是否有节点
ListNote* tail = pHead->prev;//记录尾的地址
ListNote* tailPrev = tail->prev;//记录尾的前一个节点的地址,也就是新尾
//将新尾与头相连形成循环
tailPrev->next = pHead;
pHead->prev = tailPrev;
free(tail);释放要删除的节点
tail = NULL;
}
双向链表头删
实现和尾删差不多;需要记录实际头的下一个节点的地址;以便与虚设的头相连;代码如下:
//头删
void ListNotePopFront(ListNote* pHead)
{
assert(pHead);
assert(pHead->next != pHead);
ListNote* head = pHead->next;//记录实际头的地址(要是删除的地址)
ListNote* headNext = head->next;//记录头的下一个节点的地址;
//将虚设头结点与实际头结点相连
pHead->next = headNext;
headNext->prev = pHead;
free(head);//释放要删除的节点
head = NULL;
}
双向链表查找
查找需要遍历整个链表;需要注意的是遍历是从实际的头开始;也就虚设头结点的下一个;也要注意遍历的结束条件,因为是循环链表,链表的尾的下一个节点是虚设头结点,虚设头结点也就是遍历结束的条件,代码如下:
//查找
ListNote* ListNoteFind(ListNote* pHead, LNTypeData x)
{
assert(pHead);
assert(pHead->next != pHead);
ListNote* cur = pHead->next;
while (cur != pHead)//遍历结束条件
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
双向链表在pos前进行插入
这里需要记录pos位置节点的前一个节点,以便插入新节点的相连;代码如下:
//在pos位置前插入
void ListNoteInsert(ListNote* pos, LNTypeData x)
{
assert(pos);
ListNote* posPrev = pos->prev;//记录pos前的节点
ListNote* NewNote = BuyListNote(x);//创建新节点
//插入节点,并相连
posPrev->next = NewNote;
NewNote->next = pos;
pos->prev = NewNote;
NewNote->prev = posPrev;
}
双向链表删除pos位置的节点
这里需要记录pos位置的前一个节点和后一个节点,代码如下:
//删除pos位置的数据
void ListNoteErase(ListNote* pos)
{
assert(pos);
ListNote* posPrev = pos->prev;//记录pos前一个节点
ListNote* posNext = pos->next;//记录pos后一个节点
//前后节点进行连接
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);//释放pos位置的节点
pos = NULL;
}
有了在pos前进行插入函数和删除pos位置的节点函数,那我们之前写的头删尾删,头插尾插都可调用这两个函数进行实现,不用再写代码。
双向链表打印
这里和查找一样需要遍历整个链表;那么需要注意的也就一样了,即:遍历是从实际的头开始;也就虚设头结点的下一个;也要注意遍历的结束条件,因为是循环链表,链表的尾的下一个节点是虚设头结点,虚设头结点也就是遍历结束的条件;代码如下:
//打印
void ListNotePrint(ListNote* pHead)
{
assert(pHead);
ListNote* cur = pHead->next;
while (cur != pHead)
{
printf("%d ",cur->data);
cur = cur->next;
}
printf("\n");
}
双向链表销毁
链表的销毁需要一个节点一个节点进行释放,注意事项与打印相同;代码如下:
//销毁
void ListNoteDestory(ListNote* pHead)
{
assert(pHead);
ListNote* cur = pHead->next;
while (cur != pHead)
{
ListNote* next = cur->next;
free(cur);
cur = NULL;
cur = next;
}
free(pHead);
}
带头双向循环链表结构虽然复杂,但只要理清它的链接方式,实现起来我觉得反而比单链表简单。
本篇博客就到这了,因为我也是个初学者难免会出现错误,如果您在阅读中发现错误,请前辈指正,感激不尽!!!