𝙉𝙞𝙘𝙚!!👏🏻‧✧̣̥̇‧✦👏🏻‧✧̣̥̇‧✦ 👏🏻‧✧̣̥̇: Solitary-walk
⸝⋆ ━━━┓
- 个性标签 - :来于“云”的“羽球人”。 Talk is cheap. Show me the code
┗━━━━━━━ ➴ ⷯ本人座右铭 : 欲达高峰,必忍其痛;欲戴王冠,必承其重。
👑💎💎👑💎💎👑
💎💎💎自💎💎💎
💎💎💎信💎💎💎
👑💎💎 💎💎👑 希望在看完我的此篇博客后可以对你有帮助哟👑👑💎💎💎👑👑 此外,希望各位大佬们在看完后,可以互赞互关一下,看到必回
👑👑👑💎👑👑👑
目录
1. 双向链表的初始化
2. 双向链表的销毁
3. 双向链表的尾插
4. 双向链表的头插
5. 双向链表的尾删
6.双向链表的头删
7. 双向链表的指定位置之后的插入
8. 双向链表的指定位置的删除
9. 双向链表的按值查找
10.链表打印
首先在我们进行各项具体任务之前,咱还是需要把前期工作准备好的
1.先进行自定义链表的声明
2.一些函数的声明
3.必要头文件的引入
首先我们要知道什么是双向链表,顾名思义,对于一个结点而言既有指向自己的指针,也有指向其他结点的指针域
如图所示:
链表结构体类型的定义代码如下:
typedef struct ListNode
{
DataType data;//数据域
struct ListNode* pre;//前驱域
struct ListNode*next;//后继域
}ListNode;
1.初始化
注意这里的带头双向链表不同于前面单链表的头节点
为了好区分,这里我们称之为"哨兵位"
哨兵位只是占一个位置,并不存储任何有效的数据
当我们只有哨兵位这个结点,思考一下,如何变成一个双向循环链表
你想对了吗??
phead->pre = phead->next = phead;//让他变成双向循环链表
初始化代码之不采用 传参的方法
ListNode* Init()//初始化,采用不传参的形式,但是需把哨兵位这个结点返回
{
//自己需要创建一个哨兵位
ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
assert(phead);//可能开辟失败
phead->data = -1;
phead->pre = phead->next = phead;//记住这里要让他变成双向循环链表
return phead;
}
初始化代码之采用 传参的方法
注意这里我们形参是用二级指针来接收的
因为实参我们需要传链表的地址,否则我们是无法完成初始化的
因为函数形参只是实参的一份临时拷贝,对形参的临时修改并不会影响我们的实参
void Init(ListNode** pphead)//初始化,采用传参的形式
{
//注意一下操作都是基于 phead是哨兵位来进行的 他只占一个位置,并不存储任何有效数据
assert(pphead);
*pphead = (ListNode*)malloc(sizeof(ListNode));
if (*pphead == NULL)
{
perror("malloc fail\n");
return 1;//既然开辟失败,没必要执行下面代码
}
(*pphead)->data = -1;//注意优先级顺序
(*pphead)->next = (*pphead)->pre = NULL;//构建一个双向循环链表
}
2.销毁
说白了,其实就是一个结点一个结点进行删除
这自然就需要遍历了
我们在删除之前需要先找到删除结点后一个结点
关键是我们实参到底是指针还是指针地址
让要删除的结点为 del = phead->next;
那么要删除的结点下一个结点为 del->next
我们不妨试一下~~~
实参为指针
void Destroy(ListNode* phead)//链表销毁;想一下,传一级指针还是二级? 在这里我们传入一级指针,为了保持接口一致性
{
//销毁我们是一个一个进行删除,自然就需要遍历
assert(phead);
ListNode* del = phead->next;
while (del != phead)
{
ListNode* next = del->next;
free(del);
/*del = NULL;*/ // ?
del = next;
}
//来到这说明,此时只有一个哨兵位
free(phead)
phead = NULL;
}
注意当我们只是简单调用销毁函数的时候,代码并不是我们所设想的一样
正常来说,我们的plist的值应该为NULL
为了解决这个问题我们就需要在传一级指针的时候,手动把plist 置为空
实参为指针地址
代码如下,但是问题又来了
void LTDestroy(ListNode** pphead)
{
assert(pphead && *pphead);
ListNode* cur = (*pphead)->next;
while ( cur!=*pphead )
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(*pphead);
*pphead = NULL;
}
对于这个问题容本up主先买个关子,欲知后事如何 且听下回分解
为了保持接口一致性,我们这里就使用一级指针来接收
3.尾插
顾名思义,是在尾结点后面进行插入,注意这里在哨兵位的前面进行插入也是可以滴~~~
接下来就是改变指针的指向
1)首先既然是尾插就需要为要插进来的数据开辟结点
这里涉及到创建结点的函数
2)其次是处理指针指向
3)先处理node的pre ,next
4)处理 原来尾结点,哨兵位
尾插草图如下:
结点创建函数的代码如下:
ListNode* ListBuyNode(x)//创建结点
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node == NULL)
{
perror("malloc fail\n");
return;
}
//来到这,说明空间开辟成功
node->data = x;
node->next = node->pre = NULL;
return node;
}
尾插完整代码:
void PushBack(ListNode* phead,DataType x)//尾插
{
assert(phead);
//为插入的数据开辟空间
ListNode* node = ListBuyNode(x);
//先处理node 的前驱,后继
node->pre = phead->pre; // phead->pre->next原来尾结点的后继,node->pre = phead->pre->next这里出现了野指针的问题 phead->pre->next没有具体指向
node->next = phead;
//接下来在处理 原来尾结点,哨兵位
/*,这样写是错的,以下2句话不可以颠倒
已经把node这个结点给覆盖掉了,在执行65行代码时,node 是未知的,因为这个结点已经被覆盖了,phead->pre->next此时他的指向也就是未知的,调试时不会报错,但是遍历读取他就会报错了,就像你在打印函数里进行打印时,出现野指针
phead->pre = node;
phead->pre->next = node;*/
phead->pre->next = node;
phead->pre = node;//别忘了,每尾插进来的结点最终都是一个新的尾结点
}
4.头插
首先是在哨兵位是后面进行的,每插入进来应该结点,此节点就会成为一个新的头节点
同上,需要先创建一个结点
其次找到原来头节点 phead->next
头插草图如下:
头插完整代码如下:
void PushFront(ListNode* phead,DataType x)//头插
{
//头插是在哨兵位的后面进行插入
assert(phead);
//为插入的数据开辟空间
ListNode* node = ListBuyNode(x);
//处理node 的后继,前驱
node->pre = phead;
node->next = phead->next;
//处理phead ,phead->next
/*这样写也是对的
phead->next->pre = node;*/
phead->next = node;//头节点要进行更新
phead->next->pre = node;
}
5.尾删
注意我们不能上来就行删除
需要先找到要删除结点 (del)的前一个也就是 del->pre
这里我们一定要注意结点的先后顺序
完整代码如下:
void PopBack(ListNode* phead)//尾删
{
//注意不能直接删除,需要先找到尾结点的前一个结点
assert(phead);
//先判断以下是否为空,有2种写法
/*if (phead->pre == phead)
{
return 9;
}*/
assert(phead->next != phead);//是否为空
ListNode* del = phead->pre;//把要删除的结点先保存起来
//删除之前,先要构成一个新的双向循环链表
del->pre->next = phead;
phead->pre = del->pre;
/*错误的,顺序不能颠倒,因为此时 del->pre已经被覆盖了
phead->pre = del->pre;//新的尾结点
del->pre->next = phead;*/
free(del);
del = NULL;
}
7. 指定位置之后的插入
草图如下:
1)要为插入的数据创建结点
2)找到指定位置(pos)之后的结点 (pos->next)和之前的结点(pos->pre)
3) 改变指针指向
指定位置之后插入对应完整代码:
void InsertAfter(ListNode* pos, DataType x)//指定位置之后插入
{
assert(pos);
//为插入的数据开辟空间
ListNode* node = ListBuyNode(x);
//处理 node 的前驱,后继
node->next = pos->next;
node->pre = pos;
//处理 pos 和 pos->next的pre
pos->next = node;
pos->next->pre = node;
}
8. 双向链表的指定位置的删除
对应草图如下:
直接进行删除是不行滴~~~
先要找到指定位置(pos)之后的结点 (pos->next)和之前的结点(pos->pre)
其次改变指针走向
void Earse(ListNode* pos)//指定位置删除
{
assert(pos);
//需要找到pos之前的一个结点和之后的一个结点
pos->next->pre = pos->pre;
pos->pre->next = pos->next;
free(pos);
pos = NULL;
}
9. 双向链表的按值查找
这里自然就需要一个一个进行遍历了
按值查找对应完整代码:
ListNode* Find(ListNode* phead, DataType x)//在链表中按值查找,若是找到则返回对应的结点
{
assert(phead);
ListNode* pcur = phead->next;//定义一个用来循环遍历的指针
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;//直接返回对应结点
}
pcur = pcur->next;
}
printf("没有找到\n");
}
10链表打印
方法同上,对链表进行遍历
void Print(ListNode* phead)//链表打印
{
assert(phead);
ListNode* pcur = phead->next;//定义一个用来 遍历的指针,注意他初始值是 phead->next
while (pcur != phead)
{
printf("%d-> ", pcur->data);
pcur = pcur->next;//记得更新
}
printf("\n");
}
List.c对应完整代码
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//void Init(ListNode** pphead)//初始化,采用传参的形式
//{
// //注意一下操作都是基于 phead是哨兵位来进行的 他只占一个位置,并不存储任何有效数据
// assert(pphead);
// *pphead = (ListNode*)malloc(sizeof(ListNode));
// if (*pphead == NULL)
// {
// perror("malloc fail\n");
// return 1;//既然开辟失败,没必要执行下面代码
// }
//
// (*pphead)->data = -1;//注意优先级顺序
// (*pphead)->next = (*pphead)->pre = NULL;//构建一个双向循环链表
//}
ListNode* Init()//初始化,采用不传参的形式,但是需把哨兵位这个结点返回
{
//自己需要创建一个哨兵位
ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
assert(phead);//可能开辟失败
phead->data = -1;
phead->pre = phead->next = phead;//记住这里要让他变成双向循环链表
return phead;
}
ListNode* ListBuyNode(x)//创建结点
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node == NULL)
{
perror("malloc fail\n");
return;
}
//来到这,说明空间开辟成功
node->data = x;
node->next = node->pre = NULL;
return node;
}
void Print(ListNode* phead)//链表打印
{
assert(phead);
ListNode* pcur = phead->next;//定义一个用来 遍历的指针,注意他初始值是 phead->next
while (pcur != phead)
{
printf("%d-> ", pcur->data);
pcur = pcur->next;//记得更新
}
printf("\n");
}
void PushBack(ListNode* phead,DataType x)//尾插
{
assert(phead);
//为插入的数据开辟空间
ListNode* node = ListBuyNode(x);
//先处理node 的前驱,后继
node->pre = phead->pre; // phead->pre->next原来尾结点的后继,node->pre = phead->pre->next这里出现了野指针的问题 phead->pre->next没有具体指向
node->next = phead;
//接下来在处理 原来尾结点,哨兵位
/*,这样写是错的,以下2句话不可以颠倒
已经把node这个结点给覆盖掉了,在执行65行代码时,node 是未知的,因为这个结点已经被覆盖了,phead->pre->next此时他的指向也就是未知的,调试时不会报错,但是遍历读取他就会报错了,就像你在打印函数里进行打印时,出现野指针
phead->pre = node;
phead->pre->next = node;*/
phead->pre->next = node;
phead->pre = node;//别忘了,每尾插进来的结点最终都是一个新的尾结点
}
void PushFront(ListNode* phead,DataType x)//头插
{
//头插是在哨兵位的后面进行插入
assert(phead);
//为插入的数据开辟空间
ListNode* node = ListBuyNode(x);
//处理node 的后继,前驱
node->pre = phead;
node->next = phead->next;
//处理phead ,phead->next
/*这样写也是对的
phead->next->pre = node;*/
phead->next = node;//头节点要进行更新
phead->next->pre = node;
}
void PopBack(ListNode* phead)//尾删
{
//注意不能直接删除,需要先找到尾结点的前一个结点
assert(phead);
//先判断以下是否为空,有2种写法
/*if (phead->pre == phead)
{
return 9;
}*/
assert(phead->next != phead);//是否为空
ListNode* del = phead->pre;//把要删除的结点先保存起来
//删除之前,先要构成一个新的双向循环链表
del->pre->next = phead;
phead->pre = del->pre;
/*错误的,顺序不能颠倒,因为此时 del->pre已经被覆盖了
phead->pre = del->pre;//新的尾结点
del->pre->next = phead;*/
free(del);
del = NULL;
}
void PopFront(ListNode* phead)//头删
{
assert(phead);
assert(phead->next != phead);//确保不为空
ListNode* del = phead->next;
//以下2句没有先后顺序之分
del->next->pre = phead;
phead->next = del->next;
free(del);
del = NULL;
}
void InsertAfter(ListNode* pos, DataType x)//指定位置之后插入
{
assert(pos);
//为插入的数据开辟空间
ListNode* node = ListBuyNode(x);
//处理 node 的前驱,后继
node->next = pos->next;
node->pre = pos;
//处理 pos 和 pos->next的pre
pos->next = node;
pos->next->pre = node;
}
void Earse(ListNode* pos)//指定位置删除
{
assert(pos);
//需要找到pos之前的一个结点和之后的一个结点
pos->next->pre = pos->pre;
pos->pre->next = pos->next;
free(pos);
pos = NULL;
}
ListNode* Find(ListNode* phead, DataType x)//在链表中按值查找,若是找到则返回对应的结点
{
assert(phead);
ListNode* pcur = phead->next;//定义一个用来循环遍历的指针
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;//直接返回对应结点
}
pcur = pcur->next;
}
printf("没有找到\n");
}
void Destroy(ListNode* phead)//链表销毁;想一下,传一级指针还是二级? 在这里我们传入一级指针,为了保持接口一致性
{
//销毁我们是一个一个进行删除,自然就需要遍历
assert(phead);
ListNode* del = phead->next;
while (del != phead)
{
ListNode* next = del->next;
free(del);
/*del = NULL;*/ // ?
del = next;
}
//来到这说明,此时只有一个哨兵位
free(phead);
phead = NULL;
}
void LTDestroy(ListNode** pphead)
{
assert(pphead && *pphead);
ListNode* cur = (*pphead)->next;
while ( cur!=*pphead )
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(*pphead);
*pphead = NULL;
}
List.h对应完整代码
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>
//双向链表的实现
typedef int DataType;
typedef struct ListNode {
DataType data;//数据域
struct ListNode* pre;//前驱域
struct ListNode*next;//后继域
}ListNode;
//接口函数的声明
//void Init(ListNode** pphead);//初始化,采用传参的形式
ListNode* Init();//初始化,采用不传参的形式,但是需把哨兵位这个结点返回
void Print(ListNode* phead);//链表打印
void PushBack(ListNode* phead,DataType x);//尾插,这里传入一级指针即可,因为返回的头节点我们不需要进行更改
void PushFront(ListNode* phead, DataType x);//头插
void PopBack(ListNode* phead);//尾删
void PopFront(ListNode* phead);//头删
void InsertAfter(ListNode* pos,DataType x);//指定位置之后插入
void Earse(ListNode* pos);//指定位置删除
ListNode* Find(ListNode*phead,DataType x);//按值查找,若是找到则返回对应的结点
void Destroy(ListNode* phead);//链表销毁;想一下,传一级指针还是二级?为了保持接口一致性我们传入一级指针
ok,以上就是我要为大家进行share的一些基本内容,都来到这里了,要是感觉我写的还不错的话,各位大佬烦劳点个赞,互关以下呗~~~