手撕数据结构 —— 单链表(C语言讲解)

news2024/10/10 18:53:28

目录

1.为什么要有链表

2.什么是链表

3.链表的分类

4.无头单向非循环链表的实现

SList.h中接口总览

具体实现

链表节点的定义

打印链表

申请结点

尾插

头插

尾删

头删

查找

在pos位置之前插入

在pos位置之后插入

删除pos位置

删除pos位置之后的值

5.完整代码附录


1.为什么要有链表

如果你学习过顺序表的话,应该清楚顺序表的缺点,如果你并不了解顺序表的话,推荐阅读一下这篇文章——手撕数据结构——顺序表(C语言讲解)

我们先来简单回顾一下顺序表,顺序表是在连续的内存空间上按顺序存储数据元素。

顺序表的优点:

  • 顺序表支持下标的随机访问和修改,并且时间复杂度是O(1),效率高。
  • 顺序表在尾部进行插入和删除效率高,时间复杂度是O(1)。

顺序表的缺点:

  • 头部和中部进行插入和删除效率都不高,时间复杂度是O(N)。
  • 动态顺序表扩容有一定消耗,尤其是动态扩容(需要开辟新空间,拷贝数据,释放旧空间)
  • 扩容造成的空间浪费(一般扩容是2倍扩容,假如容量从1000扩容到2000,但是我们只需要插入5个数据,就会浪费995个空间)

针对顺序表的缺点,有人设计出了另一种数据结构 —— 链表。

2.什么是链表

链表是一种利用不连续的内存空间存储数据元素的数据结构,并且元素之间可以不按顺序进行存储。

正是因为链表不按顺序存储,要想找到下一个数据就需要记录下一个数据的地址,因此,链表中的数据是包含在一个结构体中,我们通常称这个结构体变量为结点。最后一个结点没有下一个结点,就指向空。

链表的特点是按需申请和释放。

链表逻辑示意图如下:

需要注意的是:

  • 我们申请结点的时候,一般是使用动态内存管理的函数申请内存空间(malloc、realloc),这些函数是从堆上申请空间的。
  • 从堆上申请的空间,是按照一定的策略来分配的,这取决于操作系统,因此,两次申请的空间可能连续,也可能不连续。
  • 链表在逻辑结构上是连续的,在物理上结构上不一定连续,这取决于系统动态分配内存的位置。

3.链表的分类

链表大致可以分为这几类:单向还是双向、带不带头、循环还是非循环。

单向 or 双向:

带头 or 不带头:

循环 or 非循环:

通过数学知识,我们可以知道 2*2*2 = 8,也就是说,不同的分类可以组合出八种链表。 在众多的链表中,我们重点学习无头单向非循环链表带头双向循环链表。

无头单向非循环链表:也叫单链表,该链表结构简单,一般不会单独用来存储数据,而是用来作为其他数据结构的子结构。例如:哈希桶、图的邻接表……

带头双向循环链表:该链表是结构最复杂的链表,一般用来单独存储数据,实际中使用的链表,都是带头双向循环链表。

4.单链表的实现

说明一下:

实现链表的时候,主要实现无头单向非循环链表(单链表)带头双向循环链表,本篇文章中只实现无头单向非循环链表(单链表);带头双向循环链表的实现在下一篇文章中呈现。

实现链表我们定义两个文件,分别是SList.hSList.c,SList.h文件用来存放声明,SList.c文件用来存放定义。

SList.h中接口总览

具体实现

链表节点的定义

链表是由一个个动态申请的结点构成的,因此,我们要实现链表的第一件事就是先定义结点。链表的结点有两个成员,分别是数据域和指针域。

  • 数据域用来存放数据。
  • 指针域用来存放下一个结点的地址。

代码如下所示: 

打印链表

打印链表只需要知道链表的起始结点即可,创建一个临时变量作为phead的替身,每次打印完当前结点之后,cur就往后走。直到cur为空,表示没有元素了。

  • 注意:指针是可以作为逻辑条件判断的。

申请结点

结点的申请需要使用malloc函数,注意申请的类型是结点的类型,也就是结构体类型。

  • 结点申请成功之后,我们将数据域赋值为x,将指针域赋值为NULL。

尾插

