单向链表的实现

前言:继顺序表后的又一个线性结构——链表,这里将单向链表的实现。


目录

链表简介:

多文件实现:

SList.h:

SList.c实现函数的详解:

因为插入数据需要创建节点,很频繁,所以直接将创建新节点分装成一个函数:

尾插:

头插:

 将链表中的数据打印:

头删:

尾删:

查找:

插入:

1.在指定位置之前插入节点:

2.在指定位置之后插入节点:

删除指定位置的节点:

删除指定位置之后的节点:

销毁链表:

全部源码:

结语:


链表简介:

图解:

上图是链表的逻辑结构,逻辑上是线性的,而在内存中不是连续的,因为链表的节点都是由malloc函数开辟的,所以在内存中不连续;链表是由一个一个的节点构成,节点就是上图中的一个一个的长方形。

节点的结构:

typedef int SLTDataType;
struct SListNode
{
	struct SListNode* next;//存放下一个节点的地址
	SLTDataType data;//存放该节点的数据
}

链表与顺序表对比

顺序表在逻辑上和物理上都是线性的,

而链表只在逻辑上是线性的。

顺序表的缺点:

1:顺序表需要增容,增容时,使用的是realloc函数,在原来的空间后没有足够的空间就需要去内存的其他区域去开辟空间,这时需要将原来的数据拷贝进新的空间,效率低:

2:将顺序表中的数据删除,不会释放空间,浪费空间:举个例子,你以前的顺序表中存储了1000个数据,你现在将数据都删除了,那么那块空间现在没有数据并且还没释放,造成了空间浪费。

链表优点:

每想存储一个数据时就需要用malloc开辟一个空间,若删除数据可以直接把存放数据的空间释放。


多文件实现:

文件名称负责的功能
SList.h链表的定义,需要用到库里的头文件的包含,链表需要实现函数的声明。
SList.c链表函数的实现
test.c测试接口(这里大家可以自己测,自己写)

SList.h:

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


typedef int SLTDataType;

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

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//打印链表中的数据
void SLTPrint(SLTNode* pphead);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除指定位置的数据
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除指定位置之后的数据
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SLTDestroy(SLTNode** pphead);

SList.c实现函数的详解:

因为插入数据需要创建节点,很频繁,所以直接将创建新节点分装成一个函数:

SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;//使新节点指向的下一个节点为NULL
	return newnode;
}

x: 想创建的节点中存储的数据

尾插:

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	SLTNode* newnode = SLTBuyNode(x);//创建一个新节点
    //链表为空
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
    //链表不为空
	else
	{
        //pcur->nextd等于NULL时跳出循环
		while (pcur->next)
		{
			pcur = pcur->next;
		}
        //此时pcur指向最后一个节点
		pcur->next = newnode;
	}
}

为什么要传二级指针呢?

如果尾插时,为空链表,那么尾插时会改变头结点的值,而想在头插函数中改变头结点的值,那么就需要传头结点的地址,头结点是一级指针,一级指针的地址需要二级指针接收,所以参数是二级指针,这一点很重要,以后的函数也有需要传二级指针的。

所以在插入数据时有两种情况:

1,链表为空

2,链表不为空,因为是尾插,所以需要找到最后一个节点,并在其后面插入一个新节点。

头插:

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
}

老问题为什么传二级指针?

因为是头插,所以头结点会指向新创建的节点,需要改变头结点的值,若传一级指针,只是头结点的临时拷贝,改变函数中的参数的值不会改变,头结点的值,所以需要传二级指针

考虑是否为空链表:

1:空链表,只需要在头结点后插入一个新节点

2:不为空链表:不仅需要在头结点后插入一个新节点,还需要让新节点的next指向原来的链表的第一个节点。

图解:

 将链表中的数据打印:

void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
    //直到pcur指向NULL为止
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

打印效果:

这里有一段代码:

int main()
{
	SLTNode* list = NULL;
	SLTPushBack(&list, 1); 
	SLTPushBack(&list, 2);
	SLTPushBack(&list, 3);
	SLTPrint(list);
	return 0;
}

头删:

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);//防止是无效链表和空链表
	SLTNode* tmp = (*pphead)->next;
	free(*pphead);
	*pphead = tmp;
}

assert(pphead&&*pphead)就相当于assert(pphead!=NULL&&*pphead!=NULL)

pphead!=NULL防止其指向无效链表

*pphead!=NULL防止空链表

这个在之后的函数实现也会有。

为什么传二级指针?

