双向循环链表
文章目录
前言
在开始讲解带头双向循环链表之前,我们先来看一个带哨兵位的头结点的单链表和不带哨兵位的头结点的单链表的区别:

- 不带哨兵位的头结点
不带哨兵位的头结点,在链表的系列操作函数中可能会修改到指向存储有效数据的第一个结点的指针plist,对在第一元素结点前插入结点和删除第一结点时,其操作与其他结点的操作不统一,我们需要在这系列操作中分别考虑不同情况
- 带哨兵位的头结点
这个结点不存储有效数据,链表的系列操作函数永远不会修改到指向存储有效数据的第一个结点的指针plist,因为拥有这个带哨兵位的头节点,对在第一元素结点前插入结点和删除第一结点时,其操作与其他结点的操作就统一了。这个带哨兵位的头节点看起来简单一点,为什么我们的单链表不设计这个结构呢?这个头节点的结构在实际中很少出现,包括哈希桶、邻接表做子结构都是不带头,其次就是OJ中给的链表基本都是不带头的,都有先入为主思维,我们直接写带头,容易对后面的学习产生误导。
我们链表的结构有很多,但是实际中我们最常用的还是下面两种结构:

- 无头单向非循环链表
结构简单,一般不会单独用来存数据,实际中更多的是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。这种结构在笔试面试中会出现很多,因为他有很多的可考之处。
- 带头双向循环链表
结构很复杂,一般单独用来存数据,实际中使用的链表数据结构都是带头双向循环链表,这个结构虽然很复杂,但是代码实现它的一系列增删查改等会很简单,下面我们就实现带头双向循环链表的一系列操作。
下面我们就来看带头双向循环链表的基本操作:
文件的创建
首先我们先创建三个文件:
List.h对相关头文件的包含,以及实现双向链表的结构和函数的声明
List.c对实现双向链表增删查改等操作的函数进行定义
test.c文件进行双向链表相关函数和双向链表功能的测试

双向链表结构的定义
了解了带头双向循环链表的结构,我们先定义这样的一个结构体:
typedef struct ListNode
{
int data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
和我们前面讲过的顺序表和单链表一样,我们为了方便我们数据类型的变化,我们进行类型重定义,在想要换我们的双向链表的数据类型时,直接在typedef这里修改就可以了,如下:
typedef int LTDataType;
typedef struct ListNode
{
int data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
我们定义好结构之后,首先在测试文件中创建一个头结点phead,先赋值为NULL:
void Test1()
{
LTNode* phead = NULL;
}
int main()
{
Test1();
return 0;
}
接下来我们对下面的带头双向链表操作进行讲解:
// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(LTNode* plist);
// 双向链表打印
void ListPrint(LTNode* plist);
// 双向链表尾插
void ListPushBack(LTNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(LTNode* plist);
// 双向链表头插
void ListPushFront(LTNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(LTNode* plist);
// 双向链表查找
ListNode* ListFind(LTNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(LTNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(LTNode* pos);
我们写的双向链表是带头的,所以我们首先需要创建头结点:
创建返回链表的头结点
值传递时:
// 创建返回链表的头结点.
LTNode* ListCreate(LTNode* plist)
{
plist = (LTNode*)malloc(sizeof(LTNode));
if (plist == NULL)
{
perror("malloc plist");
return NULL;
}
plist->next = plist;
plist->prev = plist;
return plist;
}

我们malloc一个结点,这个结点不存储数据,初始化时我们将plist的next指向它自己,plist的prev指向它自己,因为plist出函数会销毁,并且里面的形参的改变不会影响实参的改变,所以我们在值传递时需要将plist返回
地址传递:
// 创建返回链表的头结点.
LTNode* ListCreate(LTNode** plist)
{
assert(plist);
*plist = (LTNode*)malloc(sizeof(LTNode));
if (*plist == NULL)
{
perror("malloc plist");
return NULL;
}
(*plist)->next = *plist;
(*plist)->prev = *plist;
}
为了代码的统一性,我们这里就使用值传递,因为后面的函数我们都会使用值传递,因为有了哨兵位的头结点我们不会改变phead的指向。
有双向链表的创建那就有双向链表的销毁:
双向链表的销毁
// 双向链表销毁
void

本文详细介绍了带头双向循环链表的结构、创建、销毁、打印、插入、删除、查找等操作。通过值传递的方式创建头结点,讨论了双向链表在数据结构中的应用,并提供了相应的C语言实现代码。
1784

被折叠的 条评论
为什么被折叠?



