一:结构体的创建和函数的声明(List.h)

- 带头双向循环链表的结构如图,看起来是不是有亿点复杂,但是使用起来却十分方便
- 可以看到,它有一个不存储数据的头节点,每个节点中有三个值,一个是prev指针,一个是数据域data,和一个next指针改变
- 其中头结点的prev指向尾节点,尾结点的next指向头节点,形成了循环
- 其余节点的prev指向上一个节点,next指向下一个节点,构成了双向
- 所以定义结构体的时候也需要有这三个变量
- 注意data的类型不要直接写死成int或者其它类型,先将它的类型重定义为ListDataType
- 之后要想改变data的类型,只需改变typedef后的类型即可
- 这些操作都放在List.h中完成,便于其它文件调用
- 剩下就是进行函数的声明,我们需要的函数有初始化,打印,头插头删尾插尾删,查找,任意位置插入删除,销毁这几个函数。
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int ListDataType;
typedef struct ListNode
{
ListDataType data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
LTNode* ListInit();
void ListPushBack(LTNode* phead, ListDataType x);
void ListPrint(LTNode* phead);
void ListPopBack(LTNode* phead);
void ListPushFront(LTNode* phead, ListDataType x);
void ListPopFront(LTNode* phead);
LTNode* ListFind(LTNode* phead, ListDataType x);
void ListInsert(LTNode* pos, ListDataType x);
void ListErase(LTNode* pos);
void ListDestroy(LTNode* phead);
二:函数的具体实现(List.c)
1. ListInit(初始化链表)

- 由于刚开始只有一个节点,也就是头结点,又要构成双向循环,所以头结点的prev和next都指向的是头结点自己
- 这里初始化没有传二级指针,而是采取了返回头结点的方式。所以需要先malloc申请一个头结点phead
- 然后将phead的prev和next都指向phead自己,初始化就大功告成了。
- 代码如下:
LTNode* ListInit()
{
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
phead->prev = phead;
phead->next = phead;
return phead;
}
2. ListPrint(打印链表)
- 首先先断言链表不为空,为空就不用打印了
- 然后就是利用cur指针从头节点的下一个节点遍历链表(头结点不存储有效数据)节点打印数据域data,问题是终止条件是什么
- 单链表时在cur为为NULL时就到尾了,可是带头双向链表没有指向NULL的节点
- 其实也很简单,当cur再次为头结点phead时说明已经遍历链表一遍了,这就是终止条件
- 我这里是为了再每个节点后打印->方便观看,所以是让cur的next不为phead为终止条件,最后再单独打印最后一个节点,如果只有头结点就不打印
- 代码如下:
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur->next != phead)
{
printf("%d->", cur->data);
cur = cur->next;
}
if (phead->next != phead)
{
printf("%d\n", cur->data);
}
}
3.BuyListNode(申请新节点)
- 此函数为辅助函数,是为了后面头插,尾插和任意位置插入时申请头结点,减少代码的重复
- 实现起来也十分容易,申请一个节点将其进行初始化后返回
- 代码如下:
LTNode* BuyListNode(ListDataType x)
{
LTNode* NewNode = (LTNode*)malloc(sizeof(LTNode));
NewNode->data = x;
NewNode->next = NULL;
NewNode->prev = NULL;
return NewNode;
}
4. ListPushBack (尾插)
- 比起单链表的尾插,带头双向循环链表进行尾插就十分容易,因为它不用遍历寻找尾结点,头结点phead的prev指向的就是尾结点
- 先断言防止链表为初始化而为空链表
- 再使用BuyListNode函数申请一个新节点NewNode,通过头节点的prev找到尾结点
- 将尾结点tail的next链接上NewNode,再将NewNode的prev指向tail
- 此时NewNode为新的尾结点,将其next指向头结点phead,再将phead的prev指向新的尾结点NewNode,尾插就完成了
- 当然之后完成ListInsert以后就可以直接调用它完成尾插
void ListPushBack(LTNode* phead, ListDataType x)
{
assert(phead);
LTNode* tail = phead->prev;
LTNode* NewNode = BuyListNode(x);
tail->next = NewNode;
NewNode->prev = tail;
NewNode->next = phead;
phead->prev = NewNode;
}
5. ListPopBack(尾删)
- 进行尾删时我们不仅要断言链表不为空,并且要断言链表不能只有头节点,因为我们不希望将头结点也删除
- 之后通过phead的prev找到尾结点tail,创建变量tailprev指向tail的上一个节点
- 因为tail节点要被释放,所以tailprev是尾删后链表的新尾节点
- 将tailprev的next指向头结点phead,phead的prev指向tailprev
- 最后释放tail节点,就完成了尾删
- 完成ListErase后可以直接调用完成尾删
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->prev;
tail->prev->next = phead;
phead->prev = tail->prev;
free(tail);
tail = NULL;
}
6. ListPushFront(头插)
- 这里说是头插但并不是在头结点phead前面插入,无论如何phead都是头节点,它就是个哨兵,起的站岗作用,所以头插其实是在头结点phead后面进行插入
- 创建变量next指向第二个节点就是头结点的下一个节点,之后在next前面进行插入新节点
- 之后的工作就是将phead的next指向NewNode,NewNode的prev指向phead
- NewNode的next指向next,next的prev指向NewNode完成双向链接
- 完成ListInsert以后就可以直接调用它完成头插
void ListPushFront(LTNode* phead, ListDataType x)
{
assert(phead);
LTNode* NewNode = BuyListNode(x);
LTNode* next = phead->next;
phead->next = NewNode;
NewNode->prev = phead;
NewNode->next = next;
next->prev = NewNode;
}
7. ListPopFront(头删)
- 进行头删时我们不仅要断言链表不为空,并且要断言链表不能只有头节点,因为我们不希望将头结点也删除
- 并且头删删除的也并不是头节点,是头节点的下一个节点
- 之后通过phead的next找到要删除的结点next,创建变量Nextnext指向next的下一个节点
- 然后将Nextnext和头节点phead进行链接,具体实现不赘述了
- 最后释放next节点,就完成了头删
- 完成ListErase后可以直接调用完成头删
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* next = phead->next;
LTNode* nestNest = next->next;
phead->next = nestNest;
nestNest->prev = phead;
free(next);
next = NULL;
}
8. ListFind(查找链表元素)
- 这个函数很少单独使用,更多的是为了辅助ListInsert,ListErase这两个函数,通过ListFind找到要插入删除的节点,然后将其地址传给ListInsert,ListErase进行插入或删除
- 实现也很简单,使用cur指针遍历链表,对比每一个节点data的值和x的值,x代表使用者想要查找的值
- 如果相等就返回节点地址,如果找不到就返回NULL
- 通过ListFind还可以实现链表元素的修改

