【数据结构】链表详解

☃️个人主页: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\n");
}

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.结尾

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

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

  • 47
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 46
    评论
链表是一种常见的数据结构,用于存储一系列的元素。在Java中,链表可以使用LinkedList类来实现。下面是关于Java链表的详细解释: 链表由节点(Node)组成,每个节点包含两部分:数据域(存储元素的值)和指针域(指向下一个节点的指针)。链表的头节点是链表的起始点,尾节点是链表的结束点。 Java中的LinkedList类实现了List接口和Deque接口,因此可以用作列表和双端队列。LinkedList在内存中以链式结构存储元素,相邻元素通过节点的指针连接。 与数组相比,链表的一个优势是可以快速地插入和删除元素。由于链表中的元素在内存中不连续存储,所以插入和删除只需要改变指针的指向,而不需要移动其他元素。然而,链表的一个劣势是访问特定索引的元素比较慢,因为需要从头节点开始遍历链表。 下面是一些常用的操作示例: 1. 创建一个空链表: LinkedList<String> list = new LinkedList<>(); 2. 在链表末尾添加元素: list.add("A"); list.add("B"); list.add("C"); 3. 在指定位置插入元素: list.add(1, "D"); // 在索引为1的位置插入元素D 4. 获取链表的大小: int size = list.size(); 5. 获取指定索引位置的元素: String element = list.get(2); // 获取索引为2的元素 6. 修改指定索引位置的元素: list.set(0, "E"); // 将索引为0的元素修改为E 7. 删除指定索引位置的元素: list.remove(1); // 删除索引为1的元素 8. 遍历链表: for (String element : list) { System.out.println(element); } 需要注意的是,LinkedList类不是线程安全的,如果在多线程环境中使用链表,需要进行适当的同步处理。此外,由于链表中的元素不是连续存储,所以对于大量元素的访问和操作,可能会导致性能下降。在这种情况下,可以考虑使用ArrayList或其他更适合的数据结构
评论 46
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值