尾插数据时,直接链接上去即可,但是,我们要考虑链表是否为空:

  • 如果链表为空,我们需要改变的是结构体的指针,需要传递二级指针。
  • 如果链表不为空,我们需要增加结点,使用结构体指针即可。(不同的结构体指针可以指向同一个结点,并操控同一个结点)

头插

进行头插操作,我们需要改变的也是结构体的指针,所以也需要传递二级指针。

  • 头插对于链表是否为空的情况处理是一样的。

尾删

进行尾部删除时需要考虑三种情况:

  • 链表为空:如果链表为空,不能删除。
  • 链表只有一个结点:如果链表只有一个结点,我们需要改变结构体指针,这也是为什么传递二级指针的原因。
  • 链表有一个以上的结点:此时,我们进行尾删时,只需要改变结构体,使用一级指针即可。

 

头删

头删需要判断链表是否为空,改变的也是结构体指针,所以也需要传递二级指针。

  • 头删只需要释放第一个节点,并改变pphead的值即可。

查找

遍历查找即可,找到返回对应结点的指针,没找到返回NULL。

  • 查找并不改变结构体指针,传递一级指针即可。

在pos位置之前插入

在pos位置之前插入需要记录pos的上一个位置。

该接口需要考虑三种情况:

  • 首先,链表不能为空,因为我们要在指定位置之前插入,如果链表为空,就不能指定位置了。
  • 在第一个结点前插入,这不就是头插吗?复用头插即可。
  • 在其他地方插入,链接顺序需要从后往前进行,并且还需要提前保存pos位置的上一个位置。

在pos位置之后插入

在pos位置之后插入,不需要记录上一个位置的情况,并且,也不需要判断pos的位置情况,因为,不管pos在哪里,都是一样的操作。

删除pos位置

删除pos位置和在pos位置之前插入一样,需要记录pos位置的上一个位置,需要分三种情况讨论:

  • 删除的时候,链表不能为空,删除位置不能为空。
  • 删除位置如果是第一个位置,直接复用头删即可。
  • 删除其他位置,需要记录上一个位置。

删除pos位置之后的值

删除pos位置之后的值不需要记录pos的上一个位置,但是需要注意不能删除尾结点,因为尾结点没有下一个结点。

5.完整代码附录

SList.h

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

typedef int SLTDataType;

typedef struct SListNode                                     //定义结点 
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

void SLTPrint(SLTNode* phead);                               //打印链表 

SLTNode* BuySListNode(SLTDataType x);                        //申请结点 

void SLTPushBack(SLTNode** pphead, SLTDataType x);           //尾插 

void SLTPushFront(SLTNode** pphead, SLTDataType x);          //头插 
 
void SLTPopBack(SLTNode** pphead);                           //尾删 

void SLTPopFront(SLTNode** pphead);                          //头删 

SLTNode* SLTFind(SLTNode* phead, SLTDataType x);             //查找 

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x); //在pos之前插入x

void SLTInsertAfter(SLTNode* pos, SLTDataType x);              //在pos之后插入x

void SLTErase(SLTNode** pphead, SLTNode* pos);                 //删除pos位置的元素 

void SLTEraseAfter(SLTNode* pos);                              //删除pos的后一个位置

SList.c

#include"SList.h"

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

	printf("NULL\n");
}

// 申请结点 
SLTNode* BuySListNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

// 尾插 
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);

	SLTNode* newnode = BuySListNode(x);

	if (*pphead == NULL)
	{
		// 改变的结构体的指针,所以要用二级指针
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}

		// 改变的结构体,用结构体的指针即可
		tail->next = newnode;
	}	
}

// 头插 
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);

	SLTNode* newnode = BuySListNode(x);

	newnode->next = *pphead;
	*pphead = newnode;
}

// 尾删 
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);

	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}

		free(tail->next);
		tail->next = NULL;
	}
}

// 头删 
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);

	assert(*pphead);

	SLTNode* newhead = (*pphead)->next;
	free(*pphead);
	*pphead = newhead;
}

// 查找元素 
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

//在pos位置之前插入x 
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);

	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		SLTNode* newnode = BuySListNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

// 在pos之后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);

	SLTNode* newnode = BuySListNode(x);
	pos->next = newnode;
	newnode->next = pos->next;
}

