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

news2024/11/19 23:38:58

目录

链表的分类

带头双向循环链表的实现

带头双向循环链表的结构

带头双向循环链表的结构示意图

空链表结构示意图

单结点链表结构示意图

 多结点链表结构示意图

链表创建结点

双向链表初始化

销毁双向链表

打印双向链表

 双向链表尾插

尾插函数测试

双向链表头插

头插函数测试

 双向链表尾删

尾删函数测试

双向链表头删

头删函数测试

双向链表查找

双向链表pos位置前插

插入函数测试

 双向链表删除pos位置的结点

删除函数测试

利用 ListInsert()函数改造头插尾插函数

尾插函数改造版本

头插函数改造版本

利用ListEarse()函数改造头删 尾删函数

头删函数改造版本

尾删函数改造版本

计算双向链表长度


链表的分类

  • 单向/双向

单向列表:每一个结点结构中只保存下一结点的地址,所以很难从后一结点找到前一节点;

双向列表:每一个结点结构中不仅保存下一结点的地址,还保存上一节点的地址;方便寻找前一节点和后一节点;

 

  • 带头/不带头

带头:在头结点之前有一个哨兵位结点,哨兵位的数据域不存储有效数据,指针域指向头结点

不带头:没有哨兵位结点,尾插尾删考虑头结点情况;

 

  • 循环/非循环

循环:头结点与尾结点相连;

非循环:头结点与尾结点不相连;

上述情况相互组合,共有8种情况,  实际中使用的链表数据结构,都是带头双向循环链表,带头双向循环链表虽然结构复杂,但是其结构具有很多优势,实现反而简单;

带头双向循环链表的实现

带头双向循环链表的结构

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* prev;//前址域-存放前一个结点的地址
	LTDataType data;//数据域
	struct ListNode* next;//后址域-存放后一个结点的地址
}ListNode;

逻辑图:

物理图:

带头双向循环链表的结构示意图

  • 空链表结构示意图

由图可知,head->prev=head; head->next=head;

  • 单结点链表结构示意图

由图可知:

head->next=FirstNode;

head->prev=FirstNode;

FirstNode->prev=head;

FirstNode->next=head;

  •  多结点链表结构示意图

由图可知:

head->next=firstnode;

head->prev=tail;

tail->next=head;

firstnode->prev=head;

链表创建结点

//创建链表结点,返回链表结点地址
ListNode* BuyListNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc failed:");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

双向链表初始化

 注:函数调用时得到动态开辟的链表空间起始地址的两种方案如下

方案一: 当传参时为链表结点的地址,函数的形参设计为二级指针,只有通过传址调用,可以将动态开辟的链表的起始地址带出函数;

方案二: 设计函数的返回类型为结点指针,返回动态开辟的链表结点指针,如此可以得到链表空间的起始地址;

//初始化链表(空链表)
ListNode* ListInit()
{
	//创建哨兵位结点
	ListNode* head = BuyListNode(0);//0不是有效数据

	//初始化哨兵位结点的指针域
	head->next = head;
	head->prev = head;

	return head;
}

销毁双向链表

  • 循环遍历释放结点,包含哨兵位结点;
  • 释放前保存下一结点地址,避免地址丢失;
//销毁链表,包含哨兵位结点
void DestoryList(ListNode* phead)
{
	assert(phead);

	//创建寻址指针
	ListNode* cur = phead;
	//断开循环链表
	phead->prev->next = NULL;
	while (cur != NULL)
	{
		//记录下一结点地址
		ListNode* next = cur->next;
        //释放当前结点
		free(cur);
		//寻找下一节点
		cur = next;
	}
	return;
}

打印双向链表

  • 循环遍历链表打印数据,不显示哨兵位结点的数据域;
  • 以哨兵位头结点作为结束标志;
void PrintList(ListNode* phead)
{
	assert(phead != NULL);

	ListNode* cur = phead->next;

	printf("phead<==>");
	while (cur != phead)
	{
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

 双向链表尾插

  •  尾插先找尾,哨兵位的前址域即为尾结点即tail=head->prev;
  • 当链表为空时,连接的逻辑关系相同(创建三个指针变量,按照新结点的前址域指向谁,谁指向新结点,新结点的后址域指向谁,谁指向新结点进行连接);
void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead);
    
	//寻找尾结点
	ListNode* tail = phead->prev;
    //创建新结点
	ListNode* newnode = BuyListNode(x);

   //尾插
	newnode->prev = tail;
	tail->next = newnode;

	newnode->next = phead;
	phead->prev = newnode;
}
尾插函数测试
void Test1()
{
	ListNode* plist=ListInit();
	
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	PrintList(plist);
}
int main()
{
	Test1();
	return 0;
}

