目录
[1. 前言:](#1. 前言:)
[2. 双向链表](#2. 双向链表)
[2.2 双向链表节点的定义](#2.2 双向链表节点的定义)
[3. 双向链表的实现](#3. 双向链表的实现)
[3.2 双向链表的打印](#3.2 双向链表的打印)
[3.3 双向链表的尾插](#3.3 双向链表的尾插)
[3.4 双向链表的头插](#3.4 双向链表的头插)
[3.5 双向链表的尾删](#3.5 双向链表的尾删)
[3.6 双向链表的头删](#3.6 双向链表的头删)
[3.7 双向链表的查找](#3.7 双向链表的查找)
[3.8 双向链表在指定位置之后插入数据](#3.8 双向链表在指定位置之后插入数据)
[3.9 双向链表删除指定位置数据](#3.9 双向链表删除指定位置数据)
[3.10 双向链表的销毁](#3.10 双向链表的销毁)
[4. 删除指定位置数据和双向链表销毁的问题](#4. 删除指定位置数据和双向链表销毁的问题)
[5. 顺序表和单链表的优缺点](#5. 顺序表和单链表的优缺点)
[6. 完整代码](#6. 完整代码)
[6.1 ListNode.h](#6.1 ListNode.h)
[6.2 ListNode.c](#6.2 ListNode.c)
1. 前言:
在前面我们提到了单链表是不带头单项不循环链表 ,在单链表中,其实是没有头节点这个概念的,但是为了方便理解,我们将单链表里面的第一个有效节点称之为头节点,这是一种错误的说法,接下来我们将学习双向链表,带头双向循环链表
2. 双向链表
2.1双向链表的结构
在这里我们可以看到双向链表是有两个指针的,一个指向前一个节点的前驱指针prev和一个指向下一个节点的后继指针next。
这里我们说的头节点,其实就是哨兵位,它是不存储任何元素,相当于是一个用来"放哨的",
我们知道双向链表是循环的,如果我们需要遍历链表的话,容易进入死循环,这里哨兵位就可以用来避免死循环的发生。
2.2 双向链表节点的定义
这里我们知道了双向链表的结构,那我们就可以定义节点
typedef int LNDataType;
typedef struct ListNode
{
LNDataType data;//数据
struct ListNode* next;//指向下一个节点的指针
struct ListNode* prev;//指向前一个节点的指针
}ln;
3. 双向链表的实现
ListNode.h
//包含之后可能用到的库函数头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LNDataType;//类型重命名,方便后续替换
typedef struct ListNode//类型重命名,简化写法
{
LNDataType data;//数据
struct ListNode* next;//指向下一个节点的指针
struct ListNode* prev;//指向前一个节点的指针
}ln;
//链表初始化
ln* Init();
//链表的打印
void* LNPrint(ln* phead);
//链表尾插
void* LNPushBack(ln* phead, LNDataType x);
//链表头插
void* LNPushFront(ln* phead, LNDataType x);
//链表尾删
void* LNPopBack(ln* phead);
//链表头删
void* LNPopFront(ln* phead);
//链表查找
void* LNFind(ln* phead, LNDataType x);
//链表在指定位置之后插入数据
void* LNInsert(ln* pos, LNDataType x);
//链表删除指定位置数据
void* LNErase(ln* pos);
//链表销毁
void* LNDestroy(ln* phead);
3.1双向链表的初始化
我们之前在单链表中初始化都是在main函数里面初始化的,因为单链表为空的时候,链表是空链表,但是我们双向链表为空的时候,此时链表只剩下一个头节点。
当想要创建一个双向链表的时候,那我们必须要给他初始化一个头节点
ListNode.c
ln* LNBuyNode(LNDataType x)
{
ln* newnode = (ln*)malloc(sizeof(ln));
if(newnode == NULL)
{
perror("malloc");
exit(1);
}
newnode->data = x;
//双向链表是循环的,此时我们要创建一个头节点,就需要让他自己指向自己
newnode->next = newnode->prev = newnode;
return newnode;
}
ln* LNInit()
{
ln* phead = LNBuyNode(-1);//这里初始化需要给它一个值,我们就设置具有辨识度的一个值给它
return phead;
}
test.c
void test()
{
ln* plist = NULL;
plist = LNInit();
}
int main()
{
test();
return 0;
}
测试双向链表的初始化
3.2 双向链表的打印
思路:
这里我们直接利用循环遍历链表来进行打印
ListNode.c
void* LNPrint(ln* phead)
{
//这里我们创建一个指针变量指向头节点的下一个节点
ln* pcur = phead->next;
while (pcur != phead)//这里我们不需要打印头节点,也防止死循环
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
这里我们并没有插入数据,所以链表此时是空链表
3.3 双向链表的尾插
思路:
这里我们要进行尾插,那我们就先将新节点指向下一个节点的指针指向头节点,将新节点指向前一个节点的指针指向链表的尾节点,然后再将链表的尾节点指向下一个节点的指针指向新节点,最后将头节点指向前一个节点的指针指向新节点。这样双向链表的尾插就完成了。
这里可能有些疑惑,我们之前在实现单链表尾插的时候,都是传的二级指针,这里为什么会传一级指针,因为我们在插入数据之前,链表必须初始化到只有一个节点的情况,我们不需要改变哨兵位的地址,所以这里我们只需要传一级指针即可。
代码:
ListNode.c
//这里我们在单链表的时候都是传递二级指针,这里为什么传一级指针
//因为插入数据之前,链表必须初始化到只有一个节点的情况
//我们不需要改变哨兵位的地址,所以这里我们传一级指针即可
void* LNPushBack(ln* phead, LNDataType x)
{
ln* newnode = LNBuyNode(x);//创建新节点
newnode->next = phead;//将新节点指向下一个节点的指针指向头节点
newnode->prev = phead->prev;//将新节点指向前一个节点的指针指向头节点的尾节点
phead->prev->next = newnode;//将链表的尾节点的指向下一个节点的指针指向新节点
phead->prev = newnode;//将头节点指向前一个节点的指针指向新节点
}
测试双向链表打印方法和双向链表尾插方法
3.4 双向链表的头插
思路:
这里我们首先要明白,这里的头插,不是指在哨兵位的前面进行插入,而是在哨兵位指向的下一个节点的前面进行插入。所以我们要进行头插,首先得将新节点的next指针指向头节点的下一个节点,将新节点的prev指针指向头节点,然后将头节点指向的下一个节点的prev指针指向新节点,再将头节点的next指针指向新节点。
代码:
ListNode.c
void* LNPushFront(ln* phead, LNDataType x)
{
ln* newnode = LNBuyNode(x);//创建新节点
newnode->next = phead->next;//将新节点指向下一个节点的指针指向头节点的下一个节点
newnode->prev = phead;//将新节点指向前一个节点的指针指向头节点
phead->next->prev = newnode;//将头节点的下一个节点指向前一个节点的指针指向新节点
phead->next = newnode;//将头节点指向下一个节点的指针指向新节点
}
测试双向链表的头插方法
3.5 双向链表的尾删
思路:
这里进行尾删,我们首先创建一个指针变量指向链表的尾节点,然后将尾节点指向的前一个节点指向下一个节点的指针指向头节点,然后再将头节点指向前一个节点的指针指向尾节点指向的前一个节点,最后释放掉尾节点,并将他置为空这样双向链表的尾删就完成了。
代码:
ListNode.c
void* LNPopBack(ln* phead)
{
assert(phead && phead->next != phead);//这里判断phead是否为空指针,并且链表是否为空链表
//创建一个指针指向链表的尾节点
ln* pcur = phead->prev;
pcur->prev->next = phead;//将链表尾节点指向的前一个节点指向的下个节点的指针指向头节点
phead->prev = pcur->prev;//将头节点指向前一个节点的指针指向尾节点指向的前一个节点
free(pcur);//释放掉pcur
pcur = NULL;//防止pcur变成野指针
}
测试双向链表的尾删
3.6 双向链表的头删
思路:
我们想要进行头删,首先我们要创建一个指针变量指向头节点的下一个节点,然后将头节点的下一个节点的下一个节点指向前一个节点的指针指向头节点,然后再头节点指向下一个节点的指针指向头节点的下一个节点的下一个节点,最后释放掉头节点指向的下一个节点,并将它置空,这样我们双向链表的头删就完成了。
代码:
ListNode.c
void* LNPopFront(ln* phead)
{
assert(phead && phead->next!=phead);//这里判断phead是否为空指针,并且链表是否为空链表
//创建一个指针指向头节点指向的下一个节点
ln* pcur = phead->next;
pcur->next->prev = phead;//将头节点的下一个节点指向的下一个节点指向前一个节点的指针指向头节点
phead->next = pcur->next;//将头节点指向下一个节点的指针指向头节点下一个节点的下一个节点
free(pcur);//释放掉pcur
pcur = NULL;//防止pcur变成野指针
}
测试双向链表的头删
3.7 双向链表的查找
思路:
我们直接遍历链表进行查找
代码:
ListNode.c
void* LNFind(ln* phead, LNDataType x)
{
//创建一个指针变量指向头节点的下一个节点
ln* pcur = phead->next;
while (pcur != phead)//遍历查找
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}//没有找到返回空指针
return NULL;
}
测试双向链表查找
3.8 双向链表在指定位置之后插入数据
思路:
这里我们将新节点指向下一个节点的指针指向pos指向的下一个节点,再将新节点指向前一个节点的指针指向pos,最后将pos指向下一个节点指向前一个节点的指针指向新节点,将pos指向下一个节点的指针指向新节点。这样我们双向链表在指针位置之后插入数据就完成了。
代码:
ListNode.c
void* LNInsert(ln* pos, LNDataType x)
{
assert(pos);//判断pos是否是空指针
//创建新节点
ln* newnode = LNBuyNode(x);
newnode->next = pos->next;//将新节点指向下一个节点的指针指向pos的下一个节点
newnode->prev = pos;//将新节点指向前一个节点的指针指向pos
pos->next->prev = newnode;//将pos指向的下一个节点指向前一个节点的指针指向新节点
pos->next = newnode;//将pos指向下一个节点的指针指向新节点
}
测试双向链表在指定位置之后插入数据
3.9 双向链表删除指定位置数据
思路:
我们直接将pos的前一个节点指向下一个节点的指针指向pos的下一个节点,再将pos的下一个节点指向前一个节点的指针指向pos的前一个节点,最后释放掉pos,这样双向链表删除指定位置数据就完成了
代码:
ListNode.c
void* LNErase(ln* pos)
{
assert(pos);//判断pos是否为空指针
pos->prev->next = pos->next;//将pos的前一个节点指向下一个节点的指针指向pos的下一个节点
pos->next->prev = pos->prev;//将pos的下一个节点指向前一个节点的指针指向pos的前一个节点
free(pos);//释放掉pos
pos = NULL;//pos置为空
}
测试双向链表删除指定位置数据
3.10 双向链表的销毁
思路:
我们遍历链表,将链表里面的有效节点一个一个的删除就可以了,最后在删除掉头节点
代码:
ListNode.c
void* LNDestroy(ln* phead)
{
assert(phead && phead->next != phead);//这里判断phead是否为空指针,并且链表是否为空链表
//创建一个指针变量指向头节点的下一个节点
ln* next = phead->next;
while (next != phead)
{
ln* pcur = next;
free(pcur);
next = next->next;
}
free(phead);//释放掉头节点
}
测试双向链表销毁
4. 删除指定位置数据和双向链表销毁的问题
在这里,我们发现我们在删除指定位置数据和双向链表销毁的时候传的也是一级指针,实际上删除指定位置数据和链表销毁参数理论上要传二级指针,因为我们需要让形参的改变影响实参,但是我们为了保持接口的一致性才传的一级指针。
传一级指针存在的问题,当形参phead置为NULL后,实参plist不会被修改为NULL,因此结果方法就是 我们在调用完方法之后手动将实参置为NULL。
5. 顺序表和单链表的优缺点
补充前面的单链表与顺序的对比:
|-------------|------------------|-----------------|
| 不同点 | 顺序表 | 链表(单链表) |
| 存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
| 随机访问 | 支持O(1) | 不支持O(N) |
| 任意位置插入和删除数据 | 可能需要移动元素,效率低O(N) | 只需要修改指针指向 |
| 空间 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
| 应用场景 | 元素高校存储+频繁访问 | 任意位置插入和删除频繁 |
6. 完整代码
6.1 ListNode.h
//包含之后可能用到的库函数头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LNDataType;//类型重命名,方便后续替换
typedef struct ListNode//类型重命名,简化写法
{
LNDataType data;//数据
struct ListNode* next;//指向下一个节点的指针
struct ListNode* prev;//指向前一个节点的指针
}ln;
//链表初始化
ln* LNInit();
//链表的打印
void* LNPrint(ln* phead);
//链表尾插
void* LNPushBack(ln* phead, LNDataType x);
//链表头插
void* LNPushFront(ln* phead, LNDataType x);
//链表尾删
void* LNPopBack(ln* phead);
//链表头删
void* LNPopFront(ln* phead);
//链表查找
void* LNFind(ln* phead, LNDataType x);
//链表在指定位置之后插入数据
void* LNInsert(ln* pos, LNDataType x);
//链表删除指定位置数据
void* LNErase(ln* pos);
//链表销毁
void* LNDestroy(ln* phead);
6.2 ListNode.c
#include"ListNode.h"
ln*LNBuyNode(LNDataType x)
{
ln* newnode = (ln*)malloc(sizeof(ln));
if(newnode == NULL)
{
perror("malloc");
exit(1);
}
newnode->data = x;
//双向链表是循环的,此时我们要创建一个头节点,就需要让他自己指向自己
newnode->next = newnode->prev = newnode;
return newnode;
}
ln* LNInit()
{
ln* phead = LNBuyNode(-1);//这里初始化需要给它一个值,我们就设置具有辨识度的一个值给它
return phead;
}
void* LNPrint(ln* phead)
{
//这里我们创建一个指针变量指向头节点的下一个节点
ln* pcur = phead->next;
while (pcur != phead)//这里我们不需要打印头节点,也防止死循环
{
printf("%d->",pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//这里我们在单链表的时候都是传递二级指针,这里为什么传一级指针
//因为插入数据之前,链表必须初始化到只有一个节点的情况
//我们不需要改变哨兵位的地址,所以这里我们传一级指针即可
void* LNPushBack(ln* phead, LNDataType x)
{
ln* newnode = LNBuyNode(x);//创建新节点
newnode->next = phead;//将新节点指向下一个节点的指针指向头节点
newnode->prev = phead->prev;//将新节点指向前一个节点的指针指向头节点的尾节点
phead->prev->next = newnode;//将链表的尾节点的指向下一个节点的指针指向新节点
phead->prev = newnode;//将头节点指向前一个节点的指针指向新节点
}
void* LNPushFront(ln* phead, LNDataType x)
{
ln* newnode = LNBuyNode(x);//创建新节点
newnode->next = phead->next;//将新节点指向下一个节点的指针指向头节点的下一个节点
newnode->prev = phead;//将新节点指向前一个节点的指针指向头节点
phead->next->prev = newnode;//将头节点的下一个节点指向前一个节点的指针指向新节点
phead->next = newnode;//将头节点指向下一个节点的指针指向新节点
}
void* LNPopBack(ln* phead)
{
assert(phead && phead->next!=phead);//这里判断phead是否为空指针,并且链表是否为空链表
//创建一个指针指向链表的尾节点
ln* pcur = phead->prev;
pcur->prev->next = phead;//将链表尾节点指向的前一个节点指向的下个节点的指针指向头节点
phead->prev = pcur->prev;//将头节点指向前一个节点的指针指向尾节点指向的前一个节点
free(pcur);//释放掉pcur
pcur = NULL;//防止pcur变成野指针
}
void* LNPopFront(ln* phead)
{
assert(phead && phead->next!=phead);//这里判断phead是否为空指针,并且链表是否为空链表
//创建一个指针指向头节点指向的下一个节点
ln* pcur = phead->next;
pcur->next->prev = phead;//将头节点的下一个节点指向的下一个节点指向前一个节点的指针指向头节点
phead->next = pcur->next;//将头节点指向下一个节点的指针指向头节点下一个节点的下一个节点
free(pcur);//释放掉pcur
pcur = NULL;//防止pcur变成野指针
}
void* LNFind(ln* phead, LNDataType x)
{
//创建一个指针变量指向头节点的下一个节点
ln* pcur = phead->next;
while (pcur != phead)//遍历查找
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}//没有找到返回空指针
return NULL;
}
void* LNInsert(ln* pos, LNDataType x)
{
assert(pos);//判断pos是否是空指针
//创建新节点
ln* newnode = LNBuyNode(x);
newnode->next = pos->next;//将新节点指向下一个节点的指针指向pos的下一个节点
newnode->prev = pos;//将新节点指向前一个节点的指针指向pos
pos->next->prev = newnode;//将pos指向的下一个节点指向前一个节点的指针指向新节点
pos->next = newnode;//将pos指向下一个节点的指针指向新节点
}
void* LNErase(ln* pos)
{
assert(pos);//判断pos是否为空指针
pos->prev->next = pos->next;//将pos的前一个节点指向下一个节点的指针指向pos的下一个节点
pos->next->prev = pos->prev;//将pos的下一个节点指向前一个节点的指针指向pos的前一个节点
free(pos);//释放掉pos
pos = NULL;//pos置为空
}
void* LNDestroy(ln* phead)
{
assert(phead && phead->next != phead);//这里判断phead是否为空指针,并且链表是否为空链表
//创建一个指针变量指向头节点的下一个节点
ln* next = phead->next;
while (next != phead)
{
ln* pcur = next;
free(pcur);
next = next->next;
}
free(phead);//释放掉头节点
}