【数据结构】带头+双向+循环链表(DList)(增、删、查、改)详解

news2024/11/16 5:40:38

一、带头双向循环链表的定义和结构

1、定义

带头双向循环链表,有一个数据域两个指针域。一个是前驱指针,指向其前一个节点;一个是后继指针,指向其后一个节点。

// 定义双向链表的节点
typedef struct ListNode
{
	LTDataType data; // 数据域
	struct ListNode* prev; // 前驱指针
	struct ListNode* next; // 后继指针
}ListNode;

2、结构

带头双向循环链表:在所有的链表当中 结构最复杂,一般用在 单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多 优势,实现反而简单了。

二、带头双向循环链表接口的实现

1、创建文件

  • test.c(主函数、测试顺序表各个接口功能)
  • List.c(带头双向循环链表接口函数的实现)
  • List.h(带头双向循环链表的类型定义、接口函数声明、引用的头文件)


2、List.h 头文件代码 

// List.h
// 带头+双向+循环链表增删查改实现
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int LTDataType;

// 定义双向链表的节点
typedef struct ListNode
{
	LTDataType data; // 数据域
	struct ListNode* prev; // 前驱指针
	struct ListNode* next; // 后继指针
}ListNode;

// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x);
// 创建返回链表的头结点
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
// 双向链表的判空
bool ListEmpty(ListNode* phead);
// 获取双向链表的元素个数
size_t ListSize(ListNode* phead);

三、在 List.c 上是西安各个接口函数

1、动态申请一个新结点

// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}

2、创建返回链表的头结点(初始化头结点)

// 创建返回链表的头结点
ListNode* ListCreate()
{
	ListNode* phead = (ListNode*)malloc(sizeof(ListNode)); // 哨兵位头结点
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

也可以用下面这个函数(道理一样):

// 初始化链表
void ListInit(ListNode** pphead)
{
	*pphead = BuyListNode(-1); // 动态申请一个头节点
	(*pphead)->prev = *pphead; // 前驱指针指向自己
	(*pphead)->next = *pphead; // 后继指针指向自己
}

头指针初始指向 NULL,初始化链表时,需要改变头指针的指向,使其指向头节点,所以这里需要传二级指针。 


初始化带头双向循环链表,首先动态申请一个头结点头结点的前驱和后继指针都指向自己,形成一个循环

image-20210903220045099


3、双向链表的销毁

// 双向链表的销毁
void ListDestroy(ListNode** pphead)
{
	assert(pphead);
	assert(*pphead);

	ListNode* cur = (*pphead)->next;
	while (cur != *pphead)
	{
		ListNode* next = cur->next; // 记录cur的直接后继节点
		free(cur);
		cur = next;
	}
	free(*pphead); // 释放头节点
	*pphead = NULL; // 置空头指针
}

销毁链表,最后要将头指针 plist 置空,所以用了二级指针来接收。这里也可以用一级指针,但要在函数外面置空 plist 。

一级指针写法:

void ListDestroy(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

4、双向链表的打印

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

	ListNode* cur = phead->next; // 记录第一个节点
	printf("head <-> ");
	while (cur != phead)
	{
		printf("%d <-> ", cur->data);
		cur = cur->next;
	}
	printf("head\n");
}

5、双向链表的尾插

// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead); // 头指针不能为空

	/* ListNode* newnode = BuyListNode(x); // 动态申请一个节点
	ListNode* tail = phead->prev; // 记录尾节点

	tail->next = newnode; // 尾节点的后继指针指向新节点
	newnode->prev = tail; //2、新节点的前驱指针指向尾节点

	newnode->next = phead; // 新节点的后继指针指向头节点
	phead->prev = newnode; // 头节点的前驱指针指向新节点 */

    ListInsert(phead, x);
}


6、双向链表的尾删

// 双向链表的尾删
void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除

	/* ListNode* tail = phead->prev; // 记录尾节点
	ListNode* tailPrev = tail->prev; // 记录尾节点的直接前驱
	
	tailPrev->next = phead; // 尾节点的前驱节点的next指针指向头节点
	phead->prev = tailPrev; // 头节点的prev指针指向尾节点的前驱节点
	free(tail); // 释放尾节点 */

    ListErase(pHead->prev);
}


7、双向链表的头插

