【C语言】超详细的单链表实现及接口执行逻辑解析~

【C语言】超详细的单链表实现及接口执行逻辑解析~_第1张图片图片出处:The world's biggest drone photo and video sharing platform | SkyPixel.com

前言

  在上两篇博文中,我写了顺序表及以顺序表为底层结构实现通讯录项目的相关内容,这都是线性表的一种,本文将详细介绍另一种线性表数据结构——链表。


目录

前言

链表的概念及结构:

  概念:

  结构:

链表的实现:

(1)头文件

1))链表单个节点结构创建

2))声明链表的各个接口

(2)源文件

1))打印链表

2))尾插

1/创建一个新的节点

2/尾插

3))头插

4))尾删

5))头删

6))在指定节点之前插入

 1/查找某值在链表中第一次出现的节点

 2/在指定节点之前插入

7))在指定节点之后插入

8)) 删除pos位置节点

9)) 删除pos位置之后节点

10))销毁链表 

单链表源码:

(1)头文件源码

(2)源文件源码


链表的概念及结构:

  概念:

  链表是线性表的一种,其逻辑结构是线性,物理存储结构是非连续、非顺序的。

  链表分为八种,本文讲解的是无头单向不循环链表,简称单链表。

  结构:

  链表的结构跟火车车厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只 需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢。

【C语言】超详细的单链表实现及接口执行逻辑解析~_第2张图片

  ⻋厢是独⽴存在的,且每节⻋厢都有⻋⻔。想象⼀下这样的场景,假设每节⻋厢的⻋⻔都是锁上的状 态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从⻋头⾛到⻋尾? 最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。

  

  那么在链表中,每节车厢是什么样的呢?

  【C语言】超详细的单链表实现及接口执行逻辑解析~_第3张图片

   如上便是链表的结构示意图。与顺序表不同的是,链表的每节“车厢”都是独立申请下来的空间,我们称之为“节点”/“结点”,而每个节点中保存下一节点地址的指针变量便是钥匙。

  节点的组成主要有两部分:当前节点的要保存的数据和保存下一节点的地址(指针变量)。

  那么为什么需要指针变量保存下一个节点的位置呢?

  答:在链表结构中,每个节点都是独立申请的空间,而节点与节点之间唯一的联系便是节点中存储的下一节点地址,我们只有通过该地址才能找到下一个节点。这样的设计可以使我们无需连续的空间也可以实现存储并管理数据,相较于顺序表,不仅省去了调整空间与拷贝数据的过程,更是大大提高了空间的利用率。

  

链表的实现:

  当大家对链表有了初步的认识后,便可跟着我的思路来尝试一步一步构思并实现链表这一数据结构。按照惯例,本次依旧是按照声明与实现过程分离的方式进行,这样更有益于搭建代码的整体框架。

(1)头文件

  在头文件中,我们需要做三件事:

  • 包含所要用到的库函数头文件
  • 创建链表单个节点的结构
  • 声明链表的各个接口  

  包含头文件我省略不讲,接下来直接开始创建链表单个节点的结构。

1))链表单个节点结构创建

【C语言】超详细的单链表实现及接口执行逻辑解析~_第4张图片

2))声明链表的各个接口

  到此,我们需要构思一下,在该链表中我们需要对其实现一些申明接口呢?

  在本文我需要实现的接口如下:

  • 打印链表
  • 尾插
  • 头插
  • 尾删
  • 头删
  • 指定节点之前插入
  • 指定节点之后插入
  • 删除pos位置节点
  • 删除pos位置之后节点
  • 销毁链表

   

在头文件中声明如下:(形参为初步设想,最终头文件以文末源码为准)

【C语言】超详细的单链表实现及接口执行逻辑解析~_第5张图片

  此处我在略微解释一下上图中谈到的程序接口传参统一的必要性,举个例子:若是我们写了一个程序 交给别人使用,但是在该程序中对同一个参数在不同接口传参的形式各种各样,这就会让使用者很苦恼,所以我们要尽量避免此类情况。

