双向链表常见接口实现

一 . 链表的分类

链表的结构非常多样,以下情况组合起来就有8种( 2 * 2 * 2 ) 链表结构 : 

1.1 单向/双向

1 . 单向 : 只能往一个方向遍历 (仅有一个指针 --> 指向下一个结点的地址), 如下图 : 只能从d1找到d2 , d2 找不到d1

2 . 双向 : 能从两个方向遍历 ( 有指向下一个结点的地址--后继,也有指向上一个结点的地址---前驱) , 如下面的第二幅图 , d2 可以找到d1 , 也可找到d3

1.2 带头/不带头

注意 : 这里的  “带头”   跟前面我们说的   “头结点”   是两个概念!
前面讲单链表的时候,会表述   “头结点”---> 链表首结点(第一个结点),这并不严谨,只是为了更好的去理解  链表首个位置有效  的结点 , 实际上这个表述是错误的

因为链表分类中存在 一种带头链表,里面的头结点不存储任何有效元素 , 只是负责占位置(哨兵位)


"哨兵位" ---  作用 : 站在这里“放哨” ,不需要判断链表是否为空!因为在对链表进行插入等操作时,要先判断链表是否为空再执行,这样代码重复率很高,有些冗余。

如果带头链表中只有头结点 , 我们称这个链表为空 

 

1.3 循环/不循环 

结合以上分析 : 我们可以推出

--------->单链表 : 单向不带头不循环链表 

在接下来学习的双链表是:

--------->双链表 : 双向带头循环链表

我们可以这么去记 , 双链表和单链表的各类的类型都相反

虽然有这么多的类型 : 

但是常用的还是单链表和双链表:

1 . 单链表 : 结构简单 , 一般不会单独用来存储数据 . 实际上更多的是左右数据结构的子结构 , 如哈希图 , 图的邻接表等 , 另外的这种结构在笔试面试中会出现很多

2 . 双链表 : 结构复杂,一般单独存储数据.实际中使用的链表结构,基本上都是双链表(双向带头循环链表) , 虽说结构复杂 , 但是代码实现后会发现结构会带来很多优势,实现反而简单了!

二 . 双向链表常见接口实现

2.1 双链表的结构

带头链表⾥的头结点,实际为“哨兵位”,哨兵位结点不存储任何有效元素,只是站在这里“放哨的”,在进行插入等操作时 , 无需判断链表是否为空 .

2.2 初始化

两种方式 : 

1 . 返回值的方式(返回结点)   ---> 主要使用这种方法

2 . 传参数形式

(初始化的时候 , 要保证是循环的 ----> 自己指向自己)

 方式一 : 返回值的方式 

方式二 : 参数形式 

注意 : 初始化时 , 需要形参的改变影响实参 , 所以需要传地址 , 这里需要用到二级指针!

2.3 尾插

注意 : 

在进行尾插的时候 , 不需要像单链表一样使用二级指针 , 因为"哨兵位"phead的结点不会改变 , 尾插一个结点是通过访问结点的成员变量来实现的 .

1 . 创建一个新节点 : 向操作系统申请一块结点大小的空间 ---> 存储需要尾插的结点 , 在后续可能还需要再插入结点 , 这里可以先把创建一个结点的代码进行封装(buyNode)

2 . 尾插 : 尾插时 , 先对newnode 指向进行更改 , 这样可以保证原链表的指向不被改变 ,对于尾插的逻辑思考不明白的 , 可以先画图 , 然后思考尾插一个结点时 , 有影响的结点有什么 ? 影响的指向是什么 ?

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = buyNode(x);
	//phead phead->prev  newnode
	newnode->prev = phead->prev;
	newnode->next = phead;
	phead->prev->next = newnode;
	phead->prev = newnode;
}
//申请一块结点大小的空间 --- 创建新结点
LTNode* buyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail!");
		return 1;
	}
	node->data = x;
	node->next = node->prev = node;
}

2.4 头插

头插需要注意的是 : 往哪里插入结点?

==> 插入到"哨兵位"之后 , 第一个有效结点之前 !

