线性表之单链表(详解)

news2024/11/28 12:48:18

🍕博客主页:️自信不孤单

🍬文章专栏:数据结构与算法

🍚代码仓库:破浪晓梦

🍭欢迎关注:欢迎大家点赞收藏+关注

文章目录

  • 🍥前言
  • 🍉链表
    • 1. 链表的概念及结构
    • 2. 链表的分类
    • 3. 单链表的实现
      • 3.1 动态申请一个节点
      • 3.2 打印单链表
      • 3.3 单链表尾插
      • 3.4 单链表尾删
      • 3.5 单链表头插
      • 3.6 单链表头删
      • 3.7 单链表查找
      • 3.8 在指定位置后插入数据
      • 3.9 在指定位置前插入数据
      • 3.10 删除指定位置之后的数据
      • 3.11 删除指定位置的数据
      • 3.12 单链表的销毁
    • 4. 接口测试


🍥前言

在前一文章我们已经学习了顺序表,但是我们发现顺序表还有一些小缺点满足不了我们的需求,例如:

  • 中间/头部的插入删除,时间复杂度为O(N)。
  • 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  • 增容一般是呈2倍的增长,势必会有一定的空间浪费。

基于这些问题,我们来学习一种新的数据结构——链表,而链表就可以完美解决以上问题了。
在这篇文章中我们来重点学习一下单链表的实现。

🍉链表

1. 链表的概念及结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

理解

链表是由一系列结点组成的,结点可动态的生成。每个节点是一个结构体,而且每一个节点在堆上的开辟是随机的,我们可以通过结构体指针来维护每一个节点,将每一个节点链接起来,这样就形成了一条链。
链表中的节点是这样的:

在这里插入图片描述

  • DataType 表示要存放的某类型的数据。
  • *next 表示该结构体类型的指针,一般将此指针赋值为下一个结点的地址,这样就可以通过这个节点的指针找到下一个节点了。

链表的结构:

在这里插入图片描述

2. 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

  1. 单向或者双向

在这里插入图片描述

  1. 带头或者不带头

在这里插入图片描述

  1. 循环或者非循环

在这里插入图片描述

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

在这里插入图片描述

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是用代码实现以后会发现结构会带来很多优势,实现反而简单了。

3. 单链表的实现

首先来创建两个文件来实现单链表:

  1. SList.h(节点的声明、接口函数声明、头文件的包含)
  2. SList.c(单链表接口函数的实现)

接着创建 test.c 文件来测试各个接口

如图:
在这里插入图片描述

SList.h 文件内容如下:

#pragma once

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

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SListNode;

// 动态申请一个节点
SListNode* BuySListNode(SLTDataType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDataType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType x);
// 在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDataType x);
// 在pos位置之前插入x
void SListInsert(SListNode** pplist, SListNode* pos, SLTDataType x);
// 删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 删除pos位置的值
void SListErase(SListNode** pplist, SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode* plist);

接下来,我们在 SList.c 文件中实现各个接口函数。

3.1 动态申请一个节点

在堆上申请一个节点结构体大小的空间,并用该节点存放数据 x,节点的 next 指针指向 NULL,返回节点的地址。

SListNode* BuySListNode(SLTDataType x)
{
	SListNode* ret = (SListNode*)malloc(sizeof(SListNode));
	if (NULL == ret)
	{
		perror("malloc fail");
		return NULL;
	}
	ret->data = x;
	ret->next = NULL;
	return ret;
}

3.2 打印单链表

注意:这里不需要的 plist 进行断言。plist 为空,则打印 NULL。

void SListPrint(SListNode* plist)
{
	while (plist)
	{
		printf("%d->", plist->data);
		plist = plist->next;
	}
	printf("NULL\n");
}

3.3 单链表尾插

尾插分为两种情况:

  1. 当链表为空时,头指针 plist 指向 NULL。此时要改变 plist 的指向,让 plist 指向新开辟好的结点,就需要用二级指针来改变一级指针的值(如果传参传的是 plist,并用一级指针作为形参,形参的改变不会影响实参,那么 plist 就不会被改变)。
  2. 当链表非空时,只需通过循环找到尾节点,并将为节点的 next 指针赋值为新开辟好节点的地址。