(2)源文件

  在源文件板块,我会对上述已声明的接口一一实现,同时若是有必要,也会增加其他接口或是改动原接口等等。需要注意的是,我会对在本文中第一次出现的重难点进行讲解,但在讲解之后再遇到同类逻辑则不再提及。

  在实现之前,需要在源文件中包含我们所写的头文件,如下:

【C语言】超详细的单链表实现及接口执行逻辑解析~_第6张图片

  1))打印链表

  在实际使用场景下,链表是不需要该接口的,但是在此处实现该接口可以让我们更好地观察链表的增删变化,故将其放在第一个实现。

  打印链表接口实现如下:

【C语言】超详细的单链表实现及接口执行逻辑解析~_第7张图片

  循环方式图示:

【C语言】超详细的单链表实现及接口执行逻辑解析~_第8张图片

   此逻辑在后续接口中将被大量用到,后续将不再做过多解释。

2))尾插

  在链表中,不论是尾插、头插、任意插,都需要创建一个新的节点,故我将创建新节点这一重复代码包装为函数,让其它接口可以直接调用。

  1/创建一个新的节点

【C语言】超详细的单链表实现及接口执行逻辑解析~_第9张图片

2/尾插

【C语言】超详细的单链表实现及接口执行逻辑解析~_第10张图片

3))头插

【C语言】超详细的单链表实现及接口执行逻辑解析~_第11张图片

 

 4))尾删

【C语言】超详细的单链表实现及接口执行逻辑解析~_第12张图片

 5))头删

【C语言】超详细的单链表实现及接口执行逻辑解析~_第13张图片

 

6))在指定节点之前插入

  在实现之前,我需要请大家思考一个问题:在链表中如何指定节点?貌似这确实值得我们深思,链表不同于顺序表,其没有固定的下标。那么我们需要通过什么来指定节点呢?地址?可操作性不强。那就只剩下节点中的值了,我们可以通过查找节点中的值来找到指定节点!

  但是使用该方式来指定节点貌似不能称之为指定,因为链表中每个节点的值并不是唯一的,故准确来说,应当是查找该值在链表中第一次出现的节点

   因为我们后续会有很多接口需要指定节点,故我将其封装成接口。

  1/查找某值在链表中第一次出现的节点

【C语言】超详细的单链表实现及接口执行逻辑解析~_第14张图片

  需要注意的是:若是需要调用其它指定节点插入/删除...等接口时,需要先调用该接口找到指定节点,再将指定节点传给其他接口。

 2/在指定节点之前插入

【C语言】超详细的单链表实现及接口执行逻辑解析~_第15张图片

 

 

7))在指定节点之后插入

【C语言】超详细的单链表实现及接口执行逻辑解析~_第16张图片

8)) 删除pos位置节点

【C语言】超详细的单链表实现及接口执行逻辑解析~_第17张图片

9)) 删除pos位置之后节点

【C语言】超详细的单链表实现及接口执行逻辑解析~_第18张图片

10))销毁链表 

【C语言】超详细的单链表实现及接口执行逻辑解析~_第19张图片 

 

 

   至此,一个单链表数据结构接口便全部完成,由于链表与之前数组的存储形式不同,故本文中很多地方我都画图解释,希望能对大家有所帮助。

单链表源码:

(1)头文件源码

  ChainList.h

#pragma once

#include 
#include 
#include 

//定义该顺序表储存的数据类型
typedef int SLDataType;

//定义链表单个节点结构
typedef struct SLNode
{
	SLDataType x;//数据
	struct SLNode* next;//保存下一个节点地址的指针变量
}SLNode;

//尾插
void SLNPushBack(SLNode** pphead, SLDataType x);

//头插
void SLNPushFront(SLNode** pphead, SLDataType x);

//尾删
void SLNPopBack(SLNode** pphead);

//头删
void SLNPopFront(SLNode** pphead);

//查找某值在链表中第一次出现的节点
SLNode* SLNFind(SLNode** pphead, SLDataType x);

//在指定节点之前插入
void SLNInsert(SLNode** pphead, SLNode* pos, SLDataType x);

//在指定节点之后插入
void SLNInsertAfter(SLNode* pos, SLDataType x);

//删除pos位置节点
void SLNErase(SLNode** pphead, SLNode* pos);

//删除pos位置之后节点
void SLNEraseAfter(SLNode* pos);

