数据结构之双向带头循环链表

目录

一.双向链表的定义

二.双向链表的创建

1.双向链表结构

2.双向链表的头文件与函数定义

三.双向带头循环链表的实现

1.初始化

2.动态申请节点

 3.打印双向链表函数

4.尾部插入节点 

测试 

 5.头插函数

 测试

6.尾删函数

测试

7.头删函数

测试: 

8.链表查找函数 

测试: 

 9.在链表指定的位置前插入函数 

测试 

 10.在链表的pos位置处删除节点

 测试

 11.求链表的长度函数

测试: 

 12.释放链表空间

总代码

一.DoubleNode.h 

二.DoubleNode.c

三.Test.c


一.双向链表的定义

之前有一篇博客写着单链表详解,今天学习双向链表。

单向链表特点:

  1. 我们可以轻松的到达下一个节点,但是回到前一个节点是很难的。
  2. 只能从头遍历到尾或者从尾遍历到头(一般从头到尾)

双向链表特点: 

  1. 每次在插入或删除某个节点时, 需要处理四个节点的引用, 而不是两个. 实现起来要困难一些
  2. 相对于单向链表, 必然占用内存空间更大一些.
  3. 既可以从头遍历到尾, 又可以从尾遍历到头

双向链表的定义:

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。下图为双向链表的结构图。 

从上中可以看到,双向链表中各节点包含以下 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();
}

重拾真的是一个艰难的过程,我都要忘记我是一个正在努力奋斗的姑娘了~

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值