void SListPushBack(SListNode** pplist, SLTDataType x)
{
	assert(pplist);
	if (*pplist)
	{
		SListNode* cur = *pplist;
		while (cur->next)
		{
			cur = cur->next;
		}
		cur->next = BuySListNode(x);
	}
	else
	{
		*pplist = BuySListNode(x);
	}
}

3.4 单链表尾删

分两种情况讨论即可。

void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);
	SListNode* cur = *pplist;
	if (cur->next)
	{
		while (cur->next->next)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;
	}
	else
	{
		free(cur);
		cur = NULL;
		*pplist = NULL;
	}
}

3.5 单链表头插

void SListPushFront(SListNode** pplist, SLTDataType x)
{
	assert(pplist);
	SListNode* tmp = *pplist;
	*pplist = BuySListNode(x);
	(*pplist)->next = tmp;
}

3.6 单链表头删

void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);
	SListNode* del = *pplist;
	*pplist = (*pplist)->next;
	free(del);
	del = NULL;
}

3.7 单链表查找

返回所找到节点的指针,没找到则返回 NULL。

注:查找函数可以配合指定位置操作函数来使用。

SListNode* SListFind(SListNode* plist, SLTDataType x)
{
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

3.8 在指定位置后插入数据

void SListInsertAfter(SListNode* pos, SLTDataType x)
{
	assert(pos);
	SListNode* tmp = pos->next;
	pos->next = BuySListNode(x);
	pos->next->next = tmp;
}

3.9 在指定位置前插入数据

注意分情况讨论,判断 pos 位置是否为头指针的位置。

void SListInsert(SListNode** pplist, SListNode* pos, SLTDataType x)
{
	assert(pos);
	assert(pplist);
	if (*pplist == pos)
	{
		SListPushFront(pplist, x);
		return;
	}
	SListNode* cur = *pplist;
	while (cur)
	{
		if (cur->next == pos)
		{
			SListNode* tmp = cur->next;
			cur->next = BuySListNode(x);
			cur->next->next = tmp;
			return;
		}
		cur = cur->next;
	}
}

3.10 删除指定位置之后的数据

void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);
	SListNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

3.11 删除指定位置的数据

注意分情况讨论,判断 pos 位置是否为头指针的位置。

void SListErase(SListNode** pplist, SListNode* pos)
{
	assert(pos);
	assert(pplist);
	if (*pplist == pos)
	{
		free(*pplist);
		*pplist = NULL;
		return;
	}
	SListNode* cur = *pplist;
	while (cur)
	{
		if (cur->next == pos)
		{
			SListNode* del = pos;
			cur->next = del->next;
			free(del);
			return;
		}
		cur = cur->next;
	}
}

3.12 单链表的销毁

不要忘记把 plist 置空。

void SListDestroy(SListNode** pplist)
{
	assert(pplist);
	SListNode* del = *pplist;
	while (del)
	{
		SListNode* cur = del->next;
		free(del);
		del = cur;
	}
	*pplist = NULL;
}

注:在每个接口函数中一定要合理地使用assert函数断言防止对空指针的引用。

4. 接口测试

test.c 文件内容如下:

#include "SList.h"

void Test()
{
	SListNode* plist = NULL;

	//头插
	SListPushFront(&plist, 3);
	SListPrint(plist);
	SListPushFront(&plist, 1);
	SListPrint(plist);

	//尾插
	SListPushBack(&plist, 5);
	SListPrint(plist);

	//指定位置后插
	SListNode* insert = SListFind(plist, 3);
	SListInsertAfter(insert, 4);
	SListPrint(plist);

	//指定位置前插
	SListInsert(&plist, insert, 2);
	SListPrint(plist);

	//头删
	SListPopFront(&plist);
	SListPrint(plist);
	SListPopFront(&plist);
	SListPrint(plist);

	//尾删
	SListPopBack(&plist);
	SListPrint(plist);

	//指定位置后删
	SListNode* del = SListFind(plist, 3);
	SListEraseAfter(del);
	SListPrint(plist);

	//指定位置删除
	SListErase(&plist, del);
	SListPrint(plist);

	SListDestroy(&plist);
}

