【数据结构】链表

目录

写在开头:我们站在巨人的肩膀上

前言

单链表的结构

单链表的节点,创建,打印

创建节点

创建

打印

单链表的增删查改销

增加

头插:

尾插:

后插:

前插:

删除

后删:

尾删:

查找

更改

销毁

链表和顺序表的对比

小结:


前言

上一篇文章我们详细讲解了顺序表的增删查改,我们发现,在实现数据的增加和删除的时候,需要频繁的挪动数据,并且很多功能实现前需要检查容量并且考虑增容,而链表的数据不必连续,不用担心容量的问题,那究竟如何实现呢,让我们开始今天的内容吧

**本文先介绍链表中的单链表,之后会介绍双向带头循环链表

单链表的结构

与顺序表不同,链表不需要申请一块连续的空间,链表的空间并不连续,他是通过指针来链接在一起,我们将链表的一个成员称为一个节点

 一个节点由两部分组成,第一部分是数据部分data,第二部分是指向下一个节点的指针部分next

 这就是一个由四个节点构成的链表

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

这样我们就自定义一个链表的结构


单链表的节点,创建,打印

创建节点

和顺序表不同,我们链表并不能一口气申请很多空间,而是需要的时候就插入一个节点,因此我们需要一个函数能够申请一个节点

声明:

SLTNode* BuySListNode(SLTDataType x);

函数实现:

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

创建

当然,如果你想像顺序表那样初始化一个简单的链表,可以再封装一个函数:

声明:

SLTNode* CreateSList(int n);

实现:

SLTNode* CreateSList(int n)
{
	SLTNode* phead = NULL;	
	SLTNode* ptail = NULL;
	for (int i = 0;i<n;i++)
	{
		SLTNode* newnode = BuySListNode(i);
		if (phead == NULL)
		{
			phead = ptail = newnode;
		}
		else
		{
			ptail->next = newnode;
			ptail = newnode;
		}
	}
}

要实现上述函数,有几点需要注意,如果是第一个节点(头节点),需要将第一个节点的地址给phead和ptail,并且要用ptail继续指向后续节点,不然找不到头节点


打印

打印需要我们对链表遍历

声明:

void SListPrint(SLTNode* plist)

实现:

void SListPrint(SLTNode* plist)
{
	SLTNode* cur = plist;
	while (cur)
	{
		printf("%d",cur->data);
		cur = cur->next;
	}
	printf("\n");
}

打印的过程相对简单,需要注意的是,使用cur而不是plist来遍历数组,防止找不到头节点

单链表的增删查改销

增加

头插:

声明:

void SLTPushFront(SLTNode** pphead, SLTDataType x);

实现思路:

如果想要把newnode链接到链表最前面,只需要让newnode的next指向phead即可,但我们会发现,原来的头节点phead不再是头节点了,因此我们需要更改phead的值,所以我们在声明的时候,选择传递了一个二级指针pphead,这样就可以更改头节点了。

代码:

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

尾插:

声明:

void SLTPushBack(SLTNode** pphead, SLTDataType x);

实现思路:

我们需要先找到当前链表的尾部节点,然后将尾节点和新建的节点链接起来即可

代码实现:

void SListPushBack(SLTNode** pplist, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	SLTNode* ptail = *pplist;
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	ptail->next = newnode;

}

后插:

声明:

void SListInsertAfter(SLTNode* pos, SLTDataType x);

思路:

先使用prev保存pos节点的下一个位置,再让pos的next指向新开辟的节点newnode,最后让newnode的next指向prev保存的节点即可

代码:

void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySListNode(x);
	SLTNode* prev = pos->next;
	pos->next = newnode;
	newnode->next = prev;
}

前插:

声明:

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

实现思路:

先要找到pos前一个节点的位置,然后进行链接即可,需要注意的是,如果pos的位置就是头节点,我们直接调用之前写的头插即可

代码实现:

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	if (*pphead == pos)
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SLTNode* cur = *pphead;
		SLTNode* newnode = BuySListNode(x);
		while (cur)
		{
			if (cur->next == pos)
			{
				break;
			}
			cur = cur->next;
		}
		newnode->next = pos;
		cur->next = newnode;

	}
}

删除

删除与增加的大致思路类似,在这里我们只实现后删和尾删

后删:

声明:

void SListEraseAfter(SLTNode* pos);

实现思路:

同样的,我们需要先记录要删除节点的下一个位置,再完成链接即可,这里同样要判断pos位置

的next是否为空,如果为空,那就无法删除下一个节点(因为并不存在下一个节点),直接返回即可。 

代码实现:

void SListEraseAfter(SLTNode* pos) 
{
	assert(pos);

	if (pos->next == NULL)
	{
		return;
	}
	else
	{
		SLTNode* next = pos->next->next;
		free(pos->next);
		pos->next = next;

	}
	

}

尾删:

声明:

void SLTPopBack(SLTNode** pphead);

实现思路:

 同样的,我们需要找到尾部的前一个位置,如果ptail的next不为空,就把ptail赋值给prev,然后ptail走向下一个,之后释放掉ptail指向的位置即可

代码实现:

​
void SListPopBack(SLTNode** pplist)
{
	
	assert(*pplist);
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SLTNode* prve = NULL;
		SLTNode* ptail = NULL;

		while (ptail->next)
		{
			prve = ptail;
			ptail = ptail->next;
		}

		prve->next = NULL;
		free(ptail);

	}	
}

​

查找

其实在之前的某个位置插入或删除中需要使用查找来找到pos位置,所以我们需要实现一个函数来查找。

声明:

SLTNode* SListFind(SLTNode* plist, SLTDataType x);
​

也较为简单,这里直接贴上代码:

SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
	assert(plist);
	SLTNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

更改

我们使用查找找到指针就可以访问其中的数据,并且可以对其修改,也较为简单,如果感兴趣,可以自行尝试一下修改

销毁

因为是动态开辟的空间,所以我们需要对其销毁,我们也通过一个函数来实现

声明:

void SLTDestroy(SLTNode** pphead);

实现思路:

我们让ptail从头节点开始,如果ptail不为NULL的话,就把ptail赋值给prev,然后释放prev即可

代码实现:

void SLTDestroy(SLTNode** pphead)
{
	SLTNode* ptail = *pphead;
	SLTNode* prev = NULL;
	while (ptail)
	{
		prev = ptail;
		ptail = ptail->next;
		free(prev);
	}
	*pphead = NULL;
}

**同样需要注意,最后要把头节点赋值为NULL,所以这里依然传递的是二级指针

链表和顺序表的对比

这样,我们就学完了顺序表和链表两种数据结构,我可以从以下几个角度来对比两者

  1. 存储空间上:顺序表在物理空间上连续,链表只在逻辑上连续
  2. 随机访问:顺序表能够实现随机访问,但链表不能
  3. 任意位置插入或删除元素:顺序表需要整体挪动,而链表只需要修改指针
  4. 容量:顺序表需要考虑容量,但链表不用考虑
  5. 应用场景:顺序表应用于频繁访问和高效存储,而链表频繁删除和任意位置删除

小结:

天生我材必有用,任何一种结构都有最适合应用的场景,我们在生活学习中也要不断扩展自己的知识面,精进自己的技术。

最后,如果你有所收获,请不要忘记给笔者关注点赞收藏,这是我更新最大的动力,我们下次再见。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

蓝色学者i

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

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

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

打赏作者

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

抵扣说明:

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

余额充值