目录
一.双向链表的定义
之前有一篇博客写着单链表详解,今天学习双向链表。
单向链表特点:
- 我们可以轻松的到达下一个节点,但是回到前一个节点是很难的。
- 只能从头遍历到尾或者从尾遍历到头(一般从头到尾)
双向链表特点:
- 每次在插入或删除某个节点时, 需要处理四个节点的引用, 而不是两个. 实现起来要困难一些
- 相对于单向链表, 必然占用内存空间更大一些.
- 既可以从头遍历到尾, 又可以从尾遍历到头
双向链表的定义:
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。下图为双向链表的结构图。
从上中可以看到,双向链表中各节点包含以下 3 部分信息:
指针域:用于指向当前节点的直接前驱节点;
数据域:用于存储数据元素。
指针域:用于指向当前节点的直接后继节点;
双向循环链表的定义:
它的特征为:带头+双向+循环。
双向链表也可以进行首尾连接,构成双向循环链表,如下图所示
在创建链表时,只需要在最后将收尾相连即可(创建链表代码中已经标出)。其他代码稍加改动即可。 如图:双向链表的head结点就是哨兵位头节点,它用于指向第一个结点地址,而每个结点的后指针都相互指向下一个结点的前指针。
带头:就是哨兵位头节点,哨兵位头节点是用动态申请(malloc、calloc、realloc)下的一个节点,它的里面绝不存储有效数据,即它不可以作为节点进行存值,但可以存储节点的地址 ,它最大的优势就是让链表的起始指针永远指向哨兵位头节点,由哨兵位头节点去插入或删除节点,这样做就不会修改链表起始指针,也就用不到二级指针;而单链表中没有哨兵头节点,所以常常要改变起始指针的指向,使用二级指针接收实参,才能去改变实参!!!
双向:单链表只有一个指针,所以一个节点只能链接一个地址;而双链表的节点中有两个指针地址,既可以链接它前面的节点地址,也可以链接它后面的节点地址,十分方便。
循环:便是双链表中头尾使用环去链接,链表的最后一个节点不会指向NULL,而是与头节点相互链接,便组成了循环。
双链表的节点结构用 C 语言实现为:
//双向链表的结构设计
typedef struct Dlist
{
int date;//数据域:保存有效值
struct Dlist* next;//向后指针域:保存下一个节点的地址(如果没有,指向NULL)
struct Dlist* prev;//向前指针域:保存上一个节点的地址(如果没有,指向NULL)
}Dlist;
二.双向链表的创建
1.双向链表结构
共有三部分:一个数据域,两个指针域
//双向链表的结构设计
typedef struct Dlist
{
int date;//数据域:保存有效值
struct Dlist* next;//向后指针域:保存下一个节点的地址(如果没有,指向NULL)
struct Dlist* prev;//向前指针域:保存上一个节点的地址(如果没有,指向NULL)
}Dlist;
2.双向链表的头文件与函数定义
//双向链表操作函数声明:
//初始化
Dlist* DlistInit();
//申请节点
Dlist* BuyNode(int val);
//头插
void DListPushFront(Dlist* phead,int x);
//尾插:
void DListPushBack(Dlist* phead, int x);
//在pos位置前插
void DListInsert(Dlist* pos,int x);
//链表在指定位置pos处删除节点
void DListErase(Dlist* pos);
//头删
void DListPopFront(Dlist* phead);
//尾删
void DListPopBack(Dlist* phead);
//求链表的长度函数
size_t DListSize(Dlist* phead);
//查找(如果值允许重复,我们找val值第一次出现的位置,将其地址返回出来)
Dlist* DListFind(Dlist* phead,int x);
//释放空间
void DListDestory(Dlist* phead);
//打印
void DListPrint(Dlist* phead);
三.双向带头循环链表的实现
1.初始化
//初始化
void DlistInit(struct Dlist*phead)
{
Dlist* Guard = (struct Dlist*)malloc(sizeof(struct Dlist));//哨兵位头结点
if (Guard == NULL)
{
perror("malloc fail:");
return -1;
}
Guard->next = Guard;
Guard->prev = Guard;
return Guard;
}
初始化便是要创建哨兵位头节点,因为哨兵位头节点不存储有效数据,且该开始两指针并不知道指向谁,所以根据双向链表循环的特性,就让该结点的两个指针自己指向自己。
2.动态申请节点
//申请节点
Dlist* BuyNode(int val)
{
Dlist* newnode = (struct Dlist*)malloc(sizeof(struct Dlist));
if (newnode == NULL)
{
perror("malloc fail:");
return -1;
}
newnode->date = val;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
因为刚开辟好的节点,还没有进行链接,所以都先置为空。
3.打印双向链表函数
打印链表是遍历每一个节点,因为双向循环链表的尾节点不指向NULL,头尾相连,所以需要新的限制条件——哨兵位头节点,哨兵位头节点不存储有效数据,所以遍历指针只要遇到哨兵节点就会停止,即打印完毕!
//打印双链表
void DListPrint(Dlist* phead)
{
assert(phead);
Dlist* cur = phead->next;
printf("guard<==>\n");
while (cur != phead)
{
printf("%d<==>", cur->date);
cur = cur->next;
}
printf("\n");
}
4.尾部插入节点
//尾插:
void DListPushBack(Dlist* phead, int x)
{
assert(phead);
Dlist* tail = phead->prev;//找到尾 尾节点指针处在哨兵位头节点的prev
Dlist* newnode = BuyDLTNode(x);//新节点
//phead tail newnode
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
}
测试
注意:phead就等价于哨兵位头节点!
5.头插函数
//头插
void DListPushFront(Dlist* phead, int x)
{
assert(phead);
Dlist* first = phead->next;
Dlist* newnode = BuyNode(x);
newnode->next = first;
first->prev = newnode;
newnode->prev = phead;
phead->next = newnode;
}
测试
6.尾删函数
关于头尾部的删除,需要进行检查链表是否为空,所以,我封装了一个检查函数:
//暴力检查
bool DListEmpty(Dlist* phead)
{
assert(phead);
return phead->next == phead;
}
注:bool类型的返回值为true、false两种,若是phead->next==phead说明链表为空,那么assert(!DListEmpty(phead)==assert(NULL);函数就报错,且停止下面语句的执行;若是 phead->next !=phead说明链表不为空,那么assert(!DListEmpty(phead)这条语句就不起作用,检查通过,继续下面语句的执行。
//尾删
void DListPopBack(Dlist* phead)
{
assert(phead);
assert(!DListEmpty(phead));
Dlist* tail = phead->prev;
Dlist* tailprev = tail->prev;
tailprev->next = phead;
phead->prev = tailprev;
free(tail);
tail = NULL;
}
测试
7.头删函数
//头删
void DListPopFront(Dlist* phead)
{
assert(phead);
assert(!DListEmpty(phead));
Dlist* first = phead->next;
Dlist* tailnext = first->next;
tailnext->prev = phead;
phead->next = tailnext;
free(first);
first = NULL;
}
测试:
8.链表查找函数
查找函数可以用来查找某个节点,还可以通过对查找到的节点进行修改、对指定位置的增删功能实现。
//查找
Dlist* DListFind(Dlist* phead, int x)
{
assert(phead);
Dlist* cur = phead->next;
while (cur != phead)
{
if (cur->date == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
测试:
9.在链表指定的位置前插入函数
//在pos位置前插
void DListInsert(Dlist* pos, int x)
{
assert(pos);
Dlist* prev = pos->prev;
Dlist* newnode = BuyNode(x);
newnode->next = pos;
pos->prev = newnode;
prev->next = newnode;
newnode->prev = prev;
}
测试
10.在链表的pos位置处删除节点
//链表在指定位置pos处删除节点
void DListErase(Dlist * pos)
{
assert(pos);
Dlist* next = pos->next;
Dlist* prev = pos->prev;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}
测试
11.求链表的长度函数
这个函数与打印函数原理相同,都是通过指针遍历每一个节点去统计节点个数。
//求链表的长度函数
size_t DListSize(Dlist* phead)
{
assert(phead);
size_t n = 0;
Dlist* cur = phead->next;
while (cur != phead)
{
n++;
cur = cur->next;
}
return n;
}
测试:
12.释放链表空间
//释放空间
void DListDestory(Dlist* phead)
{
assert(phead);
Dlist* cur = phead->next;
Dlist* f = NULL;
while (cur != phead)
{
f = cur;
cur = cur->next;
free(f);
f = NULL;
}
free(phead);
phead = NULL;
}
总代码
一.DoubleNode.h
#pragma once
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdbool.h>
//双向链表的结构设计
typedef struct Dlist
{
int date;//数据域:保存有效值
struct Dlist* next;//向后指针域:保存下一个节点的地址(如果没有,指向NULL)
struct Dlist* prev;//向前指针域:保存上一个节点的地址(如果没有,指向NULL)
}Dlist;
//双向链表操作函数声明:
//初始化
Dlist* DlistInit();
//申请节点
Dlist* BuyNode(int val);
//头插
void DListPushFront(Dlist* phead,int x);
//尾插:
void DListPushBack(Dlist* phead, int x);
//在pos位置前插
void DListInsert(Dlist* pos,int x);
//链表在指定位置pos处删除节点
void DListErase(Dlist* pos);
//头删
void DListPopFront(Dlist* phead);
//尾删
void DListPopBack(Dlist* phead);
//求链表的长度函数
size_t DListSize(Dlist* phead);
//查找(如果值允许重复,我们找val值第一次出现的位置,将其地址返回出来)
Dlist* DListFind(Dlist* phead,int x);
//释放空间
void DListDestory(Dlist* phead);
//打印
void DListPrint(Dlist* phead);
二.DoubleNode.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"DoubleNode.h"
//初始化
Dlist* DlistInit()
{
Dlist* Guard = (struct Dlist*)malloc(sizeof(struct Dlist));//哨兵位头结点
if (Guard == NULL)
{
perror("malloc fail:");
return -1;
}
Guard->next = Guard;
Guard->prev = Guard;
return Guard;
}
//申请节点
Dlist* BuyNode(int val)
{
Dlist* newnode = (struct Dlist*)malloc(sizeof(struct Dlist));
if (newnode == NULL)
{
perror("malloc fail:");
return -1;
}
newnode->date = val;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
//打印双链表
void DListPrint(Dlist* phead)
{
assert(phead);
Dlist* cur = phead->next;
printf("guard<=>");
while (cur != phead)
{
printf("%d<=>", cur->date);
cur = cur->next;
}
printf("NULL");
printf("\n");
}
//尾插:
void DListPushBack(Dlist* phead, int x)
{
assert(phead);
Dlist* tail = phead->prev;//找到尾 尾节点指针处在哨兵位头节点的prev
Dlist* newnode = BuyNode(x);//新节点
//phead tail newnode
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
}
//头插
void DListPushFront(Dlist* phead, int x)
{
assert(phead);
Dlist* first = phead->next;
Dlist* newnode = BuyNode(x);
newnode->next = first;
first->prev = newnode;
newnode->prev = phead;
phead->next = newnode;
}
//暴力检查
bool DListEmpty(Dlist* phead)
{
assert(phead);
return phead->next == phead;
}
//尾删
void DListPopBack(Dlist* phead)
{
assert(phead);
assert(!DListEmpty(phead));
Dlist* tail = phead->prev;
Dlist* tailprev = tail->prev;
tailprev->next = phead;
phead->prev = tailprev;
free(tail);
tail = NULL;
}
//头删
void DListPopFront(Dlist* phead)
{
assert(phead);
assert(!DListEmpty(phead));
Dlist* first = phead->next;
Dlist* tailnext = first->next;
tailnext->prev = phead;
phead->next = tailnext;
free(first);
first = NULL;
}
//查找
Dlist* DListFind(Dlist* phead, int x)
{
assert(phead);
Dlist* cur = phead->next;
while (cur != phead)
{
if (cur->date == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//在pos位置前插
void DListInsert(Dlist* pos, int x)
{
assert(pos);
Dlist* prev = pos->prev;
Dlist* newnode = BuyNode(x);
newnode->next = pos;
pos->prev = newnode;
prev->next = newnode;
newnode->prev = prev;
}
//链表在指定位置pos处删除节点
void DListErase(Dlist * pos)
{
assert(pos);
Dlist* next = pos->next;
Dlist* prev = pos->prev;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}
//求链表的长度函数
size_t DListSize(Dlist* phead)
{
assert(phead);
size_t n = 0;
Dlist* cur = phead->next;
while (cur != phead)
{
n++;
cur = cur->next;
}
return n;
}
//释放空间
void DListDestory(Dlist* phead)
{
assert(phead);
Dlist* cur = phead->next;
Dlist* f = NULL;
while (cur != phead)
{
f = cur;
cur = cur->next;
free(f);
f = NULL;
}
free(phead);
phead = NULL;
}
三.Test.c
test1()
{
Dlist* plist = DlistInit();
//传参时,不需要传plist1的地址,因为函数不会改变plist1的指向,
// 因为plist1永远指向哨兵位头节点,
//使用哨兵位头节点进行改变,所以用plist传递,用一级指针接收就行
DListPushBack(plist, 1);
DListPushBack(plist, 2);
DListPushBack(plist, 3);
DListPushBack(plist, 4);
DListPushBack(plist, 5);
DListPrint(plist);
int size = DListSize(plist);
printf("%d\n", size);
DListDestory(plist);
}
int main()
{
test1();
}
重拾真的是一个艰难的过程,我都要忘记我是一个正在努力奋斗的姑娘了~