运行结果:

双向链表头插

  • 头插前先保存哨兵位结点的下一节点即原先真正的首节点;
  • 按照按照新结点的前址域指向谁,谁指向新结点,新结点的后址域指向谁,谁指向新结点进行连接从而实现头插,链表为空时,头插逻辑仍然相同;
//链表头插
void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);

	//保存原先的首节点
	ListNode* firstnode = phead->next;
	//创建新结点
	ListNode* newnode = BuyListNode(x);

	//头插
	newnode->prev = phead;
	phead->next = newnode;

	newnode->next = firstnode;
	firstnode->prev = newnode;
}
头插函数测试
void Test2()
{
	ListNode* plist = ListInit();
	ListPushFront(plist, 10);
	ListPushFront(plist, 20);
	ListPushFront(plist, 30);
	ListPushFront(plist, 40);
	ListPushFront(plist, 50);
	PrintList(plist);

}
int main()
{
	Test2();
	return 0;
}

运行结果:

 双向链表尾删

  • 链表中只剩哨兵位结点,此时链表为空,不再进行尾删;
  • 尾删前记录前一节点的地址,方便修改逻辑关系;
//链表尾删
void ListPopBack(ListNode* phead)
{
	assert(phead);

	//链表中只剩哨兵位的情况
	assert(phead->next != phead);

	//查找尾结点
	ListNode* tail = phead->prev;
	//保存尾结点的上一节点
	ListNode* tailprev = tail->prev;

   //尾删
	free(tail);
   //建立链接关系
	tailprev->next = phead;
	phead->prev = tailprev;

}
尾删函数测试
void Test3()
{
	ListNode* plist = ListInit();

	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	PrintList(plist);

	ListPopBack(plist);
	PrintList(plist);

	ListPopBack(plist);
	PrintList(plist);

	ListPopBack(plist);
	PrintList(plist);

}
int main()
{
	Test3();
	return 0;
}

运行结果:

双向链表头删

  • 链表中只剩哨兵位结点,此时链表为空,不再进行头删;
  • 头删前记录下一节点的地址,方便修改逻辑关系;
//链表头删
void ListPopFront(ListNode* phead)
{
	assert(phead);
	//只剩哨兵位,不再头删
	assert(phead->next != phead);

	//保存原先的首节点
	ListNode* head = phead->next;

	//保存首结点的下一节点
	ListNode* headnext = phead->next->next;

	//头删
	free(head);
	//建立链接关系
	headnext->prev = phead;
	phead->next = headnext;

}
头删函数测试
void Test4()
{
	ListNode* plist = ListInit();

	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	PrintList(plist);

	ListPopFront(plist);
	PrintList(plist);

	ListPopFront(plist);
	PrintList(plist);

	ListPopFront(plist);
	PrintList(plist);
}
int main()
{
	Test4();
	return 0;
}

运行结果:

双向链表查找

  • 循环遍历链表,从首节点开始遍历,以哨兵位头结点作为结束标志;
  • 根据数据域进行查找,找到返回数据域的结点地址,找不到返回空指针;
ListNode* ListFind(ListNode* phead, LTDataType x)
{
	assert(phead);

	//创建遍历指针
	ListNode* cur = phead->next;

	//遍历链表
	while (cur != phead)
	{
		if ((cur->data) == x)
		{
			//找到返回下标
			return cur;
		}
		cur = cur->next;
	}
	//没找到返回空指针
	return NULL;
}

双向链表pos位置前插

  • 前插时保存pos位置的前一个节点,方便修改逻辑关系;
  • 按照按照新结点的前址域指向谁,谁指向新结点,新结点的后址域指向谁,谁指向新结点进行链接;
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos != NULL);

	//创建新结点
	ListNode* newnode = BuyListNode(x);
	//保存pos位置的前一个结点
	ListNode* posprev = pos->prev;

	//前插
	newnode->prev = posprev;
	posprev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
}
插入函数测试
void Test5()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);

	int x = 0;
	printf("请输入查找的数值:");
	scanf("%d", &x);
	ListNode* pos = ListFind(plist, x);
	if (pos == NULL)
	{
		printf("要查找的值不存在\n");
		return;
	}
	//在查找到数值前插入100
	ListInsert(pos, 100);
	PrintList(plist);

}
int main()
{
	Test5();
	return 0;
}