因为是头删需要改变头结点的值,让他指向原来链表的头结点的下一个节点。

尾删:

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	SLTNode* prev = *pphead;
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		while (pcur->next)
		{
			prev = pcur;
			pcur = pcur->next;
		}
		free(pcur);
		prev->next = NULL;
	}
}

为什么要传二级指针?

当链表中有一个数据时,删除这个数据,链表就为空链表了,所以头结点指向空,改变了头结点的值。

为什么要定义prev和pcur指针呢?

在删除最后一个节点后,需要将最后一个节点的前一个节点的next置为NULL,使其成为新的尾结点。

因为链表是单向的所以不能通过最后一个节点找到前一个节点,所以需要引入prev指针,prev指针指向pcur指向的节点的前一个节点,所以当pcur指针指向最后一个节点时,prev指向最后一个节点的前一个节点,所以删除最后一个节点后,再将prev->next=NULL,就可以了。

图解:

查找:

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
			return pcur;
		pcur = pcur->next;
	}
	return NULL;
}

从头结点开始查找,一个节点一个节点的走,如果中途找到该数据,则直接返回该节点的地址;若走完最后一个节点都没找到该数据,那么返回NULL

插入:

1.在指定位置之前插入节点:

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead&&*pphead);
	assert(pos);//防止pos为NULL
	SLTNode* pcur = *pphead;
	SLTNode* prev = *pphead;
	SLTNode* newnode = SLTBuyNode(x);
	if (pos == *pphead)
	{
		*pphead = newnode;
		newnode->next = pos;
	}
	else
	{
		while (pcur!= pos)
		{
			prev = pcur;
			pcur = pcur->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

二级指针:若在头结点之前插入会改变头结点的值,所以用二级指针。

pcur和prev:因为插入需要找到pos的前一个节点,使前一个节点的next指向新的节点

插入分两种情况:

在头结点前插入和在其他位置插入

2.在指定位置之后插入节点:

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

因为在pos之后插入所以不需要用到prev和pcur,也不需要用到二级指针。

注意代码顺序:如果先改pos->next,那么就找不到pos的下一个节点了。

删除指定位置的节点:

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);//该链表不能为空链表
	assert(pos);
	SLTNode* pcur = *pphead;
	SLTNode* prev = *pphead;
	if (pos == *pphead)
	{
		*pphead = pos->next;
		free(pos);
		pos = NULL;
	}
	else
	{
		while (pcur != pos)
		{
			prev = pcur;
			pcur = pcur->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

当删除的位置为头结点时,删除该节点会改变头结点的值,所以需要二级指针。

所以删除时分两种情况:1,pos为头结点  2,pos 不为头结点

删除指定位置之后的节点:

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos&&pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

注意:pos->next不为NULL否则就会对NULL进行解引用引发错误。

要用del保存要删除的节点的地址否则就会找不到该节点。

图解:

销毁链表:

void SLTDestroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	SLTNode* prev = *pphead;
	while (pcur)
	{
		prev = pcur;
		pcur = pcur->next;
		free(prev);
	}
	*pphead = NULL;
}

思路:用前后指针prev和pcur防止,如果只用一个指针pcur,先释放则找不到下一个节点了,若先使pcur走到下一个节点则找不到上一个节点。

所以用前后指针的释放prev指向的节点,然后再让prev指向pcur,pcur在指向下一个节点,直到pcur走到NULL为止。

图解:


全部源码:

SList.h

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


typedef int SLTDataType;

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

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//打印链表中的数据
void SLTPrint(SLTNode* pphead);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除指定位置的数据
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除指定位置之后的数据
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SLTDestroy(SLTNode** pphead);

SList.c

#include "SList.h"

SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}


void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}


void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
}

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		while (pcur->next)
		{
			pcur = pcur->next;
		}
		pcur->next = newnode;
	}
}



void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	SLTNode* prev = *pphead;
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		while (pcur->next)
		{
			prev = pcur;
			pcur = pcur->next;
		}
		free(pcur);
		prev->next = NULL;
	}
}


void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* tmp = (*pphead)->next;
	free(*pphead);
	*pphead = tmp;
}


SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
			return pcur;
		pcur = pcur->next;
	}
	return NULL;
}


void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead&&*pphead);
	assert(pos);
	SLTNode* pcur = *pphead;
	SLTNode* prev = *pphead;
	SLTNode* newnode = SLTBuyNode(x);
	if (pos == *pphead)
	{
		*pphead = newnode;
		newnode->next = pos;
	}
	else
	{
		while (pcur!= pos)
		{
			prev = pcur;
			pcur = pcur->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos->next);
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}


