《数据结构》(三)线性表之单链表的表示及实现

11 篇文章 2 订阅
5 篇文章 0 订阅

今天带来的是单链表(单向不带头非循环链表),是对这几天学习的一个总结,大家有什么疑惑,或者不同的见解都可以和我讨论


请添加图片描述

字数很多,很详细,大家收藏起来,慢慢品尝
今天我们将讨论线性表另外一种存储结构————链式存储结构由于它不要求逻辑上相邻的元素在物理地址上也相邻,因此它没有顺序存储结构所具有的弱点,但失去了顺序表中可以随机存取的优点。


链表的概念

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 ,也是线性表的其中一种,它的长度也可以根据需求来进行变化,这一点和顺序表是很相似的。


单链表的概念

看下图,一个大框框由两部分组成,一个是data,另一个是next,data是用来存储数据元素信息的被叫做数据域,而next是用来存储后继存储位置的域被称为指针域,这两个合称为结点。 指针域中存储的信息叫做指针或链,由N个结点组成的就叫做链表,在此基础上由一个指针链接起来的就叫做单链表,后续还会有双链表…单向不带头链表…等等。
在这里插入图片描述
上图,整个链表的的存取打印必须从头指针开始进行,头指针表示链表中第一个结点(即第一个数据元素的地址)的存储位置,这里的头指针就是phead/plist, 由于最后一个数据元素没有直接后续,又为了方便链接新节点,所以链表最后一个结点的指针为“空”(NULL)。


比较单链表与顺序表的比较

由上一节顺序表可见,线性表的顺序存储结构的特点是逻辑关系上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中任一元素,然而从另一方面来看,这个特点也铸就了这种存储结构的弱点:在作任插入和删除操作时,需要移动大量元素,而链式存储结构的特点是逻辑关系上两个元素在物理位置上不需要相邻。简图如下,方便理解:
在这里插入图片描述


下面就总结一下:

顺序表的优点:
1.数据是依次存储的,可以直接通过下标来访问需要的数据;
2.插入数据的时候,可以动态增容。
顺序表的缺点:
1.空间不够我们频繁增容,增容的过程中会导致一定的性能消耗;
2.我们一般扩容都是二倍以上的扩容,如果扩容的空间多的话,同样也会存在一定的空间浪费;
3.根据上一篇我们会发现头插,任插,和头删,任删的效率比较低,时间复杂度都是O(N)。


单链表的优点:
1.可以按照自己的需求来增加或者删除结点(按需分配),避免空间浪费,节省空间
单链表的缺点:
1.访问其中一个元素必须从链表的头部开始遍历,没有顺序表那么方便,相对于顺序表来说,时间复杂度和空间复杂度比较大;
2.存储空间不连续,数据元素之间使用指针相连,每个数据元素只能访问周围的一个元素。


单链表的实现

1.线性表单链表存储结构的定义

要存多个数据我们一般就用结构体来表示,Data就是数据域,用来存放数据的,SListNode*next就是指针域,用于存放后续结点的地址(也可以说是指向下一个结点的指针), 这就是单链表存储结构的定义。
在这里插入图片描述

typedef int SLTDataType;//这里是将int重命名为SLDataType,也是增强代码可维护性
struct SListNode
{
	SLTDataType Data;//数据域
	struct SListNode* next;//定义指向下一个结点的结构体指针--指针域
};
typedef struct SListNode SLTNode;//将struct SListNode重命名为SLTNode

2.链表结构的创建

🎤这就是开辟新结点,就像是顺序表中的空间开辟,需要用到申请结点的接口无非也就所有的插入函数能用的到,x就是需要插入的新数据,newnode就是新结点,newnode->next=NULL是方便存放下一个结点的地址。

SLTNode* BuySListNode(SLTDataType x)//开辟新结点函数
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//动态申请一个空间
	newnode->Data = x;//将新数据放到结构体的数据域中
	newnode->next = NULL;//链接下一个结点

	return newnode;
}