运行结果:

 双向链表删除pos位置的结点

  • 链表删除pos位置处的结点前先保存前结点和后结点的地址,方便处理链接关系;
//双向链表删除pos位置
void ListEarse(ListNode* pos)
{
	assert(pos);

	//保存pos位置处的前一个和后一个结点;
	ListNode* posprev = pos->prev;
	ListNode* posnext = pos->next;
	//删除pos位置结点
	free(pos);

	//建立前后节点的链接关系
	posprev->next = posnext;
	posnext->prev = posprev;

}
删除函数测试
void Test6()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	PrintList(plist);

	int x = 0;
	printf("请输入删除的数值:");
	scanf("%d", &x);
	ListNode* pos = ListFind(plist, x);
	if (pos == NULL)
	{
		printf("要删除的值不存在\n");
		return;
	}
	
	ListEarse(pos);
	PrintList(plist);
}
int main()
{
	Test6();
	return 0;
}

运行结果:

利用 ListInsert()函数改造头插尾插函数

  • 尾插函数改造版本
void Listpushback(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead, x);
}
  • 头插函数改造版本
void Listpushfront(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}

利用ListEarse()函数改造头删 尾删函数

  • 头删函数改造版本
void Listpopfront(ListNode* phead)
{
	assert(phead);
	//只剩哨兵位,不再头删
	assert(phead->next != phead);

	ListEarse(phead->next);
}
  • 尾删函数改造版本
void Listpopback(ListNode* phead)
{
	assert(phead);

	//链表中只剩哨兵位的情况
	assert(phead->next != phead);

	ListEarse(phead->prev);
}

计算双向链表长度

int ListLength(ListNode* phead)
{
	assert(phead);

	int size = 0;
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}
	return size;
}

 

 

 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1064594.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《Spring Boot前世今生》

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

VMware Workstation Pro详解

零、文章目录 VMware Workstation Pro详解 1、虚拟机介绍 &#xff08;1&#xff09;介绍 VMware Workstation Pro 是行业标准桌面 Hypervisor&#xff0c;使用它可在 Windows 或 Linux 桌面上运行 Windows、Linux 和 BSD 虚拟机。VMware官网地址&#xff1a;https://www.v…

【Python】如何使用PyInstaller打包自己写好的代码

使用PyInstaller打包自己写好的代码 零、需求 最近接到一个小单&#xff0c;需要批量修改文档内容&#xff0c;用Python做好后要打包成exe程序给客户的Win7电脑使用&#xff0c;此时需要用到PyInstaller打包自己的代码&#xff0c;想到还要有给用户试用的需求&#xff0c;所以…

第六章 数字化工作方法与应用

第一节 题目 A C 第二节 题目 A C B D A 右侧原因型&#xff0c;在左侧是对策型。主要记得右侧是原因行就可以了。 C B D 字面意思。 ABCD CD

笔训【day3】

目录 选择题 1、二维数组初始化 2、[]优先级高于* ​编辑 3、for初始化只执行一次​编辑 4、大小端 编程题 1、字符串中找出最长连续数字串 2、数组中出现次数超过一半的数 选择题 1、二维数组初始化 C&#xff1a;多初始化了一行 D&#xff1a;不能中间用两个逗号跳过。…

外包做了3个月,技术退步明显。。。。。

先说一下自己的情况&#xff0c;大专生&#xff0c;17年通过校招进入广州某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试…

【群智能算法】光学显微镜算法 OMA算法【Matlab代码#59】

文章目录 【获取资源请见文章第4节&#xff1a;资源获取】1. 光学显微镜算法&#xff08;OMA&#xff09;1.1 物镜放大倍数1.2 目镜放大倍数 2. 部分代码展示3. 仿真结果展示4. 资源获取说明 【获取资源请见文章第4节&#xff1a;资源获取】 1. 光学显微镜算法&#xff08;OMA&…

win10睡眠快捷方式

新建快捷方式 如下图 内容如下 rundll32.exe powrprof.dll,SetSuspendState 0,1,0 下一步 点击完成即可。 特此记录 anlog 2023年10月6日

ssm+vue的培训机构运营管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的培训机构运营管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结…

Linux工具(三)

