双向链表专题
1.双向链表的概念和结构
前面我们知道我们学习的单链表是单向不带头不循环链表
而今天我们学习的双向链表恰恰相反——双向带头循环链表
我们来看看双向链表的节点定义:
struct ListNode
{
int data;
srtuct ListNode* next; // 指向下一个节点
struct ListNode* prev; // 指向前一个节点
}
注意:
我们之前学习的单链表在空的时候就是一个NULL什么都没有
但是双向链表中并不是这样的,双向链表为空的时候,是存在一个哨兵位(也就是头节点)的 ,而这个哨兵位的next指针指向NULL
2.双向链表的实现
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
# include<stdio.h>
# include<assert.h>
# include<stdlib.h>
// 定义双向链表的节点的结构
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
// 声明双向链表的函数(方法)
// 双向链表的初始化
void LTInit(LTNode** pphead);
// 初始化的第二个思路
LTNode* LTInit2();
// 双向链表的打印
void LTPrint(LTNode* phead);
//双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x); // 如果传二级指针 但是不去修改哨兵位的位置 也是ok的
// 为什么这里传的是phead的一级指针呢 因为我们传进去的是哨兵位,不管是尾删还是头删我们都不改变哨兵位节点的位置
// 双向链表的头插
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);
双向链表的初始化:
初始化的代码:
// 申请节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc");
exit(1);
}
node->data = x;
// 我们知道双向链表是一个带头双向循环链表
// 这里对next和prev指针的初始化不能置为空 要让其循环起来 也就是让这两个指针指向自己就行了
node->next = node->prev = node;
return node;
}
// 双向链表的初始化
void LTInit(LTNode** pphead)
{
// 先给双向链表申请一个 哨兵位(头节点)
*pphead = LTBuyNode(-1);// 这个-1没有意义 只是因为必须传个值给哨兵位而已
}
// 初始化的第二个思路
LTNode* LTInit2()
{
LTNode* phead = LTBuyNode(-1); // 手动创建一个哨兵位
return phead;
}
调试代码:
void Test01()
{
// 测试双向链表的初始化
LTNode* plist = NULL;
LTInit(&plist);
}
int main()
{
Test01();
return 0;
}
下面是调试的监视窗口 可以看到代码没有问题
双向链表的打印:
我们要先思考:
- 双向链表是一个带头双向循环链表,要考虑哨兵位的存在
- 遍历链表的时候循环什么时候停止
我们来看看代码如何是实现的:
// 双向链表的打印
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next; // 让pcur指向第一个有效节点
if (pcur == phead)
{
printf("该双向链表为空,无法打印!\n");
return;
}
// 遍历双向链表 找到每一个节点
while (pcur != phead)// 判断pcur是否是哨兵位 是的话就退出循环
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
// 走到这里已经打印完毕了
printf("\n");
}
测试代码:
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPrint(plist);// 1->2->
双向链表的尾插:
//双向链表的尾插
void LTPushBack(LTNode* pphead, LTDataType x);
我们先来思考一下:
为什么传入的是一级指针呢?
因为不管是头删还是尾删 ,头插还是尾插 我们都不需要去改变哨兵位的位置
因此传入一级指针 就ok了
尾插的思路:
- 申请一个要尾插的节点newnode 存入用户输入的数据
- 将newnode的prev指针指向原链表的尾节点 phead->prev
- 将newnode的next指针指向哨兵位 newnode->next = phead
- 将原链表的尾节点的next指针指向newnode ,也就是phead->prev->next=newnode
- 将哨兵位的prev指针指向newnode 也就是phead->prev = newnode
来看一下尾插的代码如何实现:
// 双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead); // 哨兵位不能为空
LTNode* newnode = LTBuyNode(x); // 申请插入的节点
// 开始尾插
newnode->next = phead; // 让新节点的next指针指向哨兵位
newnode->prev = phead->prev; // 让新节点的prev指针指向原链表的尾节点 (phead->prev)就是尾节点,
phead->prev->next = newnode;// 让原链表的尾节点的next指针指向newnode
phead->prev = newnode;// 让哨兵位的prev指针指向newnode
//要记住这个代码的顺序和上面的顺序不能调换,不然phead->prev被提前修改 就会出现错误
}
尾插的测试代码:
// 测试尾插
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPrint(plist);// 1->2->
双向链表的头插:
我们知道在哨兵位之前插入元素其实就是尾插
头插是插入到第一个有效节点和哨兵位之间
头插的思路:
- 申请一个newnode作为要插入的节点
- 让newnode的next指针指向原链表的第一个节点 也就是newnode->next = phead->next
- 让newnode的prev指针指向哨兵位,也就是newnode->prev = phead
- 再让原链表的第一个节点的prev指针指向newnode,也就是phead->next->prev = newnode
- 再让哨兵位的next指针指向newnode,也就是phead->next = newnode
- 注意了第五步的顺序和第四部不能调换
我们来看一下头插实现的代码:
// 双向链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
// 开始头插
// 先改变不会被受影响的newnode节点 如果先改变哨兵位 可能导致无法找到第一个节点之类的问题
newnode->next = phead->next; // 让newnode的next指针指向原链表的第一个节点
newnode->prev = phead;// 让newnode的prev指针指向哨兵位
phead->next->prev = newnode;// 让第一个节点的prev指针指向newnode
phead->next = newnode; // 让哨兵位的next指针指向newnode
如果一定要调换上面两句代码的顺序 也可以这样改
//phead->next = newnode;
//newnode->next->prev = newnode;
}
测试代码:
// 测试头插
LTPushFront(plist, 2);
LTPushFront(plist, 1);
LTPrint(plist);
双向链表的尾删:
先思考尾删的时候收到影响的节点和节点存储的指针有哪些
我们来看代码的实现:
// 双向链表的尾删
void LTPopBack(LTNode* phead)
{
// phead不能为NULL 也就是必须是一个有效的双向链表
assert(phead);
// 并且这个双向链表不能为空
assert(phead->next != phead);// phead不指向自己则不是空
LTNode* del = phead->prev; // 让del保存尾节点
// 先让该改变的指针改变,再去尾删
del->prev->next = phead;// 让尾节点的前一个节点的next指针指向哨兵位
phead->prev = del->prev;// 让哨兵位的prev指针指向尾节点的前一个节点
// 尾删
free(del);
del = NULL;
}
测试代码:
// 测试尾删
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
双向链表的头删:
头删的思路:
- 首先用del记下要删除的节点phead->next
- 再让del的下一个节点的prev指针指向哨兵位
- 让哨兵位的next指针指向del的下一个节点
来看代码的实现:
// 双向链表的头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);// 双向链表不能为空
LTNode* del = phead->next;
//头删
del->next->prev = phead; // 让del的下一个指针的prev指针指向哨兵位
phead->next = del->next;// 让哨兵位的next指针指向del的下一个节点
free(del);
del = NULL;
}
测试代码:
// 测试头删
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPopFront(plist);
LTPopFront(plist);
LTPopFront(plist);
LTPrint(plist);
双向链表的查找:
查找的代码:
// 双向链表的查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* pcur = phead->next;// pcur指向的是第一个节点
// 遍历双向链表
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
// 走到这里已经遍历了所有节点了
return NULL;
}
测试代码:
// 测试查找
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTNode* ret = LTFind(plist, 1);
if (ret == NULL)
{
printf("找不到\n");
}
else
{
printf("找到了\n");
}
在链表指定位置pos之后插入数据:
思路:
而之前大同小异
但是要注意 当pos是尾节点的时候 相当于尾插,但是我们不能直接调用尾插函数,因为我们尾插函数的形参是一级指针 我们传入是相当于传值调用,无法改变实参的值。
因此只能重新编写尾插函数
我们来看看代码如何实现的:
// 在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是尾节点 上面的代码都能完成
}
测试代码:
// 测试 在pos位置之后插入数据
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTNode* ret = LTFind(plist, 3);
LTInsert(plist, 5);// 头插
LTInsert(ret, 5);// 尾插
LTPrint(plist);// 5->1->2->3->5->
在链表pos指定位置删除:
思路:
和之前也是大同小异
我们直接来看代码:
// 删除pos位置数据
void LTErase(LTNode* pos)
// 理论上是要传入二级指针的 不然我们在函数内部的修改pos为NULL 是无法传回给实参的 但是为了接口一致性 我们用一级指针作为形参
{
assert(pos);// pos不能为空 没有数据怎么删除
// 理论上来说 还要加一个pos != phead 的校验 但是没有phead参数 其实就是pos不能是哨兵位
// 删除
// pos->prev pos pos->next 删除pos 让前一个节点和后一个节点相连
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
测试代码:
// 测试 删除pos位置的数据
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTNode* ret = LTFind(plist, 1);
LTErase(ret);// 删除完之后ret变成了野指针
ret = NULL; // 因为我们传入的是ret一级指针,在函数内部对pos进行NULL置空不会影响到ret 因此需要手动置空才能实现置空
LTPrint(plist);// 2->3->
链表的销毁:
思路:
- 让pcur去指向第一个节点,next记录下pcur的下一个节点
- 删除pcur 让pcur移动到next
- 循环往复
- 直至pcur == phead 也就是移动到哨兵位了
- 这个时候删除哨兵位
我们来看看代码是如何实现的:
// 链表的销毁
void LTDestroy(LTNode* phead)// 为了保持接口一致性才设置的一级指针
{
assert(phead);
// 销毁
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
// 走到这里pcur指向哨兵位
free(phead);
phead = NULL; // 由于传入的是一级指针,这里的NULL无法让实参置为NULL
// 因此外边需要手动 将实参置为NULL 这是为了接口一致性所造成的缺陷
}
测试代码:
// 测试链表的销毁
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTDestroy(plist);
// 由于是一级指针的实参 这里是传值调用 函数里对phead置为NULL 这边plist是不会置为NULL的 需要手动NULL
plist = NULL;
注意了!!!:
LTErase和LTDestroy参数理论上要传二级,因为我们需要让形参的改变影响到实参,但是为了保持接口一致性才传的一级~传一级存在的问题是,当形参phead置为NULL后,实参plist不会被修改为NULL,因此解决办法是: 调用完方法后手动将实参置为NULL~