// 双向链表的头插
void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);

	/* ListNode* newnode = BuyListNode(x); // 申请新节点
	ListNode* pheadNext = phead->next; // 记录第一个节点
	
	// 头节点和新节点建立链接
	phead->next = newnode;
	newnode->prev = phead;

	// 新节点和第一个节点建立链接
	newnode->next = pheadNext;
	pheadNext->prev = newnode; */

    ListInsert(phead->next, x);
}


8、双向链表的头删

// 双向链表的头删
void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除

	/* ListNode* pheadNext = phead->next; // 记录第一个节点

	// 头节点和第一个节点的后继节点建立链接
	phead->next = pheadNext->next;
	pheadNext->next->prev = phead;
    free(pheadNext); // 头删 */

    ListErase(phead->next);
}


9、查找双向链表中的元素

// 在双向链表中查找元素,并返回该元素的地址
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;  //没找到 返回NULL
}

10、在指定pos位置之前插入元素

// 在指定pos位置之前插入元素
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* newnode = BuyListNode(x); // 申请一个节点
	ListNode* posPrev = pos->prev; // 记录pos的直接前驱

	// pos的直接前驱和新节点建立链接
	posPrev->next = newnode;
	newnode->prev = posPrev;

	// 新节点和pos建立链接
	newnode->next = pos;
	pos->prev = newnode;
}

实现了该函数后,可以尝试改进头插函数(pos相当于链表的第一个节点)和尾插函数(pos相当于链表的头节点),这样写起来更简便


11、删除指定pos位置的元素

// 删除指定pos位置的元素
void ListErase(ListNode* pos)
{
	assert(pos);

	ListNode* posPrev = pos->prev; // 记录pos的直接前驱
	ListNode* posNext = pos->next; // 记录pos的直接后继

	// pos的直接前驱和直接后继建立链接
	posPrev->next = posNext;
	posNext->prev = posPrev;
	
	free(pos); // 释放pos位置的元素
    //pos = NULL;
}

实现了该函数后,可以尝试改进函数(pos相当于链表的第一个节点)和尾删函数(pos相当于链表的最后一个节点),这样写起来更简便


12、双向链表的判空

// 双向链表的判空
bool ListEmpty(ListNode* phead)
{ 
	assert(phead);
	return phead->next == phead; //为空 返回ture 否则返回false
}

13、获取双向链表的元素个数

// 获取双向链表的元素个数
size_t ListSize(ListNode* phead)
{
	assert(phead);

	size_t size = 0;
	ListNode* cur = phead->next; // 记录第一个节点
	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

四、代码整合

// List.c
#include "List.h"

// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}

// 创建返回链表的头结点
ListNode* ListCreate()
{
	ListNode* phead = (ListNode*)malloc(sizeof(ListNode)); // 哨兵位头结点
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

// 双向链表的销毁
void ListDestroy(ListNode** pphead)
{
	assert(pphead);
	assert(*pphead);

	ListNode* cur = (*pphead)->next;
	while (cur != *pphead)
	{
		ListNode* next = cur->next; // 记录cur的直接后继节点
		free(cur);
		cur = next;
	}
	free(*pphead); // 释放头节点
	*pphead = NULL; // 置空头指针
}

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

	ListNode* cur = phead->next; // 记录第一个节点
	printf("head <-> ");
	while (cur != phead)
	{
		printf("%d <-> ", cur->data);
		cur = cur->next;
	}
	printf("head\n");
}

// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead); // 头指针不能为空
    ListInsert(phead, x);
}

// 双向链表的尾删
void ListPopBack(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除
    ListErase(pHead->prev);
}

// 双向链表的头插
void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);
    ListInsert(phead->next, x);
}

// 双向链表的头删
void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除
    ListErase(phead->next);
}

// 在双向链表中查找元素,并返回该元素的地址
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;  //没找到 返回NULL
}

// 在指定pos位置之前插入元素
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* newnode = BuyListNode(x); // 申请一个节点
	ListNode* posPrev = pos->prev; // 记录pos的直接前驱

	// pos的直接前驱和新节点建立链接
	posPrev->next = newnode;
	newnode->prev = posPrev;

	// 新节点和pos建立链接
	newnode->next = pos;
	pos->prev = newnode;
}

