前言
在讲双向链表之前,我会先总结一下前面的知识点,如需直接看双向链表的,可以直接跳转到双向链表的实现去阅读~~
链表的分类
在上一篇的8道算法题,我提到了用哨兵位可以很好地进行插入,这个哨兵位就是头结点!还有在解决约瑟夫问题时,我提到了使用循环链表的概念,循环链表就是头尾相连,形成一个环。
链表的分类有这几种情况:带不带头(头结点==哨兵位),单向还是双向(就是一个节点只能找到下一个节点的就是单向,如果既能找到上一个节点又能找到下一个节点就是双向),循环还是不循环(头尾相连的就是循环,头尾不相连的就是不循环)
所以链表一共有八大类,那我们回顾一下什么是单链表,单链表实际上就是不带头单向不循环的链表,这里我要讲的双向链表实际上是带头双向循环的链表,只要我们会这两个链表的实现,其他的链表实现也是很简单的~~
链表的优势
在C语言进阶的第一篇文章中,我带大家实现了动态顺序表,但是动态顺序表还是有存在空间浪费的出现,举个例子,我一共有101个数据需要保存,但是顺序表在第一百零一的时候会进行2倍或3倍扩容,假设扩2倍,那就是变成200容量,这时候就有99个空间浪费了,链表就可以实现一个数据一个数据的申请节点,不会有空间的浪费,但是单链表有一个缺陷,尾插尾删等操作时需要遍历所有节点导致效率不高,这时候我们就可以使用双向链表来大幅减少这种遍历的出现,也就是我会在下面提到的链表结构。
但是链表也有一个缺点,在数据量少的情况下,链表其实浪费的空间可能更大,因为链表结构是需要带一个或者两个指针的~~
任何事物都有两面性,我们需要根据实际情况来选择合适的数据结构来解决问题才是最perfect!!!
双向链表的实现
双向链表的含义
双向链表是带头结点(哨兵位),每一个结点都能找到前一个和后一个节点,并且头尾是相连的。形成带头双向循环的链表,双向链表就是它的简称。
那我们来定义双向链表的结构体:
typedef int ListDataType;
typedef struct ListNode
{
ListDataType data;
struct ListNode* next;
struct ListNode* prev;
}ListNode;
初始化和创建新节点
注意双向链表是一个带头的循环的链表,带头意味着我们在初始化要创建一个头结点,那头结点的两个指针怎么处理?由于这是双向链表,需要头尾相连,因此我们将头节点的两个指针都指向自己!既然如此,我们就写一个函数来创建新节点,让每个新节点的指针开始都指向自己~~
//创建新节点
ListNode* CreatNewnode(ListDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc fail"); exit(1);
}
newnode->data = x;
newnode->next = newnode->prev = newnode;
return newnode;
}
注意了,由于创建新节点的函数我们需要传入一个值,那我们的头结点就随便传入一个值,但是我们自己要知道头结点知识一个站岗的,不会存放有效值。
会不会有人会问,为什么不专门写一个函数来创建头结点,你可以自己尝试,我觉得没有必要,多写一个和创建新节点的代码感觉很浪费也没有很大必要~~
//初始化
void ListInit(ListNode** pphead)
{
*pphead = CreatNewnode(-1);
}
头指针的传参问题
我们要知道初始化链表的时候就创建好头结点了,头指针就是指向这个头结点,所以我们不需要改变头结点,它是一个放哨的,与链表是共存亡的,所以我们一般情况下传一级指针就可以了~~
简单来说:头指针是指向头结点的,只要头结点还存在,头指针就没有必要发生改变~~
尾插
尾插操作,我们需要改变三个节点,分别是newnode,head,还有原来的尾节点head->prev。
//尾插
void ListPushBack(ListNode* phead, ListDataType x)
{
ListNode* newnode = CreatNewnode(x);
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
这里建议大家先将newnode 的next和prev两个指针先连接好,毕竟改变newnode的指向不会影响到另外两个节点,这时候我们就要考虑剩下两个节点怎么连接,为了避免找不到d3这个节点,所以我先改变d3这个节点,再改变head的节点~~
头插
头插,我们需要改变三个节点,newnode,head,d1;我们还是先改变newnode(不会对另外两个产生影响),再改变d1(防止改变head的时候找不到d1),最后改变head。
//头插
void ListPushFront(ListNode* phead, ListDataType x)
{
ListNode* newnode = CreatNewnode(x);
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
}
打印
注意头结点存放的值是无效的,所以从头结点的下一个结点开始打印,由于链表是循环的,所以当回到头结点的时候就要停止打印(循环停止的条件)
//打印
void ListPrint(ListNode* phead)
{
ListNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
尾删
为了更好的进行删除操作,我使用一个变量保存要删除的节点,便于要删除的节点的前一个节点与头结点进行连接~~
//尾删
void ListPopBack(ListNode* phead)
{
assert(phead && phead->next != phead);
ListNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
头删
注意这里的头删是删除第一个有效的节点,不是我们的哨兵位!!!
还是一样,使用一个变量保存要删除的节点
//头删
void ListPopFront(ListNode* phead)
{
assert(phead && phead->next != phead);
ListNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
查找
从第一个有效节点开始遍历链表进行查找即可~~
//查找
ListNode* ListFind(ListNode* phead, ListDataType x)
{
ListNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
写这一个查找函数是为了方便我们后续指定位置的相关操作~~
还有要注意如果找不到就会放回NULL,所以后面的指定位置操作是记得判断pos是否有效!!!
指定位置删除
需要改变三个节点pos,pos->prev,pos->next这三个节点~~
//指定位置删除
void ListPopPos(ListNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
这里要注意了pos 如果就是头指针的话,是不可以进行删除操作的,由于我没有传头指针,所以没有判断这个条件~~
删除指定位置之前的数据
这里要注意,指定位置之前需要有有效的节点才能进行删除!!!所以这里你可以判断一下,传入头指针是为了更好地进行判断处理~~
//删除指定位置前的数据
void ListPopPosFront(ListNode* phead, ListNode* pos)
{
assert(phead);
assert(pos && pos->prev != phead);
ListNode* del = pos->prev;
del->prev->next = pos;
pos->prev = del->prev;
free(del);
del = NULL;
}
删除指定位置之后的数据
这里要注意,指定位置之后需要有有效的节点才能进行删除!!!所以这里你可以判断一下,传入头指针是为了更好地进行判断处理~~
//删除指定位置之后的数据
void ListPopPosAfter(ListNode* phead, ListNode* pos)
{
assert(phead);
assert(pos && pos->next != phead);
ListNode* del = pos->next;
del->next->prev = pos;
pos->next = del->next;
free(del);
del = NULL;
}
在指定位置之前插入数据
//在指定位置之前插入数据
void ListPushPosFront(ListNode* pos, ListDataType x)
{
assert(pos);
ListNode* newnode = CreatNewnode(x);
newnode->next = pos;
newnode->prev = pos->prev;
pos->prev->next = newnode;
pos->prev = newnode;
}
在指定位置之后插入数据
//在指定位置之后插入数据
void ListPushPosAfter(ListNode* pos, ListDataType x)
{
assert(pos);
ListNode* newnode = CreatNewnode(x);
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
销毁链表
销毁链表我们需要传入头结点的二级指针了,因为头结点也需要进行销毁,头指针要置为NULL
但是这里我却使用了一级指针,是为了保持接口的一致性~~ 因为上面的函数除了初始化都是传一级指针,也是为了方便别人来使用我们的接口函数,减少使用者的记忆负担~~
//销毁链表
void ListDestroy(ListNode* phead)
{
assert(phead);
ListNode* pcur = phead->next;
while (pcur != phead)
{
ListNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
如果你还想让接口函数更加完美,我们可以改变一下初始化函数的:
//初始化
ListNode* ListInit()
{
ListNode* phead = CreatNewnode(-1);
return phead;
}
小结
在实现双向链表的时候,我们可以通过画图理解的方式进行理解和书写相应的代码~~
封装函数
List.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int ListDataType;
typedef struct ListNode
{
ListDataType data;
struct ListNode* next;
struct ListNode* prev;
}ListNode;
//初始化
void ListInit(ListNode** pphead);
//打印
void ListPrint(ListNode* phead);
//插入
void ListPushBack(ListNode* phead, ListDataType x);
void ListPushFront(ListNode* phead, ListDataType x);
//删除
void ListPopBack(ListNode* phead);
void ListPopFront(ListNode* phead);
//查找
ListNode* ListFind(ListNode* phead, ListDataType x);
//指定位置删除
void ListPopPos(ListNode* pos);
//删除指定位置前的数据
void ListPopPosFront(ListNode* phead, ListNode* pos);
//删除指定位置之后的数据
void ListPopPosAfter(ListNode* phead, ListNode* pos);
//在指定位置前插入数据
void ListPushPosFront(ListNode* pos, ListDataType x);
//在指定位置之后插入数据
void ListPushPosAfter(ListNode* pos, ListDataType x);
//销毁链表
void ListDestroy(ListNode* phead);
List.c
#include "List.h"
//创建新节点
ListNode* CreatNewnode(ListDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc fail"); exit(1);
}
newnode->data = x;
newnode->next = newnode->prev = newnode;
return newnode;
}
//初始化
void ListInit(ListNode** pphead)
{
*pphead = CreatNewnode(-1);
}
//尾插
void ListPushBack(ListNode* phead, ListDataType x)
{
ListNode* newnode = CreatNewnode(x);
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
//打印
void ListPrint(ListNode* phead)
{
ListNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//头插
void ListPushFront(ListNode* phead, ListDataType x)
{
ListNode* newnode = CreatNewnode(x);
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
}
//尾删
void ListPopBack(ListNode* phead)
{
assert(phead && phead->next != phead);
ListNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
//头删
void ListPopFront(ListNode* phead)
{
assert(phead && phead->next != phead);
ListNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
//查找
ListNode* ListFind(ListNode* phead, ListDataType x)
{
ListNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//指定位置删除
void ListPopPos(ListNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
//删除指定位置前的数据
void ListPopPosFront(ListNode* phead, ListNode* pos)
{
assert(phead);
assert(pos && pos->prev != phead);
ListNode* del = pos->prev;
del->prev->next = pos;
pos->prev = del->prev;
free(del);
del = NULL;
}
//删除指定位置之后的数据
void ListPopPosAfter(ListNode* phead, ListNode* pos)
{
assert(phead);
assert(pos && pos->next != phead);
ListNode* del = pos->next;
del->next->prev = pos;
pos->next = del->next;
free(del);
del = NULL;
}
//在指定位置前插入数据
void ListPushPosFront(ListNode* pos, ListDataType x)
{
assert(pos);
ListNode* newnode = CreatNewnode(x);
newnode->next = pos;
newnode->prev = pos->prev;
pos->prev->next = newnode;
pos->prev = newnode;
}
//在指定位置之后插入数据
void ListPushPosAfter(ListNode* pos, ListDataType x)
{
assert(pos);
ListNode* newnode = CreatNewnode(x);
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
//销毁链表
void ListDestroy(ListNode* phead)
{
assert(phead);
ListNode* pcur = phead->next;
while (pcur != phead)
{
ListNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
双向链表完结撒花~~