void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* pcur = *pphead;
	SLTNode* prev = *pphead;
	if (pos == *pphead)
	{
		*pphead = pos->next;
		free(pos);
		pos = NULL;
	}
	else
	{
		while (pcur != pos)
		{
			prev = pcur;
			pcur = pcur->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos&&pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}


void SLTDestroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	SLTNode* prev = *pphead;
	while (pcur)
	{
		prev = pcur;
		pcur = pcur->next;
		free(prev);
	}
	*pphead = NULL;
}

结语:

又写完一篇数据结构的实现,cheers!

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

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

相关文章

《中医病证分类与代码》-中医疾病分类数据库

《中医病症分类与代码》由国家中医药管理局2020年底修订&#xff0c;目的是为中医疾病及证候的分类提供统一的规范。规定2021年起&#xff0c;各中医机构的临床科室及基层中医药的医师都应按照最新修订的《中医病症分类与代码》规范来填报病案及病历。 中医病证分类与代码数据库…

C++STL详解(一)— string类

string 类对象的常见容量操作 函数名称 功能 size 返回字符串有效字符长度length返回字符串有效字符长度capacity返回空间总大小clear清空有效字符empty检测字符串是否为空串&#xff0c;是返回true,否则返回falsereserve对容量进行改变resize扩容初始化 size和length 文档解…

Linux系统(centos,redhat,龙芯,麒麟等)忘记密码,怎么重置密码

Linux系统&#xff08;centos,redhat,龙芯&#xff0c;麒麟等&#xff09;忘记密码&#xff0c;怎么重置密码&#xff0c;怎么设置新的密码 今天在操作服务器时&#xff0c;DBA忘记了人大金仓数据库的kingbase密码&#xff0c;他的密码试了好多遍&#xff0c;都不行。最后只能…

sublime text中文---功能强大、操作便捷的代码编辑神器

Sublime Text是一款极受欢迎的代码编辑器&#xff0c;以其出色的性能、丰富的功能和优雅的用户界面赢得了广大开发者的青睐。它支持多种编程语言&#xff0c;包括HTML、CSS、JavaScript、Python等&#xff0c;让开发者能够轻松编辑和调试各种代码。Sublime Text拥有强大的自定义…

配置路由器实现互通

1.实验环境 实验用具包括两台路由器(或交换机)&#xff0c;一根双绞线缆&#xff0c;一台PC&#xff0c;一条Console 线缆。 2.需求描述 如图6.14 所示&#xff0c;将两台路由器的F0/0 接口相连&#xff0c;通过一台PC 连接设备的 Console 端口并配置P地址&#xff08;192.1…

SpringBoot是如何实现main方法启动Web项目的?

一、问题解析 在Spring Boot中&#xff0c;通过SpringApplication类的静态方法run来启动Web项目。当我们在main方法中调用run方法时&#xff0c;Spring Boot使用一个内嵌的Tomcat服务器&#xff0c;并将其配置为处理Web请求。 当应用程序启动时&#xff0c;Spring Boot会自动扫…

C#学习笔记11:winform上位机与西门子PLC网口通信_下篇

今日终于到了winform上位机与西门子PLC网口通信的系列收为阶段了&#xff0c;一直没一口气更新完&#xff0c;手头上也没有可以测试用的PLC设备&#xff0c;虚拟仿真用到的博图软件也不想下载&#xff08;会让我电脑变卡&#xff09;。 于是等了些日子购买西门子PLC&#xff0…

JIT在汽车行业中的革命性应用:颠覆传统制造模式,引领智能制造新时代

随着科技的飞速发展和市场竞争的日益激烈&#xff0c;汽车行业正面临着前所未有的变革。其中&#xff0c;准时制生产&#xff08;Just-In-Time&#xff0c;简称JIT&#xff09;作为一种先进的生产管理方式&#xff0c;已经在汽车行业中得到了广泛应用&#xff0c;成为推动汽车产…

密码学 | 椭圆曲线 ECC 密码学入门(三)

目录 7 这一切意味着什么&#xff1f; 8 椭圆曲线密码学的应用 9 椭圆曲线密码学的缺点 10 展望未来 ⚠️ 原文地址&#xff1a;A (Relatively Easy To Understand) Primer on Elliptic Curve Cryptography ⚠️ 写在前面&#xff1a;本文属搬运博客&#xff0c;自己留…