==> 如果插入到"哨兵位"之前 , 就是尾插了!

void LTPushFront(LTNode* phead, LTDataType x)
{
	LTNode* newnode = buyNode(x);
	newnode->next = phead->next;
	newnode->prev = phead;
	phead->next->prev = newnode;
	phead->next = newnode;
}

 

2.5 打印链表

打印链表意味着我们需要遍历链表 , 涉及遍历就需要思考结束条件 , 在单链表中 , 遍历结束的条件是 pucr == NULL 时终止 , 在双向链表中 , 不存在NULL, 而具有独特标志性的结点 , 就是"哨兵位"  -- > 头结点 !!!

所以当 pcur == phead 时候 , 就终止遍历 !

//打印链表
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

2.6 判断链表是否为空

在双向链表中 , 当只存在一个 " 哨兵位" 的时候 ,判断双向链表为空 , 此时 的head , next 指针都指向"哨兵位" 自己

//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

2.7 尾删

1 . 尾删时 , 先判断链表是否为空 ---> 断言 --> 调用LTEmpty 函数

2 . 明确删除尾结点所影响的结点

3 . 改变有影响结点的指向

4 . 删除尾结点 (free)

//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->prev;
	//phead del->prv del
	del->prev->next = phead;
	phead->prev = del->prev;

	//删掉尾结点
	free(del);
	del = NULL;
}

2.8 头删

1 . 头删时 , 先判断链表是否为空 ---> 断言 --> 调用LTEmpty 函数

2 . 明确删除头结点所影响的结点( head  del  del->next )

3 . 改变结点的指向

4 . 删除头结点( free)

//头删
void LTPopFront(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->next;
	//phead  del  del->next
	del->next->prev = phead;
	phead->next = del->next;
	free(del);
	del = NULL;
}

2.9 查找数据

定义一个指针变量 (pcur) , 指向链表中第一个有效的结点( head->next) , 开始遍历 , 如果找到了 , 返回该结点 ,  没找到 , 返回NULL

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	//查找有效结点
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}
	//查找
	LTNode* find =  LTFind(plist, 1);
	if (find == NULL)
	{
		printf("没找到!\n");
	}
	else {
		printf("找到了\n");
	}

}

2.10 指定位置插入数据

注意 : 在指定位置之前 或者指定位置之后插入数据 , 代码基本是一样的 , 因为双链表是循环的 

1 . 向操作系统申请一块结点的空间

2 . 找到在指定位置插入数据所影响的结点

3 . 改变结点的方向

//在指定位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = buyNode(x);
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next->prev = newnode;
	pos->next = newnode;
}

2.11 指定位置删除数据

1 . 明确要删除的结点所影响的结点 ( pos->prev , pos , pos->next )

2 . 改变结点方向

//删除指定位置的数据
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
}

2.12 销毁链表

链表中的结点是独立存在的 , 所以销毁链表的时候 , 需要一个一个结点的释放空间 , 同时也包括头指针 !  

1 . 定义一个指针变量 , 指向头结点的下一个结点 ( pcur = head->next)

2 . 定义一个指针变量 next, 存储pcur的下一个结点 ---> pcur 指向的结点释放完之后 , 继续释放下一个结点

3 . 遍历链表

4 . 释放头结点 

这里传的是二级指针 , 因为需要形参的改变影响实参 (把哨兵位也给释放掉 , 并置为NULL)


//销毁链表
void LTDestory(LTNode** pphead)
{
	assert(pphead);
	LTNode* pcur = (*pphead)->next;
	while (pcur != (*pphead))
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(*pphead);
	*pphead = NULL;
}

代码写到这里时 , 我们可以发现 , 除了以上的销毁链表的代码 , 其他使用的都是一级指针 , 为了减少我们的记忆成本 , 并保持代码接口一致性  , 我们可以使用一级指针 , 但是 ,一级指针中形参的改变并不影响实参 , 最后头结点的地址并不会改变 , 所以我们还需要额外的把地址置为NULL

//销毁链表 -- 一级指针
void LTDestory(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}
	//销毁链表 -- 一级指针
	LTDestory(plist);
	plist = NULL;

 2.13 总代码

