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

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

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.mfbz.cn/a/55711.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode[面试题04.08]首个共同祖先

难度&#xff1a;Medium 题目&#xff1a; 设计并实现一个算法&#xff0c;找出二叉树中某两个节点的第一个共同祖先。不得将其他的节点存储在另外的数据结构中。注意&#xff1a;这不一定是二叉搜索树。 例如&#xff0c;给定如下二叉树: root [3,5,1,6,2,0,8,null,null,7,…

Shiro框架基本使用

一、创建maven项目&#xff0c;引入依赖 <dependencies><dependency><groupId>org.apache.directory.studio</groupId><artifactId>org.apache.commons.codec</artifactId><version>1.8</version></dependency><!-- …

【Redis深度专题】「核心技术提升」探究Redis服务启动的过程机制的技术原理和流程分析的指南(集群指令分析—上篇)

探究Redis服务启动的过程机制的技术原理和流程分析的指南&#xff08;Redis集群管理&#xff09; Redis集群管理查看集群中各个节点状态集群(cluster)cluster info的执行效果指令结果分析 cluster nodes的执行效果指令结果分析 节点(node)CLUSTER MEETCLUSTER FORGETCLUSTER RE…

Excel透视表与python实现

目录 一、Excel透视表 1、源数据 2、数据总分析 3、数据top分析 二、python实现 1、第一张表演示 2、第二张表演示 一、Excel透视表 1、源数据 1&#xff09;四个类目&#xff0c;每类50条数据 2&#xff09;数据内容 2、数据总分析 1&#xff09;选择要分析的字段&…

TCP的三次握手以及四次断开

TCP的三次握手和四次断开&#xff0c;就是TCP通信建立连接以及断开的过程 目录 【1】TCP的三次握手过程 ---- TCP建立连接的过程 【2】TCP的四次挥手 ---- TCP会话的断开 注意&#xff1a; 【1】TCP的三次握手过程 ---- TCP建立连接的过程 三次握手的过程&#xff1a…

TPC-DS 标准介绍、工具下载地址

目录 一、TPC-DS标准介绍 1. DMS介绍 2. TCP-DS概念 二、数据库模型 1. 数据库模型介绍 2. 数据库模型包含内容 三、数据生成器 1. 数据生成器介绍 2. 数据生成器包含内容 四、查询集合 1. 查询集合介绍 2. 查询集合包含的88个标准化查询和17个基准统计函数 五、性…

easyui实用点

easyui实用点 1.下拉框&#xff08;input框只能选不能手动输入编辑&#xff09; data-options"editable:false"//不可编辑2.日期框&#xff0c;下拉框&#xff0c;文本框等class class"easyui-datebox"//不带时分秒 class"easyui-datetimebox"…

【C++】C++入门

1.C关键字 2.命名空间 变量、函数和后面学到的类都是大量存在的&#xff0c;这些变量、函数和名称都将存在于全局作用域中&#xff0c;可能会导致一些冲突&#xff0c;比如命名冲突。使用命名空间的目的是对标识符的名称进行本地化&#xff0c;以避免命名冲突和名字污染。 2.1…

Oracle设置某个表字段递增

当Oracle设置字段递增创建触发器 先建一个序列&#xff0c;打开PLSQL 找到Sequences&#xff0c;右击新建 根据自己的需要填写 然后添加触发器&#xff0c;点新建-程序窗口-空白 --TEST_ID为触发器的名字&#xff0c;TEST是添加触发器的表名 CREATE OR REPLACE TRIGGER &qu…

【Ubuntu 18.04 搭建 DHCP 服务】

参考Ubuntu官方文档&#xff1a;https://ubuntu.com/server/docs/how-to-install-and-configure-isc-dhcp-server dhcpd.conf 手册页 配置&#xff1a;https://maas.io/docs/about-dhcp 实验环境规划 Ubuntu 18.04&#xff08;172.16.65.128/24&#xff09;dhcp服务端Ubuntu…