int main()
{
	Test();
	return 0;
}

运行结果:

在这里插入图片描述

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

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

相关文章

肝一肝设计模式【二】-- 工厂模式

系列文章目录 肝一肝设计模式【一】-- 单例模式 传送门 肝一肝设计模式【二】-- 工厂模式 传送门 文章目录 系列文章目录前言一、简单工厂模式二、工厂方法模式三、抽象工厂模式写在最后 前言 在实际开发过程中&#xff0c;构建对象往往使用new的方式来构建&#xff0c;但随着…

Nginx搭建以及使用(linux)

1.概念 Nginx是一个高性能的HTTP和反向代理服务器&#xff0c;它可以用来处理静态文件&#xff0c;负载均衡&#xff0c;反向代理等功能。 Nginx的来历是这样的&#xff1a;它是由俄罗斯人伊戈尔赛索耶夫为Rambler.ru站点开发的&#xff0c;第一个公开版本发布于2004年…

java的构造方法

构造方法是 Java中最重要的方法&#xff0c;也是 Java语言中最基本的方法&#xff0c;它直接影响程序的结构。java中不允许重复使用构造方法&#xff0c;但可以重复使用构造函数。 1. Java中只有构造函数可以使用被调用方提供的参数&#xff08;如&#xff1a; int&#xff09;…

作为一名8年测试工程师,因为偷偷接私活被····

接私活 对程序员这个圈子来说是一个既公开又隐私的话题&#xff0c;不说全部&#xff0c;应该大多数程序员都有过想要接私活的想法&#xff0c;当然&#xff0c;也有部分得道成仙的不主张接私活。但是很少有人在公开场合讨论私活的问题&#xff0c;似乎都在避嫌。就跟有人下班后…

Linux进程通信——共享内存

共享内存 共享内存原理与概念函数接口的介绍与使用shmgetshmctlshmatshmdt通信 共享内存的特点共享内存的内核结构 system V消息队列&#xff08;了解&#xff09;system V——初识信号量信号量的预备概念理解信号量信号量的接口与结构 IPC资源的组织方式 共享内存 原理与概念…

在 Edge 中安装 Tampermonkey 的步骤

以下是在 Edge 中安装 Tampermonkey 的步骤&#xff1a; 目录 1. 打开 Edge 浏览器&#xff0c;进入 Tampermonkey 官网&#xff1a;[https://www.tampermonkey.net/](https://www.tampermonkey.net/)。2. 点击页面上方的“下载”按钮&#xff0c;选择“Microsoft Edge”选项。…

2.2.2 redis,memcached,nginx网络组件

课程目标&#xff1a; 1.网络模块要处理哪些事情 2.reactor是怎么处理这些事情的 3.reactor怎么封装 4.网络模块与业务逻辑的关系 5.怎么优化reactor? io函数 函数调用 都有两个作用&#xff1a;io检测 是否就绪 io操作 1. int clientfd accept(listenfd, &addr, &…

BigInteger和BigDecimal

BigInteger 当一个整数很大&#xff0c;大到long都无法保存&#xff0c;就可以使用BigInteger这个类 使用方法&#xff1a;new import java.math.BigInteger;//记得引包 BigInteger bigInteger new BigInteger("33333333333399999999999");//用字符串传入 System…

STM32物联网实战开发(6)——PWM驱动LED灯

PWM驱动LED灯 之前是使用标准库函数配置引脚输出PWM控制呼吸灯&#xff0c;因为开发板上的蜂鸣器是有源的&#xff0c;所以这次还是用来确定LED灯&#xff0c;这次使用的是HAL库&#xff0c;用CubeMX软件初始化PWM功能 PWM输出原理 Period&#xff1a;周期&#xff0c;单位是秒…

10个最流行的向量数据库【AI】

矢量数据库是一种将数据存储为高维向量的数据库&#xff0c;高维向量是特征或属性的数学表示。 每个向量都有一定数量的维度&#xff0c;范围从几十到几千不等&#xff0c;具体取决于数据的复杂性和粒度。 推荐&#xff1a;用 NSDT场景设计器 快速搭建3D场景。 矢量数据库&…

