数据结构初阶-单链表

        链表的结构非常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:

        而我们主要要熟悉的单链表与双向链表的全称分别为:不带头单向不循环链表,带头双向循环链表,当我们对这两种链表熟悉后,便能不费力的推出另外四种链表。我们本文主要介绍单链表。

 一,单链表的概念与结构

1.1概念       

         概念:链表是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

        这里我们可以联想平时我们所坐的高铁或火车,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉/ 加上,不会影响其他车厢,每节车厢都是独立存在的。

        那,在链表中我们的火车车厢又是怎么样的:

 1.2结点

        与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为“结点/结点”。结点的组成主要有两个部分:当前结点要保存的数据和保存下⼀个结点的地址(指针变量)。

        图中指针变量 plist保存的是第⼀个结点的地址,我们称plist此时“指向”第⼀个结点,如果我们希望 plist“指向”第⼆个结点时,只需要修改plist保存的内容为0x00B0。

1.3链表的性质

1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续

2、结点⼀般是从堆上申请的

3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续

        结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码:(因为我们存储的数据类型不定,所以我们可以先将我们要存储的数据类型先定义下):

typedef int SLDataType;


typedef struct SLTistNode
{
	SLDataType Data;
	struct SLTistNode* next;
}SLTNode,*PSLTNode;

        当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个结点的地址(当下⼀个结点为空时保存的地址为空)。

接下来让我们来实现单链表的常用功能,实现的功能大多与顺序表类似:

二,单链表功能的实现

2.1链表的初始化

        在介绍它的初始化之前,我们先来介绍什么是带头(哨兵位)和不带头,对于带头,则是链表头部初始时便有一个哨兵位节点,但不存放任何数据,由于我们的单链表为不带头链表,所以我们这里初始化直接引用上面的结构体,设置我们的链表头为空指针即可:

PSLTNode plist = NULL;

2.2获取新节点函数SLTBuyNode

       在对链表进行头尾插之前,由于我们每次插入节点都需要创建新的节点,所以我们则合理直接分装一个函数,后面实现头尾插功能直接调用即可,老样子,我们先给出代码:

PSLTNode SLTBuyNode(SLDataType x)
{
	PSLTNode node = (PSLTNode)malloc(sizeof(SLTNode));
	if (node == NULL)
	{
		perror("newnode:");
		exit(1);
	}
	node->Data = x;
	node->next = NULL;
	return node;
}

       由于我们的链表内存的大小是动态变化的,所以我们需要使用malloc函数来开辟节点的空间。为了确保我们代码的健壮性,我们需要检验空间开辟是否成功。然后将所需要插入的数据赋值给新节点的data,同时将新节点的next置为空。

2.3尾插函数SLTPushBack

       由于我们这里是在链表的尾端进行尾插,所以我们直接将当前的链表尾节点的指针设置为我们要创建的新节点的地址:

void SLTPushBack(PSLTNode* pphead, SLDataType x)
{
	assert(pphead);
	PSLTNode newcode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newcode;
	}
	else
	{
		PSLTNode pcur = *pphead;
		while (pcur->next)
		{
			pcur = pcur->next;
		}
		pcur->next = newcode;
	}
}

        由于我们初次插入节点时,链表此时为空,所以我们在其为空时直接将指向新节点的指针设置为指向链表头节点的指针,但尾插我们需要遍历链表找到尾节点,所以时间复杂度为O(N)。

2.4头插函数SLTPushFront

       相对于尾插法,我们的头插法就简单许多,只需要将要插入的新节点的next指针直接设置为指向链表头节点的指针,然后再将头节点的指针设置为指向新节点的指针即可:

void SLTPushFront(PSLTNode* pphead, SLDataType x)
{
	assert(pphead);
	PSLTNode newcode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newcode;
	}
	else
	{
		newcode->next = *pphead;
		*pphead = newcode;
	}
}

  我们可以明显的看到,头插法的方式没有遍历数组,它的时间复杂度为O(1)。

