C——双向链表

news2024/12/26 22:54:05

一.链表的概念及结构

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。什么意思呢?意思就是链表在物理结构上不一定是连续的,但在逻辑结构上一定是连续的。链表是由一个一个的节点连接而成的。

我们借助这个图来理解链表的物理结构上的不连续和逻辑结构上的连续。这上面的6个节点在内存空间的地址不是连续的,但是他们在逻辑上却是连续的,1->2->3->4->5->6。

与链表相似的还有顺序表,顺序表与链表相同都是线性表的一种。而顺序表的底层其实就是数组,所以顺序表在物理结构上是连续的,在逻辑结构上也是连续的。 

二.链表的分类

我们从上图可以得知,链表一共有2*2*2种。

 分别为:

单向带头循环链表单向带头不循环链表单向不带头循环链表单向不带头不循环链表双向带头循环链表双向带头不循环链表双向不带头循环链表双向不带头不循环链表

而在这么多种的链表中,最常用的只有单向不带头不循环链表(也称单链表),以及双向带头循环链表(也称双向链表)。我们今天来了解这两种之一的双向链表。

三.双向链表的结构

双向链表全称为:双向带头循环链表。怎么理解这里面的每一个修饰词呢?我们先来看一下双向链表的结构。

四.实现双向链表 

我们在实现双向链表的时候可以将所有的链表所需的函数的声明都放到一个List.h中,将函数的定义放到一个List.c中,我们还需要一个test.c用来测试我们的双向链表中的方法。

4.1链表的元素——节点的创建

节点是链表的组成元素,而对于双向链表来说,每一个节点不仅要存储数据还要存储前一个节点的地址和后一个节点的地址,没有哪一种内置类型可以同时包含这三种,所以我们节点的创建要用到自定义类型——结构体。

struct ListNode
{
	int val;
	struct ListNode* prev;
	struct ListNode* next;
};

这样的结构体就可以表示一个节点了嘛?难道我们的节点只能存储整型嘛?当然不是,我们的节点可以存储任意数据,但是我们如果直接这样写的话,等到代码量大了,如果我们想要该链表存储字符型,我们到时候要修改的地方非常多。所以我们有一个一劳永逸的方法:

typedef int ListValType;

我们可以给int类型利用typedef关键字起一个新名字ListValType,我们结构体内部定义 int类型的成员时不再使用int a;而使用ListValType a;这两种的效果是一样的。以后我们想修改链表存储数据的类型的时候只需要将最前面的重命名语句中的int类型改为其他类型即可。

我们在创建节点的时候要写struct ListNode这么长一串,我们也可以利用typedef关键字给该结构体类型起一个新名字,避免了结构体名太长的问题。

所以我们节点的定义最终为:

typedef int ListValType;

typedef struct ListNode
{
	ListValType val;
	struct ListNode* prev;
	struct ListNode* next;
}ListNode;

4.2双向链表的初始化

双向链表是带头链表,而这个头就是头节点(哨兵位)。所以双向链表的初始化其实就是创建一个头节点。头节点也是节点,所以双向链表的初始化其实就是创建一个节点,只不过这个节点没有有效的值。

//创建节点
ListNode* Buynode(ListValType x)
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (node == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	node->val = x;
	node->next = NULL;
	node->prev = NULL;
	return node;
}

//双向链表的初始化
ListNode* ListInit()
{
	//创建一个头节点(哨兵位)
	ListNode* phead = Buynode(-1);
	return phead;
}

上面的代码可以完成双向链表的初始化嘛?不行!

修改后的代码为: 

 我们来写一个测试函数,来判断我们的链表的初始化是否正确。

我们调试看到,头节点的next指针和prev指针都指向了他自己,并且val = -1,说明我们的初始化没有问题。

4.3尾插 

我们创建好了新节点后想要将该节点插入到链表的尾部,怎么插入呢?插入的时候我们要注意指针指向的改变。我们来画图分析尾插的过程。

第一步:先将新节点连接到链表中

第二步:改变链表中指针的指向 

我们发现,将newnode作为新节点插入到链表中后,原链表中有的指针的指向需要改变。我们继续来画图分析哪些改变了,要怎么修改?

通过上面两幅图的分析,我们已经了解了尾插的规则,现在我们来实现双向链表的尾插方法:

//尾插
void ListPushBack(ListNode* phead,ListValType x)
{
	assert(phead);//判断该双向链表是否有效

	ListNode* newnode = Buynode(x);
	//head head->prev newnode
	newnode->prev = phead->prev;
	newnode->next = phead;

	phead->prev->next = newnode;
	phead->prev = newnode;
}

