一、双向循环链表(带头双向循环链表)
上一篇已经介绍过链表的相关概念,并且对单链表进行了详细讲解,这一篇将继续对双链表进行详细讲解。
既然已经学习了单链表,那为什么还要学习带头双向循环链表呢?因为单链表是存在缺陷的,它是单向非循环链表,我们只能通过指针plist(单链表为非空链表时,plist指向第一个结点的位置,单链表为空链表时,plist的值为NULL)去查找它的结点。当单链表为非空链表时,我们只能通过指针plist直接找到它的第一个结点,而其它的结点都需要通过指针plist从前往后遍历查找。这将会在很大程度上限制我们的操作,非常不方便,而双向循环链表可以很好的解决这些问题。
双向链表结点结构体:
一个数据域,两个指针域,两个指针分别指向前一个结点和后一个结点。
代码"typedef int LTDataType"重命名是因为当结点所存数据类型发生改变时方便统一更换,只要在这里将int改成相应的数据类型就行(结点结构体里的变量data的数据类型就是LTDataType)
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* prev;//指向前一个结点
struct ListNode* next;//指向后一个结点
}LTNode;
带头双向循环链表的结构:
当它为空链表时,只有一个头结点,该头结点的两个两个指针都指向自己。
当它为非空链表时。
从带头双向循环链表的结构就可以看出来,它可以由给出的,一个结点找到其它任意的结点,而且可以由头节点直接找到尾结点,任何时候它的指针都不可能为NULL值,所以用起来特别方便。
二、函数的实现
1、动态申请一个结点
//申请一个节点
LTNode* BuyLTNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL) //检查动态申请是否失败
{
perror("malloc fail");
exit(-1);
}
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
2、初始化
为什么需要初始化,因为带头双向循环链表不管是不是空链表都会有一个头结点(要注意此处头节点是指哨兵位头结点,而不是第一个存数据的结点,不要搞混了),
//初始化
LTNode* LTInit()
{
LTNode* phead=BuyLTNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
3、尾插
对于带头双向循环链表可以通过phead快速找到尾结点,从而进行尾插操作,而且不管被尾插的链表是不是空链表,都不会改变phead的值。这样不需要对链表为空链表时进行特殊的考虑(这是带头结点对于尾插的优势)
//尾插
void LTPushBack(LTNode* phead,LTDataType x)
{
assert(phead); //判断指针是否为空
LTNode* newnode = BuyLTNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
}
4、 尾删
尾删需要注意不要将头结点删掉。对于带头双向循环链表可以快速通过尾结点找到前一个结点,这样有利于尾删,而且尾删时也不会改变phead的值。
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next !=phead);//防止头节点被删除
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
phead->prev = tailPrev;
tailPrev->next = phead;
free(tail);//将被删的结点释放掉
}
5、头插
不管链表为不为空都可以直接进行头插,也不会改变phead的值。
//头插
void LTPushFront(LTNode* phead,LTDataType x)
{
assert(phead);
LTNode* newnode=BuyLTNode(x);
newnode->next= phead->next;
phead->next->prev = newnode;
phead->next= newnode;
newnode->prev=phead;
}
6、头删
头删比较简单,但要注意不要将头结点删掉。
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);//防止将头结点删掉
LTNode* cur = phead->next;
LTNode* next = cur->next;
free(cur);
phead->next = next;
next->prev = phead;
}
7、打印
从第一个存储数据的结点开始依次往后,当前结点不是头结点时,对其进行打印操作。
//打印
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
8、查找
从第一个存储数据的结点开始依次往后,当前结点不是头结点时,对其进行查找操作,如果找到则返回该结点地址,如果最终没有找到则返回NULL。
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
9、在pos位置之前插入
因为“带头双向循环链表”本身已经带头结点了,所以不管pos位置是第一个结点还是其它存储数据的结点,对其进行”在pos位置之前插入“操作都不会改变phead的值。
这里解释下为什么“带头双向循环链表”只有“在pos位置之前插入”而不像单链表还有“在pos位置之后插入”,因为单链表从pos位置找后一个结点可以通过pos->next直接找到,但找前一个结点却只能从前往后遍历查找,所以单链表“在pos位置之后插入”的操作可以很便利的插入数据,但“带头双向循环链表”不需要这样,因为它是循环链表,它通过pos位置查找前一个结点和查找后一个结点是同样方便的。
//在pos之前插入
void LTInsert(LTNode* pos,LTDataType x)
{
assert(pos);//判断指针不为空
LTNode* prev = pos->prev;
LTNode* newnode = BuyLTNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
10、删除pos位置
因为“带头双向循环链表”本身已经带头结点了,所以不管pos位置是第一个结点还是其它存储数据的结点,对其进行“删除pos位置”操作都不会改变phead的值。
//删除pos位置
void LTErase(LTNode* pos)
{
assert(pos);//判断指针不为空
LTNode* first = pos->prev;
LTNode* second= pos->next;
free(pos);
first->next = second;
second->prev = first;
}
11、判空
如果phead->next == phead,则该链表为空。
//判空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
12、求链表长度
//求链表长度
size_t LTSize(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
size_t size = 0;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
13、销毁
先将存储数据的第一个结点到最后一个结点释放掉,再将头结点释放掉。
这里还要注意,在调用完该函数之后,要将该链表的头指针置为空,因为该函数该从实参到形参是传值(头指针变量的值),而不是传址(头指针变量的地址),无法通过将形参phead置为NULL从而达到将实参也变为NULL的效果。
//销毁
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
三、 源代码
List.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
LTNode* BuyLTNode(LTDataType x);
//初始化
LTNode* LTInit();
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//打印
void LTPrint(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos前插
void LTInsert(LTNode* pos, LTDataType x);
//判空
bool LTEmpty(LTNode* phead);
//求链长
size_t LTSize(LTNode* phead);
//销毁
void LTDestroy(LTNode* phead);
List.c
#include"list.h"
//申请一个节点
LTNode* BuyLTNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
//初始化
LTNode* LTInit()
{
LTNode* phead=BuyLTNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
//尾插
void LTPushBack(LTNode* phead,LTDataType x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next !=phead);
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
phead->prev = tailPrev;
tailPrev->next = phead;
free(tail);
}
//头插
void LTPushFront(LTNode* phead,LTDataType x)
{
assert(phead);
LTNode* newnode=BuyLTNode(x);
newnode->next= phead->next;
phead->next->prev = newnode;
phead->next= newnode;
newnode->prev=phead;
}
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* cur = phead->next;
LTNode* next = cur->next;
free(cur);
phead->next = next;
next->prev = phead;
}
//打印
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
//在pos之前插入
void LTInsert(LTNode* pos,LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyLTNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
//删除pos位置
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* first = pos->prev;
LTNode* second= pos->next;
free(pos);
first->next = second;
second->prev = first;
}
//判空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
//求链表长度
size_t LTSize(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
size_t size = 0;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
//销毁
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
Test.c
#include"list.h"
int main()
{
LTNode* phead = LTInit();
LTPushBack(phead, 100);
LTPrint(phead);
LTPopBack(phead);
LTPrint(phead);
LTPushFront(phead, 200);
LTPushFront(phead, 300);
LTPushFront(phead, 400);
LTPrint(phead);
LTNode* pos = LTFind(phead, 400);
LTInsert(pos, 500);
LTPrint(phead);
pos = LTFind(phead, 500);
LTErase(pos);
LTPrint(phead);
LTDestroy(phead);
phead = NULL;
return 0;
}
四、 总结
通过对单链表(不带头单向非循环链表)和带头双向循环链表的学习,我们可以发现带头双向循环链表优势更明显,虽然它看起来结构更复杂,但用起来却更方便。