继Linux工具&#xff08;一&#xff09;和Linux工具&#xff08;二&#xff09;&#xff0c;下面我们就来讲解Linux最后的两个工具&#xff0c;分别是代码托管的版本控制器git和代码调试器gdb。 目录 1.git-版本控制器 从0到1的实现git代码托管 检测并安装git 新建git仓库…

rk3588 香橙派 vncserver连接 图形界面远程登陆

直接使用windows远程连接失败 然后看有用vncserver 进行连接的方案 需要再rk3588板端执行&#xff1a; sudo apt-get install xfce4 sudo apt0get update vncserver vncpasswd配置vnc 密码。不配置只看密码 下载vncserver 后不会自动启动&#xff0c;不会启动 需要执行 vncse…

Spring基础以及核心概念(IoC和DI)

1.Spring是什么 Spring是包含了众多工具方法的IoC容器 2.loC&#xff08;Inversion of Control &#xff09;是什么 IoC:控制反转,Spring是一个控制反转容器(控制反转对象的生命周期) Spring是一个loC容器&#xff0c;我们之前学过的List/Map就是数据存储的容器&#xff0c;to…

【1.1】神经网络:关于神经网络的介绍

✅作者简介&#xff1a;大家好&#xff0c;我是 Meteors., 向往着更加简洁高效的代码写法与编程方式&#xff0c;持续分享Java技术内容。 &#x1f34e;个人主页&#xff1a;Meteors.的博客 &#x1f49e;当前专栏&#xff1a; 神经网络&#xff08;随缘更新&#xff09; ✨特色…

黑马点评-02使用Redis代替session,Redis + token机制实现

Redis代替session session共享问题 每个Tomcat中都有一份属于自己的session,所以多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时可能会导致数据丢失 用户第一次访问1号tomcat并把自己的信息存放session域中, 如果第二次访问到了2号tomcat就无法获取到在1号…

7 航空公司客户价值分析

第7章 航空公司客户价值分析 7.1 了解航空公司现状与客户价值分析7.1.1 了解航空公司现状7.1.2 认识客户价值分析7.1.3 熟悉航空客户价值分析的步骤与流程 7.2 预处理航空客户数据7.2.1 处理数据缺失值与异常值7.2.2 构建航空客户价值分析的关键特征1. RFM模型介绍2. RFM模型结…

Golang--channel+waitGroup控制并发量

文章目录 channelwaitGroup控制并发量前言示例 channelwaitGroup控制并发量 前言 golang的goroutine非常轻量级&#xff0c;同时启动数万协程都没问题。如果不对并发量进行控制&#xff0c;比如同时产生数百万的协程&#xff0c;会压垮服务器通过控制channel缓冲区的大小&…

阿里云服务器支持免费更换公网IP吗?

阿里云服务器支持免费更换公网IP吗&#xff1f;支持&#xff01;创建6小时以内的云服务器ECS可以免费更换三次公网IP地址&#xff0c;超过6小时的云服务器&#xff0c;可以将公网固定IP地址转成弹性EIP&#xff0c;然后通过换绑EIP的方式来更换IP地址。阿里云服务器网分享阿里云…

3D孪生场景SDK:Viwer 孪生世界

NSDT 编辑器 提供三维场景构建、场景效果设计、场景服务发布全流程工具等&#xff0c;其场景编辑器支持资产管理、灯光设置、骨骼动画等功能&#xff1b;致力于协助资源不足的中小企业及个人快速开发数字孪生场景&#xff0c;帮助企业提高生产力、实现降本增效。 NSDT编辑器简…

SLAM面试笔记(8) — 计算机视觉面试题

目录 问题1&#xff1a;目标检测的算法分类 问题2&#xff1a;卷积神经网络的组成 问题3&#xff1a;输入层的作用 问题4&#xff1a;卷积层作用 问题5&#xff1a;卷积核类型 问题6&#xff1a;11卷积核作用 问题7&#xff1a;卷积核是否越大越好 问题8&#xff1a;棋…

Day-08 基于 Docker安装 Nginx 镜像-负载均衡

1、反向代理后&#xff0c;自然而然就引出了负载均衡,下面简单实现负载均衡的效果; 2、实现该效果需要再添加一个 Nginx &#xff0c;所以要增加一个文件夹。 /home|---mutou|----nginx|----conf.d|----html|----conf.d2|----html3 1.创建 html3 文件夹&#xff0c; 新建 index…