线性表之单链表(详解)

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

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

🍚代码仓库:破浪晓梦

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


🍥前言

在前一文章我们已经学习了顺序表,但是我们发现顺序表还有一些小缺点满足不了我们的需求,例如:

  • 中间/头部的插入删除,时间复杂度为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);
	SListNode* cur = *pplist;
	if (*pplist == pos)
	{
		*pplist = (*pplist)->next;
		free(cur);
		return;
	}
	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;
}

运行结果:

在这里插入图片描述

  • 23
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 41
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 41
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

自信不孤单

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值