
链表 总共可以 排列组合 8种 2*2*2 是否带头
但是只要学会了 单链表 和 双向链表 就 一通百通了,因为是相关的,两个都是对应的极端 只需要在此基础上 稍微加 减 一个部分即可
是否双向 前后 都有指针 只看有无 两个指针 一个指向前一节点
另一个 指向另一个节点

是否循环 首尾相连
在实现任意一种 数据结构时 优先是实现该结构的 结构体

双向链表的 结构体就是 这样子组成的 需要有两个指针域 一个类似单链表 指向下个节点的指针 next
另一个指向 前一个节点的指针 prev


定义结构
typedef int LTDataType;
//定义双向链表节点的结构
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
本节 主要学习 双链表的 创建新节点 初始化 增删查改 打印
头插 尾插 头删 尾删 打印 任意位置插入 清空 查找

1.创建新节点 并 初始化
当两种链表 为空时


去申请节点
c
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}

主要 节点的 next 和 prev指针 该怎么指向呢?
//初始化 参数的形式
//void LTInit(LTNode** pphead)
//{
// //给双向链表创建一个哨兵位
// *pphead = LTBuyNode(-1);
//}
注释的是第一种写法 通过二级指针接收 直接修改头节点
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
第二种写法 返回值 返回
就是通过返回值,把新创建的哨兵位头结点地址交给调用者,从而让调用者的头指针指向它,实现初始化的效果。
2.简单介绍 哨兵位
超级认识 不可修改地址 不可以删除 (设定)
带头 就是因为带了这个头节点
之前我们在单链表的实现时 将第一个存储有效数据的 节点叫做 头节点 但是其实是不准确的 应该叫做 首节点的
哨兵位 又叫做 哑节点 不存放有效数据 的 辅助节点
将其作为一个哨兵一样 永远存在于链表的最前面 站岗
当然不是 有头 所以就是 带头 有两个指针 双向 但是 不循环
所以我们要让其循环起来 怎们办 ? 直接让其自循环 起来 就
3 .独家理解 就是 每个节点满足 该结构的 逻辑 双向 带头 循环
让其两个指针都指向 该节点 即可
百万撤离 --- 理解相当于每个节点都有 两只手
4.尾插 代码展示 可复制 
c
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;
}
观察 变化
先修改 newnode的两只手 的指向 让它的prev指向前一个 结构体指针
让它的next指针指向 哨兵位 头节点 1 和 2


再进行下一步操作喽 另外两条线路的解释 3 4 

phead->prev->next = newnode;
phead->prev//这个就是原来的尾节点
3 要这样子理解 通过引用后 得到的尾部节点 再引用改变后面一个指向 改为 新的节点 对应关系 
4 改变哨兵位的前指针的指向
5.打印双链表 肯定要循环 主要是循环结束条件的控制
不是while(pcur != NULL ) 因为后面根本就没有 NULL
但是再双链表中有个特殊的节点叫做 哨兵位
c
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
LTNode* pcur = phead->next;
别弄错了 要指向第一个有效节点 首节点 注意一下细节
6.头插 看连环画 你就会了



c
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;
}
phead->next->prev = newnode;等价于
d1 -> prev = newnode;就是右边的绿色的线
你要计算一步看才更好的理解
phead->next = newnode;
这两行代码不能完全 交换位置
phead->next = newnode;
newnode->next->prev = newnode;
思考一下 交换位置 主要是第二句话 等价于 newnode->prev 指向新节点 就不是 让d1指向新节点,找不到了
**但是 可以有另外一种写法 (精妙)
phead->next = newnode;
newnode->next->prev = newnode;
就可以 之间的关系 是先利用 newnode 与 d1之间关系 找到 d1然后在 改变前指针的指向**
删除之前 要判断 双向链表是否为空
结合之前说的 双向 链表为空时 只剩下 一个头 哨兵位
7.尾删 如果只有一个哨兵位还删除个屁 没有地方删除了 
c
void LTPopBack(LTNode* phead)
{
// 检查链表有效性及非空性(不能只有哨兵位)
assert(phead && phead->next != phead);
// 获取要删除的尾节点
LTNode* del = phead->prev;
// 调整指针关系
del->prev->next = phead;
phead->prev = del->prev;
// 释放节点内存
free(del);
del = NULL;
}
同样的需要找尾节点 在双链表中 直接就是 哨兵位的前一节点的就是 尾部节点
重点理解 del 的运用就是 保留 原来的尾部节点的地址 然后 再删除 ,解决的是,改变指向后 找不到原来的尾部节点的 情况

8.头删 主要修改 两个 节点 主要看哪一个手 ,指针要改变
总的只有 一个节点的一边的指针指向 看看那个受到影响
只有 红绿 的 修改 别想多了 做都是方法
其次是 利用del 的临时变量 保存要删除的数据
删除的操作就是直接跳过该节点就好了
9.在指定位置之后插入
9.1得先找到 这个位置 需要遍历 查找 对比 
c
LTNode* LTFind(LTNode* phead, LTDataType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
插入的连环画





代码如图 

c
// 在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
// pos newnode pos->next
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
pos->next->prev = newnode;
别忘了 可以替换为 newnode->next->prev = newnode;
因为前面 已经改变了 新节点的指向 所以 只要能找到 对应的节点 再改变 对应的指向 就ok
10.在指定位置删除 主要 就是 搞清楚 到底哪几个会变动 然后通过 改变 指向 来跳过 就 删除了 就 前面 后面 本来位置 之间的关系 保持 链条 而且是 双链表的特点

11 清空链表 销毁 

重点 这个 的问题是要在主函数里面 手动将 传入的首节点指针 置为空 NULL
c
void LTDesTroy(LTNode** pphead)
{
if (*pphead == NULL) return;
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(*pphead);
*pphead = NULL;
}




