【数据结构】-单链表的增删查改以及相关的oj练习题

作者:低调
作者宣言:写好每一篇博客


前言

Hello,大家好,我们又见面了,今天我将给大家带来关于链表方面的知识,相信大家限你在对于链表还是不太了解,相信你看完这篇博客,会充分链接链表的相关知识,并且去使用他。
我们将从链表的概念,和怎么实现多个接口方面重点去介绍链表,并指出他和顺序表的差别,和自身的优缺点。接下来我们进入正文:

以下是本篇文章正文内容,下面案例可供参考

一、为什么要有链表及其概念?

其实链表也是用来存储数据的,通过之前的顺序表学习(不懂的可以看一下我之前写的关于顺序表的博客link),我们知道他也是存储数据的,那我们为什么还要学习链表的相关知识呢,直接把顺序表学会不就好了。俗话说:每件事出现总会有它出现的道理,接下来我们讲讲为什么会有链表这样的结构。

顺序表的问题及思考:
问题:

  1. 中间/头部的插入删除,时间复杂度为O(N)。
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
    思考:
    如何解决以上问题呢?下面给出了链表的结构来看看。

大家先把这几个问题记一下,下面会通过链表解决这些问题

1.1链表的概念

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
逻辑结构:
在这里插入图片描述
物理结构:
在这里插入图片描述
通过逻辑结构我们知道链表是存储一个数据就开辟一个空间,不存在空间浪费的情况,不存在扩容时出现内存消耗的情况,完美的解决的顺序表的第二,三个问题。通过物理结构,链表在内存中的存储不是连续的,以节点为单位存储,所以链表不支持随机访问。这是他的缺点。其实链表不止一种结构,接下来看看链表有几种结构。

1.2链表的几种结构

实际中要实现的链表的结构非常多样,以下情况组合起来就有8种链表结构:

  1. 单向、双向
  2. 带头、不带头
  3. 循环、非循环
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
    在这里插入图片描述1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
    2.带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
    来很多优势,实现反而简单了,后面我们代码实现了就知道了。

但今天我们我们重点所讲的是无头单向非循环链表他的多个接口的实现。

二、单链表的实现

我们以存入整型数据为例

typedef int SLTDateType;
typedef struct SListNode
{
 SLTDateType data;//存放有效数据
 struct SListNode* next; //用来找到下一个数据的地址
 }SListNode;

我们·通过结构体来完成这个结构,定义一个data,来存储数据,因为数据的地址不是连续的,所以定义一个指针来指向下一个数据,防止数据的丢失。

接下来重点介绍他的所有结构:
和顺序表一样我们也要创建两个原文件,一个头文件。
在这里插入图片描述

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
//单链表的销毁
void SListDestory(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x);
// 单链表删除pos位置的值
void SListErase(SListNode** pplist, SListNode* pos);

让我们一一完成接口的实现吧

2.1动态申请一个节点

看到这里,大家会有一个疑惑,为什么没有初始化,这个就得益于链表的特点了,他是申请一个结点就放数据进去,没有数据就不需要申请,每个结点都是有效数据,不相识顺序表,他需要开一部分空间,等着数据入进来,而且所开辟的空间不一定都存放了优先数据,所以需要初始化。

SListNode* BuySListNode(SListDateType x)
{
	SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
	if (newNode == NULL)
	{
		printf("申请内存失败\n");
		exit(-1);
	}
	newNode->date = x;
	newNode->next = NULL;
	return newNode;
}

大家可以看到,申请之后就需要存放数据。因为一开始就只有一个数据,所以要将他指向的下一个数据赋值为空指针,方便存储选一个数据。

2.2单链表的打印

void SListPrint(SListNode * phead)
{

	SListNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->date);
		cur = cur->next;
	}
	printf("NULL");
	printf("\n");

在这里插入图片描述

我们可以按照图来在看看代码为啥这么实现,需要一个指针表示头指针,然后层序遍历这个链表去打印。

2.3单链表的销毁

void SListDestory(SListNode* phead)
{
	SListNode* next=phead;
	SListNode* cur=phead;
	while (cur != NULL)
	{
		next = cur->next;//保存节点的下一个地址,避免上一个释放找不到下一个地址。
		free(cur);
		cur = NULL;
		cur = next;
	}
}

链表的销毁跟打印的原理一样,但注意要多定义一个指针来保存即将释放节点的下一个地址。

2.4单链表的尾插

