一、什么是带头循环双向链表?
1.特点:
1.带头:有哨兵位节点,它不用存储数据。对链表进行插入删除操作时也不会影响改节点。
2.双向:组成链表的结构体中的结构体成员有数据,上一个节点的地址和下一个节点的地址
3.循环:链表的头结点存储了尾结点的地址,链表的尾结点存储了头节点的地址。
2.优点:
相比单向链表,双向循环链表的优点在于它的尾插找尾巴非常的快 因为它的头节点同时存储了上一个节点的地址,头的上一个即尾。相比无头链表,带头链表的好处在于当没有节点的时候,可以直接通过访问结构体成员的方式来增加相应的指针,而无头的话需要直接对地址进行修改,传变量的时候还需要传递二级指针 不仅不好理解,还易在实现的时候出错。
二、实现接口
1.前置准备
1.1需要的三个文件
先创建两个.c文件,再创建一个头文件,分别命名为test.c,list.c,list.h
test.c用来测试写好的接口 list.c存放实现接口的代码
list.h则存放对应函数,头文件,结构体的声明,这样在想使用链表的接口时,直接引用list.h即可,不需要引用别的头文件。
创建好的环境如图
1.2结构体的创建和头文件的引用
这些内容放在list.h的文件中,到时引用就可以一条龙带走,不需要再引用别的内容
#pragma once//防止头文件二次引用 #include<stdio.h> #include<assert.h> #include<stdlib.h> typedef int LTDateType; //这样创建结构体数据类型,不仅是为了和int做区分 //也是为了到时更好的替换,想换直接把int改了就行 typedef struct listnode { struct listnode* prev;//存放上一个节点的地址 struct listnode* next;//存放下一个节点的地址 LTDateType data;//该节点存放的数据 }listnode;
2.接口实现
2.1函数创建新节点
创建节点,虽然简单,但我们在很多操作中都会用到,因此把它单独分装成一个接口
listnode* buy_listnode(LTDateType x) { listnode*newnode=(listnode*)malloc(sizeof(listnode)); if (newnode == NULL) { perror("buy_listnode");//错误提示 exit(-1);//节点都没创建出来,直接退出程序 } newnode->data = x;//将新节点的数据初始化成我们需要的 newnode->next = NULL;//不清楚插入的方式,先初始化成空 newnode->prev = NULL; }
2.2打印链表内容
非常经典的操作,遍历一遍链表即可,唯一需要注意的便是,哨兵节点不是链表中真正的成员,它只能算是实现链表的辅助,因此跳过哨兵节点进行打印
void print_list(listnode* phead) { assert(phead);//哨兵节点地址不可能为空 listnode* head = phead->next; //哨兵位节点不存储有效数据,因此phead->next才是头节点 printf("head<=>");//纯属美观 while (head != phead)//当head和phead相等意味着已经遍历完一遍链表 { printf("%d<=>", head->data); head = head->next; } printf("\n"); }
2.3尾插新节点
void list_pushback(listnode*phead,LTDateType x) //尾插一个新节点,此节点存储x { listnode* newnode = buy_listnode(x); //创建一个我们需要的新节点 listnode* tail = phead->prev; //先找尾,尾很显然是哨兵位节点的上一个节点 tail->next = newnode; newnode->prev = tail; newnode->next = phead; phead->prev = newnode; }
后面的4行代码是核心,单独在文章中解释,创建了一个新节点,要把它放到链表的末端,尾节点我们已经找到了,接下来就是链接即可
首先明确,新的尾巴是创建出来的新节点,但还没进行链接之前,尾巴还是之前的尾巴
原始链表
第一步:
第二步:
第三步:
第四步:
测试代码:
#include"list.h" void test1() { listnode* plist=NULL; plist=init_listnode(plist); list_pushback(plist,1); list_pushback(plist,2); list_pushback(plist,3); list_pushback(plist,4); print_list(plist); } int main() { test1(); }
测试效果:
2.4头插新节点
这里我就不再画图了,自己画一遍比看别人画一万遍都来的快
void list_pushfront(listnode* phead, LTDateType x) { listnode* head = phead->next;//找到头节点 listnode* newnode = buy_listnode(x);//创建新节点 head->prev = newnode; newnode->next = head; phead->next = newnode; newnode->prev = phead; }
测试代码:
void test2() { listnode* plist = NULL; plist = init_listnode(plist); list_pushfront(plist, 1); list_pushfront(plist, 2); print_list(plist); list_pushfront(plist, 10086); print_list(plist); } int main() { test2(); }
测试效果:
2.5头删节点
需要注意的一点便是,我们删的节点不是哨兵节点,哨兵节点是不存放有效数据的,我们删除的是头节点
void list_popfront(listnode*phead) { assert(phead); if (phead->next == phead) { printf("链表为空,操作失败\n");//为空就别删了 return; } listnode* head = phead->next;//找到头节点 phead->next = head->next; head->next->prev = phead; free(head);//链接完成,彻底删除 }
测试代码:
void test3() { listnode* plist = NULL; plist = init_listnode(plist); list_pushfront(plist, 1); list_pushfront(plist, 2); print_list(plist); list_pushfront(plist, 10086); print_list(plist); list_popfront(plist); print_list(plist); list_popfront(plist); print_list(plist); list_popfront(plist); print_list(plist); list_popfront(plist); print_list(plist); } int main() { test3(); }
测试效果: