您现在的位置是:首页 >技术杂谈 >【数据结构】链表详解网站首页技术杂谈

【数据结构】链表详解

fighting小泽 2024-06-08 00:00:02
简介【数据结构】链表详解

☃️个人主页:fighting小泽
?作者简介:目前正在学习C语言和数据结构
?博客专栏:数据结构
?️欢迎关注:评论??点赞??留言??

前言

在前面我们已经学习过了有关顺序表的知识,但是我们知道顺序表是存在着一些问题的

问题:

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

所以我们能不能寻求其他的解决方案?

  1. 不扩容
  2. 按需申请释放
  3. 解决头部或者中间插入删除需要挪动数据的问题

顺序表的一切根源就是一块连续的空间,那我们能不能用不连续的空间存储数据呢?

这个时候就要用到我们的链表了。

在这里插入图片描述

一.链表

1.1 链表的概念及结构

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

在这里插入图片描述

在这里插入图片描述

1.2链表的分类

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

  1. 单向的或双向的
    在这里插入图片描述
  2. 带头或者不带头
    在这里插入图片描述
  3. 循环或者非循环
    在这里插入图片描述

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

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

二.单链表的特征

单链表中,每一个节点存储一个数据和指向下一个节点的指针,最后一个节点指向NULL。
在这里插入图片描述

  • 当我们需要进行头插头删等操作时,需要改变头指针的地址,就需要传入二级指针
  • 当我们想要修改某个结构体变量,(结构体的 next 或者结构体的 data) 时,我们需要传结构体的地址,就需要一级指针。

2.1单链表的优缺点

优点:

  • 单链表增加删除节点简单,遍历的时候不会死循环。
  • 逐个申请、释放空间,没有空间浪费。

缺点:

  • 只能从头到尾遍历。只能找到后继,不能找到前驱,也就是只能前进。

三.SList.h

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLTDataType;

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

void SLTprint(SLTNode* phead);//打印

void SLTPushFront(SLTNode** pphead, SLTDataType x);//前插

SLTNode* BuyTNode(SLTDataType x);//malloc一个新节点

void SLTPushBack(SLTNode** pphead, SLTDataType x);//尾插

void SLTPopBack(SLTNode** pphead);//尾删

void SLTPopFront(SLTNode** pphead);//头删

SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//查找某个节点

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在pos指针处插入元素
void SLTInsertAfter(SLTNode* pos, SLTDataType x);//在pos指针后插入元素

void SLTErase(SLTNode** pphead, SLTNode* pos);//删除pos指针指向的节点
void SLTEraseAfter(SLTNode* pos);//删除pos指针后一个节点

void SLTDestroy(SLTNode* phead);//删除列表,释放maollc的节点

四.SList.c

4.1单链表的打印

遍历一遍链表打印即可

void SLTprint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}

	printf("NULL
");
}

4.2malloc一个新节点

SLTNode* BuyTNode(SLTDataType x)
{
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
	if (node == NULL)
	{
		perror("malloc");
		return 0;
	}
	node->data = x;
	node->next = NULL;
	return node;
}

4.3单链表的销毁

迭代释放链表节点

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

4.4单链表的头插头删

单链表的优势就是头插头删
对于头删,我们要断言二级指针指向的一级指针是否为空。若为空说明该链表没有元素,assert会报错。

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuyTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* tail = *pphead;
	*pphead = tail->next;
	free(tail);
}

4.5单链表的尾插尾删

尾插时,需要分类讨论链表是否为空的情况

尾删时,同样需要链表是否为空。同样需要分开讨论仅剩一个头节点或剩余多个节点的情况。若只剩一个节点,删除后将头指针置 NULL 。若剩余多个节点,则删除最后一个节点后将尾节点的 next 置 NULL 。

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuyTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
		return;
	}
	else
	{
		SLTNode* tail = *pphead;
		while ((tail->next) != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

void SLTPopBack(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* tail = *pphead;
	if (tail->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	while (tail->next->next != NULL)
	{
		tail = tail->next;
	}
	free(tail->next);
	tail->next = NULL;
}

4.6单链表的查找(修改)

遍历一遍链表,查找到返回结构体的指针,反之返回NULL
因为已经找到了结构体的指针,所以可以直接修改结构体的变量

查找函数是要配合下面的pos节点插入、删除函数使用的

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}

4.7在pos前插入、删除节点

注意要分类讨论 pos 是否为头节点
此接口不常用,因为单链表的前一个节点不好找,算法效率低

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	if (*pphead == pos)
	{
		SLTPushFront(pphead, x);
		return;
	}
	SLTNode* cur = *pphead;
	while (cur)
	{
		if (cur->next == pos)
		{
			SLTNode* newnode = BuyTNode(x);
			newnode->next = cur->next;
			cur->next = newnode;
			return;
		}
		else
			cur = cur->next;
	}
}
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);
	SLTNode* cur = *pphead;
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
		//*pphead = cur->next;
		//free(cur);
		return;
	}
	while (cur)
	{
		if (cur->next == pos)
		{
			cur->next = pos->next;
			free(pos);
			return;
		}
		else
			cur = cur->next;
	}
}

4.8在pos后插入、删除节点

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);


	SLTNode* newnode = BuyTNode( x);
	newnode->next = pos->next;
	pos->next = newnode;		
}
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	SLTNode* cur = pos->next;
	pos->next = cur->next;
	free(cur);
}

5.结尾

这些就是我给大家分享的关于链表的知识啦,希望我们都能有所收获!
先赞后看,养成习惯!!^ _ ^
码字不易,大家的支持就是我坚持下去的动力,点赞后不要忘了关注我哦!

如有错误,还请您批评改正(。ì _ í。)

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。