void SListPushBack(SListNode** pphead, SListDateType x)//尾插
{
	SListNode* newNode= BuySListNode(x);//开辟的节点用指针变量来接收
	if (*pphead == NULL)//第一次插入会为空指针
	{
		*pphead=BuySListNode(x);
	}
	else
	{
		SListNode* tail = *pphead;
		while (tail->next!= NULL)//遍历找到最后一个节点
		{
			tail = tail->next;
		}
		tail->next=newNode;
	}
}

在这里插入图片描述

我们进行插入的时候需要进行判断,避免第一个节点是空节点,我们要进行尾插,我们必须找最后一个节点才能在他的后面进行插入,有图可以看到特别清楚,最后一个节点指向新开辟的节点,在把新节点指向NULL。

2.5单链表的尾删

void SListPopBack(SListNode** pphead)//尾删
{
	    //1.没有结点
		//2.一个结点
		//3.一个以上的结点
	if (*pphead == NULL)
	{
		return;
	}
	else if((*pphead)->next==NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SListNode* tail = *pphead;
		SListNode* prev = NULL;
		while (tail->next != NULL)
		{
			prev = tail;
			tail=tail->next;
		}
		free(tail);
		prev->next = NULL;
	}
}

在这里插入图片描述

如果一开始就为空,哪么就不需要删除,直接返回,如果一个节点直接释放就行了,在对照图来理解一下一个以上节点是怎么处理的。

2.6单链表的头插

void SListPushFront(SListNode** pphead, SListDateType x)//头插
{
	SListNode* newNode = BuySListNode(x);
	newNode->next = *pphead;
	*pphead = newNode;
}

在这里插入图片描述
头插相对来说还是比较简单的,他不想顺序表一样,需要挪动元素。时间复杂度大幅度提升了。是O(1),解决了顺序表时间复杂度是O(N)的问题。

2.7单链表的头删

void SListPopFront(SListNode** pphead)//头删
{
	if (*pphead == NULL)
	{
		return;
	}
	else
	{
		SListNode* next = (*pphead)->next;
		free(*pphead);
		//*pphead=NULL;
		*pphead = next;
	}
}

如果是空就不用进行删除,直接返回。大家通过上面的销毁,也可以类比这个,头删,我们需要定义一个指针保存下一个数据的地址,防止第一个元素释放后找不到第二个元素,在把第二个元素当成新的头。这里之所以没看到置为空指针是因为他立马被下一个地址所取代了,可以不用写。

2.8单链表的查找