test .c

#include "List.h"

void Test()
{
	//初始化 -- 返回值的方式
	LTNode* plist = LTInit();

	//初始化 -- 传参的方式
	//LTNode* plist = NULL;
	//LTInit(&plist);

	//尾插
	//LTPushBack(plist, 1);
	//LTPushBack(plist, 2);
	//LTPushBack(plist, 3);
	//LTPushBack(plist, 4);
	//LTPushBack(plist, 5);

	//头插
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);

	//尾删
	//LTPopBack(plist);
	//LTPrint(plist);

	//头删
	//LTPopFront(plist);
	//LTPrint(plist);	

	//查找
	LTNode* find =  LTFind(plist, 1);
	if (find == NULL)
	{
		printf("没找到!\n");
	}
	else {
		printf("找到了\n");
	}

	//在指定位置之后插入数据
	//LTInsert(find, 99);
	//LTPrint(plist);

	//删除指定位置的数据
	//LTErase(find);
	//LTPrint(plist);

	//销毁链表 -- 二级指针
	//LTDestory(&plist);
	//销毁链表 -- 一级指针
	LTDestory(plist);
	plist = NULL;
}
int main()
{
	Test();
	return 0;
}

List . c

#include "List.h"

//申请一块结点大小的空间 --- 创建新结点
LTNode* buyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail!");
		return 1;
	}
	node->data = x;
	node->next = node->prev = node;
}

//初始化链表 -- 返回值方式
LTNode* LTInit()
{
	//LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	//phead->data = -1;
	//phead->next = phead->prev = phead;
	LTNode* phead = buyNode(-1);
	return phead;
}

//初始化 -- 传参方式
//void LTInit(LTNode** pphead)
//{
//	*pphead= (LTNode*)malloc(sizeof(LTNode));
//	(*pphead)->data = -1;
//	(*pphead)->next = (*pphead)->prev = (*pphead);
//}

//打印链表
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = buyNode(x);
	//phead phead->prev  newnode
	newnode->prev = phead->prev;
	newnode->next = phead;
	phead->prev->next = newnode;
	phead->prev = newnode;
}

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	LTNode* newnode = buyNode(x);
	newnode->next = phead->next;
	newnode->prev = phead;
	phead->next->prev = newnode;
	phead->next = newnode;
}


//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->prev;
	//phead del->prv del
	del->prev->next = phead;
	phead->prev = del->prev;

	//删掉尾结点
	free(del);
	del = NULL;
}

//头删
void LTPopFront(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->next;
	//phead  del  del->next
	del->next->prev = phead;
	phead->next = del->next;
	free(del);
	del = NULL;
}

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	//查找有效结点
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

//在指定位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = buyNode(x);
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next->prev = newnode;
	pos->next = newnode;
}

//删除指定位置的数据
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
}

//销毁链表 -- 二级指针
//void LTDestory(LTNode** pphead)
//{
//	assert(pphead);
//	LTNode* pcur = (*pphead)->next;
//	while (pcur != (*pphead))
//	{
//		LTNode* next = pcur->next;
//		free(pcur);
//		pcur = next;
//	}
//	free(*pphead);
//	*pphead = NULL;
//}

//销毁链表 -- 一级指针
void LTDestory(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}

List . h

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

//定义双向链表(结点)结构
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

//初始化双链表
//初始化 -- 返回值的方式
LTNode* LTInit();
// 
//初始化 -- 传参方式
//void LTInit(LTNode** pphead);

//判断是否为空
bool LTEmpty(LTNode* phead);
//打印链表
void LTPrint(LTNode* phead);

//尾插
void LTPushBack(LTNode* phead, LTDataType x);

//头插
void LTPushFront(LTNode* phead, LTDataType x);

//尾删
void LTPopBack(LTNode* phead);

//头删
void LTPopFront(LTNode* phead);

//查找
LTNode* LTFind(LTNode* phead , LTDataType x);

//在指定位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);

//删除指定位置的数据
void LTErase(LTNode* pos);

//销毁链表
void LTDestory(LTNode** pphead);

 三 . 顺序表与链表的分析