单链表的基本接口实现(增删查改)


1.单链表的打印

🎤这里打印这个链表并不需要对链表进行存取,所以形参部分就直接用一级指针就行了。定义一个结构体指针cur,让cur指向这个链表的头指针(也就是指向这个链表的起始位置)。
用cur这个指针去遍历这个数组并打印
如图:先打印第一个数据,然后cur=cur->next,接着向下走,当cur指向NULL的时候截止。

在这里插入图片描述

void SListPrint(SLTNode* phead)//打印这个链表,遍历这个链表,一边遍历,一边打印
{
	SLTNode* cur = phead;//cur是指向这个链表的指针
	while (cur!=NULL)
	{
		printf("%d->", cur->Data);//打印数据域中的数据
		cur = cur->next;//然后指向下一个结点,然后循环判断并继续打印
	}
	printf("NULL\n");
}

2.单链表的尾插

🎤当进行增删接口时一定要用到双指针,pphead就是plist的地址,二级指针就是指针的地址。
进行尾插第一步先进行扩容,扩容之后需要判断链表是否为空,如果是空直接将新开辟的结点连到头结点pphead即可,如果不为空就用一个指针找到尾结点,然后链接新结点。
在这里插入图片描述

void SListPushBack(SLTNode**pphead, SLTDataType x)//尾插,实参传递形参,形参是实参的一个拷贝变量,形参的改变不会影响实参,所以双重指针解引用
{   //pphead是phead的地址,二级指针就是指针的地址
	
	SLTNode* newnode = BuySListNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找到尾结点的指针
		SLTNode* tail = *pphead;
		while (tail->next != NULL)//为什么用tail->next!=NULL呢?next是最后一个结点是空指针,如果用tail的话走到的地方就找不到是哪里了
		{
			tail = tail->next;
		}

		//尾结点链接新结点
		tail->next = newnode;//将新结点newnode的地址给tail->next存起来
	}
}

3.单链表的头插

🎤新申请一个结点,将新结点的newnode->next用来存放头指针(pphead)的地址,再让头指针(pphead)指向的位置改变为新结点(newnode),很简单,大家一看基本思想就懂了。

void SListPushFront(SLTNode**pphead, SLTDataType x)//头插
{
	SLTNode* newnode = BuySListNode(x);//把新结点申请出来

	newnode->next = *pphead;//让新结点的next指向第一个老结点的pphead

	*pphead = newnode;//把新申请的结点的地址再重新放到pphead上
}

4.单链表的尾删

🎤尾删是要考虑三种情况,

1.当链表没有数据的时候,还用删个锤子,直接return ,
2.若链表只有一个结点的时候直接free释放掉,然后将头指针(pphead)置为空,
3.如果有多个结点的时候,找到最后一个结点,free释放掉,然后将被释放掉的结点的前一个结点的next置为空,但是我们怎么知道即将被释放掉的结点前一个结点的地址呢?所以我们需要两个指针,一个向前跑,一个在后面记录上一个结点的地址。
如图:

在这里插入图片描述

void SListPopBack(SLTNode** pphead)//尾删
{
	//1.链表为空的时候
	if (*pphead == NULL)
	{
		return;
	}
	//2.链表只有一个结点的时候
	else if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//3.多个结点的时候
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}

}

5.单链表的头删

🎤头删也是很简单,free释放掉第一个结点,头指针指向下一个结点即可
在这里插入图片描述

void SListPopFront(SLTNode** pphead)//头删
{
	SLTNode* next = (*pphead)->next;//优先级问题,所以加括号
	free(*pphead);
	*pphead = next;
}

6.单链表的查找

🎤通过指针向下走找到数据域中需要找到的数据,并返回指针

SLTNode* SListFind(SLTNode* phead, SLTDataType x)//查找
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->Data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

7.单链表的pos前面插入

