Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客:<但凡.
我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》
欢迎点赞,关注!
今天我们用C语言实现一个带头双向循环链表。
目录
1、什么是带头双向循环链表
带头,就是有哨兵。这个哨兵记录这这个链表的第一个节点,但是不存放数据。
双向就是我们每个节点既存放着下一个结点的地址,有存放着上一个节点的地址。
而循环呢,就是我们的尾节点链接了头节点。
现在我们用C语言实现一下这个链表,还是同样的工程体系,一个头文件两个源文件,然后实现增删查改。相信有了前两篇链表的铺垫,双向链表易如反掌。
2、带头双向循环链表的实现
2.1初始化与开辟新节点
初始化:
void Init(Dlist** head)
{
*head = BuyNode(0);
(*head)->next = *head;
(*head)->pre = *head;//让head自己指向自己
}
初始化的时候我们要让这个头节点的前后指针都指向自己:
节点的开辟:
Dlist* BuyNode(ListDate a)
{
Dlist* newnode = (Dlist*)malloc(sizeof(Dlist));
if (newnode == NULL)
{
printf("开辟失败!");
return NULL;
}
newnode->a = a;//初始化这个新建节点
newnode->next = NULL;
newnode->next = NULL;
return newnode;
}
2.2头插与尾插
头插:
void Toucha(Dlist** head, ListDate a)
{
assert(*head);//链表必须有头(存在)
Dlist* newnode = BuyNode(a);
Dlist* ph = *head;//定义个变量更清楚些
Dlist* tail = ph->next;//记录尾部变量
ph->next = newnode;
newnode->next = tail;
newnode->pre = ph;
tail->pre = newnode;
}
尾插:
void WeiCha(Dlist** head, ListDate a)
{
assert(*head);//链表必须有头(存在)
Dlist* tail = (*head)->pre;//双向循环链表的优势
Dlist* newnode = BuyNode(a);
newnode->pre = tail;
tail->next = newnode;
newnode->next = *head;
(*head)->pre = newnode;
}
需要注意的是,我们要先判断head存在然后再插入。当然了我们也可以写一个if条件,当head不存在时初始化头节点。
这里有个问题,就是只有assert里放*head才能达到断言的效果,不然头文件为空传进来就程序崩溃了,不会报错。为什么呢?
我们看图片来理解一下。倘若说我们传入的实参head是个NULL,那我们仍然会创建一个二级指针head指向这个NULL的实参,那也就是说,无论如何我们的形参head都不为空。而我们真正要检验是不是空的是我们传入的实参head。所以说assert断言的对象应该是*head。
2.3头删和尾删
头删:
void TouShan(Dlist** head)
{
assert(*head && (*head)->next);//我们的链表要有头并且有存放元素的节点
Dlist* ph = *head;//注意我们删的是第一个节点,不是哨兵
Dlist* del = (*head)->next;
ph->next = del->next;
(del->next)->pre = ph;
free(del);
del = NULL;
}
尾删:
void WeiShan(Dlist** head)
{
assert(*head && (*head)->next);
Dlist* ph = *head;
Dlist* tail = ph->pre;
Dlist* newtail = tail->pre;
newtail->next = ph;
ph->pre = newtail;
free(tail);
tail = NULL;
}
大家也看出来了,我很喜欢定义中间变量来进行删除操作。因为这样可读性更高,而且也不容易出现问题。
2.4任意节点位置插入
void Insert(Dlist** head, Dlist* pos, ListDate a)
{
assert(*head && pos);
Dlist* in = BuyNode(a);
Dlist* poss = pos->next;
pos->next = in;
in->next = poss;
poss->pre = in;
in->pre = pos;
}
其实在这里我们把*head传进来就可以了,然后断言的内容改成head&&pos。我们传入head的意义就是判断一下这个链表是否存在,并没有对head进行更改。所以说在保证head存在的情况下,如果去掉Dlist**head这个参数也是Ok的。
2.5对头插和尾插的改造
在这儿有个技巧,就是在头插和尾插时调用我们的任意位置插入,Insert传入参数的时候把head传到pos的位置就是头插,把head->pre传入就是尾插。
改造后的头插:
void Toucha(Dlist** head, ListDate a)
{
assert(head);//链表必须有头(存在)
//Dlist* newnode = BuyNode(a);
//Dlist* ph = head;//定义个变量更清楚些
//Dlist* tail = ph->next;//记录尾部变量
//ph->next = newnode;
//newnode->next = tail;
//newnode->pre = ph;
//tail->pre = newnode;
Insert(head, *head,a);
}
改造后的尾插:
void WeiCha(Dlist** head, ListDate a)
{
assert(*head);//链表必须有头(存在)
//Dlist* tail = (*head)->pre;//双向循环链表的优势
//Dlist* newnode = BuyNode(a);
//newnode->pre = tail;
//tail->next = newnode;
//newnode->next = *head;
//(*head)->pre = newnode;
Insert(head, (*head)->pre, a);
}
2.6打印链表
void print(Dlist* head)
{
assert(head);
Dlist* pcur = head->next;
while (pcur!=head)
{
printf("%d-->", pcur->a);
pcur = pcur->next;
}
}
2.7销毁链表
void des(Dlist** head)
{
assert(*head);
Dlist* pcur = (*head)->next;
Dlist* p = *head;
while (pcur!= *head)
{
p = pcur;
pcur = pcur->next;
free(p);
p = NULL;
}
free(*head);
*head = NULL;
/*free(head);
head=NULL;*/
}
其实写这里的时候我犯了一个小错,给大家说下:
比方说现在有两个节点 ,第一个节点链接者第二个节点。这时候我们把第二个节点释放了,我们再调用第一个节点->next,这时候访问的不是空指针,而是野指针!所以说我们在写链表时要规避这种问题。
另外,在这里还我们要注意,free的时候我们应该释放一级指针。如果free二级指针会报错,因为我们的二级指针时调用函数时创建的参数,他是存放在栈区的,也就是不是动态开辟出来的空间。而我们的一级指针是存放在堆区的,free的对象只能是动态开辟出来的,存放在堆区的指针(地址)。如果有不太了解的可以移步我的专栏《编程之路》,里面的动态内存详解篇提到过这个问题。
2.8测试
#include"main.h"
int main()
{
Dlist* head = NULL;
Init(&head);
Toucha(&head, 1);//1
WeiCha(&head, 2);//1 2
WeiCha(&head, 2);//1 2 2
WeiCha(&head, 2);//1 2 2 2
WeiCha(&head, 2);//1 2 2 2 2
WeiShan(&head);//1 2 2 2
TouShan(&head);// 2 2 2
Dlist*pos = (head->next)->next;
Insert(head, pos,10);//2 2 10 2
print(head);
des(&head);
}
输出结果:
3、双向循环链表的优势
第一,时间复杂度更优。我们可以发现,无论插入和删除,他的时间复杂度都是O(1),而前面的单链表和顺序表他都存在两项(插入或删除)时间复杂度为O(N)。
第二、实现尾插和头插更简单。我们直接调个Insert就好了呀!
4、三种传参方式的分析
前面我们介绍了两种传参方式,第一个是传地址,接受的形参为二级指针,第二个是c++里面的取别名,我们直接传值就行。
在这里我想介绍第三种方法,我们就传值,并且我们不会c++我不会取别名,那我们就把函数返回值从void改成NODE*,最后返回我们更改完的头节点的地址。其中NODE是我们的节点类型名。当然了这样做的坏处是我们在使用是还得给一个地址来接受返回值。我在这里只是提一下思路就不实现了,感兴趣的同学自己尝试一下哈~
在这里有个小技巧,如果你要通过函数改变一个东西的话,就得传地址,而传地址的化我们调用函数时一定得是&+变量,不然都是传值。
好了,今天的内容就分享到这,我们下期再见!