// 删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);

	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
	}
}

// 删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);

	// 检查pos是否是尾节点
	assert(pos->next);

	SLTNode* posNext = pos->next;

	pos->next = posNext->next;

	free(posNext);
	posNext = NULL;
}

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

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

相关文章

把自己的代码安装到系统环境中/conda环境

1. 安装setuptools库 2. 创建一个如下的setup.py程序 # codingutf-8 from setuptools import setupsetup(author"zata",description"This is a nir analyse api, writen by zata", ### 一句话概括一下name"nirapi", ### 给你的包取一个名字…

小熊猫C/C++的安装使用及配置教程

文章目录 软件介绍小熊猫C下载地址安装下载完得到可执行文件选择语言阅读协议接受条款组件默认即可最好不要占用系统盘 配置-使用打开会选择主题和默认语言创建项目创建源文件编译及运行 软件介绍 小熊猫C是一个简单易用的集成开发环境(IDE)。 学校里几乎用的都是Dev C这种轻量…

【数学分析笔记】第5章第1节 微分中值定理(1)

5. 微分中值定理及其应用 5.1 微分中值定理 5.1.1 极值与极值点 【定义5.1.1】 f ( x ) f(x) f(x)定义域为 ( a , b ) (a,b) (a,b)&#xff0c; x 0 ∈ ( a , b ) x_0\in(a,b) x0​∈(a,b)&#xff0c;若 ∃ O ( x 0 , ρ ) ⊂ ( a , b ) \exists O(x_0,\rho)\subset(a,b) ∃…

自动猫砂盆“智商税”还是“真香”?2024自动猫砂盆保姆级干货

平时忙着上班&#xff0c;或者一遇到出差就要离家四五天&#xff0c;没办法给毛孩子的猫砂盆铲屎&#xff0c;导致粪便堆积太久。很多铲屎官也了解到有自动猫砂盆这种东西&#xff0c;但是生怕是智商税&#xff0c;总觉得忍忍手铲也可以&#xff0c;要知道&#xff0c;猫咪的便…

众数信科 AI智能体政务服务解决方案——寻知智能笔录系统

政务服务解决方案 寻知智能笔录方案 融合民警口供录入与笔录生成需求 2分钟内生成笔录并提醒错漏 助办案人员二次询问 提升笔录质量和效率 寻知智能笔录系统 众数信科AI智能体 产品亮点 分析、理解行业知识和校验规则 AI实时提醒用户文书需注意部分 全文校验格式、内容…

无人机之穿越机飞行注意事项

一、选择合适的场地 1、寻找空旷、无障碍物的区域&#xff0c;如大型公园的空旷草坪、专门的无人飞行场地等。这样可以减少碰撞的风险&#xff0c;确保飞行安全。 2、避免在人群密集的地方飞行&#xff0c;防止对他人造成伤害。例如&#xff0c;不要在商场、学校、体育场等人…

ESP32芯片物联网技术,咖啡机智能化升级方案,实现个性化咖啡体验

随着物联网技术的飞速发展&#xff0c;我们的日常生活正在变得越来越智能化。从智能音箱到智能家居&#xff0c;现在连我们每天早晨的咖啡也能享受到智能科技的便利。 今天&#xff0c;我们就来聊聊如何通过ESP32芯片&#xff0c;将传统的咖啡机转变为一台能够远程控制、个性化…

实际开发中,java开发的准备工作

实际开发中&#xff0c;java开发的准备工作 一、IDEA工具环境设置 1、编码设置

如何在阿里云一键部署FlowiseAI

什么是FlowiseAI FlowiseAI 是一个开源的低代码开发工具&#xff0c;专为开发者构建定制的语言学习模型&#xff08;LLM&#xff09;应用而设计。 通过其拖放式界面&#xff0c;用户可以轻松创建和管理AI驱动的交互式应用&#xff0c;如聊天机器人和数据分析工具。 它基于Lang…

zotero主页面显示的标签名与信息处的标签名不一致

问题描述&#xff1a;我在网页导入了论文之后&#xff0c;自动匹配了一些该论文的信息&#xff0c;但是很多都是空的&#xff0c;最大的问题就是找不到出版物的信息&#xff1b; 解决&#xff1a;最后发现在信息中是叫刊名&#xff0c;其中年对应的是在日期部分&#xff1b; 极…

