嗨嗨大家~本期带来的内容是:带头双向循环链表的实现。在上期文章中我们提到过带头双向循环链表,那么它的实现又是怎样的呢?今天我们来一探究竟!
目录
前言
我们在上期内容中讲过,链表结构是多样化的。但在实际中最常用的只有两种:无头单向非循环链表和带头双向循环链表。前者已经在上篇博客(http://t.csdnimg.cn/s8ieT)进行了全面的讲解,现在我们来认识并实现后者。
一、认识带头双向循环链表
1 认识双向链表
单链表虽然能够实现从任一结点出发沿着链能找到其前驱结点,但时间耗费是O(n)。如果希望从表中快速确定某一个结点的前驱,另一个解决方法就是在单链表的每个结点里再增加一个指向其前驱的指针域prev。这样形成的链表中就有两条方向不同的链,称之为双(向)链表。
与单链表类似,双向链表也可增加头结点使双向链表的某些运算变得方便。同时双向链表也可以有循环表,称为双向循环链表。 由于在双向链表中既有前向链又有后向链,所以寻找任一结点的直接前驱结点与直接后继结点都变得非常便捷。
2 带头双向循环链表的定义
带头双向循环链表:结构最复杂,一般用于单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
二、带头双向循环链表的实现
2.1 定义
代码实现:
//定义
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
分析:这里与单链表的定义不同,带头双向循环链表要定义两个指针:前驱指针prev和后继指针next。前驱指针prev用于指向当前结点的上一个结点,后继指针next用于指向当前结点的下一个结点。
2.2 创建结点
代码实现:
//创建结点
LTNode* BuyListNode(LTDataType x)
{
//动态开辟一个结点node
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;
}
分析:结点的创建主要是通过调用malloc函数来实现,初始化时要将前驱指针和后继指针都置为NULL。
2.3 初始化
- 带头双向循环链表的初始化可以使用两种方法:传二级指针和设置返回值。
方法一:
代码实现:
//初始化
void ListInit(LTNode** phead)
{
//这里需要传入二级指针,即传地址,才能实现对链表的修改
//判空
assert(phead);
//创建头结点
*phead = BuyListNode(-1);
//因为是带头双向链表,故将头结点的前驱指针和后继指针均指向它们自己
(*phead)->next = *phead;
(*phead)->prev = *phead;
}
方法二:
代码实现:
////初始化
LTNode* ListInit()
{
//创建头结点
LTNode* phead = BuyListNode(-1);
//因为是带头循环双向链表,故将头结点的前驱指针和后继指针均指向它们自己
phead->next = phead;
phead->prev = phead;
//返回头结点
return phead;
}
注意:
- 若想要改变头指针,就要传二级指针;不需要改变头指针的话,便传入一级指针。
- 在使用带头结点的单链表时:
- 初始化链表头指针需要传二级指针;
- 销毁链表需要传二级指针;
- 插入、删除、遍历、清空结点用一级指针即可。
- 不带头结点的单链表,除了初始化和销毁,插入、删除和清空结点也需要二级指针。
2.4 链表的判空
代码实现:
bool ListEmpty(LTNode* phead)
{
//判空
assert(phead);
//如果phead->next等于phead,则链表为空,返回true
//如果phead->next不等于phead,则链表不为空,返回false
return phead->next == phead;
}
分析:若phead->next等于phead,则链表为空,