当前位置: 首页 > article >正文

线性表之单链表(详解)

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

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

🍚代码仓库:破浪晓梦

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

文章目录

  • 🍥前言
  • 🍉链表
    • 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.kler.cn/a/16322.html

相关文章:

  • 基于STM32设计的森林火灾监测系统(华为云IOT)_263
  • Python 编程入门指南(一)
  • 树莓派(Raspberry Pi)Pico 2 C_C++开发环境配置(Docker+SDK)
  • 在JPA和EJB中用乐观锁解决并发问题
  • Java结合ElasticSearch根据查询关键字,高亮显示全文数据。
  • Linux基础1
  • c语言和cpp里面的强制类型转换
  • 嵌入式linux学习笔记--虚拟局域网组网方案分享,基于自组zerotier -planet 网络的方案
  • RabbitMQ面试题
  • Linux系统操作案例-配置Nginx的负载均衡与转发
  • 110页智慧农业解决方案(农业信息化解决方案)(ppt可编辑)
  • Spring 5 笔记 - AOP
  • 【python知识】__init__.py的来龙去脉
  • 【Mysql】基础篇:DML(data manipulation language)语句:增、删、改数据库数据总结
  • pod之debug初始化容器
  • 最优化方法Python计算:一元函数搜索算法——二分法
  • 【iOS】GCD学习
  • 【机器学习学习】第一天:入门指南
  • spring
  • QT实现固高运动控制卡示波器
  • 【SQL篇】面试篇之子查询
  • 一文解决MySQL突击面试,关键知识点总结
  • 解除Word的编辑保护【简单版】
  • 智能网联汽车城市化的进程和思考
  • next(), nextLine(),nextInt()报错分析
  • UG NX二次开发(C++)-建模-修改NXObject或者Feature的颜色(一)