SListNode* SListFind(SListNode* phead, SListDateType x)//查找
{
	SListNode* cur = phead;
	while (cur)
	{
		if (cur->date == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

这里就是遍历一遍链表找到那个元素返回他的地址。

2.9单链表在pos位置之后插入x

void SListInsertafter(SListNode* pos, SListDateType x)//从任意位置的后面插入
{
	assert(pos);
	SListNode* newNode = BuySListNode(x);
	newNode->next = pos->next;
	pos->next = newNode;
}

在这里插入图片描述
那我们思考一下,为什么我会标记1、2步,大家想象可不可以反过来。答案时可以的,但是代码会麻烦一点点,因为要在创建一个变量保存pos的下一个地址。我先给大家上代码,看着代码去分析:

void SListInsertafter(SListNode* pos, SListDateType x)//从任意位置的后面插入
{
	assert(pos);
	SListNode* newNode = BuySListNode(x);
	SListNode* next=pos->next;
	pos->next=newNode;
	newNode->next=next;
}

我们需要定义一个指针先来保存pos的下一个地址,防止pos指向newNode后,新节点找不到下一个地址。
思考:如果在pos之前插入呢?
提示:需要先遍历链表找到pos之前一个的地址,在传参的时候需要把头地址传进来,在来进行插入。大家可以去尝试一下,可以先画图去理解。

2.10单链表删除pos位置的值

void SListEraseafter(SListNode** pphead, SListNode* pos)//把pos位置删除
{
	   assert(pos);
		SListNode* next = pos->next;
		SListNode* prev = *pphead;
		SListNode* Pos = prev->next;
		if (pos != *pphead)
		{
			while (Pos != pos)
			{
				prev = Pos;
				Pos = Pos->next;
			}
			prev->next = next;
			free(pos);
		}
		else
		{
			free(pos);
			*pphead = Pos;
		}
		
}

在这里插入图片描述
大家通过图来理解一下代码时怎么实现,相信大家如果思考上面的问题后,对这个理解起来应该不成问题。

三、单链表的运行结果

#include "SList.h"
int main()
{
	SListNode* PList= NULL;
	SListPushBack(&PList, 1);//尾插
	SListPushBack(&PList, 2);//尾插
	SListPushBack(&PList, 3);//尾插
	SListPushBack(&PList, 4);//尾插
	printf("尾插后的结果:");
	SListPrint(PList);

	SListPopBack(&PList);//尾删
	SListPopBack(&PList);//尾删
	printf("尾删后的结果:");
	SListPrint(PList);


	SListPushFront(&PList,1);//头插
	SListPushFront(&PList,2);//头插
	SListPushFront(&PList,3);//头插
	SListPushFront(&PList,4);//头插
	printf("头插后的结果:");
	SListPrint(PList);

	SListPopFront(&PList);//头删
	SListPopFront(&PList);//头删
	printf("头删后的结果:");
	SListPrint(PList);

	SListNode* pos=SListFind(PList,1);//查找
	if (pos != NULL)
	{
		pos->date = 30;//修改查找的数字
		printf("修改后的结果:");
		SListPrint(PList);
	}
	else
	{
		printf("没有查找的数字");
	}
SListInsertafter(pos,10);//在pos之后位置插入
printf("插入后的结果:");
SListPrint(PList);

SListEraseafter(&PList,pos);//在pos位置删除
printf("删除后的结果:");
SListPrint(PList);
SListDestory(PList);
	return 0;
}

在这里插入图片描述

四、单链表的细节补充

我们需要使用的头文件包括:
在这里插入图片描述
两个原文件只需要调用自己定义的头文件即可:

#include "SList.h"

注:在实现接口时需要改变链表里面的内容需要传参数的地址,才能改变里面的内容。

顺序表和链表的区别和联系:
顺序表:
优点:
空间连续、支持随机访问
缺点:

  1. 中间或前面部分的插入删除时间复杂度O(N)
  2. 2.增容的代价比较大。
    链表:
    缺点:
    以节点为单位存储,不支持随机访问
    优点:
  3. 任意位置插入删除时间复杂度为O(1)
  4. 没有增容消耗,按需申请节点空间,不用了直接释放。

为什么会有链表,就是因为他能针对顺序表的缺点做出改善,而顺序表也能对链表的不足之处加以完善,所以说顺序表和链表试试相辅相成的,各有各的优势,各有各的不足。希望读者都能熟练的掌握这两种结构

五、oj练习题

5.1反转链表

在这里插入图片描述
在这里插入图片描述

typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head){
    ListNode*n1=NULL;
    ListNode*n2=head;
    if(head==NULL)
        return NULL;
    ListNode*n3=n2->next;
    while(n2!=NULL)
    {
        n2->next=n1;
        n1=n2;
        n2=n3;
       if(n3!=NULL)
       {
        n3=n3->next;
       }
    }
    return n1;
    }

我的那个图是针对一般情况下的,两个if都是对极端情况的判断,防止对空指针进行解引用。大家可以下去自己分析一下极端情况。

在这里插入图片描述

typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head){
  ListNode*cur=head,*newhead=NULL;
        while(cur)
        {
            ListNode*next=cur->next;
            cur->next=newhead;
            newhead=cur;
            cur=next;
        }
        return newhead;}

这里不管是链表为空,还是极端情况,这个代码都符合要求。

5.2链表的中间结点

在这里插入图片描述

struct ListNode* middleNode(struct ListNode* head){
struct ListNode*show=head;
struct ListNode*fast=head;
while(fast&&fast->next)//快慢指针
{
    show=show->next;
    fast=fast->next->next;
}
return show;
}

这里我们将使用到快慢指针,慢指针一次走一步,慢指针一次走两步,这样就可以找到中间结点。后面的题目会经常用得到快慢指针。

六、总结

相信大家看到这里对单链表又更加了解了,链表也是数据结构里比较重要的一个结构了,但单链表在实际当中用的特别少,主要使用带头双向循环链表,他不像单链表进行尾插尾删时候去记录最后一个位置的地址,非常方便,那我们为什么还要学习单链表呢,一是我们学习是循序渐进的,从简单结构到复杂结构,二是单链表1在oj这方面容易出题。如果大家对带头双向循环链表不理解,请尽情期待我的下一篇博客。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

橘柚!

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

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

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

打赏作者

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

抵扣说明:

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

余额充值