概念
对链表而言,双向均可遍历是最方便的,另外首尾相连循环遍历也可大大增加链表操作的便捷性。
因此,双向循环链表,是在实际运用中是最常见的链表形态。

基本操作
与普通的链表完全一致,双向循环链表虽然指针较多,但逻辑是完全一样。基本的操作包括:
1. 节点设计
2. 初始化空链表
3. 增删节点
4. 链表遍历
5. 销毁链表
节点设计
双向链表的节点只是比单向链表多了一个前向指针。示例代码如下所示:
typedef int DATA;
typedef struct node
{
// 以整型数据为例
DATA data;
// 指向相邻的节点的双向指针
struct node *prev;
struct node *next;
}NODE;
初始化
所谓初始化,就是构建一条不含有效节点的空链表。
以带头结点的双向循环链表为例,初始化后,其状态如下图所示:

在初始空链表的情况下,链表只有一个头结点,下面是初始化示例代码:
int dlist_create(NODE** head,DATA data)
{
// 创建新节点(申请内存空间)
NODE *pNew = (NODE*)malloc(sizeof(NODE));
if(!pNew)
return -1;
// 给节点赋初值
pNew -> data = data;
// 前后指针默认都指向NULL
pNew -> prev = pNew -> next = NULL;
// 将新节点作为头节点
*head = pNew;
return 0;
}
插入节点
与单链表类似,也可以对双链表中的任意节点进行增删操作,常见的有所谓的头插法、尾插法等,
即:将新节点插入到链表的首部或者尾部,示例代码是:
头插法
将新节点插入到链表的头部
// 将新节点pNew,插入到链表的首部
int dlist_addHead(NODE** head,DATA data)
{
// 创建新节点并申请内存
NODE *pNew = (NODE*)malloc(sizeof(NODE));
if(!pNew)
return -1;
// 给新节点赋值
pNew -> data = data;
pNew -> prev = NULL;
// 后针指向头指针
pNew -> next = *head;
// 如果头指针存在
if(*head)
// 头指针的前指针指向新节点
(*head) -> prev = pNew;
// 新插入的节点作为新的头节点
*head = pNew;
return 0;
}
尾插法
将新节点插入到链表的尾部
// 将新节点pNew,插入到链表的尾部
int dlist_addTail(NODE** head,DATA data)
{
// 创建节点并申请内存
NODE *pNew = (NODE*)malloc(sizeof(NODE));
if(!pNew)
return -1;
// 初始化节点
pNew -> data = data;
pNew -> prev = NULL;
pNew -> next = NULL;
// 用来记录尾节点,默认头节点就是尾节点
NODE* p = *head;
if(!p)
{
// 头节点不存在,新插入的节点作为头节点
*head = pNew;
return 0;
}
// 通过循环,查找尾节点
while(p -> next)
{
p = p -> next;
}
// 尾节点的后指针指向新插入的节点
p -> next = pNew;
// 新插入的节点的前指针指向尾节点
pNew -> prev = p;
// 此时的新节点作为了新的尾节点
return 0;
}
中间插法
将新节点插入到链表的指定位置
// 将新节点pNew,插入到链表的指定位置
int dlist_insert(NODE** head,DATA pos,DATA data)
{
NODE *pNew = (NODE*)malloc(sizeof(NODE));
if(!pNew)
return -1;
pNew -> data = data;
pNew -> prev = NULL;
pNew -> next = NULL;
NODE* p = *head, *q = NULL;
if(!p)
{
*head = pNew;
return 0;
}
if(memcmp(&(p -> data),&pos,sizeof(DATA)) == 0)
{
pNew -> next = p;
p -> prev = pNew;
*head = pNew;
return 0;
}
while(p)
{
if(memcmp(&(p -> data),&pos,sizeof(DATA)) == 0)
{
pNew -> next = p;
pNew -> prev = q;
p -> prev = pNew;
q -> next = pNew;
return 0;
}
q = p;
p = p -> next;
}
q -> next = pNew;
pNew -> prev = q;
return 0;
}
剔除节点
注意,从链表中将一个节点剔除出去,并不意味着要释放节点的内容。当然,我们经常在剔除了一
个节点之后,紧接着的动作往往是释放它,但是将“剔除”与“释放”两个动作分开,是最基本的函数
封装的原则,因为它们虽然常常连在一起使用,但它们之间并无必然联系,例如:当我们要移动一
个节点的时候,实质上就是将“剔除”和“插入”的动作连起来,此时就不能释放该节点了。
在双向链表中剔除指定节点的示例代码如下:
// 将data对应的节点从链表中剔除
int dlist_delete(NODE** head,DATA data)
{
NODE* p = *head;
if(!p)
return -1;
if(memcmp(&(p -> data),&data,sizeof(DATA)) == 0)
{
if(p -> next == NULL)
{
*head = NULL;
free(p);
return 0;
}
*head = p -> next;
p -> next -> prev = NULL;
free(p);
return 0;
}
while(p)
{
if(memcmp(&(p -> data),&data,sizeof(DATA)) == 0)
{
p -> prev -> next = p -> next;
if(p -> next == NULL)
p -> prev -> next = NULL;
else
p -> next -> prev = p -> prev;
free(p) ;
return 0;
}
p = p -> next;
}
return -1;
}
链表的遍历
对于双向循环链表,路径可以是向后遍历,也可以向前遍历。
下面是根据指定数据查找节点,向前、向后遍历的示例代码,假设遍历每个节点并将其整数数据输
出:
// 根据指定数据查找节点
NODE* dlist_find(const NODE* head,DATA data)
{
const NODE* p = head;
while(p)
{
if(memcmp(&(p -> data),&data,sizeof(DATA)) == 0)
return (NODE*)p;
p = p -> next;
}
return NULL;
}
// 向前|向后遍历
void dlist_showAll(const NODE* head)
{
const NODE* p = head;
while(p)
{
printf("%d ",p -> data);
p = p -> next;// 向后遍历
// p = p -> prev;// 向前遍历
}
printf("\n");
}
修改链表
我们也可以针对链表中的数据进行修改,只需要提供一个修改的源数据和目标数据即可。
示例代码如下:
int dlist_update(const NODE* head,DATA old,DATA newdata)
{
NODE* pFind = NULL;
if(pFind = dlist_find(head,old))
{
pFind -> data = newdata;
return 0;
}
return -1;
}
销毁链表
由于链表中的各个节点被离散地分布在各个随机的内存空间,因此销毁链表必须遍历每一个节点,
释放每一个节点。
注意:
销毁链表时,遍历节点要注意不能弄丢相邻节点的指针
示例代码如下:
void dlist_destroy(NODE** head)
{
NODE *p = *head, *q = NULL;
while(p)
{
q = p;
p = p -> next;
free(q);
}
*head = NULL;
}