目录
ID:HL_5461
引言
咱先看看带头双向循环链表大概是啥样滴~
与之前的单链表不同,带头双向循环链表(以下简称链表)有三个特点:1.有一个哨兵位的头结点,也就说无论链表有没有存储数据,它都有一个不变的头结点;2.有两个指针,一个和单链表一样是指向下一个的next指针,一个是指向前一个的prev指针;3.尾结点的下一个是头结点,它逻辑上是一个环形,即尾结点的next指向哨兵位的头结点,头结点的prev指向尾结点。
OK,咱开始进正题!
一、定义
我们将链表结点的结构体分为三部分,一个指向前结点的prev指针,一个指向后结点的next指针,一个存放数据的data。同样的,方便存放数据的类型修改,我们将数据类型typedef成LTDataType。
下面是结点结构体的图示:
图示
下面是代码:
typedef int LTDataType;//定义LTDataType为int,方便以后修改存储的数据类型
typedef struct ListNode
{
LTDataType data;//结点数据
struct ListNode* next;//指向下一个结点的指针
struct ListNode* prev;//指向上一个结点的指针
}LTNode;
二、开辟新结点
这里和单链表差不多,我们直接写一个开辟新结点的函数,但由于该链表还有哨兵位的头结点,所以我们还需要一个初始化的函数,这个暂且按下不表,先看开辟结点。
首先我们直接使用malloc函数为结点开辟一个结点结构体大小的空间,同时返回该结点的指针,将之赋值给newnode。当然,对于malloc函数开辟空间,很重要一点就是判断开辟是否失败,即指针是否成功,失败结束程序,成功我们就可以将要存储的数据存进新开辟的结点空间中的data里,同时为了避免使用野指针,将该结点的prev置为空,next置为空。
来看代码:
LTNode* BuyLTNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));//为结点开辟空间
if (!newnode)//开辟失败
{
perror("BuyLTNode");//打印失败原因
exit(-1);//以错误形式退出程序
}
newnode->data = x;//令data为x
newnode->next = NULL;//next指针为空
newnode->prev = NULL;//prev指针为空
return newnode;//返回新结点地址
}
三、初始化
对于初始化函数,我们在函数中开辟一个新结点作为头结点,至于该结点的data,我们将其置成0,算是养成好习惯吧。当然你也可以把头结点的data拿来记录链表长度,每新增一个结点++一下,每删除一个结点--一下。不过我不建议你这样做,因为我们存储的数据不一定是int类型的,也有可能是char之类的,此时如果我们还把头结点的data作为链表长度输出……这到了公司会被辞退的吧……所以考虑到代码的灵活性,这篇文章里的头结点data就做个摆设就好。
啊,跑题了,咱继续!
此时头结点有了,但这还不够,我们还有对头结点的prev指针和next指针初始化,使它们都指向头结点自己。初始化后的链表大概长这个样子:
至此,我们的链表已经有了一个雏形,之后的增删查改都是小case啦~不过在开始增删查改之前我们还是看看初始化这部分的代码:
LTNode* LTInit()
{
LTNode* phead = BuyLTNode(0);//为头结点开辟结点
phead->next = phead;//头结点的next指针指向头结点自己
phead->prev = phead;//头结点的prev指针指向头结点自己
return phead;//返回头结点地址
}
四、插入
老规矩,咱还是分成头插、尾插、中间插入。不过由于链表是带头的,尾插就不需要考虑是否为头插的问题了,而且由于是循环的,所以尾插我们也需要修改尾结点的next和头结点的prev,这使得尾插和中间插入没了太大区别。具体,各位可以在后续讲解中细细体会。
1.头插
先来看头插,很简单,直接上图,清晰明了。
先用 BuyLTNode函数为新结点开辟空间。
用first记录第一个有效结点的地址。当然也可以参考单链表的插入操作,先令新结点的next指向phead->next,即这里的first结点,然后再令phead->next->prev = newnode,之后再修改phead的next和newnode的prev,这样我们就不需要first指针了。但由于双向链表的指针有两个,比较多,为避免弄错,不止这里,包括后面的操作,我们都采用用指针记录要修改结点的位置的方法。这样一来,我们就无需考虑指针的修改顺序了。
修改指针。(请勿在意这跟双螺旋似的线条,我尽力了……)
看代码:
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);//头指针不为空
LTNode* newnode = BuyLTNode(x);//开辟新结点
LTNode* first = phead->next;//用first指针记录下一个结点
newnode->next = first;//新结点的后一个指向first结点
newnode->prev = phead;//新结点的前一个指向头结点
phead->next = newnode;//头结点的后一个指向新结点
first->prev = newnode;//first结点的前一个指向新结点
}
2.尾插
和头插一样,我们同样需要一个tail指针记录尾结点,方便我们后续修改指针,这样就不用考虑修改的顺序了。
修改指针。(我知道这图好丑,别吐槽了 T^T )
看代码叭~
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);//头指针不为空
LTNode* newnode = BuyLTNode(x);//开辟新结点
LTNode* tail = phead->prev;//用tail指针记录尾结点
newnode->prev = tail;//新结点的前一个指向tail结点
newnode->next = phead;//新结点的后一个指向头结点
phead->prev = newnode;//头结点的前一个指向新结点
tail->next = newnode;//tail结点的后一个指向新结点
}
3.中间插入
就像之前强调的,加头循环消除了第一个结点和尾结点的特殊性,所以其实无论方法上还是本质上它和头插尾插没啥区别(其实准确说这三种插入都无甚区别)。
这里的插入,我们指pos前插入。
开辟新结点。prev指针用于记录pos前一个结点。
修改指针。
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);//pos指针不为空
LTNode* newnode = BuyLTNode(x);//开辟新结点
LTNode* prev = pos->prev;//用prev指针记录前一个结点
newnode->next = pos;//新结点的后一个指向next结点
newnode->prev = prev;//新结点的前一个指向prevl结点
prev->next = newnode;//prev结点的后一个指向新结点
pos->prev = newnode;//next结点的前一个指向新结点
}
五、删除
1.头删
用first指针记录要删除的第一个结点,用second记录第一个结点的后一个结点。
释放第一个结点。
修改指针。
void LTPopFront(LTNode* phead)
{
assert(phead);//头指针不为空
assert(phead->next != phead);//链表不为空
LTNode* first = phead->next;//用first指针记录要删除结点
LTNode* second = phead->next->next;//用second指针记录要删结点的后一个结点
free(first);//释放要删除结点
phead->next = second;//头结点的后一个指向second结点
second->prev = phead;//second结点的前一个指向头结点
}
2.尾删
tail指针记录尾结点,tailprev记录尾结点的前一个结点。
释放尾结点。
修改指针。
void LTErase(LTNode* pos)
{
assert(pos);//pos指针不为空
LTNode* prev = pos->prev;//用prev指针记录前一个结点
LTNode* next = pos->prev;//用next指针记录下一个结点
free(pos);//释放pos结点
prev->next = next;//prev指针的后一个指向next
next->prev = prev;//next指针的前一个指向prev
}
3.中间删除
我们需要两个指针,分别指向pos前一个和pos后一个。释放掉pos结点,然后将prev和next结点链接即可。
创建prev指针指向pos前一个结点,next指针指向pos后一个结点。
释放pos结点。
修改指针。
void LTErase(LTNode* pos)
{
assert(pos);//pos指针不为空
LTNode* prev = pos->prev;//用prev指针记录前一个结点
LTNode* next = pos->prev;//用next指针记录下一个结点
free(pos);//释放pos结点
prev->next = next;//prev指针的后一个指向next
next->prev = prev;//next指针的前一个指向prev
}
六、查找
查找还是采用使用指针cur遍历链表,然后比较data的方法,这与单链表没啥区别,唯一的区别在于结束遍历的条件不是cur == NULL,而是cur == phead,由此,cur初始值应为phead->next,而不是phead。
so easy~麻麻再也不用担心我的查找~(bushi)不过多赘述,直接看代码。
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);//头指针不为空
LTNode* cur = phead->next;//用cur指针遍历链表
while (cur != phead)//当没有循环回头指针
{
if (cur->data == x)//如果找到了
{
return cur;//返回此时地址
}
cur = cur->next;//否则cur指针后移
}
return NULL;//没找到返回空指针
}
七、修改
应该没啥操作会比修改更简单了吧~咱还是直接上代码。
void LTModify(LTNode* pos, LTDataType x)
{
assert(pos);//pos指针不为空
pos->data = x;//将pos位置的data改为x
}
八、计算链表长度
咱比平常再加一个操作哈~
思想很简单,跟查找一样遍历链表,每经过一个结点size++就行~
int LTSize(LTNode* phead)
{
assert(phead);//头指针不为空
int size = 0;//用size记录链表长度
LTNode* cur = phead->next;//用cur指针遍历链表
while (cur != phead)//当没有循环回头指针
{
size++;//长度加1
cur = cur->next;//cur指针后移
}
return size;//返回链表长度
}
结尾
带头双向循环链表的操作详解到此就结束了,这是本篇代码码云指路:
若有错误,欢迎大家批评斧正!