【数据结构】单链表

目录

介绍

单链表的增删查改

头插:

 尾插:

头删:

尾删:

销毁函数

总结


介绍

单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。

对于单链表来说,有一个类似的概念叫做顺序表,他们都能存储数据向后遍历,但是顺序表与链表不同,他们各自有自己的优缺点

顺序表缺点:
头部或中间的插入删除的时间复杂度为O(N)
增容是异地扩容,消耗大
扩容一般为2倍增长,造成浪费
解决不扩容,按需申请释放,头部中间插入删除需要挪动数据的问题
造成这些问题的原因:连续的物理空间
空间的释放不能分期,申请多少释放多少
单链表最后一个节点的指针指向NULL

顺序表优点:

空间连续、支持随机访问

链表优点:

任意位置插入删除时间复杂度为O(1)
没有增容问题,插入一个开辟一个空间

缺点:

不支持随机访问

在日常使用中,如果我们知道空间大小,或需要随机访问,就可以使用顺序表,这里我们介绍单链表的使用方法

单链表的增删查改

在写单链表的增删查改之前,我们首先需要创建一个单链表,也就是说,我们要写一个初始化函数

我们先新建一个头文件和两个.c源文件,test.c和Slist.c在头文件中创建单链表结构体,而要想创建链表,我们需要知道链表的基本原理,链表是通过结构体和结构体指针链接在一块的,每一个节点都是一个空间,而这个空间里不仅有存储的数据,还有指向下一个空间的指针,即结构体嵌套,这样一个接一个的链接在一起,就成为了链表

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
	//一个结构体的地址
	//结构体嵌套,用结构体指针,结构体不能直接嵌套结构体
}SLTNode;

void SLTPrint(SLTNode* phead);
void SLPushFront(SLTNode** pphead, SLTDataType x);
void SLPushBack(SLTNode** pphead, SLTDataType x);
void SLPopFront(SLTNode** pphead);
void SLPopBack(SLTNode** pphead);

SLTNode* STFind(SLTNode* phead, SLTDataType x);
//在pos之前插入
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos位置的值
void SLErase(SLTNode** pphead, SLTNode* pos);

void SLDestroy(SLTNode** pphead);

创建好后如上图,对于链表中存储数据的类型,我们可以重新定义为SLTDataType,让后面的数据类型都称作SLTDataType,这样如果我们需要储存不同类型的数据的话,只需要在头文件中修改相应的类型即可,之后,我们用pphead在函数中表示这个链表,在test.c文件中创建一个plist指针用来表示我们创建的链表,这也能解释为什么我们的函数都是二级指针,即改变一级指针需要用二级指针,改变结构体指针,就需要传结构体指针的地址,这里可能有同学有疑问:那我们能不能不用二级指针呢?当然可以,如果我们将void类型改为结构体类型,让每个函数都有返回值,再让plist去接受每个函数的返回值,那么也能达到我们的目的,但显然这样的方法不如二级指针便利。

在链表中增加一个数据,看起来十分简单,也确实十分简单,我们需要创建一个函数,用来malloc一个新的空间,这样才能做到对链表中的数据进行添加,增加数据有头插和尾插

SLTNode* BuyLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

这是创建节点的函数,要注意创建节点后需要对新节点进行赋值,避免出现野指针。

头插:

我们只需要将buyltnode函数创建好的空间链接到链表中即可,也就是说,用指针将他们连接起来,当链表为空的时候怎么办呢?显然,这是头插,是从链表的表头开始插入,与链表是否为空无关,他都可以正常插入,不需要分情况讨论。

 如上图,连接节点使用next指针,即newnode->next = *pphead,连接后,我们知道,pphead是链表的头,newnode不过是临时存储新空间的变量,我们不可能用newnode来作为新表头进行操作,所以需要将*pphead = newnode,将pphead的位置移动到newnode处,这样一来,头插就实现了

void SLPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//链表为空是*pphead为空,pphead与*pphead不同,pphead为空解引用就出现空指针
	//他是头指针plist的地址
	SLTNode* newnode = BuyLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

 尾插:

尾插的逻辑与头插大致相同,但是,我们需要判断一下链表是否为空,否则就会出现这样的错误写法

void SLPushBack(SLTNode* phead, SLTDataType x)
{
	SLTNode* tail = phead;
	while (tail != NULL)
	{
		tail = tail->next;
	}
	SLTNode* newnode = BuyLTNode(x);
	tail = newnode;
}

这个函数错误很多,首先没有用二级指针,这导致他无法对plist进行修改,都是形参,其次,当链表为空时,tail会直接脱离链表,即便传值成功,也不会连接到链表中,且tail是局部变量,出了这个函数后,会自动销毁

所以,我们应该这样写:

void SLPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuyLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	//两种情况:空链表和非空链表
	SLTNode* tail = *pphead;
	while (tail->next != NULL)
	{
		tail = tail->next;
	}
	tail->next = newnode;
}

