【数据结构】无头+单向+非循环链表(SList)(增、删、查、改)详解

news2024/11/17 7:32:49

一、链表的概念及结构

1、链表的概念

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


 :之所以引出链表,是因为顺序表存在一些缺点

  • 顺序表在中间 / 头部的插入和删除,需要挪动很多数据,时间复杂度为 O(N),效率低下。
  • 增容需要申请新空间,拷贝数据,释放旧空间。消耗不小。
  • 增容一般是一次增长 2 倍,那就一定会造成空间浪费。例如当前的容量为 100,满了以后增容到 200,这时再继续插入 5 个数据,后面不再插入,那么就浪费了 95 个数据空间。

2、链表的组成

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

每个结点包括两个部分:

  1. 数据域:存储数据元素。
  2. 指针域:存储下一个结点地址。

3、链表的结构

(1)链表的物理结构(现实中)

(2)链表的逻辑结构(想象中)


 

  • 链式结构在逻辑上是连续的,但在物理上不一定连续。
  • 现实中的结点一般都是上申请出来的
  • 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。

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

无头单向非循环链表结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。


1、创建文件

  • test.c(主函数、测试顺序表各个接口功能)
  • SList.c(单链表接口函数的实现)
  • SList.h(单链表的类型定义、接口函数声明、引用的头文件)


2、SList.h 头文件代码

// SList.h
// 无头+单向+非循环链表增删查改实现
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int SLTDateType;

typedef struct SListNode
{
    SLTDateType data; // 数据域
    struct SListNode* next; // 指针域
}SListNode;

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 销毁单链表中所有节点
void SListDestory(SListNode** pphead)
// 单链表打印
void SListPrint(SListNode* phead);
// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pphead, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pphead);
// 单链表头删
void SListPopFront(SListNode** pphead);
// 单链表查找
SListNode* SListFind(SListNode* phead, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 求单链表长度
int SListSize(SListNode* phead);
// 单链表判空
bool SListEmpty(SListNode* phead);

三、在 SList.c 中实现各个接口函数

1、动态申请一个节点

// 动态申请一个节点
SListNode* BuyListNode(SLTDataType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

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

// 销毁单链表中所有节点
void SListDestory(SListNode** pphead)
{
    assert(pphead);
	SListNode* cur = *pphead;
	while (cur)
	{
		SListNode* next = cur->next;
		free(cur); // 释放节点
		cur = next;
	}
	*pphead = NULL;
}

assert() 放在函数里面检查参数,一方面是为了安全,另一方面是为了防止其他人在调用该函数时,出现不正确的使用,导致错误传入参数,这样可以及时提醒到他。所以写代码时一定要考虑到其他人不正确的使用该函数时的场景,以此避免不必要的麻烦。 


3、单链表打印

// 单链表打印
void SListPrint(SListNode* phead)
{
    // 不需要断言 -- 空链表也可以打印
    SListNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

4、单链表尾插

// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);  //动态申请一个节点

	if (*pphead == NULL) // 单链表中没有节点时
	{
		*pphead = newnode; // plist指向新节点
	}

	else // 单链表中已经有节点时
	{
		SListNode* tail = *pphead;
		while (tail->next != NULL) // 找到单链表中的最后一个节点
		{
			tail = tail->next;
		}
		tail->next = newnode; // 最后一个节点的next指向新节点
	}
}

  错误写法❌:

(传一级指针的值,用一级指针接收)指针传值,相当于把 plist 指针变量的值拷贝给 phead,给 phead 赋值 newnode,phead 的改变不会影响 plist

// 错误写法:
// 单链表尾插
void SListPushBack(SListNode* phead, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);  //动态申请一个节点

	if (phead == NULL) // 单链表中没有节点时
	{
		phead = newnode; // plist指向新节点
	}

	else // 单链表中已经有节点时
	{
		SListNode* tail = phead; // 找到尾节点
		while (tail->next != NULL) // 找到单链表中的最后一个节点
		{
			tail = tail->next;
		}
		tail->next = newnode; // 最后一个节点的next指向新节点
	}
}

因为当链表为空时,我们需要改变 plist 的指向,使其指向第一个节点。而初始 plistphead 都指向 NULL,调用函数后,phead 指向了新的节点,而 plist 还是指向 NULL 的。


 解决方法:

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


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


5、单链表头插

// 单链表的头插
void SListPushFront(SListNode** pphead, SLTDataType x)
{
	assert(pphead);
	SListNode* newnode = BuyListNode(x); // 动态申请一个节点
	newnode->next = *pphead; // 新节点的next指针指向plist指向的位置
	*pphead = newnode; // plist指向头插的新节点
}


6、单链表尾删

// 单链表的尾删
void SListPopBack(SListNode** pphead)
{
	assert(pphead);
    assert(*pphead); //链表为空就无法再进行尾删了

	// 温柔处理方式
	/*if (*pphead == NULL)
	{
		return;
	}*/

	// 粗暴处理方式
	assert(*pphead);

	if ((*pphead)->next == NULL) // 链表一个节点
	{
		free(*pphead);
		*pphead = NULL;
	}

	else // 链表中有多个节点
	{
        // 写法一:
		/* SListNode* prev = NULL;
		SListNode* tail = *pphead;
		while(tail->next != NULL) // 找到链表的尾节点的上一个节点
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail); // 删除尾节点
		tail = NULL;
		prev->next = NULL;  // 置空 */

        //写法二:
		SListNode* tail = *pphead;
		while (tail->next->next != NULL) // 找到链表的尾节点的上一个节点
		{
			tail = tail->next;
		}
		free(tail->next); // 删除尾节点
		tail->next = NULL; // 置空
	}
}


  •  单链表只有一个节点时,删除节点plist 指向 NULL
  • 单链表有多个节点时,先找到单链表尾节点的上一个节点删除尾节点,然后将该节点的 next 指向 NULL
  • 因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址

7、单链表头删

// 单链表头删
void SListPopFront(SListNode** pphead)
{
	assert(pphead);
    assert(*pphead); // 链表为空就无法再进行头删了

	/*if (*pphead == NULL)
	{
		return;
	}
	else
	{
		SListNode* next = (*pphead)->next;
		free(*pphead);
		*pphead = next;
	}*/

	SListNode* cur = *pphead; // 保存头节点的地址
	*pphead = cur->next; // plist指向头节点的下一个节点
	free(cur); // 删除头节点
}

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


8、单链表查找指定值的节点

// 单链表查找
SLTNode* SListFind(SListNode* phead, SLTDataType x)
{
	SListNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur; // 找到了 返回该节点的地址
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL; // 未找到,返回NULL
}


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

// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos); // pos位置不能为空
	SListNode* newnode = BuySListNode(x); // 动态申请一个节点
	newnode->next = pos->next; // 新节点的next指针指向pos位置后一个节点
	pos->next = newnode; // pos位置的next指向新节点
}

