数据结构の单链表

目录

前言

 单链表的介绍

单链表结构分析:

理解节点之前的链接关系:

链表的优点

单链表的实现

定义节点结构体

初始化和销毁

初始化:

销毁:

打印链表和查找节点

打印链表:

查找节点:

创建节点

尾插与头插

尾部插入数据:

头部插入数据:

尾删与头删

指定位置之前插入与指定位置之后插入

指定位置之前插入:

指定位置之后插入:

删除指定节点与删除指定位置之后的节点

删除指定节点:

删除指定节点之后的节点:

后记

附代码

SList.h文件

SList.c文件


前言

哈喽——大家好鸭,欢迎来到小鸥的博客~

新人进步中~~

有各种不足欢迎大家指出:海盗猫鸥-CSDN博客

那么我们正式开始:

在之前的博客中我们介绍了顺序表这个数据结构,今天我们就继续讲解下一个数据结构——单链表吧!

 单链表的介绍

链表和顺序表一样也是线性表的一种,链表的逻辑结构是线性的,但他的物理结构不一定是线性的;链表又详细分为了8种,具体的分类方式这里先按下不表,之后我还会再出一篇双向链表的博客,在那里再进行解读。

单链表结构分析:

链表的结构就像一个火车一样,一个接着一个;也和我们生活中的链子相似,一个一个的连在一起,所以称为链表;而链表中的每一个节点都是单独申请的,就像火车车厢,可以按需要随意的添加和删减。

理解节点之前的链接关系:

每一个节点都存放着一份数据,但每一个节点之间,不能无缘无故的链接起来,所以还要有一个办法能用来链接每个节点。

是的,你想得没错,这里我们就要用到指针啦!让每一个节点存储数据的同时,在存放一个指针,让这个指针指向下一个节点的地址,这样我们就可以通过一个节点找到后面的所有节点啦!

图解:

1.每一个节点的next里都存放了下一个节点的地址;

2.第一个节点的地址存放在phead指针中;

3.尾节点的next指向NULL,以此表示结尾。

链表的优点

由于链表的每一个节点都是通过指针来链接起来的,相互可以是独立的,并且每有一个数据,就可以申请一个节点将其储存,再加入到链表中,所以相对于顺序表,链表的优点有:

1.空间利用率相对更高,不会浪费空间;

2.管理更加便捷,在进行删除和添加数据时更加便捷;

单链表的实现

上面我们提到,每个节点要存放数据,以及一个指针,那么可想而知,链表的节点也是一个自定义的结构体

定义节点结构体

typedef int SLTDataType;
//单链表节点
//数据+下一个节点的地址
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

这里存储的数据类型我们也像顺序表中一样,进行了重命名,方便以后需要时的修改。

初始化和销毁

在创建单链表时,会定义一个指针plist,置为NULL时表示没有节点,后续使他指向链表的第一个节点。

    SLTNode* plist = NULL;

后续使用的二级指针pphead即为&plist。

pphead == &plist 表示的是plist的地址;

*pphead == plist 表示的是第一个节点的地址,若没有节点则为初始值NULL;

初始化:

由于单链表是没有实际上的头节点哨兵位,会在接触双链表时讲解)的,第一个节点就是直接有效的数据节点,所以只需要在定义单链表时,将定义的指向第一个节点的指针置为NULL即可,不需要单独的函数来初始化;

销毁:

//销毁链表
void SLTDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	while (*pphead)
	{
		SLTNode* next = (*pphead)->next;
		free(*pphead);
		*pphead = next;
	}
	*pphead = NULL;
}

1.判断指向第一个有效节点的指针是否定义,以及单链表是否存在有效节点;

2.循环释放动态申请空间;

3.在将最后一个节点释放后,将*pphead即plist指针置为空,回到初始状态。

打印链表和查找节点

注意:

打印和查找都是基于当前int数据类型实现的,当数据类型改变时打印和查找的方式就会随之改变;

打印链表:

即为遍历链表将数据打印:

//打印
void SLTPrint(SLTNode* phead)
{
	while (phead)
	{
		printf("%d->", phead->data);
		phead = phead->next;
	}
	printf("NULL\n");
}

1.打印不会修改plist的内容,所以不需要传址调用,参数为一级指针就足够了(二级指针也对);

2.本次单链表的数据类型是int类型,所以%d打印phead->data;

3.从phead指向的第一个有效节点开始循环往后打印;

4.phead = phead->next 表示将指向当前节点的指针phead的指向改为下一个节点,因为phead->next存放的就是下一个节点的地址(链表的遍历)。

查找节点:

即对比待查找数据是否存在在链表中,存在返回其地址;

//查找数据
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;//找到后返回当前数据所在的节点的地址
		}
		pcur = pcur->next;
	}
	return NULL;
}

1.和打印同理,传参一级指针即可;

2.遍历链表,遇到相同的数据就直接返回当前节点地址;

3.不存在目标数据时,返回空指针。

创建节点

在想要存放数据时,我们就需要创建一个新的节点来存放它,再对这个新节点进行各种操作。

//创建节点
SLTNode* SLTBuyNode(SLTDataType x)
{
	//开辟动态空间
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);//0为正常退出码
	}
	//开辟成功
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

1.定义一个指向新节点的指针,并开辟动态内存;

2.若开辟成功,将要存放的数据放到新节点的data成员中,将next指针置为空;

尾插与头插

尾部插入数据:

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	//头指针必须存在
	assert(pphead);
	//创建节点
	SLTNode* newnode = SLTBuyNode(x);

	//1.链表没有节点时
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	//2.链表有节点时
	else
	{
		SLTNode* ptail = *pphead;
		//使用临时节点指针找到最后一个节点
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//找到了尾节点
		ptail->next = newnode;
	}
}

1.创建新节点存放数据;

2.若当前链表没有节点,则直接将链表指针*pphead指向新节点,使其成为第一个有效节点;

若存在有效节点,则定义一个ptail指针,从头开始遍历链表,当ptail->next为NULL时,即表示ptail当前指向的就是尾节点,使其next指针指向newnode即可。

头部插入数据:

头插就是在第一个有效节点之前插入数据。

头插不需要考虑没有节点的情况,因为此时*pphead是NULL,而newnode->next原本也是NULL,*pphead=newnode就直接完成了操作。

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//创建新节点
	SLTNode* newnode = SLTBuyNode(x);
	
	newnode->next = *pphead;
	*pphead = newnode;
}

1.创建新节点存放数据;

2.让新节点的next指向第一个节点,再让原本指向第一个有效节点的指针*pphead(plist)指向新节点即可。

注:

newnode->next = *pphead;
*pphead = newnode;

这两步是不能交换位置的,由于原本第一个节点的地址存放在*pphead中,如果先将*pphead指向newnode,原本的地址就被覆盖掉了,就无法将原本第一个节点的地址给newnode的next了。

尾删与头删

尾部删除:

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	//只有一个节点时
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//多个节点时
	else
	{
		SLTNode* ptail = *pphead;
		SLTNode* prev = *pphead;
		//找到尾节点ptail和倒数第二个节点prev
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		//prev  ptail  ptail->next
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}

1.删除数据相较于插入数据,需要额外保证链表中必须要存在节点

2.定义两个指针,一个用来表示尾部节点,一个表示尾节点的前一个节点(prev用于在将尾节点释放后,将新尾节点的next置为NULL);

3.遍历链表找到尾节点和倒数第二个节点,释放尾节点,将ptail指针置为NULL,将prev->next置为NULL;

4.只有一个节点时,上面的方法就不适用了,所以单独为一种情况,直接释放*pphead指向的节点并置为NULL即可。

头部删除:

和头插一样,头删也不需要像尾插尾删那样考虑额外特殊情况

//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	//->操作符的优先级大于*
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

1.assert断言和尾删同理;

2.定义一个临时的next结构体指针,存放第二个节点的地址;

3.释放第一个有效节点,将*pphead指向next,使第二个节点成为新的首节点。

指定位置之前插入与指定位置之后插入

注:指定位置,指的是指定的数据所在的节点位置。

指定位置的操作,都有一个pos结构体指针,它指向的就是目标节点;pos指针是使用上文的查找节点函数SLTFind函数来得到的。

指定位置之前插入:

//指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && &pphead);
	assert(pos);
	//创建包含数据x的新节点
	SLTNode* newnode = SLTBuyNode(x);
	//链表只有一个节点时		
	if (pos == *pphead)
	{
		//就相当于只有头插
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
		
	}
}

1.指定位置的操作必须存在节点才能进行,没有节点时无法得到pos指针;

2.创建新节点存放数据;

3.定义一个prev指针,遍历链表,使其指向pos目标节点的上一个节点;

4.在prev节点后面插入新节点;

5.若只有一个节点时,上面的方法同样不适用,所以单独讨论;当只有一个节点时,在这个节点前插入数据,就相当于头插,直接调用头插函数即可。

指定位置之后插入:

//指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	//创建储存x的新节点
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

1.直接使用pos的地址就能直接插入数据,所以不需要SLTNode** pphead参数;