LTNode* ListFind(LTNode* phead, ListDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
9. ListInsert(任意位置前插入节点)
- pos参数一般是基于ListFind查找而来,故这两个函数经常搭配使用
- 之后创建变量posprev指向pos的上一个节点,申请新节点将其prev指向posprev,将其next指向pos
- posprev的next指向新节点NewNode,pos的prev指向NewNode
- 这样pos位置之前的插入就完成了,并且可以替代头插尾插

void ListInsert(LTNode* pos, ListDataType x)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* NewNode = BuyListNode(x);
NewNode->prev = posprev;
posprev->next = NewNode;
NewNode->next = pos;
pos->prev = NewNode;
}
10. ListErase(删除任意位置节点)
- pos参数一般是基于ListFind查找而来,故这两个函数经常搭配使用
- 之后创建变量posprev指向pos的上一个节点,创建变量posnext指向pos的下一个节点
- 将posprev与posnext链接,释放pos,就完成pos位置的删除了,并且可以替代掉尾删头删

void ListErase(LTNode* pos)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
pos = NULL;
}
11. ListDestroy(销毁链表)
- 最后是销毁链表,这里有个很容易犯的错误就是直接free(phead),这样只是释放了头节点,链表的其它节点并未释放,会造成内存泄露。
- 所以我们需要通过cur遍历链表所有节点一个一个进行释放,并且防止释放了cur找不到cur的下一个节点,需要先创建临时变量进行保存
- 最后将phead置为空指针代表链表为空,即销毁完成。
void ListDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
phead = NULL;
}