为什么不在pos位置之前插入?
  • 单链表不适合在 pos 位置之前插入,因为需要遍历链表找到 pos 位置的前一个节点,时间复杂度为O(N)。
  • 单链表更适合在 pos 位置之后插入,如果在后面插入,只需要知道 pos 位置即可,简单得多。
  • C++ 官方库里面单链表给的也是在之后插入


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

// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
	assert(pos); // pos位置不能为空
	assert(pos->next); // pos位置不能是尾节点
	SListNode* next = pos->next; // 保存pos位置的后一个节点
	pos->next = pos->next->next;
	free(next); // 释放pos位置的后一个节点
}

为什么不删除pos位置?
void SListErase(SListNode** pphead, SLTNode* pos) // O(N)
{
	assert(pphead);
	assert(pos);

	if (*pphead == pos)
	{
		/* *pphead = pos->next;
		free(pos); */
		SListPopFront(pphead);
	}
	else
	{
		SListNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		//pos = NULL;
	}
}
  • 删除 pos 位置同样需要传入单链表的二级指针。
  • 单链表不适合在 pos 位置插入,因为需要遍历链表找到 pos 位置的前一个节点,以改变其指向,时间复杂度大。


11、求单链表长度

// 求单链表长度
int SListSize(SListNode* phead)
{
	int size = 0;
	SListNode* cur = phead;
	while (cur)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

12、判断单链表是否为空

// 单链表判空
bool SListEmpty(SListNode* phead)
{
    // 写法一:
	return phead == NULL;
	
	// 写法二:
	//return phead == NULL ? true : false;
}

四、代码整合

// SList.c
#include "SList.h"

// 动态申请一个节点
SListNode* BuyListNode(SLTDataType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

// 销毁单链表中所有节点
void SListDestory(SListNode** pphead)
{
    assert(pphead);
	SListNode* cur = *pphead;
	while (cur)
	{
		SListNode* next = cur->next;
		free(cur); // 释放节点
		cur = next;
	}
	*pphead = NULL;
}

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

// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);  //动态申请一个节点

	if (*pphead == NULL) // 单链表中没有节点时
	{
		*pphead = newnode; // plist指向新节点
	}

	else // 单链表中已经有节点时
	{
		SListNode* tail = *pphead;
		while (tail->next != NULL) // 找到单链表中的最后一个节点
		{
			tail = tail->next;
		}
		tail->next = newnode; // 最后一个节点的next指向新节点
	}
}

// 单链表的头插
void SListPushFront(SListNode** pphead, SLTDataType x)
{
	assert(pphead);
	SListNode* newnode = BuyListNode(x); // 动态申请一个节点
	newnode->next = *pphead; // 新节点的next指针指向plist指向的位置
	*pphead = newnode; // plist指向头插的新节点
}

// 单链表的尾删
void SListPopBack(SListNode** pphead)
{
	assert(pphead);
    assert(*pphead); //链表为空就无法再进行尾删了
	assert(*pphead);

	if ((*pphead)->next == NULL) // 链表一个节点
	{
		free(*pphead);
		*pphead = NULL;
	}
	else // 链表中有多个节点
	{
		SListNode* tail = *pphead;
		while (tail->next->next != NULL) // 找到链表的尾节点的上一个节点
		{
			tail = tail->next;
		}
		free(tail->next); // 删除尾节点
		tail->next = NULL; // 置空
	}
}

// 单链表头删
void SListPopFront(SListNode** pphead)
{
	assert(pphead);
    assert(*pphead); // 链表为空就无法再进行头删了

	SListNode* cur = *pphead; // 保存头节点的地址
	*pphead = cur->next; // plist指向头节点的下一个节点
	free(cur); // 删除头节点
}

// 单链表查找
SLTNode* SListFind(SListNode* phead, SLTDataType x)
{
	SListNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur; // 找到了 返回该节点的地址
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL; // 未找到,返回NULL
}

// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos); // pos位置不能为空
	SListNode* newnode = BuySListNode(x); // 动态申请一个节点
	newnode->next = pos->next; // 新节点的next指针指向pos位置后一个节点
	pos->next = newnode; // pos位置的next指向新节点
}

// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
	assert(pos); // pos位置不能为空
	assert(pos->next); // pos位置不能是尾节点
	SListNode* next = pos->next; // 保存pos位置的后一个节点
	pos->next = pos->next->next;
	free(next); // 释放pos位置的后一个节点
}

// 求单链表长度
int SListSize(SListNode* phead)
{
	int size = 0;
	SListNode* cur = phead;
	while (cur)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

// 单链表判空
bool SListEmpty(SListNode* phead)
{
    // 写法一:
	return phead == NULL;
}

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

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

相关文章

Element UI如何自定义样式

简介 Element UI是一套非常完善的前端组件库&#xff0c;但是如何个性化定制其中的组件样式呢&#xff1f;今天我们就来聊一聊这个 举例 就拿最常见的按钮el-button来举例&#xff0c;一般来说默认是蓝底白字。效果图如下 可是我们想个性化定制&#xff0c;让他成为粉底红字应…

在windows上安装minio

1、下载windows版的minio&#xff1a; https://dl.min.io/server/minio/release/windows-amd64/minio.exe 2、在指定位置创建一个名为minio文件夹&#xff0c;然后再把下载好的文件丢进去&#xff1a; 3、右键打开命令行窗口&#xff0c;然后执行如下命令&#xff1a;(在minio.…

【数据结构】栈(Stack)的实现 -- 详解

一、栈的概念及结构 1、概念 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在表尾进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出 LIFO&#xff08;Last In First Out&#xff09;的原则。 压栈…

Android Glide预处理preload原始图片到成品resource 预加载RecyclerViewPreloader,Kotlin

Android Glide预处理preload原始图片到成品resource & 预加载RecyclerViewPreloader&#xff0c;Kotlin <uses-permission android:name"android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name"android.permission.READ_MED…

集合的概述

基本的集合有六种&#xff0c;分别是Vector、ArrayList、LinkedList、TreeSet、HashSet、LinkedHashSet 其中Vector、ArrayList、LinkedList实现了List接口&#xff0c;LinkedHashSet实现了HashSet接口&#xff0c;TreeSet、HashSet实现了Set接口 List和Set又实现了Collectio…

Anaconda原理解析及使用

anaconda想必大家都不陌生&#xff0c;属于使用python的重要工具&#xff0c;更是学习机器学习、深度学习的必备工具。在搭建环境过程中&#xff0c;感觉出现的许多问题根源在于对于anaconda的基本原理理解不到位&#xff0c;导致许多无效操作。为此&#xff0c;我重温了一遍an…

TypeScript实战篇 - TS实战:花田APP的架构

目录 TS实现花田APP的聊天Node端 整体架构 项目拆分 项目的特点 模型层 所有系统都是模型的外设 模型层的优势 TS实现花田APP的聊天Node端 整体架构 项目拆分 代号&#xff1a;huatian 5个独立的npm包 huatian/ui 花田的主项目huatian/component 花田组件库huatian/…

❤️创意网页:高考加油倒计时网页文字加多版 - 增加祝福语句和下雪背景效果

✨博主&#xff1a;命运之光 &#x1f338;专栏&#xff1a;Python星辰秘典 &#x1f433;专栏&#xff1a;web开发&#xff08;简单好用又好看&#xff09; ❤️专栏&#xff1a;Java经典程序设计 ☀️博主的其他文章&#xff1a;点击进入博主的主页 前言&#xff1a;欢迎踏入…

Emacs之改造最快的文件搜索工具fd-dired(基于fd命令)(一百二十一)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

RAID相关知识

简介 RAID &#xff08; Redundant Array of Independent Disks &#xff09;即独立磁盘冗余阵列&#xff0c;通常简称为磁盘阵列。RAID技术将多个单独的物理硬盘以不同的方式组合成一个逻辑磁盘&#xff0c;从而提高硬盘的读写性能和数据安全性。 数据组织形式 分块&#x…

Flowable-任务-手动任务

定义 手动任务是预期在没有任何业务流程执行引擎或任何应用程序的帮助下执行的任务&#xff0c;它用于建 模那些引擎不需要知道的人所做的工作&#xff0c;以及那些不存在已知系统或 UI 界面的人所做的工作&#xff0c;一 般完善流程结构描述&#xff0c;不被引擎执行。例如&a…

cookie登录b站获取cookie登录billbill教程

利用cookie免账号密码登录b站cookie登录哔哩哔哩cookie登录billbill教程 1.获取cookie 以Edge浏览器为例&#xff0c;随便找一个人私聊&#xff0c;按下F12&#xff0c;选到网络(network)&#xff0c;在筛选器里填send_msg&#xff0c;如下图所示。如果没有网络(network)&…

@TableId(type = IdType.ASSIGN_ID)

最近一直在使用mybatis plus ,上篇说没有添加ID 那不得学习一把 本来想不去添加主键&#xff0c;但是暂时还没发现mybatis plus增么 可以不设置主键的情况下修改&#xff0c;想想还是不行&#xff0c;主要我不想去多写代码&#xff08;肯定不是因为懒&#xff09;&#xff0c;…

巨人互动|Google海外户Google SEO常见术语

随着越来越多的人开始建立网站和在线业务&#xff0c;谷歌搜索引擎优化&#xff08;SEO&#xff09;变得越来越重要。要在谷歌上获得更高的排名&#xff0c;您需要掌握许多不同的术语和技术。在本篇文章中&#xff0c;我们将介绍一些常见的谷歌SEO术语&#xff0c;以帮助您了解…

perf 分析MySQL底层函数调用

文章目录 一、安装软件包二、数据采集2.1 perf top2.2 perf record 三、数据加工和解读 一、安装软件包 sudo yum install -y perf git clone https://github.com/brendangregg/FlameGraph二、数据采集 2.1 perf top perf top -g -p pidof mysqld 第一列&#xff1a;符号引…

指针的应用练习(数组与指针的关系)

如果对指针不是那么熟悉&#xff0c;我这里有几篇指针相关入门&#xff0c;不知道能不能帮助到你 http://t.csdn.cn/BbVwT http://t.csdn.cn/eqBng http://t.csdn.cn/hwNXp 看完后&#xff0c;检测一下这两段代码是否能透彻理解 &#xff08;1&#xff09; #include<s…

<MyBatis>前台同一个参数传多个条件查询方式(传数组或者拼接字符串)

方式一&#xff1a;前台传参为数组&#xff0c;后台SQ查询案例&#xff1a; 一般为多选场景&#xff1a;查询&#xff1b; 举例如下&#xff1a; 传值&#xff1a;“status” : [“保存”,“关闭”], 不传值&#xff1a;“status”: [], 传给后台&#xff1a; 控制层&#xff1…

虚拟机中Linux的IP地址配置详解

目录 第一章、虚拟机中Linux的IP地址配置详解1.1&#xff09;什么是IP地址1.2&#xff09;如何查看自己电脑ip地址1.3&#xff09;虚拟机NAT模式中Linux的IP地址设置有什么要求 第二章、使用Linux中的编辑命令进行网卡信息文件的配置 友情提醒 先看文章目录&#xff0c;大致了…

c++静态代码扫描工具clang-tidy详细介绍

clang-tidy 文章目录 clang-tidy1. 什么是clang-tidy2. clang-tidy可以解决什么问题3. 工作原理4. 如何使用clang-tidy4. 总结5. 举例说明&#xff1a; 1. 什么是clang-tidy Clang-Tidy是一个由LLVM项目提供的开源工具&#xff0c;是一个静态分析工具&#xff0c;用于进行静态…

Emacs之point-undo代码步骤记忆前进/回退(一百二十二)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…