2.创建节点后,直接将节点插入到pos节点之后即可;

注:后面两步赋值操作和上文头插中同理,同样不能交换先后顺序,不然将导致出错。

删除指定节点与删除指定位置之后的节点

删除指定节点:

原理和尾删有相似

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead&&* pphead);
	assert(pos);
	//当pos为第一个节点
	if (pos == *pphead)
	{
		*pphead = pos->next;
		free(pos);
		pos = NULL;
		//头删
		//SLTPopFront(pphead);
		
	}
	//pos不为第一个节点
	else
	{
		//定义prev用来寻找pos的前一个节点
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//prev pos pos->next
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

1.先进行断言判空,保证函数正常运行;

2.当目标节点不是第一个节点时,原理就和尾删相似,只不过将找到尾节点和倒数第二个节点的操作,改为了找到pos节点之前的一个节点(pos节点作为参数已经找到了);

3.将pos节点排除到链表之外,即prev->next = pos->next操作(尾删中,是将prev节点的next置为NULL,以此来表示尾节点);

4.释放pos节点,将指针置为NULL。

删除指定节点之后的节点:

和指定位置之后插入一样,不需要SLTNode** pphead参数,直接使用pos指针就可完成操作

//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	//定义一个del为pos的下一个节点,即为要删除的节点
	SLTNode* del = pos->next;
	//将del节点的下一个节点给pos->next
	pos->next = del->next;
	//释放节点
	free(del);
	del = NULL;
}

1.定义一个del指针,表示要删除的节点;

2.让pos->next指向del的下一个节点;

3.释放节点并置NULL。

后记

单链表的介绍和实现就到这里结束啦!有讲得不清楚或者不足的地方大家可以在评论区或者私信指出喔~

那么我们下篇再见——

附代码

大家可以在自行实现后做参考


SList.h文件

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
//单链表节点
//数据+下一个节点的地址
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;
//打印
void SLTPrint(SLTNode* phead);
//初始化(创建节点)
SLTNode* SLTBuyNode(SLTDataType x);
//查找数据
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos(指定)节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SLTDesTroy(SLTNode** pphead);

SList.c文件

#include "SList.h"
//打印
void SLTPrint(SLTNode* phead)
{
	while (phead)
	{
		printf("%d->", phead->data);
		phead = phead->next;
	}
	printf("NULL\n");
}
//初始化(创建节点)
SLTNode* SLTBuyNode(SLTDataType x)
{
	//开辟动态空间
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);//0为正常退出码
	}
	//开辟成功
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	//头指针必须存在
	assert(pphead);
	//创建节点
	SLTNode* newnode = SLTBuyNode(x);

	//1.链表没有节点时
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	//2.链表有节点时
	else
	{
		SLTNode* ptail = *pphead;
		//使用临时节点指针找到最后一个节点
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//找到了尾节点
		ptail->next = newnode;
	}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//创建新节点
	SLTNode* newnode = SLTBuyNode(x);
	
	newnode->next = *pphead;
	*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	//只有一个节点时
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//多个节点时
	else
	{
		SLTNode* ptail = *pphead;
		SLTNode* prev = *pphead;
		//找到尾节点ptail和倒数第二个节点prev
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		//prev  ptail  ptail->next
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	//->操作符的优先级大于*
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}
//查找数据
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;//找到后返回当前数据所在的节点的地址
		}
		pcur = pcur->next;
	}
	return NULL;
}
//指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && &pphead);
	assert(pos);
	//创建包含数据x的新节点
	SLTNode* newnode = SLTBuyNode(x);
	//链表只有一个节点时		
	if (pos == *pphead)
	{
		//就相当于只有头插
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
		
	}
}
//指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	//创建储存x的新节点
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead&&* pphead);
	assert(pos);
	//当pos为第一个节点
	if (pos == *pphead)
	{
		*pphead = pos->next;
		free(pos);
		pos = NULL;
		//头删
		//SLTPopFront(pphead);
		
	}
	//pos不为第一个节点
	else
	{
		//定义prev用来寻找pos的前一个节点
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//prev pos pos->next
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	//定义一个del为pos的下一个节点,即为要删除的节点
	SLTNode* del = pos->next;
	//将del节点的下一个节点给pos->next
	pos->next = del->next;
	//释放节点
	free(del);
	del = NULL;
}
//销毁链表
void SLTDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	while (*pphead)
	{
		SLTNode* next = (*pphead)->next;
		free(*pphead);
		*pphead = next;
	}
	*pphead = NULL;
}

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值