2.5尾删函数SLTPopBack 

       在进行尾删之前,我们需要判断链表中是否还有节点,如果没有节点,我们就不能再对链表进行删除:

void SLTPopBack(PSLTNode* pphead)
{
	assert(pphead && *pphead);
	PSLTNode pcur = *pphead;
	PSLTNode pcur1 = NULL;
	while (pcur->next)
	{
		pcur1 = pcur;
		pcur = pcur->next;
	}
	if (pcur1)
	{
		pcur1->next = NULL;
		free(pcur);
		pcur = NULL;
	}
	else
	{
		free(*pphead);
		*pphead = NULL;
	}
}

        首先我们要遍历链表找到尾节点,我们可以先对头节点的NEXT判空,如果为空,则说明链表中的节点只有一个,我们直接释放头节点即可。如果不为空,我们则直接循环找到尾节点,对其进行释放,同时将其上一个节点的NEXT置为空。

2.6头删函数SLTPopFront

         老样子,我们需要先判断链表是否还有节点,有直接先记住头节点的指针,然后让头指针指向它的NEXT即可:

void SLTPopFront(PSLTNode* pphead)
{
	assert(pphead && *pphead);
	PSLTNode pcur = *pphead;
	*pphead = pcur->next;
	free(pcur);
	pcur = NULL;
}

       我们在这里会发现,头删相比于尾删也简单很多,所以当我们在没有特殊要求的情况下,尽量使用头删/插对链表进行增删操作,这样复杂度会少很多。

2.7寻找指定节点函数SLTFind

       在实现删除/增加指定位置数据之前,由于这两个函数都需要寻找数据,所以我们可以直接将寻找函数分装为一个函数,便于我们后面直接调用:

PSLTNode SLTFind(PSLTNode phead, SLDataType x)
{
	assert(phead);
	PSLTNode pcur = phead;
	while (pcur)
	{
		if (pcur->Data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

       如果没有找到目标数据,我们直接返回空指针即可,同时我们需要遍历链表一个一个去寻找,这是我们常用的寻找方法。

2.8向指定位置之后插入数据函数SLTInsertAfter

       我们这里不介绍在之前插入数据的原因是因为此时我们需要遍历链表去找到指定位置前方的数据,所以复杂度会高很多,而这里我们删除指定位置之后则不需要遍历链表:

void SLTInsertAfter(PSLTNode pos, SLDataType x)
{
	assert(pos);
	PSLTNode newcode = SLTBuyNode(x);
	newcode->next = pos->next;
	pos->next = newcode;
}

需要注意的即是对NEXT指针指向的改变,实现难度不高。

2.9删除指定位置之后的数据函数SLTEraseAfter

 与之前的删除一样,我们需要先判断链表中是否有节点:

void SLTEraseAfter(PSLTNode pos)
{
	assert(pos && pos->next);
	PSLTNode del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

  会了我们上面的头删与尾删函数之后,其实这部分很简单。

2.10链表的销毁函数SListDestroy 

void SListDestroy(PSLTNode* pphead)
{
	assert(pphead && *pphead);
	PSLTNode pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

这里我们也需要判断链表是否为空,如果为空我们也就没必要释放链表空间 。

三,几道单链表OJ题推荐

203. 移除链表元素 - 力扣(LeetCode)

206. 反转链表 - 力扣(LeetCode)

876. 链表的中间结点 - 力扣(LeetCode)

21. 合并两个有序链表 - 力扣(LeetCode)

链表分割_牛客题霸_牛客网 (nowcoder.com)

链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

160. 相交链表 - 力扣(LeetCode)

141. 环形链表 - 力扣(LeetCode)

142. 环形链表 II - 力扣(LeetCode)

       这里尤其是最后两道,使用了Floyd龟兔赛跑算法,非常推荐去练习学习 。

       

 

  • 28
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值