前言:众所周知,链表有八种结构,由单向或双向,有头或无头,循环或不循环构成。在本篇,将介绍8种链表结构中最复杂的——双向带头循环链表。听着名字或许挺唬人的,但实际上双向带头循环链表实现起来比结构最简单的单向不带头不循环链表简单的多,是个“外强中干”的链表。这里只是说它名字唬人哦,实际上它是最优的链表结构,可以达到再任意位置插入,删除数据复杂度都是O(1)。(文末附有完整代码)
首先用你喜欢的IDE创建一个头文件
和两个源文件
,不同的文件具有以下不同的功能:
文件 | 作用 |
---|---|
list.h | 接口函数的声明 |
list.c | 接口函数的实现——链表的主体,文章的核心内容 |
test.c | 测试链表的运行逻辑 |
下文将实现的11个链表接口 | |
双向循环链表图 |
头文件的包含;结构体的创建;.c文件引用.h文件
list.h
#include
#include //assert断言所需头文件
#include //malloc动态开辟内存空间所需头文件
#include //在c语言中使用bool类型所需头文件,下文实现的ListEmpty函数用到
typedef int LTDataType; //因不知链表中存储的是什么数据类型,所以用重命名,要修改数据类型时直接将int换成其它的即可
typedef struct ListNode
{
LTDataType data;
struct ListNode* next; //指向后一个结点的指针
struct ListNode* prev; //指向前一个结点的指针
}ListNode; //将结构体struct ListNode 重命名为 ListNode,方便操作
list.c
#include"list.h" //引用头文件
test.c
#include"list.h" //引用头文件
这里的头结点就是我们常说的哨兵位,需要注意的是,哨兵位中不存储数据(即不给data赋值)。
list.h
//1.创建返回链表的头结点.
ListNode* ListInit(); //在头文件中声明
list.c
//要创建一个链表的结点,就需要开辟一个结构体,因此在.c文件中创建一个开辟新结点的函数,
//在下文中需要创建新结点时直接引用此函数即可
ListNode* ListCreate(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode)); //malloc一个结构体
if (newnode == NULL) //此处if判断malloc是否成功,常规情况下都会成功,毕竟失败了咱们链表不就创建失败了吗?
{
perror("malloc fail");
return NULL;
}
newnode->data = x; //给数据赋值
newnode->next = NULL; //next指针置空
newnode->prev = NULL; //prev指针置空
return newnode;
}
//1.创建返回链表的头结点
ListNode* ListInit()
{
ListNode* pHead = ListCreate(-1); //这里的头结点随便给一个值,开头时提过头结点不存储数据,
//这样不是前后矛盾吗?不用担心,后文中的打印函数实现时并不会打印头结点。
pHead->next = pHead; //因为链表只有一个头结点,所以前后指针都指向自己
pHead->prev = pHead;
return pHead;
}
test.c
int main()
{
ListNode* plist = ListInit();
return 0;
}
将每个创建的结点通过free函数释放,需要注意的是,因为传参我们传的是一级指针,所以需要在使用完销毁函数后手动置空。
list.h
//2.链表销毁
void ListDestory(ListNode* pHead);
list.c
//2.链表销毁
void ListDestory(ListNode* pHead)
{
assert(pHead); //断言一下pHead是否传有效数据进来
ListNode* cur = pHead->next; //存储头结点的后一个结点
while (cur != pHead) //从头结点的后一个结点遍历到头结点停止循环
{
ListNode* next = cur->next; //存储后一个结点
free(cur); //释放当前结点
cur = next; //将cur指向后一个结点,使循环继续
}
free(pHead);
//pHead = NULL; 置空无效,因为形参的改变不影响实参,在主函数中置空。
}
test.c
#include"list.h"
int main()
{
ListNode* plist = ListInit();
ListDestory(plist);
plist = NULL; //手动置空
return 0;
}
从头到尾遍历打印,注意不要打印头结点(哨兵位)
list.h
//3.链表打印
void ListPrint(ListNode* pHead);
list.c
//3.链表打印
void ListPrint(ListNode* pHead)
{
assert(pHead);
//打印头结点
printf("guard<==>"); //为了打印的美观,哨兵位这样表示
ListNode* cur = pHead->next; //存储头结点的后一个结点
while (cur != pHead) //从头结点的后一个结点遍历到头结点停止循环
{
printf("%d<==>", cur->data); //打印输出
cur = cur->next; //将cur指向后一个结点,使循环继续
}
}
test.c
#include"list.h"
int main()
{
ListNode* plist = ListInit();
ListPrint(plist);
ListDestory(plist);
plist = NULL;
return 0;
}
链表中的重要接口之一,在单链表中,进行尾插操作的话需要从头找到尾结点,而在双向链表中,头结点的前一个结点就是尾结点(pHead->prev)。从这里就可以看出双向带头循环链表实现起来的简单之处。
list.h
//4.链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
list.c
//4.链表尾插
void ListPushBack(ListNode* pHead, LTDataType x) //插入拥有两个参数,第二个参数为你要插入的数值
{
assert(pHead); //断言
ListNode* newnode = ListCreate(x); //插入需要创建新结点,这里我们开头第1部分创建的ListCreate函数就有了用武之地。
struct ListNode* tail = pHead->prev; //存储原来的尾结点,注意这个结点的存储会让你的尾插实现写的很舒服
newnode->next = pHead; //实现链接
newnode->prev = tail;
tail->next = newnode;
pHead->prev = newnode; //注意这里的pHead->prev不要再用tail代替了,因为改变tail并不会改变pHead->prev。
}
以上图为例,进行尾插操作的话,需要将新结点与d3,head链接起来,同时断开head和原尾结点d3。
上面的代码使用tail结构体指针存储了pHead->prev,这样的话下面实现链接的4行代码就可以是任意顺序,若是没有tail的话,必须在结尾改变pHead->prev,因为前面几行代码需要用到pHead->prev,若是改了它,则代码不能实现。
test.c
#include"list.h" //引用头文件
int main()
{
ListNode* plist = ListInit();
ListPushBack(plist, 4); //尾插4
ListPushBack(plist, 3); //尾插3
ListPushBack(plist, 2); //尾插2
ListPushBack(plist, 1); //尾插1
ListPrint(plist);
ListDestory(plist);
plist = NULL;
return 0;
}
很简单的一个接口,返回一个bool类型的值。因在链表尾删中需要用到,所以将其放在尾删前
list.h
//5.链表判空
bool ListEmpty(ListNode* pHead);
list.c
bool ListEmpty(ListNode* pHead)
{
assert(pHead); //断言
return pHead->next == pHead; // 若是头结点的后一个结点还是头结点,说明链表中只有头结点,
//而头结点不存放有效数据,因此只有头结点的链表就是空链表。
//该语句为真,则返回true,若为假,则返回false
}
test.c
#include"list.h" //引用头文件
int main()
{
ListNode* plist = ListInit();
if (ListEmpty(plist))
{
printf("链表为空\n");
}
ListDestory(plist);
plist = NULL;
return 0;
}
链表中的重要接口之一,双向链表找尾很方便,即pHead->prev。删除操作比起插入操作实现起来更简单。
list.h
//6.链表尾删
void ListPopBack(ListNode* pHead);
list.c
//6.链表尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead); //断言
assert(!ListEmpty(pHead));
ListNode* tail = pHead->prev;
tail->prev->next = pHead;
pHead->prev = tail->prev;
free(tail);
tail = NULL;
}
test.c
#include"list.h" //引用头文件
int main()
{
ListNode* plist = ListInit();
ListPushBack(plist, 2);
ListPushBack(plist, 1);
ListPushBack(plist, 0);
ListPrint(plist); //第一行打印
ListPopBack(plist);
printf("\n");
ListPrint(plist); //第二行打印
ListDestory(plist);
plist = NULL;
return 0;
}
list.h
//7.链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
list.c
//头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead); //断言
ListNode* newnode = ListCreate(x); //创建新结点
ListNode* head = pHead->next; //存放头结点的后一个结点
newnode->next = head;
head->prev = newnode;
pHead->next = newnode;
newnode->prev = pHead;
}
test.c
#include"list.h" //引用头文件
int main()
{
ListNode* plist = ListInit();
ListPushBack(plist, 2);
ListPushBack(plist, 1);
ListPushBack(plist, 0);
ListPushFront(plist, 3);
ListPrint(plist); //第二行打印
ListDestory(plist);
plist = NULL;
return 0;
}
跟头删一样,删除操作都要进行判空,若是空链表,则不能再进行删除操作。
list.h
list.c
//链表头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(!ListEmpty(pHead));
ListNode* head = pHead->next;
head->next->prev = pHead;
pHead->next = head->next;
free(head);
head = NULL;
}
test.c
#include"list.h" //引用头文件
int main()
{
ListNode* plist = ListInit();
ListPushBack(plist, 2);
ListPushBack(plist, 1);
ListPushBack(plist, 0);
ListPrint(plist); //第一行打印
printf("\n");
ListPopFront(plist); //头删
ListPrint(plist); //第二行打印
ListDestory(plist);
plist = NULL;
return 0;
}
查找链表中是否存在指定的数,若存在,则返回这个结点,若不存在,则返回NULL。
list.h
//9.链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
list.c
//9.链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead); //断言
ListNode* cur = pHead->next; //存储头结点的后一个结点
while (cur != pHead)
{
if (cur->data == x) //如果是,则返回该结点
return cur;
cur = cur->next;
}
return NULL; //如果没有,则返回NULL
}
test.c
#include"list.h" //引用头文件
int main()
{
ListNode* plist = ListInit();
ListPushBack(plist, 2);
ListPushBack(plist, 1);
ListPushBack(plist, 0);
ListNode* pplist = ListFind(plist,2);
if (pplist)
{
printf("链表中包含2");
}
ListDestory(plist);
plist = NULL;
return 0;
}
此时显示台窗口如下:查找链表中有没有2
list.h
//10.在链表pos位置前插入
void ListInsert(ListNode* pos, LTDataType x);
list.c
//10.在链表pos位置前插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos); //断言
ListNode* newnode = ListCreate(x); //创建新结点
newnode->next = pos;
newnode->prev = pos->prev;
pos->prev->next = newnode;
pos->prev = newnode;
}
test.c
#include"list.h" //引用头文件
int main()
{
ListNode* plist = ListInit();
ListPushBack(plist, 2);
ListPushBack(plist, 1);
ListPushBack(plist, 0);
ListNode* pos = ListFind(plist,2);
if (pos)
{
ListInsert(pos, 3);
}
ListPrint(plist);
ListDestory(plist);
plist = NULL;
return 0;
}
list.h
//11.删除链表pos位置的结点
void ListErase(ListNode* pos);
list.c
//11.删除链表pos位置的节点
void ListErase(ListNode* pos)
{
assert(pos); //断言
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
test.c
#include"list.h" //引用头文件
int main()
{
ListNode* plist = ListInit();
ListPushBack(plist, 2);
ListPushBack(plist, 1);
ListPushBack(plist, 0);
ListPrint(plist); //第一行打印
printf("\n");
ListNode* pos = ListFind(plist,2);
if (pos)
{
ListErase(pos); //删除2
}
ListPrint(plist); //第二行打印
ListDestory(plist);
plist = NULL;
return 0;
}
至此,双向带头循环链表的11个接口就写完了。怎么样,是不是感觉实现起来非常简单,说它"外强中干"丝毫为过吧。
list.h
#include
#include
#include
#include
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}ListNode;
//1.创建返回链表的头结点.
ListNode* ListInit();
//2.链表销毁
void ListDestory(ListNode* pHead);
//3.链表打印
void ListPrint(ListNode* pHead);
//4.链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
//5.链表判空
bool ListEmpty(ListNode* pHead);
//6.链表尾删
void ListPopBack(ListNode* pHead);
//7.链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
//8.链表头删
void ListPopFront(ListNode* pHead);
//9.链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
//10.链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
//11.链表删除pos位置的结点
void ListErase(ListNode* pos);
list.c 核心代码——接口的实现
#include"list.h" //引用头文件
//要创建一个链表的结点,就需要开辟一个结构体,因此在.c文件中创建一个开辟新结点的函数,
//在下文中需要创建新结点时直接引用此函数即可
ListNode* ListCreate(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode)); //malloc一个结构体
if (newnode == NULL) //此处if判断malloc是否成功,常规情况下都会成功,毕竟失败了咱们链表不就创建失败了吗?
{
perror("malloc fail");
return NULL;
}
newnode->data = x; //给数据赋值
newnode->next = NULL; //next指针置空
newnode->prev = NULL; //prev指针置空
return newnode;
}
//1.创建返回链表的头结点
ListNode* ListInit()
{
ListNode* pHead = ListCreate(-1); //这里的头结点随便给一个值,开头时提过头结点不存储数据,
//这样不是前后矛盾吗?不用担心,后文中的打印函数实现时并不会打印头结点。
pHead->next = pHead; //因为链表只有一个头结点,所以前后指针都指向自己
pHead->prev = pHead;
return pHead;
}
//2.链表销毁
void ListDestory(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next; //存储头结点的后一个结点
while (cur != pHead) //从头结点的后一个结点遍历到头结点停止循环
{
ListNode* next = cur->next; //存储后一个结点
free(cur); //释放当前结点
cur = next; //将cur指向后一个结点
}
free(pHead);
//pHead = NULL; 置空无效,因为形参的改变不影响实参,在主函数中置空。
}
//3.链表打印
void ListPrint(ListNode* pHead)
{
assert(pHead);
printf("guard<==>"); //为了打印的美观,哨兵位这样表示
ListNode* cur = pHead->next; //存储头结点的后一个结点
while (cur != pHead) //从头结点的后一个结点遍历到头结点停止循环
{
printf("%d<==>", cur->data); //打印输出
cur = cur->next; //将cur指向后一个结点,使循环继续
}
}
//4.链表尾插
void ListPushBack(ListNode* pHead, LTDataType x) //插入拥有两个参数,第二个参数为你要插入的数值
{
assert(pHead); //断言
ListNode* newnode = ListCreate(x); //插入需要创建新结点,这里我们开头第1部分创建的ListCreate函数就有了用武之地。
struct ListNode* tail = pHead->prev; //存储原来的尾结点
newnode->next = pHead; //实现链接
tail->next = newnode;
pHead->prev = newnode; //注意这里的pHead->prev不要再用tail代替了,因为改变tail并不会改变pHead->prev。
newnode->prev = tail;
}
//5.链表判空
bool ListEmpty(ListNode* pHead)
{
assert(pHead);
return pHead->next == pHead; //该语句为真,则返回true,若为假,则返回false
}
//6.链表尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead); //断言
assert(!ListEmpty(pHead));
ListNode* tail = pHead->prev;
tail->prev->next = pHead;
pHead->prev = tail->prev;
free(tail);
tail = NULL;
}
//7.链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead); //断言
ListNode* newnode = ListCreate(x); //创建新结点
ListNode* head = pHead->next; //存放头结点的后一个结点
newnode->next = head;
head->prev = newnode;
pHead->next = newnode;
newnode->prev = pHead;
}
//8.链表头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(!ListEmpty(pHead));
ListNode* head = pHead->next;
head->next->prev = pHead;
pHead->next = head->next;
free(head);
head = NULL;
}
//9.链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead); //断言
ListNode* cur = pHead->next; //存储头结点的后一个结点
while (cur != pHead)
{
if (cur->data == x) //如果是,则返回该结点
return cur;
cur = cur->next;
}
return NULL; //如果没有,则返回NULL
}
//10.在链表pos位置前插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos); //断言
ListNode* newnode = ListCreate(x); //创建新结点
newnode->next = pos;
newnode->prev = pos->prev;
pos->prev->next = newnode;
pos->prev = newnode;
}
//11.删除链表pos位置的节点
void ListErase(ListNode* pos)
{
assert(pos); //断言
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
test.c 该文件随大家喜欢测试
#include"list.h" //引用头文件
int main()
{
ListNode* plist = ListInit();
ListPushBack(plist, 2);
ListPushBack(plist, 1);
ListPushBack(plist, 0);
ListPrint(plist); //第一行打印
printf("\n");
ListNode* pos = ListFind(plist,2);
if (pos)
{
ListErase(pos); //删除2
}
ListPrint(plist); //第二行打印
ListDestory(plist);
plist = NULL;
return 0;
}
文末BB:对哪里有问题的朋友,可以在评论区留言,若哪里写的有问题,也欢迎朋友们在评论区指出,博主看到后会第一时间确定修改。最后,制作不易,如果对朋友们有帮助的话,希望能给点点赞和关注.