在尾插前,先判断该链表是否为空,为空则让头结点pphead接收第一个尾插节点,然后创建局部变量tail,让他遍历到链表的尾部,然后让tail->next = newnode,将节点与链表连接起来。这里可能有几个疑问,比如:while遍历时可不可以用tail!=NULL?这里的tail出了函数后也会销毁,为什么数据能正确传入?对于第一个问题,当然不可以,如果是tail!=NULL的话,while会遍历到tail==NULL时停止,此时tail指向NULL,脱离了链表,指向了空,再传值给他就没有了意义,同理,对于第二个问题,tail是在pphead不为空的前提下创建出来的一个变量,用来遍历这个链表,他始终指向的是链表的各个节点,那么传值给他,改变的也是链表的节点

这里再强调一下指针的问题:

这里要做的不是把newnode给phead,而是给外部传进来的plist
在TestSList1()中的SLPushFront(plist, 1)是把plist实参传给了形参//TestSList1()即测试用函数,存在于test.c文件中
SLPushFront中的phead是形参,即phead的改变不会影响到plist
比如,要改变的是int型的的数据,那么要传int型的指针
在函数中,要解引用,即改变的是*p1,*p2,改变的是指针指向的内容
同理,要改变一个指针变量的内容,比如
int a = 0   int* px = &a
那么就用int** pp1 传&px进去。
所以,要改变结构体指针,就要传结构体指针的地址

尾插的本质是上一个节点连接到下一个节点,要让tail找next指向为空的节点
不传地址无法改变plist,即便尾插成功也无法打印出来
要改变什么要用他的地址,尾插只有第一个节点要用二级指针,让plist指向
即改变结构的指针plist,但是一个函数不能用二级指针和一级指针
所以直接用二级指针就行了,解引用就是一级

删除数据与插入一样,也有头删和尾删,在写删除的时候要注意,有数据的空间才可以删除,如果一个链表为空,那他就不能进行删除,否则会导致内存泄漏,因此,我们需要在函数中加入断言,来判断链表是否为空:assert(*pphead),在这里讲一下前面的函数中的断言assert(pphead),可能有同学会问,这不就是防止链表为空的断言吗?其实不是,pphead与*pphead有很大差异,*pphead是一个指针,指向的是链表,而pphead不是,pphead为空解引用就出现空指针。

头删:

头删其实很简单,当我们写入断言后,我们甚至不需要判断他的下一个数据是否为空,我们只需要写一个”标记节点“,将pphead此时的位置存入这个标记节点,然后让*pphead = (*pphead)->next;

void SLPopFront(SLTNode** pphead)
{
	assert(*pphead);
	assert(pphead);
	/*if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* del = *pphead;
		//*pphead = del->next;
		*pphead = (*pphead)->next;
		free(del);
	}*/
	SLTNode* del = *pphead;
	*pphead = (*pphead)->next;
	free(del);
}

注释中的函数为没有断言*pphead的写法。

尾删:

尾删的大致逻辑与头删一样,不同的是,他不能像头删一样一边遍历一边删除,尾删还是需要通过局部变脸遍历链表找到尾结点进行删除,也就是说,在写入断言的同时,我们仍然需要对表头的下一个数据进行判断是否为空,防止局部变量tail指向空导致出错。

void SLPopBack(SLTNode** pphead)
{
	assert(*pphead);
	//一个节点或多个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		/*SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;*/

		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

注释中的方法运用了“标记节点”,将数据存入prev然后再让tail进行遍历,与头插的方法一致,但效率不高代码繁琐,不如下面的方法。

如果上面的内容都看懂了的话,查找函数就太简单了,创建一个cur变量来遍历链表,当找到与要查找的数据相同的节点时返回。

SLTNode* STFind(SLTNode* phead, SLTDataType x)
{
	//assert(phead);
	//没有数据就不能查是吧
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

当然这里不用断言,理由很简单,没有数据也能使用查找,不会影响链表

修改节点内容可以通过查找函数结合来实现,先用查找函数找到该节点,然后再进行修改

void SLModify(SLTNode** pphead, SLTDataType x)
{
	SLTNode* cur = STFind(*pphead, x);
	SLTDataType m;
	scanf("%d", &m);
	cur->data = m;
}

销毁函数

在链表使用完毕后,我们可以用一个销毁函数来销毁我们所使用的链表,要注意的是,链表是由各个开辟的空间连接起来的表,因此销毁函数需要我们创建一个局部变量来进行遍历并销毁。

void SLDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}

	*pphead = NULL;
}

最后不要忘了将*pphead进行置空,避免野指针。

总结

单链表是学习数据结构不可或缺的一环,如果单链表能够学会,这证明我们对于指针等方面的理解进一步深化,也为后面的学习打下了基础,希望各位多多点赞,多多努力!:D

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值