我们通过调试来判断一下我们的尾插是否正确。 观察上图,我们的尾插已经实现了。但是这样并不好观察,我们可以先实现双向链表的打印方法,这样就可以明显的看出尾插是否正确了。

4.4双向链表的打印

 双向链表的打印也就是遍历该链表就行了,我们只需要注意遍历时的起始位置和结束条件就行了。

//双向链表的打印
void ListPrint(ListNode* phead)
{
	assert(phead);

	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->val);
		pcur = pcur->next;
	}
	printf("\n");
}

我们现在来利用打印方法来测试尾插方法: 我们看到,尾插和打印方法都没有问题。

4.5头插

头插往哪插呢?头节点的前面吗?头插插的地方是头节点后面的位置。

头插的分析与尾插的分析相同,我们先将newnode连接到链表中,在判断那些指针的指向需要改变。

第一步:先将newnode连接到链表中

第二步:改变链表中指针的指向 

头插代码为: 

//头插
void ListPushFront(ListNode* phead, ListValType x)
{
	assert(phead);

	ListNode* newnode = Buynode(x);
	//phead newnode phead->next
	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode;
	phead->next = newnode;
}

我们测试一下头插代码: 

经过测试,我们看到头插方法没有问题。

4.6尾删 

尾删就是删除该链表中的最后一个节点,即head->prev。删除该节点后,链表中有的指针指向就要发生改变。

//尾删
void ListPopBack(ListNode* phead)
{
	assert(phead && phead->next != phead);//删除的链表必须是有效链表,即不能只包含头节点

	ListNode* del = phead->prev;//要删除的尾节点

	//phead del->prev del
	del->prev->next = phead;
	phead->prev = del->prev;

	free(del);
	del = NULL;
}

我们利用测试代码进行测试: 我们删除了4次,所以最后一次删除链表已经为空链表了,而头节点是一个没有值的节点,所以打印出来就是空白。

 4.7头删

我们已经知道了尾删方法,头删方法的分析方式与尾删相似,我们依旧先找到要需要改变指向的指针。我们借助图来分析:

 

//头删
void ListPopFront(ListNode* phead)
{
	assert(phead && phead->next != phead);

	ListNode* del = phead->next;

	//phead del del->next
	del->prev = phead;
	phead->next = del->next;

	free(del);
	del = NULL;
}

写完一个方法之后依旧通过测试方法来判断方法是否正确: 

走到这里,我们头删的方法也是正确的。

4.8在指定位置之后插入数据 

在指定位置之后插入数据,我们首先要保证这个指定的位置是存在的,要不然找不到怎么在它的后面插入呢?所以在插入数据之前我们得先查找这个数据在链表中的位置。

4.8.1查找节点

查找节点我们只需要遍历我们的链表就行了。如果遍历途中找到了就返回该节点,如果遍历完了链表还没有找到该节点,那就说明该链表只能中没有该节点。

