一、链表的分类
链表的结构⾮常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:
带头:指的是链表中有哨兵位节点,该节点不存储任何有效数据,属于无效节点,通过这个无效节点当头节点 让我们某些方面使用会有一些优势
双向:有两个指针分别指向前一个节点和后一个节点,可以从两个方向遍历,从前往后从后往前。
循环:尾节点不在指向NULL而是指向头节点。
我们实际中最常⽤的两种结构: 单链表 和 双向带头循环链表
1. ⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结 构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。
我们上篇博客讲了单链表,它其实就是不带头单向不循环(简称:单链表)
那么本节博客讲双链表,带头双向循环(简称 :双向链表)
二、双向链表的结构
双向链表节点中有两个指针prev和next,分别指向其前驱指针和后继指针。
链表节点的组成:数据+指向下一个节点的指针+指向前一个节点的指针。
注意:这⾥的“带头”跟前⾯我们说的“头节点”是两个概念,实际前⾯的在单链表阶段称呼不严 谨,带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨 的” “哨兵位”存在的意义: 遍历循环链表避免死循环。
三、双向链表的基本操作
老规矩依旧三个文件,一个头文件,两个源文件
我们要在双向链表实现以下操作:
1.定义双向链表
2.初始化双向链表
3.尾插
4.头插
5.尾删
6.头删
7.指定位置之后插入数据
8.删除节点
9.打印
10.查找
11.销毁
1.定义双向链表
typedef int LTDataType;
typedef struct ListNode
{
LTDataType date;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
date用来存储数据 ,next指针保存下个节点,prev指针保存前一个节点。
2.各个函数的声明
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType date;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
//声明双向链表提供的方法
//初始化
LTNode* LTInit();
//插入数据之前,链表必须要初始化到只有一个头节点的情况
//不能改变哨兵位的地址,因此一级即可
//打印
void LTPrint(LTNode* phead);
//尾插
void LTPushuBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErease(LTNode* pos);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//销毁
LTNode* LTDesTroy(LTNode* phead);
3.双向链表的的初始化及节点的申请
.c文件中实现(记得包含头文件)
//申请节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->date = x;
node->next = node->prev = node;
return node;
}
//初始化
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
我们哨兵位不需要有效数据所以我们传个-1就可以了。
双向链表不能指向NULL,所以我们需要申请一个节点作为哨兵为,让我们的哨兵位自己指向自己。
可能会有人要疑惑了,为什么不能指向空呢,为什么又要自己指向自己呢?
双向链表为空时,和单链表不一样,创建双向链表时必须要初始化,初始化为一个哨兵位,我们才能往里面插入数据。
如果指向NULL那还怎么循环起来,所以我们让它指向自己,这样就可以循环起来了。
4.尾插
无论是我们的尾插还是头插,我们都不影响哨兵位,头插是在哨兵位后面插入,哨兵位永远不变。
现在我们先画图分析一下尾插如何插入再来写代码
插入数据肯定是需要申请节点的,由图可见我们申请好了的节点叫newnode。
根据我们上面的图可以看到我们现在要让我们newnode的前驱指针要指向d3,那怎么拿到d3呢,我们哨兵位的前驱指针指向的不就是d3吗?newnode的后继指针要指向下一个节点,下一个节点不就哨兵位吗?直接让newnode的下一个节点指向哨兵即可,接着让我们d3的下一个节点指向我们新申请的尾节点newnode,再让我们哨兵位的前一个指针指向新申请好了的尾节点newnode。
现在来写代码
//尾插
void LTPushuBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
我们传进来的哨兵位要确保有效,不能为空,如果为空就不是一个有效的双向链表,所以我们要加个断言。
注意:这是因为我们的哨兵位不能被删除,节点的地址也不能被改变,所以传一级即可
5.头插
画图分析一下再来写代码
插入数据肯定是需要申请节点的,由图可见我们申请好了的节点叫newnode。
由图可见我们头插就是在哨兵位的前面插入,我们哨兵指向的下一个节点是头节点,现在让我们的newnode的前一个指针指向哨兵位,我们直接指向哨兵位就可以了,然后newnode的下一个节点要指向我们的头节点,头节点就是哨兵位的下一个节点,直接指向即可,接着我们要让d1指向我们新申请好的头节点newnode,最后让我们哨兵位指向新申请好的头节点newnode
我们来写代码:
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
}
记得加断言判断双向链表是否有效
6.打印
打印双向链表其实就是通过while循环来进行打印,由图可见我们定义了一个指针pcur来遍历我们的双向链表,那么while循环的结束条件是什么呢?,我们的pcur不能等于哨兵位,前面我们说哨兵位里面的数据是无效的,所以不能等于,那么条件就是如果等于我们的哨兵位就跳出循环。
来写一下代码:
//打印
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->date);
pcur = pcur->next;
}
printf("\n");
}
7.尾删
先画图分析一下:
根据上面的图可以看到我们通过定义一个指针del来存储d3这个尾节点,然后让我们d2的下一个节点指向我们的哨兵位,通过del指向前一个节点的nex就可以拿到我们d2的下一个节点了,直接指向哨兵位即可,接着让我们哨兵位的前一个指针指向我们d2的节点,通过del的前一个指针就可以拿到d2,最后把尾节点释放了。
现在来写代码
/尾删
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;
}
8.头删
老规矩先画图分析
由图可见我们先把头节点保存在一个指针del里面,我们让哨兵位的下一个节点不在指向我们的头节点了,而是指向我们d2,通过头节点的下一个节点就可以拿到d2,此时d2的前一个节点不在指向头节点而是指向哨兵位,最后把头节点释放。
来看代码
//头删
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
记得加断言判断链表,链表必须有效且链表不能为空(只有一个哨兵位)
9.查找
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->date == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
查找函数和打印函数非常相似,都是通过循环进行遍历,循环的判断条件也是一样的。
如果找到了就返回找到的节点,没找到则返回NULL
10.在pos位置之后插入数据
依旧画图分析
申请了一个节点为newnode
上图所示让我们的newnode的下一个节点指向我们d3这个节点,pos的下一个节点就是d3,直接指向即可,然后还要让newnode的前一个指针指向pos,直接指向就可以了,接着让我们pos的下一个节点的前驱指针指向newnode,最后让pos指向申请好的newnode的节点。
来写一下代码
//在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;
}
注意:这里我们传过来的pos要为有效节点,所以要加个断言
11.删除pos节点
依旧画图分析
如图可以看到我们d3的前一个指针不在指向pos而是指向pos的前一个指针,通过我们pos指向的下一个节点的前驱指针就可以找到d3,接着pos的前一个指针的next指针也不在指向pos而是指向pos的下一个节点,最后把pos这个节点释放掉了。
//删除pos节点
void LTErease(LTNode* pos)
{
//pos理论上来说不能为phead,但是没有参数phead,无法增加校验
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
注意:这里我们传过来的pos要为有效节点,所以要加个断言
12.销毁
//销毁
LTNode* 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;
}
通过while循环遍历挨个释放节点然后置为NULL,这里要定义一个指针用来保存下一个节点,循环条件也是不等于哨兵位,跳出循环的时候说明已经等于哨兵位了,接着释放哨兵位。
注意:
销毁和删除一定要手动置为NULL
四、双向链表所有的代码
.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType date;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
//声明双向链表提供的方法
//初始化
LTNode* LTInit();
//插入数据之前,链表必须要初始化到只有一个头节点的情况
//不能改变哨兵位的地址,因此一级即可
//打印
void LTPrint(LTNode* phead);
//尾插
void LTPushuBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErease(LTNode* pos);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//销毁
LTNode* LTDesTroy(LTNode* phead);
.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
//打印
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->date);
pcur = pcur->next;
}
printf("\n");
}
//申请节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->date = x;
node->next = node->prev = node;
return node;
}
//初始化
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
//尾插
void LTPushuBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
}
//尾删
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;
}
//头删
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->date == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//在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;
}
//删除pos节点
void LTErease(LTNode* pos)
{
//pos理论上来说不能为phead,但是没有参数phead,无法增加校验
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
//销毁
LTNode* 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;
}
测试文件.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
void ListTest01()
{
LTNode* plist = LTInit();//初始化
//测试尾插
LTPushuBack(plist, 1);
LTPrint(plist);//打印
LTPushuBack(plist, 2);
LTPrint(plist);//打印
LTPushuBack(plist, 3);
LTPrint(plist);//打印
LTPushuBack(plist, 4);
LTPrint(plist);//打印
//测试头插
LTPushFront(plist, 9);
LTPrint(plist);
LTPushFront(plist, 8);
LTPrint(plist);
//测试尾删
LTPopBack(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
//测试头删
LTPopFront(plist);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
//查找
LTNode* find = LTFind(plist, 4);
if (find == NULL)
printf("没找到\n");
else
printf("找到了\n");
//测试指定位置之后插入数据
LTInsert(find, 9);
LTPrint(plist);
//测试删除pos节点
LTErease(find);
LTPrint(plist);
find = NULL;
//销毁
LTDesTroy(plist);
plist = NULL;
}
int main()
{
ListTest01();
}