数据结构—顺序表的缺陷—单链表(详解)及接口的实现

顺序表的缺陷

1.空间不够了需要增容,增容是要付出代价
例如:增容时有可能不在原地址后增容
2.避免频繁扩容,我们满了基本都是扩2倍,
可能就会导致一定的空间浪费
3.顺序表要求数据从开始位置连续存储那么我们在头部或者中间位置
插入删除数据就需要挪动数据,效率不高;

针对顺序表的缺陷,就设计出了链表,接下来我给大家介绍链表

简单的介绍链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域

最简单的单链表

链表与顺序表的区别:
1.顺序表只要拿到首元素的地址,就可以访问后的元素,因为数组是连续的。
2.链表它拿到第一地址后,还要通过第一个地址找第二地址,因为链表它在物理上不是连续的,逻辑上是连续的。
在这里插入图片描述
如何理解物理上不是连续的?
每个结点在内存中开辟都是随机的,不是连续的,如图:
在这里插入图片描述
上述图中都有三个结点,每个结点从逻辑上链接起来就是一个链表了,每个结点用来存放数据和下一个结点的地址,所以接下来我们来创建一个结点类型的结构。
最后一个结点的next必须为NULL(代表链表的结束标志),这样才符合链表的特性。

typedef int SLTDateType;

// 链表的结构
typedef struct SListNode
{
	SLTDateType data;//存放数据
	struct SListNode* next;// 存放下一个结点的地址
}SLTNode;

有了上述的结构体,我们现在手动创建一个链表

struct ListNode {
    int val;
    struct ListNode* next;
};
int main()
{
    //
    struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
    
    n1->val = 7;
    n2->val = 7;
    n3->val = 7;
    n4->val = 7;
    
    n1->next = n2;
    n2->next = n3;
    n3->next = n4;
    n4->next = NULL;
	return 0;
}

下面我们思考如何把数据倒着放?
改变每个结点指向下一个结点的方向。

单链表接口的学习

(增、删、查、改)
常见的接口:
1.打印链表
2.头插
3.尾插
4.头删
5.尾删
6.查找某数据的结点
7.在任意结点删除
8.在任意结点的前或后插入
9.在任意结点的前或后删除
10.修改指定结点的数据


学习上述结点前我们先了解下面的一些变量名的意思:
cur(current):当前
pre(previous) : 前一个
next : 后一个
tail : 最后一个
head : 第一个
pos(postion) : 位置


1.打印链表

打印链表时,我们需要从头指针指向的位置开始,依次向后打印,直到指针指向NULL时,结束打印。

void SListPrintf(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
}
2.增加结点

当我们需要插入一个结点时,必须先创建一个新的结点,并且把该结点进行初始化,然后通过结点的地址链接到相应的位置,如何创建一个新的结点呢,
因为在后续我们经常要创建一个新结点,所以我们把创建新结点,分装成一个函数
下面我们来创建一个新结点

SLTNode * BuySListNode(SLTDateType x)
{
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));//通过malloc向内存空间申请一块结点大小的空                                                            
	if (NULL == node)                                //间,由node指针记录该位置
	{
		printf("malloc fail\n");
		exit(-1);
	}
	node->next = NULL;// 对新结点进行初始化
	node->data = x;  

	return node;
}
3.单链表头插

简单分析一下,头插有两种情况
情况一:头指针初始时指向空,代表链表一个结点也没有,这时候我们直接就可以把新结点的地址给头指针(plist),即让plist指向新结点。
情况二:链表已经有一个以上的结点
这时候把把头指针给新结点的next,然后再然头指针指向新结点即可

在这里插入图片描述

void SListPushfront(SLTNode** pplist, SLTDateType x)
{
	SLTNode* newnode = BuySListNode(x);//创建新结点
	newnode->next = *pplist;   //  
	*pplist = newnode;        //        
}

因为这代码把情况一和情况二都覆盖了,所以我们无需进行分布判断
注:这两步操作的顺序不能颠倒,若先让头指针指向新结点,那么就无法找到原来第一个结点的位置了。

4.单链表的尾插

尾插也有两种情况:
情况一:一个结点也没有,我们只需把新结点的地址给头指针即可
情况二:一个以上的结点,把新结点的地址给最后一个结点的next即可
首先我们要找到最后一个结点的地址,然后将新结点地址给它的next即可
在这里插入图片描述

//尾插
void SListPushBack(SLTNode** pplist, SLTDateType x)
{
	//情况一、一个结点都没有
	//创建一个结点
	SLTNode* NewNode = BuySListNode(x);
	//情况一个结点都没有
	if (NULL == (*pplist))
	{
		*pplist = NewNode;
	}
	else
	{
		//情况二、
		//1.先找尾结点
		SLTNode* fail = *pplist;
		while (NULL != fail->next)
		{
			fail = fail->next;
		}
		//2.把新结点地址放到尾结点里
		fail->next = NewNode;
	}
}

