一. 双链表的概念

双链表一般指的是双向循环带头结点的链表,是一种链式存储结构,它是单链表的升级版,和单链表的最大区别是:每个节点不仅能指向后继节点,还能指向前驱节点,可以双向遍历。
这里的"带头"跟平常我们说的"头节点"是两个概念,实际在单链表中的称呼不严谨。带头链表里的"头节点",实际为**"哨兵位"** ,"哨兵位"节点不存储任何有效元素 ,只是站在这里"放哨"的"哨兵位"存在的意义:遍历循环链表避免死循环。
二. 双链表
1. 单链表实现的功能(List.h 声明函数)
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
// 定义双向链表节点的结构
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
// 声明双向链表中提供的方法
// 初始化
LTNode* LTInit();
// 打印
void LTPrint(LTNode* phead);
// 尾插
void LTPushBack(LTNode* phead, LTDataType x);
// 申请节点
LTNode* LTBuyNode(LTDataType x);
// 头插
void LTPushFront(LTNode* phead,LTDataType x);
// 尾删
void LTPopBack(LTNode* phead);
// 头删
void LTPopFront(LTNode* phead);
// 查找
LTNode* LTFind(LTNode* phead, LTDataType x);
// 在 pos 位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
// 删除 pos 节点
void LTErase(LTNode* pos);
// 销毁
void LTDesTroy(LTNode* phead);
**说明:**双链表是带头链表,即它的头节点(哨兵位)是不可变的,它的作用是站岗放哨,为后续的有效节点标明位置,因此,与单链表不同的是,双链表在函数传参的过程中传的是一级指针,不再是二级指针(保证哨兵节点不可改变)
2. 函数实现(LIst.c)
1> 打印
cpp
// 打印
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
2> 申请节点
cpp
// 申请节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(1);
}
node->data = x;
node->next = node->prev = node;
return node;
}
3> 初始化
cpp
// 初始化
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
单链表为空的意思是就是空链表,链表内无存在任何节点,而双链表为空时,此时链表中只剩下一个头节点(哨兵位),无有效节点,则带表双链表为空。
因此,在插入数据之前,双链表必须初始化到只有一个头节点的情况。
那么,是否可以将哨兵节点的 prev 指针和 next 指针初始化为 NULL ? 不可以
双链表是循环链表,首尾相连,因此,链表的 prev 指针和 next 指针应该在初始化时指向哨兵节点本身。
4> 尾插
cpp
// 尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead->prev;
newnode->next = phead;
// 这两行代码不能交换位置
phead->prev->next = newnode;
phead->prev = newnode;
}

这两行代码不能交换位置:如图,phead->prev 指的是 d3 ,这里是要让 d3 指向新的节点(newnode),再让头节点前驱指向新的节点,形成新的循环。
如果代码位置交换,会找不到 d3。
5> 头插
cpp
// 头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
// 这两行代码不能交换位置
phead->next->prev = newnode;
phead->next = newnode;
}

要注意,我们这里所说的头插是指头插到第一个有效节点的前面,即头节点(哨兵位)的后面。
这两行代码不能交换位置:先改后面节点的指向,再改前面头节点的指向,因为头节点一改,就再也找不到原来的第一个节点(d1)。
6> 尾删
cpp
// 尾删
void LTPopBack(LTNode* phead)
{
// 链表必须有效且链表不能为空(只有一个哨兵位)
assert(phead && phead->next != phead);
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
// 删除 del 节点
free(del);
del = NULL;
}

删除的条件:链表有效且链表不能为空(只有一个哨兵位)
注意:在操作之前要将原来的尾节点(del=phead->prev) 保存下来,防止后续的改变指针指向而找不到原尾节点的位置了。
7> 头删
cpp
// 头删
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
// 删除 del 节点
free(del);
del = NULL;
}

删除的条件:链表有效且链表不能为空(只有一个哨兵位)
头删是指删除链表的第一个有效节点,使第二个有效节点成为新的一个有效节点。
8> 查找
cpp
// 查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
// 没有找到
return NULL;
}
9> 在 pos 位置之后插入数据
cpp
// 在 pos 位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
10> 删除 pos 节点
cpp
// 删除 pos 节点
void LTErase(LTNode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
}
11> 销毁
cpp
// 销毁
void LTDesTroy(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
// 此时 pcur 指向phead, 而 phead 还没有被销毁
free(phead);
phead = NULL;
}
注意:LTErase 和 LTDestroy 函数参数理论上要传二级指针,因为我们需要让形参的改变影响到实参,但为了保持接口一致性传一级指针。传一级指针存在的问题:当形参 phead 置为 NULL后,实参 plist 不会被修改为 NULL ,因此需要调用方法后手动将实参置为 NULL。
3. 代码演示(test.c)
cpp
#include"List.h"
void ListTest01()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPrint(plist);
LTPushBack(plist, 2);
LTPrint(plist);
LTPushBack(plist, 3);
LTPrint(plist);
LTPushBack(plist, 4);
LTPrint(plist);
LTPushFront(plist, 5);
LTPrint(plist);
LTPushFront(plist, 6);
LTPrint(plist);
LTPushFront(plist, 7);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTNode* find = LTFind(plist, 5);
if (find == NULL)
{
printf("没有找到\n");
}
else
{
printf("找到了\n");
}
LTInsert(find, 66);
LTPrint(plist);
LTErase(find);
LTPrint(plist);
LTDesTroy(plist);
}
int main()
{
ListTest01();
return 0;
}v