注 : 两者并没有谁一定好于谁的说法 ! 不同情形 , 不同需求 , 使用不同的数据结构 , 在后续中我们还会学习更多的数据结构 , 方便我们解决更多的问题 , 敬请期待

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

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

相关文章

Leetcode 841. 钥匙和房间

1.题目基本信息 1.1.题目描述 有 n 个房间&#xff0c;房间按从 0 到 n – 1 编号。最初&#xff0c;除 0 号房间外的其余所有房间都被锁住。你的目标是进入所有的房间。然而&#xff0c;你不能在没有获得钥匙的时候进入锁住的房间。 当你进入一个房间&#xff0c;你可能会在…

深度解析服务级别协议(SLA):保障业务稳定性的关键承诺

前言&#xff1a; 在当今数字化时代&#xff0c;企业的业务连续性和稳定性至关重要。服务级别协议&#xff08;SLA&#xff09;作为服务提供商与客户之间的正式承诺&#xff0c;是确保服务质量、可用性、责任的重要工具。SLA不仅定义了服务提供商应达到的服务指标&#xff0c;还…

华为ENSP Truck命令指南:从入门到精通的全方位解析 引言

博客标题&#xff1a;华为ENSP Truck命令指南&#xff1a;从入门到精通的全方位解析 引言 在网络工程的世界里&#xff0c;华为ENSP&#xff08;Enterprise Network Simulation Platform&#xff09;作为一款强大的网络模拟工具&#xff0c;为我们提供了在虚拟环境中实践和学习…

MySQL程序介绍<一>

目录 MySQL程序简介 mysqld - MySQL 服务器 ​编辑 mysql - MySQL 命令⾏客⼾端 MySQL程序简介 1.MySQL安装完成通常会包含如下程序&#xff1a; Linux系统程序⼀般在 /usr/bin⽬录下&#xff0c;可以通过命令查看 windows系统⽬录&#xff1a; 你的安装路径\MySQL Server…

【Linux-基础IO】软硬链接+动静态库

一、软硬链接 见一见 软连接 硬连接 通过观察我们发现以下几点&#xff1a; 1.ll - i后&#xff0c;软连接形成的文件有指向&#xff0c;并且软连接的Inode编号与对应文件的Inode编号不一样 2.ll - i后&#xff0c;硬连接形成的文件与对应的文件Inode编号一样 3.软连接…

从零开始在Windows系统上搭建一个node.js后端服务项目

目录 一、下载node.js及配置环境 二、搭建node.js项目及安装express框架 三、集成nodemon&#xff0c;实现代码热部署 四、Express 应用程序生成器 一、下载node.js及配置环境 网上很多安装教程&#xff0c;此处就不再赘述了 版本信息 C:\Users\XXX>node -v v20.15.0…

一个纹理分割的例子

纹理是图像分割常用的特征&#xff0c;即使不是纹理分割。Rafael Gonzalez和Richard Woods的《数字图像处理》中这部分内容片面了。 给一个利用简单的统计特征——熵进行纹理分割的例子。这个例子是说明阈值分割不是仅适用于灰度值的情况&#xff0c;也可以用于纹理&#xff…

【into outfile写文件】

简介 select * from user into outfile C:/Users/ichunqiu/Desktop/PhpStudy2018/PHPTutorial/WWW/1.txt;用法的意思就是把user表中查询到的所有字段都导出到1.txt文件中 我们之前还有学到dumpfile&#xff0c;单是它只能导出一条数据 写入shell 测试注入点 usernameadmin&…

SAP SD学习笔记09 - 受注传票中的不完全Log 和 Business Partner(取引先机能)

好久没写SD了&#xff0c;今天继续写。 上一章讲了SD的如下知识 - SD的售前的流程&#xff08;引合和見積&#xff08;询价和报价&#xff09;&#xff09; - 数据流的概念&#xff0c;主要就是后传票可以参照前传票&#xff0c;以实现数据的流动&#xff0c;减少输入 - Co…

C++之“构造函数”

