深入理解链表(SList)操作

news2024/11/27 14:44:22

目录:

  • 一、 链表介绍
    • 1.1、 为什么引入链表
    • 1.2、 链表的概念及结构
    • 1.3、 链表的分类
  • 二、 无头单向非[循环链表](https://so.csdn.net/so/search?q=循环链表&spm=1001.2101.3001.7020)的实现
    • 2.1、 [单链表](https://so.csdn.net/so/search?q=单链表&spm=1001.2101.3001.7020)的定义
    • 2.2、 动态申请一个节点
    • 2.3、 销毁(释放)所有节点
    • 2.4、 打印单链表
    • 2.5、 单链表尾插
    • 2.6、 单链表头插
    • 2.7、 单链表尾删
    • 2.8、 单链表头删
    • 2.9、 在单链表中查找指定值的节点
    • 2.10、 单链表在pos位置之后插入
    • 2.11、 单链表删除指定pos位置的节点
    • 2.12、 单链表删除指定pos位置之后的节点
    • 2.13、 求单链表长度
    • 2.14、 判断单链表是否为空
  • 三、 总结

学习链表之前,建议先学习下顺序表,下面是顺序表的直达链接:

深入理解顺序表(SeqList)

一、 链表介绍

1.1、 为什么引入链表

  • 学习链表之前,先让我们来思考一个问题:

为什么有了顺序表,还需要有链表这样的数据结构呢?

  • 顺序表存在的一些问题:
  1. 顺序表在中间/头部的插入删除,要挪动很多数据,时间复杂度为O(N),效率太低了。
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是一次增长2倍,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们
    再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间SeqList。
  • 为了更好的解决上述问题,引入了链表。

1.2、 链表的概念及结构

  • 概念

前面学习的顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,而链表是一种物理存储结构上不连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,可以实现更加灵活的动态内存管理。

  • 链表的组成

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。

每个结点包括两个部分:

1、数据域:存储数据元素

2、指针域:存储下一个结点地址

  • 链表的物理结构

可以看到,4个节点的地址并不是连续的,链表在物理结构上不一定是线性的,而在逻辑结构上是线性的

image-20210824110616542

  • 链表的逻辑结构(想象出来的)

image-20210822225322883

  • 注意

1、链式结构在逻辑上是连续的,但在物理上不一定连续

2、链表的节点是在堆上申请出来的

1.3、 链表的分类

链表的结构非常多样化

  • 单向、双向

image-20210827235931988

  • 带头结点、不带头节点(哨兵位的头节点,不存储有效数据)

image-20210828002001494

  • 非循环、循环

image-20210827235906305

  • 常用的两种结构

image-20210828000023080

二、 无头单向非循环链表的实现


2.1、 单链表的定义

image-20210831212953194

typedef int SLTDataType;

//定义单链表节点
typedef struct SListNode
{
	SLTDataType data;        //数据域
	struct SListNode* next;  //指针域
}SLTNode;

2.2、 动态申请一个节点

//动态申请一个节点
SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)  //检查是否开辟成功
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

2.3、 销毁(释放)所有节点

//销毁单链表中所有节点
void SLTDestory(SLTNode** pphead)
{
	assert(pphead);

	SLTNode* cur = *pphead;
	while (cur != NULL)  //遍历链表
	{
		SLTNode* next = cur->next;  //保存cur的下一个节点
		free(cur);  //释放节点
		cur = next;
	}
	*pphead = NULL;  //z
}
  • 作用:

销毁单链表中所有节点,指向头节点的指针置空,防止内存泄露等问题


2.4、 打印单链表

//打印单链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* ptr = phead;
	while (ptr != NULL)
	{
		printf("%d->", ptr->data);
		ptr = ptr->next;
	}
	printf("NULL\n");
}

2.5、 单链表尾插

  • 先来看一种错误写法:这个也是弄懂单链表的关键

传一级指针的值,用一级指针接收

  • 这种写法会导致一个问题:

因为当链表为空时,我们需要改变 plist 的指向,使其指向第一个节点。

而初始 plist 和 phead 都指向 NULL,调用函数后,phead 指向了新的节点,而 plist 还是指向 NULL 的。

  • 如何解决呢:

plist 是指向第一个节点的指针,想要在函数中改变 plist 的值(指向),必须要把 plist指针的地址 作为实参传过去,形参用 二级指针 接收,这样在函数中对二级指针解引用得到 plist 的值,就可以改变 plist 的值(指向)了

记住:在函数里面要改变 int,则要传 int* ,要改变 int* ,则要传 int**

  • 注意区分开这几个代表的意思哦

平时一般不用解引用两层,没啥意义,一般是解引用一层,来改变外面 plist 的指向。

  • **正确写法:**通过二级指针改变外面 plist 的指向

