[初阶数据结构】单链表

前言

📚作者简介:爱编程的小马,正在学习C/C++,Linux及MySQL。

📚本文收录于初阶数据结构系列,本专栏主要是针对时间、空间复杂度,顺序表和链表、栈和队列、二叉树以及各类排序算法,持续更新!

📚相关专栏C++及Linux正在发展,敬请期待!
目录

前言

[1. 链表](#1. 链表)

[1.1 链表的定义](#1.1 链表的定义)

[1.2 链表与顺序表相比的好处](#1.2 链表与顺序表相比的好处)

[1.3 链表的结构表示](#1.3 链表的结构表示)

[1.3.1 链表的结构形式](#1.3.1 链表的结构形式)

[1.3.2 链表的结构性质](#1.3.2 链表的结构性质)

[1.4 单链表的实现](#1.4 单链表的实现)

[1.4.1 单链表的创建](#1.4.1 单链表的创建)

[1.4.2 单链表的打印](#1.4.2 单链表的打印)

[1.4.3 单链表的动态内存申请](#1.4.3 单链表的动态内存申请)

[1.4.4 单链表的头插](#1.4.4 单链表的头插)

[1.4.5 单链表的尾插](#1.4.5 单链表的尾插)

[1.4.6 单链表的头删](#1.4.6 单链表的头删)

[1.4.7 单链表的尾删](#1.4.7 单链表的尾删)

[1.4.8 单链表的查找](#1.4.8 单链表的查找)

[1.4.9 单链表的任意位置插入的前插](#1.4.9 单链表的任意位置插入的前插)

[1.4.10 单链表任意位置的删除](#1.4.10 单链表任意位置的删除)

2.单链表的完整代码

[2.1 test.c测试函数代码](#2.1 test.c测试函数代码)

[2.2 SList.h函数声明代码](#2.2 SList.h函数声明代码)

[2.3 SList.c函数实现代码](#2.3 SList.c函数实现代码)

总结


1. 链表

1.1 链表的定义

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

本文只介绍最简单的链表结构:单链表

1.2 链表与顺序表相比的好处

1、顺序表从中间插入/头部插入,时间复杂度是O(N),因为要一次往后挪动,但是单链表是O(1),大大节省了程序运行时间。

2、 顺序表每次需要增容,到后期增容很大的时候,需要拷贝数据、开辟新空间、释放旧空间。会有不小的损耗。链表直接开辟一个结构体大小的空间即可。

3、增容一般是两倍,但是我就想多插入几个仅此而已,会造成空间的大规模浪费。链表同样更加简单且占用空间小。

1.3 链表的结构表示

首先要给大家介绍一下,就是链表中的结点是一个结构体,结点中一个变量是存储数据的,另一个变量是存储结构体地址的,上一个结点存下一个结点的地址,从而链接起来。

1.3.1 链表的结构形式

1.3.2 链表的结构性质

1、从上图可以看出,链表在逻辑上是连续的,在物理地址上是不连续的

2、现实的结点是动态内存在堆区申请出来的

3、堆上申请的空间,是按照一定的规律来的,有些可能相同,有些可能不同。

1.4 单链表的实现

1.4.1 单链表的创建

上文我们提到了,结点是一个结构体,第一个结构体变量是存储数据的,第二个是存储下一个链表的地址的

cs 复制代码
typedef int SListDataType;

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

1.4.2 单链表的打印

cs 复制代码
void PrintSList(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL");
}

为什么是这样子打印的?给大家说一下思想:

1、单链表定义了最后一个链表的地址为空指针,所以我们就定义了一个cur来遍历整个链表

2、每找到一个数据我们就打印,然后遍历链表指针cur就往后走一步, 怎么走?是不是next中存放了下一个结点的地址,那么把cur管理的结构体中next的地址赋值给cur是不是相当于向后走了一步。

1.4.3 单链表的动态内存申请

cs 复制代码
SLTNode* BuySLTNode(SListDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

比方说,我想申请一块动态内存空间,里面存储x的值,那么这个时候,就通过malloc在堆上申请一块空间,交给newnode管理,这时候把newnode中data的值赋值为x,newnode中next的值赋值为NULL后返回这块空间的地址。这是不是就很好的开辟了一个结点。如果开辟失败了就返回空指针。

1.4.4 单链表的头插

cs 复制代码
void SListPushFront(SLTNode** pphead, SListDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	assert(newnode);
	newnode->next = *pphead;
	*pphead = newnode;
}	

这里我们用newnode来管理开辟的新结点,如果开辟了失败了就不用往下了,开辟成功了往下走,我画个图来帮助大家理解上面代码的意思

1.4.5 单链表的尾插

我先给大家介绍一下,尾插有两种情况,第一种空链表,第二种,非空链表

先分析第一种情况,如果是空链表,是不是直接把newnode的地址给pphead是不是就可以了,

第二种情况,我给大家画个图一起来分析

cs 复制代码
void SListPushBack(SLTNode** pphead, SListDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	//空链表
	
	if (*pphead == NULL)
	{
		*pphead = newnode;
		return;
	}
	//非空链表
	SLTNode* tail = *pphead;
	while (tail->next != NULL)
	{
		tail = tail->next;
	}
	tail->next = newnode;
}

第二种情况,首先我们要尾插,是不是要先找尾部在哪里?尾部在哪里?是不是说tail->next是空指针,这个就是链表中最后一个元素了,找到了之后,就把tail->next存newnode的地址就可以。

1.4.6 单链表的头删

给大家说一下啊,如何删除单链表中的数?是不是只需要让上个结点存下下个结点的地址就好了。

那么头删就是让*pphead指向下下个结点,然后释放第一个结点就好了。

那么,应该这么做,看代码

cs 复制代码
void SListPopFront(SLTNode** pphead)
{
	assert(*pphead);
	
	//链表只有一个值
	SLTNode* cur = *pphead;
	*pphead = cur->next;
	free(cur);
	cur = NULL;
}

画个图给大家理解一下:

1.4.7 单链表的尾删

尾删就更简单了,还是第一步,找尾,第二部,释放空间即可。

cs 复制代码
void SListPopBack(SLTNode** pphead)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next->next != NULL)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;
	}
}

1.4.8 单链表的查找

在单链表中查找一个数,找到了就返回这个结点的地址,没找到就返回空指针。

cs 复制代码
SLTNode* SListFind(SLTNode* pphead, SListDataType x)
{
	assert(pphead);
	SLTNode* cur = pphead;
	while (cur)
	{
		if (cur->data == x)
			return cur;
		else
			cur = cur->next;
	}
	return NULL;
}

1.4.9 单链表的任意位置插入的前插

cs 复制代码
void SListInsertbefore(SLTNode** pphead, SLTNode* pos, SListDataType x)
{
	if (*pphead == NULL)
	{
		SListPushFront(pphead, x);
	}
	//在pos前插入
	else
	{
		SLTNode* newnode = BuySLTNode(x);
		SLTNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = newnode;
		newnode->next = pos;
	}
}

首先啊,如果pphead没有值,那么就相当于头插,如果链表中有值,画个图给大家理解

1.4.10 单链表任意位置的删除

cs 复制代码
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

2.单链表的完整代码

2.1 test.c测试函数代码

cs 复制代码
#include "SList.h"
SLTNode* Phead = NULL;
void Test1()
{
	//该函数测试头插和尾插
	SListPushFront(&Phead, 1);
	SListPushFront(&Phead, 2);
	SListPushFront(&Phead, 3);
	SListPushFront(&Phead, 4);
	SListPushFront(&Phead, 5);
	SListPushBack(&Phead, 2);
	SListPushBack(&Phead, 3);
	SListPushBack(&Phead, 4);
	PrintSList(Phead);
}

void Test2()
{
	//该函数测试头删和尾删
	SListPushFront(&Phead, 1);
	SListPopBack(&Phead);
	PrintSList(Phead);
}
void Test3()
{
	//查找
	SListPushFront(&Phead, 1);
	SListPushFront(&Phead, 2);
	SListPushFront(&Phead, 3);
	SListPushFront(&Phead, 4);
	SLTNode* find = SListFind(Phead, 3);
	if (find)
		find->data = 30;
	PrintSList(Phead);
}
void Test4()
{
	//任意位置插入(前插)
	SListPushFront(&Phead, 1);
	SListPushFront(&Phead, 2);
	SListPushFront(&Phead, 3);
	SListPushFront(&Phead, 4);
	SLTNode* find1 = SListFind(Phead, 3);
	SLTNode* find2 = SListFind(Phead, 4);
	if (find1)
	{
		SListInsertbefore(&Phead, find1, 40);
		SListEraseafter(find2);
		SListEraseafter(find1);
	}
	PrintSList(Phead);
}
int main()
{
	Test1();
	//Test2();
	//Test3();
	//Test4();
	return 0;
}

2.2 SList.h函数声明代码

cs 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SListDataType;

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




void PrintSList(SLTNode* phead);
//头插
void SListPushFront(SLTNode** pphead, SListDataType x);
//尾插
void SListPushBack(SLTNode** pphead, SListDataType x);
//头删
void SListPopFront(SLTNode** pphead);
//尾删
void SListPopBack(SLTNode** pphead);
//查找
SLTNode* SListFind(SLTNode* pphead, SListDataType x);
//在pos之后插入x
void SListInsertbefore(SLTNode** pphead,SLTNode* pos, SListDataType x);
//在pos之后插入x
void SListInsertafter(SLTNode* pos, SListDataType x);
//删除pos位置上的值
void SListErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的位置
void SListEraseafter(SLTNode* pos);

2.3 SList.c函数实现代码

cs 复制代码
#include "SList.h"
void PrintSList(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL");
}

SLTNode* BuySLTNode(SListDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
void SListPushFront(SLTNode** pphead, SListDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	assert(newnode);
	newnode->next = *pphead;
	*pphead = newnode;
}	


void SListPushBack(SLTNode** pphead, SListDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	//空链表
	
	if (*pphead == NULL)
	{
		*pphead = newnode;
		return;
	}
	//非空链表
	SLTNode* tail = *pphead;
	while (tail->next != NULL)
	{
		tail = tail->next;
	}
	tail->next = newnode;
}

void SListPopFront(SLTNode** pphead)
{
	assert(*pphead);
	
	//链表只有一个值
	SLTNode* cur = *pphead;
	*pphead = cur->next;
	free(cur);
	cur = NULL;
}

void SListPopBack(SLTNode** pphead)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next->next != NULL)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;
	}
}

SLTNode* SListFind(SLTNode* pphead, SListDataType x)
{
	assert(pphead);
	SLTNode* cur = pphead;
	while (cur)
	{
		if (cur->data == x)
			return cur;
		else
			cur = cur->next;
	}
	return NULL;
}
void SListInsertbefore(SLTNode** pphead, SLTNode* pos, SListDataType x)
{
	if (*pphead == NULL)
	{
		SListPushFront(pphead, x);
	}
	//在pos前插入
	else
	{
		SLTNode* newnode = BuySLTNode(x);
		SLTNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = newnode;
		newnode->next = pos;
	}
}

void SListInsertafter(SLTNode* pos, SListDataType x)
{
	assert(pos);
	//只有一个
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

void SListEraseafter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLTNode* cur = pos->next;
	pos->next = cur->next;
	free(cur);
	cur = NULL;
}

总结

1、单链表其实不难,大家一定要搞清楚指针和结构体

2、一定要动手实践一下

3、其实数据结构就是围绕着数据的增删查改显示这几个点,所以一定要搞清楚每一个代码实现的逻辑。

如果这份博客对大家有帮助,希望各位给小马一个大大的点赞鼓励一下,如果喜欢,请收藏一下,谢谢大家!!!

制作不易,如果大家有什么疑问或给小马的意见,欢迎评论区留言。

相关推荐
一休哥助手1 小时前
Redis 五种数据类型及底层数据结构详解
数据结构·数据库·redis
苏宸啊1 小时前
顺序表及其代码实现
数据结构·算法
lin zaixi()1 小时前
贪心思想之——最大子段和问题
数据结构·算法
夜雨翦春韭2 小时前
【代码随想录Day30】贪心算法Part04
java·数据结构·算法·leetcode·贪心算法
一直学习永不止步2 小时前
LeetCode题练习与总结:H 指数--274
java·数据结构·算法·leetcode·数组·排序·计数排序
Amor风信子2 小时前
华为OD机试真题---跳房子II
java·数据结构·算法
Ljubim.te3 小时前
软件设计师——数据结构
数据结构·笔记
_GR4 小时前
每日OJ题_牛客_牛牛冲钻五_模拟_C++_Java
java·数据结构·c++·算法·动态规划
无限大.4 小时前
c语言实例
c语言·数据结构·算法
@haihi5 小时前
冒泡排序,插入排序,快速排序,选择排序
数据结构·算法·排序算法