记录--一个好用的轮子 turn.js 实现仿真翻书的效果

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 国际惯例&#xff0c;官网链接 官网传送门 Github地址 github上有几个demos例子&#xff0c;介绍了基础用法。 我参考官网的例子&#xff0c;写了一个demo示例 安装 turn.js 依赖 jquery 库&#xff0…

InnoDB引擎底层逻辑讲解——架构之磁盘架构

1. System Tablespaces区域 系统表空间是change buffer&#xff08;更改缓冲区&#xff09;的存放区域&#xff0c;这是在8.0之后重新规划的&#xff0c;在5.x版本的时候&#xff0c;系统表空间还会存放innodb的数据字典undolog日志等信息&#xff0c;在8.0之后主要主要存放更…

APP开发入门:了解主流的编程语言

在过去的几年里&#xff0c;有许多程序员开始学习和使用编程语言。这其中包括C、C、 Java和 Python。尽管有许多语言可供选择&#xff0c;但大多数程序员都会选择最容易学习的编程语言。 如今&#xff0c;有很多编程语言供选择。程序员们在学习这些语言时可以自由地选择他们喜…

原子操作的重要性

原子操作&#xff1a;要么不做&#xff0c;要么一次性做完 非原子操作 其实ABCD都是对的。 B选项&#xff1a;正常执行&#xff0c;I线程执行2条语句全部执行完毕,再执行II线程重新执行一遍foo函数。 C选项&#xff1a;先执行I线程foo函数第一行代码&#xff0c;然后跳转执行…

蓝牙、GPS定位学习

启动状态&#xff08;APP&#xff09; 冷启动 指在启动应用时&#xff0c;后台没有应用的进程或者进程被杀死的情况下&#xff0c;系统会重新创建一个新的进程&#xff0c;并按照一定的顺序创建和初始化Application类和MainActivity类&#xff0c;最后显示在界面上。这个过程需…

深度学习技巧应用24-深度学习手撕代码与训练流程的联系记忆方法

大家好,我是微学AI,今天给大家介绍一下深度学习技巧应用24-深度学习手撕代码与训练流程的联系记忆方法,大家都知道深度学习模型训练过程是个复杂的过程,这个过程包括数据的收集,数据的处理,模型的搭建,优化器的选择,损失函数的选择,模型训练,模型评估等步骤,其中缺少…

/bin/bash: Resource temporarily unavailable

有现场反馈plsql无法连接数据库了&#xff0c;登录环境查看时发现从root切换到grid时报错/bin/bash: Resource temporarily unavailable [rootdb1 ~]# su - grid Last login: Thu Jul 27 18:45:04 CST 2023 su: failed to execute /bin/bash: Resource temporarily unavailab…

springboot 入门

前提是已安装java环境&#xff0c;分为三部分 一、项目构建 二、项目组成 三、常用注解 Demo源码 spring-demo: springboot 入门项目 一、springboot-stater 使用IDEA快速构建springboot项目 1、新建项目 2、选择maven&#xff0c;在选择next 3、填写好项目信息 4、pom…

学习购药系统源码:从前端到后端的技术探索

本文将带领读者探索购药系统源码&#xff0c;从前端到后端逐步深入&#xff0c;了解其核心功能和实现方式。我们将使用常见的Web技术&#xff0c;包括HTML、CSS、JavaScript、以及Python的Django框架&#xff0c;展示购药系统的技术奥秘。 前端技术探索 HTML结构搭建 购药系…

el-cascader级联选择器加载远程数据、默认开始加载固定条、可以根据搜索加载远程数据。

加载用户列表分页请求、默认请求20条数据。想添加远程搜索用户功能。原有的方法filter-method不能监听到输入清空数据的时候。这样搜索完无法返回默认的20条数据。直接监听级联选择的v-model绑定的值是无法检测到用户自己输入的。 解决思路&#xff1a; el-cascader 没有提供…
最新文章