单链表为空时,plist 直接指向新节点;

单链表不为空时,先找到单链表的尾节点,然后将尾节点的next指向新节点

//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);  //检查参数是否传错

	SLTNode* newnode = BuySListNode(x);  //动态申请一个节点

	if (*pphead == NULL)  //单链表中没有节点时
	{
		*pphead = newnode;  //plist指向新节点
	}
	else if (*pphead != NULL)  //单链表中已经有节点时
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)  //找到单链表中的最后一个节点
		{
			tail = tail->next;
		}
		tail->next = newnode;  //最后一个节点的next指向新节点
	}
}
  • 运行结果如下:

在这里插入图片描述


2.6、 单链表头插

  • 图解头插操作

image-20210824230705865

  • 代码如下

因为要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址

//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);  //检查参数是否传错

	SLTNode* newnode = BuySListNode(x);  //动态申请一个节点

	newnode->next = *pphead;  //新节点的next指针指向plist指向的位置
	*pphead = newnode;  //plist指向头插的新节点
}
  • 运行结果如下:

在这里插入图片描述


2.7、 单链表尾删

  • 图解尾删操作

image-20210824224625868

  • 代码如下

单链表只有一个节点时,删除节点,plist 指向 NULL;

单链表有多个节点时,先找到单链表尾节点的上一个节点,删除尾节点,然后将该节点的next指向 NULL;

因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。

//单链表尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);   //检查参数是否传错
	assert(*pphead);  //断言,链表不能为空

	SLTNode* tail = *pphead;

	if ((*pphead)->next == NULL)  //链表只有一个节点
	{
		free(*pphead);   //删除节点
		*pphead = NULL;  //plist置空
	}
	else  //链表中有多个节点
	{
		while (tail->next->next != NULL)  //找到链表的尾节点的上一个节点
		{
			tail = tail->next;
		}
		free(tail->next);   //删除尾节点
		tail->next = NULL;  //置空
        
        /*思路2:
        SLTNode* prev = *pphead;
		while (tail->next)  //找到链表的尾节点和它的上一个节点
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);         //删除尾节点
		prev->next = NULL;  //置空
		*/
	}
}
  • 运行结果如下:

在这里插入图片描述


2.8、 单链表头删

  • 图解头删操作

image-20210825001225011

  • 代码如下

因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。

//单链表头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);   //检查参数是否传错
	assert(*pphead);  //链表不能为空

	SLTNode* cur = *pphead;  //保存头节点的地址

	*pphead = cur->next;  //plist指向头节点的下一个节点
	free(cur);  //删除头节点
}
  • 运行结果如下:

在这里插入图片描述


2.9、 在单链表中查找指定值的节点

如果查找到,返回该节点的地址;没有查找到,返回 NULL。

//在单链表中查找指定值节点
SListNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	//遍历链表
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;  //找到了,返回该节点的地址
		}
		cur = cur->next;
	}
	return NULL;  //未找到,返回NULL
}

2.10、 单链表在pos位置之后插入

  • 图解插入操作

image-20210827234957091

  • 分析思考为什么不在pos位置之前插入?

单链表不适合在pos位置之前插入,因为需要遍历链表找到pos位置的前一个节点,

单链表更适合在pos位置之后插入,如果在后面插入,只需要知道pos位置就行,会简单很多

C++官方库里面单链表给的也是在之后插入

  • 代码如下
//单链表在指定pos位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);  //给的pos位置不能为空

	SLTNode* newnode = BuySListNode(x);  //动态申请一个节点

	newnode->next = pos->next; //新节点的next指针指向pos位置后一个节点
	pos->next = newnode;       //pos位置的next指向新节点
}
  • 作用:

先调用 SLTFind 函数在单链表中查找指定值的节点,查找到了,返回该节点的地址,也即是我们指定的 pos 位置,然后将其传给 SLTInsertAfter 函数。


2.11、 单链表删除指定pos位置的节点

  • 图解删除操作

image-20210827235619437

  • 代码如下

要考虑到两种情况:pos位置为单链表的第一个节点,pos位置为中间节点;

因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。

//单链表删除指定pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(*pphead); //链表不能为空
	assert(pos);     //给的pos位置不能为空

	//pos位置为第一个节点,相当于头删
	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}
	//pos位置为中间节点
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)  //找到pos位置的前一个节点
		{
			prev = prev->next;
		}
		prev->next = pos->next;  //pos位置的前一个节点指向pos位置的后一个节点
		free(pos);  //释放pos节点
		pos = NULL; //置空
	}
}
  • 补充一下,assert 断言

assert 放在函数里面检查参数,一方面是为了安全,另一方面也是为了防止有人调用该函数时,不正确的使用,错误传入参数,好及时提醒到他,写代码时一定要考虑到有人不正确的使用该函数时的场景,来进行避免

  • 测试其功能