手机短信验证码登录功能的开发实录(机器识别码、短信限流、错误提示、发送验证码倒计时60秒)

短信验证码登录功能 项目分析核心代码1.外部js库调用2.HTML容器构建3.javaScript业务逻辑验证4.后端验证逻辑 总结 短信验证码是通过发送验证码到手机的一种有效的验证码系统&#xff0c;作为比较准确和安全地保证购物的安全性&#xff0c;验证用户的正确性的一种手段&#xff…

Redux的基本使用,从入门到入土

目录 一、初步使用Redux 1.安装Redux 2.配置状态机 二、Redux的核心概念 1.工作流程 2.工作流程 三、优化Redux 1.对action进行优化 2.type常量 3.reducer优化 四、react-redux使用 1.安装react-redux 2.全局注入store仓库 3.组件关联仓库 五、状态机的Hook 1.u…

Day958.代码的分层重构 -遗留系统现代化实战

代码的分层重构 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于代码的分层重构的内容。 来看看如何重构整体的代码&#xff0c;也就是如何对代码分层。 一、遗留系统中常见的模式 一个学校图书馆的借书系统。当时的做法十分“朴素”&#xff0c;在点击“借阅”按钮…

如何使用osquery在Windows上实时监控文件?

导语&#xff1a;Osquery是一个SQL驱动操作系统检测和分析工具&#xff0c;它由Facebook创建&#xff0c;支持像SQL语句一样查询系统的各项指标&#xff0c;可以用于OSX和Linux操作系统。 Osquery是一个SQL驱动操作系统检测和分析工具&#xff0c;它由Facebook创建&#xff0c;…

不得不说的行为型模式-责任链模式

目录 责任链模式&#xff1a; 底层原理&#xff1a; 代码案例&#xff1a; 下面是面试中可能遇到的问题&#xff1a; 责任链模式&#xff1a; 责任链模式是一种行为型设计模式&#xff0c;它允许多个对象在一个请求序列中依次处理该请求&#xff0c;直到其中一个对象能够…

【VM服务管家】VM4.0平台SDK_2.5 全局工具类

目录 2.5.1 全局相机&#xff1a;全局相机设置参数的方法2.5.2 全局相机&#xff1a;获取全局相机列表的方法2.5.3 全局通信&#xff1a;通信管理中设备开启状态管理2.5.4 全局通信&#xff1a;接收和发送数据的方法2.5.5 全局变量获取和设置全局变量的方法 2.5.1 全局相机&…

经典重装上阵,更好用的中小手游戏鼠标,雷柏V300W上手

日常办公、玩游戏都需要用到鼠标&#xff0c;特别是对于游戏玩家来说&#xff0c;一款手感好、易定制的鼠标&#xff0c;绝对是游戏上分的利器。早先雷柏出过一款V300鼠标&#xff0c;距今已有10年历史&#xff0c;当时是很受欢迎&#xff0c;最近南卡又出了一款复刻版的V300W&…

为什么不要相信AI机器人提供的健康信息?

自从OpenAI、微软和谷歌推出了AI聊天机器人&#xff0c;许多人开始尝试一种新的互联网搜索方式&#xff1a;与一个模型进行对话&#xff0c;而它从整个网络上学到的知识。 专家表示&#xff0c;鉴于之前我们倾向于通过搜索引擎查询健康问题&#xff0c;我们也不可避免地会向Ch…

linux下的权限管理

1.shell概念 当我们在进入正文前先给大家普及一些基础概念。 广义上来讲&#xff0c;linux 发行版 linux内核 外壳程序&#xff08;这个外壳程序就相当于 windows gui&#xff08;窗口图形&#xff09;&#xff0c;linux 常用的shell 是 bash&#xff09; 所以&#xff0c…

vue基本语法

目录 一、模板语法 &#xff08;1&#xff09;文本 &#xff08;2&#xff09;原始HTML &#xff08;3&#xff09;属性Attribute &#xff08;4&#xff09;使用JavaScript表达式 二、条件渲染 &#xff08;1&#xff09;v-if&#xff0c;v-else &#xff08;2&#x…