文章目录 类的默认成员函数构造函数 类的默认成员函数 默认成员函数就是我们没有在main函数里调用&#xff0c;但是编译器会自动生成的成员函数称为默认成员函数。 C由8个默认成员函数&#xff0c;我们暂时了解6个。 默认成员函数&#xff1a;构造函数&#xff0c;析构函数&a…

面试应该问什么?

在求职者面试的过程中&#xff0c;向面试官提问是一个展现自己积极态度、对职位和公司兴趣以及进一步了解工作环境和职业发展机会的重要环节。以下是一些求职者可以在面试中向面试官提问的问题&#xff0c;这些问题旨在帮助你更全面地了解未来的工作环境、团队文化、以及个人职…

adb devices没找到安卓设备的解决办法

要想让设备让adb识别到&#xff0c;要开启设备的开发者模式&#xff0c;并且开启USB调试功能&#xff1a; 然后重新运行&#xff1a;就找到了

java 文件File类概述

前言 在Java中&#xff0c;File类是一个与文件和目录&#xff08;文件夹&#xff09;路径名相关的抽象表示形式。它是java.io包中的一个重要类&#xff0c;用于表示和操作文件系统中的文件和目录。 File类的基本概念 表示路径&#xff1a;File类既可以表示文件路径&#xff…

OpenCV高级图形用户界面(8)在指定的窗口中显示一幅图像函数imshow()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 在指定的窗口中显示一幅图像。 函数 imshow 在指定的窗口中显示一幅图像。如果窗口是以 cv::WINDOW_AUTOSIZE 标志创建的&#xff0c;图像将以原…

操作系统 和 初识进程

目录 操作系统&#xff08;OS&#xff09; 进程 操作系统&#xff08;OS&#xff09; 概念 操作系统即os&#xff0c;是一款软件。 任何计算机系统都包含一个基本的程序集合&#xff0c;称为操作系统(OS)。 操作系统的本质是一种进行软硬件管理的软件 笼统的理解&#xf…

用Java做智能客服,基于私有知识库

构建Java智能客服系统的整体思路 使用Java构建智能客服系统的整体思路是&#xff1a; 首先将客服QA文档以Word形式导入到系统中&#xff0c;通过向量化处理存入知识库。 当用户提出问题时&#xff0c;系统会根据问题内容从知识库中检索相关的上下文信息&#xff0c;并结合大…

字节跳动实习生投毒自家大模型细节曝光 影响到底有多大?

10月19日&#xff0c;字节跳动大模型训练遭实习生攻击一事引发广泛关注。据多位知情人士透露&#xff0c;字节跳动某技术团队在今年6月遭遇了一起内部技术袭击事件&#xff0c;一名实习生因对团队资源分配不满&#xff0c;使用攻击代码破坏了团队的模型训练任务。 据悉&#xf…

【动态规划】【斐波那契数列模型】解码方法

解码方法 91. 解码方法 算法原理 确定状态表示 经验题目要求&#xff1a;以 i 位置为结尾dp[i] 表示以 i 位置为结尾时&#xff0c;解码方法的总数 状态转移方程 定义好状态表示&#xff0c;我们就可以分析 i 位置的 dp 值&#xff0c;如何由 [前面] 或者 [后面] 的信息推…

Leetcode 1137. 第 N 个泰波那契数

原题链接&#xff1a;Leetcode 1137. 第 N 个泰波那契数 代码1&#xff1a; class Solution { public:int a[40];int tribonacci(int n) {a[0]0;a[1]1;a[2]1;if(n<1) return n;if(a[n]) return a[n];a[n]tribonacci(n-1)tribonacci(n-2)tribonacci(n-3);return a[n];} };代…

【LeetCode】每日一题 2024_10_19 使二进制数组全部等于 1 的最少操作次数 II(贪心)

前言 每天和你一起刷 LeetCode 每日一题~ LeetCode 启动&#xff01; 题目&#xff1a;使二进制数组全部等于 1 的最少操作次数 II 力扣每日一题刷新规律&#xff0c;昨天刷新了 I&#xff0c;那今天必定有 II。 代码与解题思路 今天的题目和昨天的非常像&#xff0c;只有一…