先调用 SLTFind 函数在单链表中查找指定值的节点,查找到了,返回该节点的地址,也即是我们指定的 pos 位置,然后将其传给 SLTErase 函数。


2.12、 单链表删除指定pos位置之后的节点

  • 图解删除操作

在这里插入图片描述

  • 代码如下
//单链表删除指定pos位置之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);       //给的pos位置不能为空
	assert(pos->next); //给的pos位置不能是尾节点

	SLTNode*del = pos->next;  //保存pos位置的后一个节点
	pos->next = pos->next->next;
	free(del);  //释放pos位置的后一个节点
    del=NULL;
}

-运行结果如下:

在这里插入图片描述


2.13、 求单链表长度

//求单链表长度
int SLTSize(SLTNode* phead)
{
	int size = 0;
	SLTNode* cur = phead;
	while (cur != NULL)  //遍历链表
	{
		size++;
		cur = cur->next;
	}
	return size;
}

2.14、 判断单链表是否为空

plist为空,返回 1 (true),非空,返回 0 (false)

//单链表判空
bool SLTEmpty(SLTNode* phead)
{
	//plist为空,返回1(true),非空,返回0(false)
	return phead == NULL;
	
	/*写法2:
	return phead == NULL ? true : false;
	*/
}

三、 总结

单链表是一种简单且高效的数据结构,适用于需要频繁插入和删除操作的场景。通过掌握单链表的创建、插入、删除、查找、修改和遍历等操作,我们可以更好地理解和应用这一数据结构,从而提高程序的效率和可靠性。

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

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

相关文章

系统架构师备考记忆不太清楚的点-信息系统-需求分析

霍尔三维结构 逻辑维:解决问题的逻辑过程 过程有明确问题、确立目标、系统综合、系统分析、优化、系统决策、实施计划 时间维:工作进度 这个纬度则是做工作计划的输出 有 规划阶段、拟定方案、研制阶段、生产阶段、安装阶段、运行阶段、更新阶段 知…

TiDB 优化器丨执行计划和 SQL 算子解读最佳实践

导读 在数据库系统中,查询优化器是数据库管理系统的核心组成部分,负责将用户的 SQL 查询转化为高效的执行计划,因而会直接影响用户体感的性能与稳定性。优化器的设计与实现过程充满挑战,有人比喻称这是数据库技术要持续攀登的珠穆…

Android SELinux——基础介绍(一)

Android 系统的安全策略是保护用户的隐私和数据不受侵害的重要保证,一个相对安全的计算环境对于确保移动设备的安全至关重要。随着新的威胁不断出现,Android 的安全策略也在不断发展和完善,以应对新的挑战。 一、概念介绍 1、SELinux SELin…

图像处理(一)——CMC特刊推荐

特刊征稿 01 期刊名称: Data and Image Processing in Intelligent Information Systems 截止时间: 提交截止日期:2024年11月15日 目标及范围: 感兴趣的主题包括但不限于: 先进的数据处理技术; 智能数据分析; 智能系统…

【论文阅读】DeepAC:实时六自由度目标跟踪的深度主动轮廓

Deep Active Contour for Real-time 6-DoF Object Tracking ICCV 2023 Project Page 【Question&Thinking】 解决的问题? RGB 视频的实时六自由度目标跟踪 现有的问题?提出的方法?达到的效果?存在的问题? Abst…

pnpm设置镜像源

# 查询当前使用的镜像源 pnpm get registry# 设置为淘宝镜像源 pnpm config set registry https://registry.npmmirror.com/# 还原为官方镜像源 pnpm config set registry https://registry.npmjs.org/参考链接 https://blog.csdn.net/weixin_45046532/article/details/139681…

谷歌地图服务地址汇总

写在前面&#xff1a; 1.参考自谷歌地图TMS地图服务地址收集&#xff0c;测试可用_谷歌地图url订阅订阅地址-CSDN博客&#xff0c;在这里备份一下&#xff0c;防止遗忘 2.需要翻墙 3.下面通过openlayers加载各风格地图 <!Doctype html> <html> <head><met…

MyBatis之ResultMap的association和collection

association例子演示 实体类演示 Data //书籍 public class Book {private String id;private String name;private String author;private Double price;private Integer del;private Date publishdate;private String info;//把出版社对象当作属性private Publisher pub;//-…

通过Express + Vue3从零构建一个用户认证与授权系统(二)数据库与后端项目搭建与实现

前言 上一篇完成了系统的相关设计文档的编写&#xff0c;本文将详细介绍如何一步步使用 TypeScript 和 Express 搭建一个模块化、类型安全的用户认证与授权系统&#xff0c;包括数据库设计、后端项目搭建、用户认证、角色与权限管理、错误处理以及 Swagger 文档集成。 项目准…