显卡的HDMI和DP接口的区别,如何给显卡选择最佳效果的显示器

1、HDMI和DisplayPort&#xff08;DP&#xff09;的区别 在显卡接口的选择上&#xff0c;HDMI和DisplayPort&#xff08;DP&#xff09;是两种常见的连接方式。它们在兼容性、性能以及分辨率等方面存在区别。具体分析如下&#xff1a; 兼容性 HDMI&#xff1a;广泛兼容多种设备…

VScode连接服务器配置c、c++编程环境

在 VS Code 中配置远程服务器的 C/C 编程环境&#xff0c;可以使用 VS Code 的 Remote-SSH 扩展来通过 SSH 连接到远程服务器&#xff0c;并在服务器上编写、编译和调试 C/C 代码。 以下是详细的配置步骤&#xff1a; 1. 在本地机器上安装 VS Code 和扩展 安装 VS Code&#…

【测试】——测试管理工具禅道 介绍与使用

&#x1f4d6; 前言&#xff1a;测试管理工具是一种并没有占据明显份额的工具。创业公司可能根本没有测试管理工具&#xff0c;而依赖Excel来管理。中小企业可能会在开源的基础上进行定制。大厂则会自研工具或者使用商业软件。本期以国产开源工具禅道为例来进行讲解。 目录 &am…

通用文件I/O模型之open

前面介绍了linux系统一切皆文件的概念&#xff0c;系统使用一套系统调用函数open()、read()、write()、close()等可以对所有文件执行I/O操作。应用程序发起的I/O请求&#xff0c;内核会将其转化为相应的文件系统操作&#xff0c;或者设备驱动程序操作。接下来我们一起了解一下o…

gitee开源商城diygw-mall

DIYGW可视化开源商城系统。所的界面布局显示都通过低代码可视化开发工具生成源码实现。支持集成微信小程序支付。 DIYGW可视化开源商城系统是一款基于thinkphp8 framework、 element plus admin、uniapp开发而成的前后端分离系统。 开源商城项目源码地址&#xff1a;diygw商城…

funasr: 报错 CUDA error: invalid device ordinal

问题描述 使用案例中的代码加载模型的时候&#xff0c;会报错 CUDA error: invalid device ordinal 运行的代码是 model AutoModel(modelmodel_dir,vad_model"fsmn-vad",vad_kwargs{"max_single_segment_time": 30000},device"cuda:0", ) 解…

C语言预处理详解(下)(31)

文章目录 前言一、命令行定义二、条件编译三、文件包含头文件被包含的方式嵌套文件包含 总结 前言 再介绍几点吧&#xff01; 一、命令行定义 许多C 的编译器提供了一种能力&#xff0c;允许在命令行中定义符号。用于启动编译过程 当我们根据同一个源文件要编译出不同的一个程序…

VScode连接远程服务器踩坑实战(新版离线vscode-server安装)

想要用VScode连接远程服务器&#xff0c;但远程服务器并没有连接外网&#xff0c;因此需要离线手动安装vscode-server但网上的方法都是旧版本的安装&#xff0c;没有新版本的配置。因此记录一下我都踩坑实战。 1、VScode扩展安装与配置 &#xff08;1&#xff09;vscode扩展安…

双十一买什么最划算?2024年双十一选购攻略汇总!

随着一年一度的双十一购物狂欢节日益临近&#xff0c;消费者们纷纷摩拳擦掌&#xff0c;准备在这个全球最大的购物盛宴中抢购心仪已久的商品。双十一不仅是一场购物的狂欢&#xff0c;更是商家们推出优惠、促销的绝佳时机。然而&#xff0c;面对琳琅满目的商品和纷繁复杂的优惠…

《数据密集型应用系统设计》笔记——第二部分 分布式数据系统(ch5-9)

第5章 数据复制 目的&#xff1a; 地理位置更近&#xff0c;降低延迟故障冗余提高读吞吐量 主节点与从节点&#xff08;主从复制&#xff09; 主从复制&#xff1a; 写请求发送给主节点&#xff0c;主节点将新数据写入本地存储&#xff1b;主节点将数据更改作为复制的日志发送…