//查找节点
ListNode* Find(ListNode* phead, ListValType x)
{
	ListNode* pcur = phead->next;
	//遍历链表
	while (pcur != phead)
	{
		if (pcur->val == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

测试代码: 

4.8.2找到节点后插入数据 

//在指定位置之后插入数据
void ListInsert(ListNode* pos, ListValType x)
{
	assert(pos);
	ListNode* newnode = Buynode(x);
	//pos newnode pos->next
	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;
}

测试代码:

4.8.3在指定位置之后插入与尾插的区别  

4.9删除pos节点

删除pos节点也需要查找该节点是否在链表中,只有该节点在链表中我们才能对其删除。

//删除pos节点
void ListErase(ListNode* pos)
{
	assert(pos);
	//pos->prev pos pos->next
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;

	free(pos);
	pos = NULL;
}

测试代码: 我们看到,我们调用完该方法后,我又手动将find置为了NULL,为什么要这样呢?在该方法内部不是已经置为NULL了嘛?

因为我们传的参数是一级指针,接收的形参也是一级指针,我们虽然已经将该空间释放掉了也将形参置为了空,但是这种传递方式是值传递,形参的改变不会影响实参,所以我们出了函数之后,最好将find也手动置为空,要不然会有野指针的风险。

4.10销毁链表

我们创建的链表是由一个一个的节点连接起来的,而节点是我们利用动态内存管理申请的空间,我们用完了之后就得还给操作系统,所以我们在使用完链表之后,也要将链表销毁。

//链表的销毁
void ListDestory(ListNode* phead)
{
	assert(phead);
	ListNode* pcur = phead->next;
	ListNode* next = pcur->next;
	while (pcur != phead)
	{
		free(pcur);
		pcur = next;
		next = pcur->next;
	}
	//到这里,所有的有效节点已经删除了,现在只需要删除头节点
	free(phead);
	phead = NULL;
}

到这里,我们双向链表的全部功能就已经实现了。

五.完整代码

5.1双链表头文件

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int ListValType;

typedef struct ListNode
{
	ListValType val;
	struct ListNode* prev;
	struct ListNode* next;
}ListNode;

//双向链表的初始化
ListNode* ListInit();

//双向链表的打印
void ListPrint(ListNode* phead);

//尾插
void ListPushBack(ListNode* phead,ListValType x);

//头插
void ListPushFront(ListNode* phead, ListValType x);

//尾删
void ListPopBack(ListNode* phead);

//头删
void ListPopFront(ListNode* phead);

//查找节点
ListNode* Find(ListNode* phead , ListValType x);

//在指定位置之后插入数据
void ListInsert(ListNode* pos, ListValType x);

//删除pos节点
void ListErase(ListNode* pos);

//链表的销毁
void ListDestory(ListNode* phead);

5.2双链表源文件

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

//创建节点
ListNode* Buynode(ListValType x)
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (node == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	node->val = x;
	node->next = node;
	node->prev = node;
	return node;
}

//双向链表的初始化
ListNode* ListInit()
{
	//创建一个头节点(哨兵位)
	ListNode* phead = Buynode(-1);
	return phead;
}

//双向链表的打印
void ListPrint(ListNode* phead)
{
	assert(phead);

	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->val);
		pcur = pcur->next;
	}
	printf("\n");
}

//尾插
void ListPushBack(ListNode* phead,ListValType x)
{
	assert(phead);//判断该双向链表是否有效

	ListNode* newnode = Buynode(x);
	//phead head->prev newnode
	newnode->prev = phead->prev;
	newnode->next = phead;

	phead->prev->next = newnode;
	phead->prev = newnode;
}

//头插
void ListPushFront(ListNode* phead, ListValType x)
{
	assert(phead);

	ListNode* newnode = Buynode(x);
	//phead newnode phead->next
	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode;
	phead->next = newnode;
}

//尾删
void ListPopBack(ListNode* phead)
{
	assert(phead && phead->next != phead);//删除的链表必须是有效链表,即不能只包含头节点

	ListNode* del = phead->prev;//要删除的尾节点

	//phead del->prev del
	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 del del->next
	del->prev = phead;
	phead->next = del->next;

	free(del);
	del = NULL;
}

//查找节点
ListNode* Find(ListNode* phead, ListValType x)
{
	ListNode* pcur = phead->next;
	//遍历链表
	while (pcur != phead)
	{
		if (pcur->val == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

//在指定位置之后插入数据
void ListInsert(ListNode* pos, ListValType x)
{
	assert(pos);
	ListNode* newnode = Buynode(x);
	//pos newnode pos->next
	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;
}

//删除pos节点
void ListErase(ListNode* pos)
{
	assert(pos);
	//pos->prev pos pos->next
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;

	free(pos);
	pos = NULL;
}

//链表的销毁
void ListDestory(ListNode* phead)
{
	assert(phead);
	ListNode* pcur = phead->next;
	ListNode* next = pcur->next;
	while (pcur != phead)
	{
		free(pcur);
		pcur = next;
		next = pcur->next;
	}
	//到这里,所有的有效节点已经删除了,现在只需要删除头节点
	free(phead);
	phead = NULL;
}

5.3测试源文件

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

void test01()
{
	ListNode* phead = ListInit();
	//测试尾插
	ListPushBack(phead,1);
	ListPrint(phead);
	ListPushBack(phead,2);
	ListPrint(phead);
	ListPushBack(phead,3);
	ListPrint(phead);
	ListPushBack(phead,4);
	ListPrint(phead);
}

void test02()
{
	ListNode* phead = ListInit();
	//测试头插
	ListPushFront(phead, 5);
	ListPrint(phead);
	ListPushFront(phead, 6);
	ListPrint(phead);
	ListPushFront(phead, 7);
	ListPrint(phead);
}

void test03()
{
	ListNode* phead = ListInit();
	//测试尾插
	ListPushBack(phead, 1);
	ListPushBack(phead, 2);
	ListPushBack(phead, 3);
	ListPushBack(phead, 4);
	ListPrint(phead);

	//链表的销毁
	ListDestory(phead);
	phead = NULL;
	ListPrint(phead);

	//ListNode* find = Find(phead, 1);

	测试删除pos节点
	//ListErase(find);//删除1节点
	//find = NULL;
	//ListPrint(phead);

	测试查找方法
	//ListNode * find = Find(phead, 1);
	if (find == NULL)
	{
		printf("找不到!");
	}
	else
	{
		printf("找到了!");
	}
	//ListInsert(find,99);//在第一个节点之后插入99
	//ListPrint(phead);
	测试头删
	//ListPopFront(phead);
	//ListPrint(phead);
	//ListPopFront(phead);
	//ListPrint(phead);
	//ListPopFront(phead);
	//ListPrint(phead);
	//ListPopFront(phead);
	//ListPrint(phead);

	测试尾删
	//ListPopBack(phead);
	//ListPrint(phead);
	//ListPopBack(phead);
	//ListPrint(phead);
	//ListPopBack(phead);
	//ListPrint(phead);
	//ListPopBack(phead);
	//ListPrint(phead);

}
int main()
{
	//test01();
	//test02();
	test03();
	return 0;
}

完!

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

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

相关文章

使用递归函数,将一串数字每位数相加求和

代码结果&#xff1a; #include<stdio.h> int DigitSum(unsigned int n) {if (n > 9)return DigitSum(n / 10) (n % 10);elsereturn n; } int main() {unsigned int n;scanf("%u", &n);int sum DigitSum(n);printf("%d\n", sum);return 0; …

持续更新|UNIAPP适配APP遇到的问题以及解决方案

在使用UNIAPP开发APP的时候遇到的一些奇奇怪怪问题记录 组件样式丢失 问题&#xff1a;组件引入界面中&#xff0c;在小程序和H5环境下样式正常&#xff0c;而在APP中却出现高度异常问题 解决&#xff1a;增加view标签将组件包裹起来即可正常显示 解决前&#xff1a; 解决后…

SCI一区 | MFO-CNN-LSTM-Mutilhead-Attention多变量时间序列预测(Matlab)

SCI一区 | MFO-CNN-LSTM-Mutilhead-Attention多变量时间序列预测&#xff08;Matlab&#xff09; 目录 SCI一区 | MFO-CNN-LSTM-Mutilhead-Attention多变量时间序列预测&#xff08;Matlab&#xff09;预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现MFO-CNN…

JAVA第二周学习笔记

文章目录 JAVA第二周学习笔记IDEA方法格式带参数及返回值的方法方法的重载方法的内存 二维数组静态初始化动态初始化 面向对象类和对象如何定义类如何得到对象注意 封装封装的优点private关键字成员变量和局部变量 this关键字构造方法作用类型特点执行时机定义重载 标准javabea…

Linux进程——进程的创建(fork的原理)

前言&#xff1a;在上一篇文章中&#xff0c;我们已经会使用getpid/getppid函数来查看pid和ppid,本篇文章会介绍第二种查看进程的方法&#xff0c;以及如何创建子进程&#xff01; 本篇主要内容&#xff1a; 查看进程的第二种方法创建子进程系统调用函数fork 在开始前&#xff…

什么是哈希表(HashTable)?

目录 一、概念 二、哈希冲突 减少哈希冲突的办法&#xff1a; 1、设计合理的哈希函数 哈希函数设计原则&#xff1a; 常用的哈希函数&#xff1a; 2、降低负载因子&#xff08;必须重点掌握&#xff09; 哈希冲突的解决 第一类&#xff1a;闭散列 第二类&…

实时监控RTSP视频流并通过YOLOv5-seg进行智能分析处理

在完成RTSP推流之后&#xff0c;尝试通过开发板接收的视频流数据进行目标检测&#xff0c;编写了一个shell脚本实现该功能&#xff0c;关于视频推流和rknn模型的部署请看之前的内容或者参考官方的文档。 #!/bin/bash # 设置脚本使用的shell解释器为bashSEGMENT_DIR"./seg…

【模板】前缀和

原题链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 前缀和模板题。 前缀和中数组下标为1~n。 前缀和&#xff1a;pre[i]pre[i-1]a[i]; 某段区间 [l,r]的和&#xff1a;pre[r]-pre[l-1] 3.…

【数学建模】2024五一数学建模C题完整论文代码更新

最新更新&#xff1a;2024五一数学建模C题 煤矿深部开采冲击地压危险预测&#xff1a;建立基于多域特征融合与时间序列分解的信号检测与区间识别模型完整论文已更新 2024五一数学建模题完整代码和成品论文获取↓↓↓↓↓ https://www.yuque.com/u42168770/qv6z0d/gyoz9ou5upv…

unity制作app(2)--主界面

1.先跳转过来&#xff0c;做一个空壳&#xff01;新增场景main为4号场景&#xff01; 2.登录成功跳转到四号场景&#xff01; 2.在main场景中新建canvas&#xff0c;不同的状态计划用不同的panel来设计&#xff01; 增加canvas和底图image 3.突然输不出来中文了&#xff0c;浪…

【19-文本数据处理:Scikit-learn中的自然语言处理技术】

文章目录 前言理解文本数据文本预处理文本清洗分词停用词去除向量化文本数据词袋模型TF-IDF变换构建文本分类模型模型评估与调优结论前言 欢迎回到我们的Scikit-learn系列,在这篇文章中,我们将探讨如何使用Scikit-learn来处理文本数据,这是自然语言处理(NLP)的基础。你将学…

为家庭公网IP配置DDNS域名

文章目录 域名配置域名更新frp配置修改 在成功完成frp改造Windows笔记本实现家庭版免费内网穿透之后&#xff0c;某天我突然发现内网穿透失效了&#xff0c;一番排查之后原来是路由器对应的公网IP更换了。果然我分到的并不是固定的公网IP&#xff0c;而是会定期变化的。为了免受…

中间件之异步通讯组件RabbitMQ入门

一、概述 微服务一旦拆分&#xff0c;必然涉及到服务之间的相互调用&#xff0c;目前我们服务之间调用采用的都是基于OpenFeign的调用。这种调用中&#xff0c;调用者发起请求后需要等待服务提供者执行业务返回结果后&#xff0c;才能继续执行后面的业务。也就是说调用者在调用…

解决IDEA下springboot项目打包没有主清单属性

1.问题出现在SpringBoot学习中 , 运行maven打包后无法运行 报错为spring_boot01_Demo-0.0.1-SNAPSHOT.jar中没有主清单属性 SpringBoot版本为 2.6.13 Java 版本用的8 解决方法 1.执行clean 删除之前的打包 2.进行打包规范设置 2.1 3.进行问题解决 (借鉴了阿里开发社区) 使用…

OpenCV(二)—— 车牌定位

从本篇文章开始我们进入 OpenCV 的 Demo 实战。首先&#xff0c;我们会用接下来的三篇文章介绍车牌识别 Demo。 1、概述 识别图片中的车牌号码需要经过三步&#xff1a; 车牌定位&#xff1a;从整张图片中识别出牌照&#xff0c;主要操作包括对原图进行预处理、把车牌从整图…

碳纤维复合材料的纳米纤维膜

碳纤维复合材料的纳米纤维膜是一种具有良好性能和应用前景的新材料。以下是关于这种材料的详细介绍&#xff1a; 制备方法&#xff1a;碳纤维复合材料的纳米纤维膜可以通过多种方法制备&#xff0c;包括化学气相沉积法、固相合成法、模板法等。其中&#xff0c;化学气相沉积法是…

Docker——部署LNMP架构

目录 一、LNMP架构概述 1.项目环境 2.服务器环境 3.需求 二、搭建Linux系统基础镜像 三、部署Nginx 1.建立工作目录 2.编写Dockerfile脚本 3.准备Nginx.conf配置文件 4.生成镜像 5.创建自定义网络 6.启动镜像容器 7.验证Nginx 三、部署Mysql 1.建立工作目录 2.编…

MathType打开的窗口太多 MathType说打开窗口太多无法复制怎么解决

在数学文档编辑中&#xff0c;MathType作为一款常用的数学公式编辑工具&#xff0c;使用过程中&#xff0c;我们常常会遇到一些问题&#xff0c;比如MathType打开的窗口过多导致软件运行缓慢甚至崩溃&#xff0c;以及在复制过程中出现“打开窗口太多&#xff0c;无法复制”的提…

2024年教你怎么将学浪视频保存到本地

你是否曾为无法将学浪视频保存到本地而烦恼&#xff1f;现在&#xff0c;我们将在2024年教给你如何解决这个问题&#xff01;只需简单几步操作&#xff0c;即可轻松将学浪视频保存到您的本地设备&#xff0c;随时随地想看就看&#xff01; 我已经将下载学浪的工具打包好了&…

使用FPGA实现并行乘法器

介绍 并行乘法器&#xff0c;那么它的输入输出就都是并行的数据了&#xff0c;相对来说&#xff0c;内部的结构就更复杂了&#xff0c;占用的资源就更多了。以后有需要完成这部分操作的话都可以调用IP核。 乘法器模块 这是一个纯组合逻辑电路&#xff0c;我们也知道&#xff0…