Solidity优质例子(二)物流的增删改查智能合约(附truffle测试)

本合约非常适合新手学习&#xff0c;其包含了基本的增删改查功能以及各个方式的不同之处的总结&#xff0c;本套合约我也编写了truffle测试&#xff0c;学习truffle测试的小伙伴也有福了~ 该合约的主要作用是通过区块链技术实现物流追踪系统的透明化、自动化与防篡改特性&#…

windows C++-移除界面工作线程(一)

本文档演示了如何使用并发运行时将 Microsoft 基础类 (MFC) 应用程序中由用户界面 (UI) 线程执行的工作移动到工作线程。 本文档还演示了如何提高冗长绘制操作的性能。 通过将阻塞性操作&#xff08;例如&#xff0c;绘制&#xff09;卸载到工作线程来从 UI 线程中移除工作&am…

Python爬虫使用示例-古诗词摘录

一、分析需求 目标地址&#xff1a; https://www.sou-yun.cn/Query.aspx?typepoem&id二、提取诗句 import os import re import requests import parsel#url https://www.sou-yun.cn/PoemIndex.aspx?dynastyTang&author14976&typeJie urlhttps://www.sou-yun.…

移动app的UI和接口自动化测试怎么进行?

标题&#xff1a;从0到1&#xff1a;移动App的UI和接口自动化测试 导语&#xff1a;移动App的快速发展使得UI和接口自动化测试成为了确保应用质量的重要环节。本文将从零开始介绍移动App的UI和接口自动化测试的基本概念以及如何进行测试。 第一部分&#xff1a;了解移动App自动…

【React】如何在MacBook的vscode中配置React环境

作者&#xff1a;CSDN-PleaSure乐事 欢迎大家阅读我的博客 希望大家喜欢 使用环境&#xff1a;Vscode 安装Node.js和npm 首先我们需要完成Node和npm的配置。 官网下载 下载安装包 首先最安全稳定的方法就是从官网下载。我们首先进入node.js的官网https://nodejs.org下载长期…

如何让你的Mac右键菜单栏更加的丰富多样

Mac电脑的右键菜单栏不如Windows的丰富&#xff0c;虽然可以在系统设置一些常用功能&#xff0c;但是种类不够丰富&#xff0c;这对于一些用惯了Windows的人来说可以说是非常的不习惯&#xff0c;不管是工作使用还是日常使用来说都有一些影响&#xff0c;如何才能让Mac的右键菜…

日常场景图像分割系统源码&数据集分享

日常场景图像分割系统源码&#xff06;数据集分享 [yolov8-seg-LSKNet&#xff06;yolov8-seg-LAWDS等50全套改进创新点发刊_一键训练教程_Web前端展示] 1.研究背景与意义 项目参考ILSVRC ImageNet Large Scale Visual Recognition Challenge 项目来源AAAI Global Al lnnov…

如何解决与kernel32.dll相关的常见错误:详细指南解析kernel32.dll文件缺失、损坏或错误加载问题

当你的电脑中出现错误kernel32.dll丢失的问题&#xff0c;会导致电脑不能出现正常运行&#xff0c;希望能够有效的帮助你有效的将丢失的kernel32.dll文件进行修复同时也给大家介绍一些关于kernel32.dll文件的相关介绍&#xff0c;希望能够有效的帮助你快速修复错误。 kernel32.…

Golang | Leetcode Golang题解之第464题我能赢吗

题目&#xff1a; 题解&#xff1a; func canIWin(maxChoosableInteger, desiredTotal int) bool {if (1maxChoosableInteger)*maxChoosableInteger/2 < desiredTotal {return false}dp : make([]int8, 1<<maxChoosableInteger)for i : range dp {dp[i] -1}var dfs …

实战OpenCV之视频处理

基础入门 视频是由一系列连续的图像帧组成的&#xff0c;这些帧按照一定的速率连续播放&#xff0c;从而形成动态画面。与视频相关的主要参数有&#xff1a;分辨率、帧率、码率、编解码器、帧类型、文件格式等&#xff0c;下面分别进行介绍。 1、帧率。表示每秒显示的图像帧数&…

(01)python-opencv基础知识入门(图片的读取与视频打开)

前言 一、图像入门 1.1 读取图像cv.imread() 1.2 数组数据转换cv.cvtColor() 1.3数据窗口展示 1.4图像保存 1.5图像的截取 1.6 图像的比例缩放 二、视频入门 参考文献 前言 OpenCV 于 1999 年由 Gary Bradsky 在英特尔创立&#xff0c;第一个版本于 2000 年问世。Vad…