// 删除指定pos位置的元素
void ListErase(ListNode* pos)
{
	assert(pos);

	ListNode* posPrev = pos->prev; // 记录pos的直接前驱
	ListNode* posNext = pos->next; // 记录pos的直接后继

	// pos的直接前驱和直接后继建立链接
	posPrev->next = posNext;
	posNext->prev = posPrev;
	
	free(pos); // 释放pos位置的元素
    //pos = NULL;
}

// 双向链表的判空
bool ListEmpty(ListNode* phead)
{ 
	assert(phead);
	return phead->next == phead; //为空 返回ture 否则返回false
}

// 获取双向链表的元素个数
size_t ListSize(ListNode* phead)
{
	assert(phead);

	size_t size = 0;
	ListNode* cur = phead->next; // 记录第一个节点
	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

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

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

相关文章

开发一个RISC-V上的操作系统(五)—— 协作式多任务

目录 往期文章传送门 一、什么是多任务 二、代码实现 三、测试 往期文章传送门 开发一个RISC-V上的操作系统&#xff08;一&#xff09;—— 环境搭建_riscv开发环境_Patarw_Li的博客-CSDN博客 开发一个RISC-V上的操作系统&#xff08;二&#xff09;—— 系统引导程序&a…

NO4 实验四:生成Web工程

1、说明 使用 mvn archetype&#xff1a;generate 命令生成 Web 工程时&#xff0c;需要使用一个专门的 archetype。这个专门生成 Web 工程骨架的 archetype 可以参照官网看到它的用法&#xff1a; 2、操作 注意&#xff1a;如果在上一个工程的目录下执行 mvn archetype&…

LeetCode208.Implement-Trie-Prefix-Tree<实现 Trie (前缀树)>

题目&#xff1a; 思路&#xff1a; tire树&#xff0c;学过&#xff0c;模板题。一种数据结构与算法的结合吧。 代码是&#xff1a; //codeclass Trie { private:bool isEnd;Trie* next[26]; public:Trie() {isEnd false;memset(next, 0, sizeof(next));}void insert(strin…

第72篇:近年HVV、红队攻防比赛中常见外围打点漏洞的分析与总结

前言 大家好&#xff0c;我是ABC_123。前一段时间我花时间花精力总结了最近两三年中&#xff0c;在攻防比赛中比较常见的Web打点漏洞类型&#xff0c;捎带着把钓鱼邮件的主题类型也总结了一下。今天分享出来&#xff0c;相信无论是对于攻击方还是防守方&#xff0c;都能从中获…

【雕爷学编程】MicroPython动手做(14)——掌控板之OLED屏幕2

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

Spring学习笔记之spring概述

文章目录 Spring介绍Spring8大模块Spring特点 Spring介绍 Spring是一个轻量级的控制反转和面向切面的容器框架 Spring最初的出现是为了解决EJB臃肿的设计&#xff0c;以及难以测试等问题。 Spring为了简化开发而生&#xff0c;让程序员只需关注核心业务的实现&#xff0c;尽…

从源码角度去深入分析关于Spring的异常处理ExceptionHandler的实现原理

ExceptionHandler的作用 ExceptionHandler是Spring框架提供的一个注解&#xff0c;用于处理应用程序中的异常。当应用程序中发生异常时&#xff0c;ExceptionHandler将优先地拦截异常并处理它&#xff0c;然后将处理结果返回到前端。该注解可用于类级别和方法级别&#xff0c;…

【RabbitMQ】golang客户端教程1——HelloWorld

一、介绍 本教程假设RabbitMQ已安装并运行在本机上的标准端口&#xff08;5672&#xff09;。如果你使用不同的主机、端口或凭据&#xff0c;则需要调整连接设置。如果你未安装RabbitMQ&#xff0c;可以浏览我上一篇文章Linux系统服务器安装RabbitMQ RabbitMQ是一个消息代理&…

STL 关于vector的细节,vector模拟实现【C++】

文章目录 vector成员变量默认成员函数构造函数拷贝构造赋值运算符重载函数析构函数 迭代器beginend size和capacityresizereserve[ ]push_backpop_backinserteraseswap vector成员变量 _start指向容器的头&#xff0c;_finish指向容器当中有效数据的下一个位置&#xff0c;_end…

Python(五十一)获取列表中的多个元素——切片操作

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

【Rust教程 | 基础系列1 | Rust初相识】Rust简介与环境配置

教程目录 前言一&#xff0c;Rust简介1&#xff0c;Rust的历史2&#xff0c;Rust的特性3&#xff0c;为什么选择Rust4&#xff0c;Rust可以做什么 二&#xff0c; Rust环境配置1&#xff0c;windows11安装2&#xff0c;Linux安装 三&#xff0c;安装IDE 前言 Rust是一种系统编…

谈谈3D打印技术

目录 1.什么是3D打印 2.3D打印与传统打印技术的不同之处 3. 3D打印带来的技术变革 1.什么是3D打印 3D打印技术&#xff0c;也称为增材制造&#xff08;Additive Manufacturing&#xff09;&#xff0c;是一种将数字模型转化为实体物体的制造方法。它通过逐层添加材料的方式&a…

一文了解MySQL中的多版本并发控制

在开始之前&#xff0c;先抛出一个问题&#xff1a;我们都知道&#xff0c;目前&#xff08;MySQL 5.6以上&#xff09;数据库已普遍使用InnoDB存储引擎&#xff0c;InnoDB相对于MyISAM存储引擎其中一个好处就是在数据库级别锁和表级别锁的基础上支持了行锁&#xff0c;还有就是…

windows环境安装elasticsearch+kibana并完成JAVA客户端查询

下载elasticsearch和kibana安装包 原文连接&#xff1a;https://juejin.cn/post/7261262567304298554 elasticsearch官网下载比较慢&#xff0c;有时还打不开&#xff0c;可以通过https://elasticsearch.cn/download/下载&#xff0c;先找到对应的版本&#xff0c;最好使用迅…

24考研数据结构-第二章:线性表

目录 第二章&#xff1a;线性表2.1线性表的定义&#xff08;逻辑结构&#xff09;2.2 线性表的基本操作&#xff08;运算&#xff09;2.3 线性表的物理/存储结构&#xff08;确定了才确定数据结构&#xff09;2.3.1 顺序表的定义2.3.1.1 静态分配2.3.1.2 动态分配2.3.1.3 mallo…

MacOS Monterey VM Install ESXi to 7 U2

一、MacOS Monterey ISO 准备 1.1 下载macOS Monterey 下载&#x1f517;链接 一定是 ISO 格式的&#xff0c;其他格式不适用&#xff1a; https://www.mediafire.com/file/4fcx0aeoehmbnmp/macOSMontereybyTechrechard.com.iso/file 1.2 将 Monterey ISO 文件上传到数据…

更简单的读取和存储对象 (Bean)

怎样才能比之前更简单的 读取和存储对象 (Bean) 呢? 答: 就两个字"使用注解", 接下来就说说如何利用注解来更简单的操作 Bean 目录 一. 前置工作 (配置扫描路径) 二. 使用注解存储 Bean 2.1 注解介绍 2.1.1 类注解存储 Bean 的默认命名规则 2.2 Controller (控…

手把手移植 simpleFOC (四):pwm 六相 篇

文章目录 系列文章目录前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 今天移植的内容&#xff0c;为定时器生在pwm&#xff0c;能按矢量数据控制电机到相应的位置 一、定时器的配置 通读了simpleFoc的代码&#xff0c;准备让定时器1生成的pwm波…

【玩转python系列】【小白必看】使用Python爬虫技术获取代理IP并保存到文件中

文章目录 前言导入依赖库打开文件准备写入数据循环爬取多个页面完整代码运行效果结束语 前言 这篇文章介绍了如何使用 Python 爬虫技术获取代理IP并保存到文件中。通过使用第三方库 requests 发送HTTP请求&#xff0c;并使用 lxml 库解析HTML&#xff0c;我们可以从多个网页上…

要单片机和RTOS有必要学习嵌入式linux吗?

学习嵌入式 Linux 是否有必要&#xff0c;取决于你的项目需求和职业发展目标。以下是一些考虑因素&#xff1a; 项目需求&#xff1a;如果你的项目需要处理复杂的网络、文件系统、多任务管理等功能&#xff0c;嵌入式 Linux 可能是更适合的选择。Linux 提供了丰富的开源软件包和…