论文略读:Window Attention is Bugged: How not to Interpolate Position Embeddings

iclr 2024 reviewer 打分 6666 窗口注意力、位置嵌入以及高分辨率微调是现代Transformer X CV 时代的核心概念。论文发现&#xff0c;将这些几乎无处不在的组件简单地结合在一起&#xff0c;可能会对性能产生不利影响问题很简单&#xff1a;在使用窗口注意力时对位置嵌入进行插…

DC-1渗透测试复现

DC-1渗透测试复现 目的&#xff1a; 获取最高权限以及5个flag 过程&#xff1a; 信息打点-cms框架漏洞利用-数据库-登入admin-提权 环境&#xff1a; 攻击机&#xff1a;kali(192.168.85.136) 靶机&#xff1a;DC_1(192.168.85.131) 复现&#xff1a; 一.信息收集 扫…

IDEA 本地库引入了依赖但编译时找不到

在使用 IDEA 开发 Maven 项目的过程中&#xff0c;有时会遇到本地库引入了依赖&#xff0c;但编译时报找不到这个依赖&#xff0c;可以使用命令处理。 打开 Terminal。 执行清理命令。 mvn clean install -Dmaven.test.skiptrue执行更新命令。 mvn -U idea:idea

怎么清除3D模型杂质?---模大狮模型网

在进行3D建模过程中&#xff0c;模型可能会受到各种杂质的影响&#xff0c;这些杂质可能来自于模型本身的结构问题、导入导出过程中的错误、或者是不当的编辑操作所留下的痕迹。清除这些杂质是保证模型质量和渲染效果的关键步骤之一。本文将介绍几种常见的清除3D模型杂质的方法…

【C++】适配器· 优先级队列 仿函数 反向迭代器

目录 适配器&#xff1a;适配器的应用&#xff1a;1. 优先级队列&#xff1a;仿函数&#xff1a;更深入的了解仿函数&#xff1a;一个关于不容易被注意的知识点&#xff1a; 2. 反向迭代器&#xff1a;list && vector&#xff1a; 适配器&#xff1a; 我们先来谈来一下…

最新IntelliJ IDEA 2024.1 安装和快速配置教程

IntelliJ IDEA 2024.1 最新版如何快速入门体验?IntelliJ IDEA 2024.1 安装和配置教程 图文解说版 文章目录 IntelliJ IDEA 2024.1 最新版如何快速入门体验?IntelliJ IDEA 2024.1 安装和配置教程 图文解说版前言 第一步&#xff1a; IntelliJ IDEA 2024.1安装教程第 0 步&…

activiti初次学习

源代码地址&#xff1a;https://gitee.com/ZSXYX/activiti.git​ 1、安装插件 首先安装下图所示activiti,不确定是哪个插件有用的&#xff0c;有时间可排除下 在resources下创建一个文件夹&#xff1a;processes,右键&#xff0c;新建 生成&#xff1a; 选中act.bpmn20.xm…

Android 使用ping命令判断当前网络状态

一. 介绍 ping命令是用来测试和诊断网络连接问题的基本命令&#xff0c;当然我们的终端设备&#xff08;手机/平板/车机&#xff09;都可以用这个命令来判断当前网络是否有流量的状态&#xff0c;本篇文章主要介绍Linux的ping命令&#xff0c;因为Android系统也是使用了Linux内…

【面经】操作系统/Linux

1、计算机的五大单元 电脑的五大单元包括&#xff1a;输入单元、输出单元、控制单元、算数逻辑单元、存储单元五大部分。其中CPU占有控制、算术逻辑单元&#xff0c;存储单元又包含内存与辅助内存&#xff1b; 2、什么是操作系统 操作系统&#xff1a;负责管理协调我们计算机…

汽车车灯用肖特基二极管,选什么型号好?

肖特基二极管种类繁多&#xff0c;有低压降肖特基二极管、通用型肖特基二极管、快速恢复型肖特基二极管、高功率肖特基二极管、汽车级肖特基二极管等等&#xff0c;其中低压降肖特基二极管和汽车级肖特基二极管是二极管厂家东沃电子的核心优势产品。关于东沃电子推出的低压降肖…

Android 接入MQTT服务器

加入MQTT库 加入库可以直接下载对应的jar包&#xff0c;也可以在build.gradle里导入&#xff0c;然后加载进入。 这里直接在build.gradle加库 dependencies {implementation(libs.appcompat)implementation(libs.material)implementation(libs.activity)implementation(libs…