🎤前面的大家理解以后,剩下的多说已经没用了。
找到pos的位置,然后将待插入的结点链接到链表上,大家看着代码和注释理解一下。

void SListInsert(SLTNode** pphead, SLTNode*pos,SLTDataType x)//pos后插入
{
	if (pos == *pphead)//如果链表中只有一个数据,那就相当于头插
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = BuySListNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = newnode;
		newnode->next = pos;
	}

}

8.单链表的任意删除

🎤找到待删除数据的位置,将pos上下两个结点链接起来,释放掉待删除数据。

void SListErase(SLTNode** pphead,SLTNode* pos)//任删
{
	if (pos == *pphead)//如果链表中第一个数据,那就相当于头删
	{
		SListPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}

}

9.单链表的修改

🎤查找函数会直接返回就是pos的地址,将待更改数据放到这个位置上就行了。

void SListModity(SLTNode**pphead, SLTNode* pos, SLTDataType x)//改
{
	pos->Data = x;
}

10.单链表的释放

🎤链表必须是由上一个结点才能找到下一个结点,不能直接释放。我们用两个指针,Destory向前走,free_记录Destory后面一个结点的门牌号,挨家挨户释放,最后要把头指针置为空哦。

void SListDestory(SLTNode**pphead)//释放函数
{
	SLTNode* Destory = *pphead;
	while (Destory != NULL)
	{
		SLTNode* free_ = Destory->next;
		free(Destory);
		Destory = free_;
	}
	*pphead = NULL;

}

到这里,我相信大家单链表已经掌握了🎉🎉🎉,有错误大家可以指出哦,有疑问也可以问我,大家共同进步,后续会持续更新《数据结构》的相关内容,大家喜欢的话可以关注一下,😚😚😚
下面是项目代码,大家参考一下。


SList

1.SList.h

#pragma once

#include<stdio.h>
#include<stdlib.h>
#include<string.h>


//多个数据,一般用结构体来表示
typedef int SLTDataType;//这里是将int重命名为SLDataType,也是增强代码可维护性
struct SListNode//取名为SListNode方便区分
{
	SLTDataType Data;//数据
	struct SListNode* next;//定义指向下一个结点的结构体指针
};

typedef struct SListNode SLTNode;//将struct SListNode重命名为SLTNode

//不会改变链表的头指针,就传一级指针
void SListPrint(SLTNode* phead);//打印

//如果会改变链表的头指针,就传二级指针
void SListPushBack(SLTNode**pphead, SLTDataType x);//尾插,//实参传递形参,形参是实参的一个拷贝变量,形参的改变不会影响实参,所以双重指针解引用
void SListPushFront(SLTNode**pphead, SLTDataType x);//头插
void SListPopBack(SLTNode** pphead);//尾删
void SListPopFront(SLTNode** pphead);//头删

SLTNode* SListFind(SLTNode*phead, SLTDataType x);
//在pos的前面插入x
void SListInsert(SLTNode** pphead,SLTNode* pos, SLTDataType x);//任插
//删除pos位置的数据
void SListErase(SLTNode** pphead, SLTNode* pos);//任删


void SListModity(SLTNode**pphead, SLTNode*pos,SLTDataType x);//更改

void SListDestory(SLTNode** pphead);//释放链表


2.SList.c

#include"SList.h"


void SListPrint(SLTNode* phead)//打印这个链表,遍历这个链表,一边遍历,一边打印
{
	SLTNode* cur = phead;//cur是指向这个结构体的指针
	while (cur!=NULL)
	{
		printf("%d->", cur->Data);//先打印第一个结点
		cur = cur->next;//然后指向下一个结点,然后循环判断并继续打印
	}
	printf("NULL\n");
}


SLTNode* BuySListNode(SLTDataType x)//开辟新结点函数
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//动态申请一个空间
	newnode->Data = x;
	newnode->next = NULL;

	return newnode;
}