//打印链表
void SLNPrint(SLNode** pphead);

//销毁链表
void SLNDstroy(SLNode** pphead);

(2)源文件源码

  ChainList.c

#define _CRT_SECURE_NO_WARNINGS 1 

#include "ChainList.h"

//打印链表
void SLNPrint(SLNode** pphead)
{
	assert(pphead);
	//创建临时变量,用以遍历
	SLNode* pcur = *pphead;
	while (pcur)
	{
		printf("%d-> ", pcur->x);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

//创建新节点
SLNode* SLNByNode(SLDataType x)
{
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
	if (node == NULL)
	{
		perror("malloc");
		return NULL;
	}
	node->x = x;
	node->next = NULL;
	return node;
}

//尾插
void SLNPushBack(SLNode** pphead, SLDataType x)
{
	SLNode* node = SLNByNode(x);
	//不论是头插、尾插、任意插,都需要创建一个新的节点
	//故将创建新节点包装成函数,以方便调用
	assert(pphead);
	//尾插分两种情况
	//1,当链表中无节点时
	if (*pphead == NULL)
	{
		*pphead = node;
	}
	//2,链表中至少有一个节点
	else
	{
		//找尾
		SLNode* pcur = *pphead;
		while (pcur->next)
		{
			pcur = pcur->next;
		}
		pcur->next = node;
	}
	
}

//头插
void SLNPushFront(SLNode** pphead, SLDataType x)
{
	assert(pphead);
	SLNode* node = SLNByNode(x);
	node->next = *pphead;
	*pphead = node;
}

//尾删
void SLNPopBack(SLNode** pphead)
{
	assert(pphead);
	//尾删需要保证其链表至少存在一个节点
	assert(*pphead);
	//尾删分两种情况:
	//1:当需要删除的节点就是首节点时
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//:当链表中至少有两个节点时,需要找尾
	else
	{
		SLNode* pcur = *pphead;
		SLNode* prev = NULL;
		while (pcur->next)
		{
			prev = pcur;
			pcur = pcur->next;
		}
		prev->next = pcur->next;
		free(pcur);
		pcur = NULL;
	}
}

//头删
void SLNPopFront(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLNode* pcur = *pphead;
	*pphead = (*pphead)->next;
	free(pcur);
	pcur = NULL;
}

//查找某值在链表中第一次出现的节点
SLNode* SLNFind(SLNode** pphead, SLDataType x)
{
	assert(pphead);
	assert(*pphead);
	SLNode* pcur = *pphead;
	while (pcur)
	{
		if (pcur->x == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

//在指定节点之前插入
void SLNInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	SLNode* node = SLNByNode(x);
	//在指定节点之前插入分两种情况
	//1,指定节点就是头节点
	if (pos == *pphead)
	{
		node->next = *pphead;
		*pphead = node;
	}
	//2,指定节点不是头节点
	else
	{
		//需要找到pos前一个节点
		SLNode* pcur = *pphead;
		while (pcur->next != pos)
		{
			pcur = pcur->next;
		}
		pcur->next = node;
		node->next = pos;
	}
}

//在指定节点之后插入
void SLNInsertAfter(SLNode* pos, SLDataType x)
{
	assert(pos);
	SLNode* node = SLNByNode(x);
	node->next = pos->next;
	pos->next = node;
}

//删除pos位置节点
void SLNErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	//删除pos位置的节点分两种情况
	//1,当pos为首节点
	if (pos == *pphead)
	{
		*pphead = (*pphead)->next;
		free(pos);
		pos = NULL;
	}
	//2,当pos不是首节点
	else
	{
		//找pos前一个节点
		SLNode* pcur = *pphead;
		while (pcur->next != pos)
		{
			pcur = pcur->next;
		}
		pcur->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

//删除pos位置之后节点
void SLNEraseAfter(SLNode* pos)
{
	assert(pos && pos->next);
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

//销毁链表
void SLNDstroy(SLNode** pphead)
{
	assert(pphead);
	SLNode* pcur = *pphead;
	SLNode* next = NULL;
	while (pcur)
	{
		next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

你可能感兴趣的:(c语言,开发语言,数据结构)