小结:尾插的时候我们需要先判断链表是否为空,若为空,则直接让头指针指向新结点即可;
若不为空,我们首先需要利用循环找到链表的最后一个结点,然后让最后一个结点的指针域指向新结点。
注:新结点创建的时候指针域就已经置空,所以尾插时不需要再将新结点的指针域置空。

5.单链表头删

头删比较简单,如果指针为空,链表无结点删除,不做处理;
否则链表有一个以上的结点,让头指针指向第二结点,然后删除第一个结点

//头删
void SListPopFront(SListNode** pplist)
{
	if (*pplist == NULL)//判断是否为空表
	{
		return;
	}
	else
	{
		SListNode* tmp = *pplist;//记录第一个结点的位置
		*pplist = (*pplist)->next;//让头指针指向第二个结点
		free(tmp);//释放第一个结点的内存空间
		tmp = NULL;//可加可不加,tmp是野指针,但是函数结束后,就找不到tmp指针了
		          // 为了养成好习惯,建议添加
	}
}

6.单链表尾删

尾删相对麻烦一些,我们需要考虑三种不同的情况:

1、当链表为空时,不做处理。
2、当链表中只有一个结点时,直接释放该结点,然后将头指针置空。
3、当链表中有多个结点时,我们需要先找到最后一个结点的前一个结点,然后将最后一个结点释放,将前一个结点的指针域置空,使其成为新的尾结点。

void SListPopBack(SLTNode** pplist)
{
  // 情况一、
	if(NULL == *pplist)
	{
		return;
	}
	//情况二、
	else if(NULL==(*pplist)->next)
	{
		free(*pplist);
		*pplist = NULL;
	}
	// 情况三、
	else
	{
		//记录尾结点前的结点指针
		SLTNode* pre = *pplist;
		//找到尾结点
		SLTNode* fail = *pplist;
		while (fail->next != NULL)
		{
			pre = fail;
			fail = fail->next;
		}
		free(fail);
		fail = NULL;
		pre->next = NULL;//结点的指针域置空

	}
}
7.查找某数据的结点

我们只需要遍历一遍链表,在遍历的过程中,若找到了目标结点,则返回结点的地址;若遍历结束也没有找到目标结点,则直接返回空指针。

SLTNode* SListFind(SLTNode* pplist, SLTDateType x)
{
	SLTNode* cur = pplist;
	while (cur!=NULL)// 如果是cur->next!=NULL
		             //那么最后一个结点的数据就访问不了了
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	//没找到返回空指针
	return NULL;
}
8.单链表的修改

找到位置,对结点的数据域进行修改;

//修改数据
void SListModify(SListNode* pos, SLTDataType x)
{
	pos->data = x;//将结点的数据改为目标数据
}
9.在任意结点删除

要删除目标结点,首先我们要判断目标结点是否为第一个结点,如果是则按照头删的方法;
若不是,我们就需要先找到待删除结点的前一个结点,然后让其指向待删除结点的后一个结点,最后才能释放待删除的结点。

void SListErasecur(SLTNode** pplist, SLTNode*pos)
{
	assert(pplist);
	if (pos == *pplist)
	{
		*pplist = pos->next;
		free(pos);
		pos = NULL;
	}
	else {
		SLTNode* pre = *pplist;
		while (pre->next != pos)
		{
			pre = pre->next;
		}
		pre->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
10.在任意结点之后插入

在pos结点后插入,先让新结点的指针域指向该位置的下一个结点,然后再让该位置的结点指向新结点即可。
在这里插入图片描述

//在给定位之后插入
void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
	assert(pos);// 防止空指针
	SLTNode* newnode = BuySListNode(x);
	//新增一个结点
		newnode->next = pos->next;
		pos->next = newnode;

}
11.在任意结点之前插入

如果上述的接口都理解,那么这接口也不是特别难
首先我们判断目标结点是否等于头指针,如果是则按照头插的方法。
如果不是,则先找到目标结点的前一个结点,然后新结点的指针域指向pos,最后把前一个结点的指针域指向新结点即可
在这里插入图片描述

void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDateType x)
{
	assert(pos);//判断空指针
	SLTNode* src = BuySListNode(x);// 创建新结点
	if (pos == *pplist)//   情况一、
	{
		src->next = pos;
		*pplist = src;
	}
	else             //  情况二、
	{
		SLTNode* pre = *pplist;
		while (pre->next != pos)// 找到前一个结点
		{
			pre = pre->next;
		}
		src->next = pre->next;// 插入新结点
		pre->next = src;

	}
}
12.在任意结点之后删除

有两种情况:
一、目标结点后没结点了
我们直接不用对其进行处理
二、目标结点后有结点
若不是最后一个结点,我们首先让地址为pos的结点指向待删除结点的后一个结点,然后将待删除结点释放即可。
在这里插入图片描述

//删除给定位置之后的值

void SListEraseAfter(SLTNode* pos)
{
	assert(pos);
	if (pos->next == NULL)
	{
		return;
	}
	SLTNode* after = pos->next;//待删除的结点
	pos->next = after->next;
	free(after);
	after = NULL;
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

2023框框

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

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

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

打赏作者

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

抵扣说明:

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

余额充值