void SListPushBack(SLTNode**pphead, SLTDataType x)//尾插,实参传递形参,形参是实参的一个拷贝变量,形参的改变不会影响实参,所以双重指针解引用
{   //pphead是phead的地址,二级指针就是指针的地址
	
	SLTNode* newnode = BuySListNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找到尾结点的指针
		SLTNode* tail = *pphead;
		while (tail->next != NULL)//为什么用next呢?next是最后一个结点是空指针,如果用tail的话会走到下一个,然后就无了
		{
			tail = tail->next;
		}

		//尾结点链接新结点
		tail->next = newnode;//将新结点newnode的地址给tail->next存起来
	}
}

void SListPushFront(SLTNode**pphead, SLTDataType x)//头插
{
	SLTNode* newnode = BuySListNode(x);//把新结点申请出来

	newnode->next = *pphead;//让新结点的next指向第一个老结点的phead

	*pphead = newnode;//把新第一个结点的地址再放到phead上
}

void SListPopBack(SLTNode** pphead)//尾删
{
	//1.链表为空的时候
	if (*pphead == NULL)
	{
		return;
	}
	//2.链表只有一个结点的时候
	else if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//3.多个结点的时候
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}

}


void SListPopFront(SLTNode** pphead)//头删
{
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}


SLTNode* SListFind(SLTNode* phead, SLTDataType x)//查找
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->Data == x)
		{
			return;
		}
		cur = cur->next;
	}
	return cur;
}

void SListInsert(SLTNode** pphead, SLTNode*pos,SLTDataType x)//任插
{
	if (pos == *pphead)
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = BuySListNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = newnode;
		newnode->next = pos;
	}

}


void SListErase(SLTNode** pphead,SLTNode* pos)//任删
{
	if (pos == *pphead)
	{
		SListPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}

}


void SListModity(SLTNode**pphead, SLTNode* pos, SLTDataType x)//改
{
	pos->Data = x;
}

void SListDestory(SLTNode**pphead)//释放函数
{
	SLTNode* Destory = *pphead;
	while (Destory != NULL)
	{
		SLTNode* free_ = Destory->next;
		free(Destory);
		Destory = free_;
	}
	*pphead = NULL;

}

3.Test.c

#define _CRT_SECURE_NO_WARNINGS 1//针对vs2022,scanf报错的举措

#include"SList.h"

void TestSList1()
{
	SLTNode* plist = NULL;
	SListPushBack(&plist, 1);//实参传递形参,形参是实参的一个拷贝变量,形参的改变不会影响实参,所以&(取地址)传址调用
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPushFront(&plist, 0);

	SListPrint(plist);

	//SListPopFront(&plist);

	//SListPrint(plist);

	//SListPopBack(&plist);
	//SListPrint(plist);

	//SListPopBack(&plist);
	//SListPopBack(&plist);
	//SListPopBack(&plist);
	SListPopBack(&plist);
	//SListPrint(plist);

	//想在3的前面插入一个30
	SLTNode* pos = SListFind(plist, 3);
	if (pos)
	{
		SListInsert(&plist, pos, 30);
	}
	SListPrint(plist);

	//想在0的前面插入一个10
	pos = SListFind(plist, 0);
	if (pos)
	{
		SListInsert(&plist, pos, 10);
	}
	SListPrint(plist);

	pos = SListFind(plist, 2);
	if (pos)
	{
		SListErase(&plist, pos);
	}
	SListPrint(plist);

	//删除10位置的数据
	pos = SListFind(plist, 10);
	if (pos)
	{
		SListErase(&plist, pos);
	}
	SListPrint(plist);

	//改--把1这个数改成20
	pos = SListFind(plist, 1);
	if (pos)
	{
		SListModity(&plist, pos, 20);
	}
	SListPrint(plist);

	//释放链表函数
	SListDestory(&plist);
	SListPrint(plist);
}

int main()
{

	TestSList1();
	return 0;
}
  • 16
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

#唐解元

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

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

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

打赏作者

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

抵扣说明:

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

余额充值