【数据结构初阶】单链表

目录

[1. 顺序表问题与思考](#1. 顺序表问题与思考)

[2. 单链表](#2. 单链表)

[2.1 概念与结构](#2.1 概念与结构)

[2.1.1 结点](#2.1.1 结点)

[2.1.2 链表的打印](#2.1.2 链表的打印)

[3. 实现单链表](#3. 实现单链表)

[3.1 第1步:尾插](#3.1 第1步:尾插)

[3.2 第2步:头插](#3.2 第2步:头插)

[3.3 第3步:尾删](#3.3 第3步:尾删)

[3.4 第4步:头删](#3.4 第4步:头删)

[3.5 第5步:查找](#3.5 第5步:查找)

[3.6 第6步:在指定位置之前插入数据](#3.6 第6步:在指定位置之前插入数据)

[3.7 第7步:在指定位置之后插入数据](#3.7 第7步:在指定位置之后插入数据)

[3.8 第8步:在pos位置删除数据](#3.8 第8步:在pos位置删除数据)

[3.9 第9步:在pos位置之后删除数据](#3.9 第9步:在pos位置之后删除数据)

[3.10 第10步:销毁单链表](#3.10 第10步:销毁单链表)

[3.11 全部代码](#3.11 全部代码)

[4. 单链表算法题](#4. 单链表算法题)

[4.1 移除链表元素](#4.1 移除链表元素)

[4.2 反转链表](#4.2 反转链表)

[4.3 链表的中间结点](#4.3 链表的中间结点)

[4.4 合并两个有序的链表](#4.4 合并两个有序的链表)

[4.5 链表分割](#4.5 链表分割)

[4.6 链表的回文结构](#4.6 链表的回文结构)

[4.7 相交链表](#4.7 相交链表)

[4.8 环形链表](#4.8 环形链表)


1. 顺序表问题与思考

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

对于这些问题我们该如何解决呢? 就是使用链表。我们想把时间复杂度降为O(1),想让空间刚好够,或者浪费少量空间等等。

2. 单链表

2.1 概念与结构

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

结合着概念以及第一节的思考,我们可以想到现在我们大部分人出远门的方式------火车。淡季的时候我们就会减少车厢的数量,旺季的时候就会增加车厢的数量,尤其是候补的人数够几个车厢就会加几个车厢,这样分配位置合理,且不会造成浪费。

注:该图片由豆包ai生成。

就如上图,火车有4节车厢,不够了我们还可以添加车厢。那么放在和它类似的链表中又是怎么样的呢?

2.1.1 结点

与顺序表不同的是,我们把上面图中的每一块申请的独立空间叫做结点。结点的组成主要有两个部分:当前结点要保存的数据和保存下一个节点的地址(指针变量)。

由上图不难看出,每个结点的空间都是独立的也就是在堆中:

其中箭头是肯定没有的。

所以我们就可以知道链表的性质:

  1. 在逻辑结构上是连续的,在物理结构上不一定是连续的。
  2. 结点一般是在堆上申请的。
  3. 从堆上申请的空间可能是连续的,也可能是不连续的。

2.1.2 链表的打印

我们有了上面知识的铺垫,就可以创建一个链表,然后将它打印出来。

复制代码
// 打印链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur != NULL)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

void Test01()
{
	// 创建一个链表
	SLTNode* node1= (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node2= (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node3= (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node4= (SLTNode*)malloc(sizeof(SLTNode));
	// 链表的初始化
	node1->data = 1;
	node2->data = 2;
	node3->data = 3;
	node4->data = 4;

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	// 打印链表
	SLTNode* plist = node1;
	SLTPrint(plist);
}

这里为什么会循环打印出来呢?我们把下一个结点的地址给了pcur,然后他就会继续识别pcur,直到pcur指向空指针也就是链表的末尾。

3. 实现单链表

我们了解了如何使用,现在我们来实现单链表。

3.1 第1步:尾插

从逻辑上我们写出了尾插的代码,但是会不会有问题呢?这段代码看起来是没有问题的。我们要养成良好的习惯,在写完一段代码后进行测试,我们来测试一下:

这和我们预期的结果不一样,为什么会这样呢?在一些小伙伴看来,plist也是地址,应该是没问题的,这里说明一下原因:因为plist中存储的是一个地址,但是在形参中是不会被改变的,所以在本质上来讲是传值调用,只不过是这个值是一个指针,所以我们这里就得使用二级指针。现在我们来看一下修改后的代码:

在进行测试的时候发现,在申请结点后发现并未让newnode初始化,我们这里给加上。

复制代码
// SList.h
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>


// 创建一个链表结构
typedef int SLTDataType;
typedef struct SListNode
{
	int data;
	struct SListNode* next; // 指向下一个节点的指针
}SLTNode;

// 打印链表
void SLTPrint(SLTNode* phead);

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


// SList.c
#include "SList.h"

// 打印链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur != NULL)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

// 申请新结点
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (malloc == NULL)
	{
		perror("malloc erro");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

// 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
    assert(pphead);
	// 在插入结点之前,我们需要申请一个新的结点

	// 申请新结点
	SLTNode* newnode = SLTBuyNode(x);
	// phead 为空的时候
	if (*pphead == NULL)
	{
		*pphead = newnode;

	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next != NULL)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

// test.c
void test02()
{
	// 创建空链表
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);
	// 打印链表
	SLTPrint(plist);
}

现在程序可以正常运行,也就是尾插代码我们已经写完。

3.2 第2步:头插

从画图中,我们知道了,让newnode的next指针指向第一个结点的地址,然后在让newnode成为第一个结点。现在我们来实现:

头插非常简单,这已经写完了,其中由于担心pphead为空指针,我们来断言一下,保险。

3.3 第3步:尾删

我们仍然通过画图明确逻辑,然后实现代码。

大体的思路我们是有了,但是还是会有缺陷,我们先来实现这段逻辑。

我们前几次的删除都是没有问题的,但是问题就是这最后一次,只有一个结点的时候,prev和ptail指向同一个结点,第一次循环就会跳出循环,所以应该做特殊处理。

这时候尾删我们已经实现。

3.4 第4步:头删

3.5 第5步:查找

复制代码
// 查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	while (phead->next)
	{
		SLTNode* pcur = phead;
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

这段代码的实现非常简单,遍历链表即可。

3.6 第6步:在指定位置之前插入数据

画图:

有了这个逻辑我们来实现一下:

这里是直接把特殊情况包含了,也就是pos属于头结点。

3.7 第7步:在指定位置之后插入数据

这里逻辑清晰后代码是很好写的。所以我们直接写代码,然后做测试。

3.8 第8步:在pos位置删除数据

我们来写代码:

3.9 第9步:在pos位置之后删除数据

其中,我们删除数据后也不能使pos->next指向空,所以我们需要断言。

3.10 第10步:销毁单链表

怎么销毁,一个一个销毁。

3.11 全部代码

复制代码
#pragma once
// SList.h
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>


// 创建一个链表结构
typedef int SLTDataType;
typedef struct SListNode
{
	int data;
	struct SListNode* next; // 指向下一个节点的指针
}SLTNode;

// 打印链表
void SLTPrint(SLTNode* phead);

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

// 头插
void SLTPushFront(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);

// 在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

// 在pos位置删除数据
void SLTErase(SLTNode** pphead, SLTNode* pos);

// 删除pos位置之后的数据
void SLTErasrAfter(SLTNode** pphead, SLTNode* pos);

// 销毁链表
void SLTDestroy(SLTNode** pphead);




#define _CRT_SECURE_NO_WARNINGS 1
// SList.c
#include "SList.h"

// 打印链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur != NULL)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

// 申请新结点
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (malloc == NULL)
	{
		perror("malloc erro");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

// 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	// 在插入结点之前,我们需要申请一个新的结点
	
	// 申请新结点
	SLTNode* newnode = SLTBuyNode(x);
	assert(pphead);
	// phead 为空的时候
	if (*pphead == NULL)
	{
		*pphead = newnode;

	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next != NULL)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

// 头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	// 申请新结点
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

// 尾删
void SLTPopBack(SLTNode** pphead)
{
	// 首先我们来断言一下,防止传过来的是空链表
	assert(pphead && *pphead );

	// 只有一个结点的情况
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else 
	{
		SLTNode* prev = NULL;
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		prev->next = NULL;
		free(ptail);
		// 养成一个好习惯,释放完空间,置为空
		ptail = NULL;
	}
}

// 头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

// 查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

// 在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && pos);
	// 申请一个新结点
	SLTNode* newnode = SLTBuyNode(x);
	if (pos == *pphead)
	{
		// 头插
		SLTPushFront(pphead, x);
	}
	// 寻找pos位置的前一个结点
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	// 找到后
	prev->next = newnode;
	newnode->next = pos;
}

// 在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	// 申请新结点
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

// 在pos位置删除数据
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && pos);
	if (pos == *pphead)
	{
		//头删
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		// 找到pos位置前的结点
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

// 删除pos位置之后的数据
void SLTErasrAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

// 销毁链表
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	while (pcur != NULL) 
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}


#define _CRT_SECURE_NO_WARNINGS 1
// test.c
#include "SList.h"

void Test01()
{
	// 创建一个链表
	SLTNode* node1= (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node2= (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node3= (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node4= (SLTNode*)malloc(sizeof(SLTNode));
	// 链表的初始化
	node1->data = 1;
	node2->data = 2;
	node3->data = 3;
	node4->data = 4;

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	// 打印链表
	SLTNode* plist = node1;
	SLTPrint(plist);
}

void test02()
{
	//// 创建空链表
	// 尾插
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	// 打印链表
	SLTPrint(plist);
	//// 头插
	//SLTNode* plist = NULL;
	//SLTPushFront(&plist, 1);
	//SLTPrint(plist);
	//SLTPushFront(&plist, 2);
	//SLTPrint(plist);
	//SLTPushFront(&plist, 3);
	//SLTPrint(plist);
	//SLTPushFront(&plist, 4);
	//SLTPrint(plist);

	//// 尾删
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	
	//// 头删
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);

	// 查找
	SLTNode* pos = SLTFind(plist, 1);
	/*if (pos)
	{
		printf("zhaodaole");
	}
	else
	{
		printf("weizhaodao");
	}*/
	//// 在指定位置之前插入数据
	/*SLTInsert(&plist, pos, 100);
	SLTPrint(plist);
	SLTInsert(&plist, pos, 100);
	SLTPrint(plist);
	SLTInsert(&plist, pos, 100);
	SLTPrint(plist);*/

	/*SLTInsertAfter(pos, 100);
	SLTPrint(plist);*/

	SLTErase(&plist, pos);
	SLTPrint(plist);
	
	

}

int main()
{
	// Test01();
	test02();
	return 0;
}

最后链表的销毁没有做测试。

4. 单链表算法题

4.1 移除链表元素

点标题就可以进入题目网址:

我们读完题,并给出了自己的思路,现在我们在服务端完成算法代码:

我们在题库中写完代码并测试且通过了。有了思路写代码就是非常快的。所以我们在以后遇到算法题的时候,一定要先整理思路,然后开始写代码。这样准确率和速度的提升都是非常巨大的。

4.2 反转链表

同上我们进行读题然后寻找思路:

我们来实现思路2的代码:

这么一看是不是特别简单。这道题就到这里了。

4.3 链表的中间结点

我们直接画图。

代码我们实现思路2。

很显然是通过的。但是我们会有一个疑问,如果把while的循环条件位置互换会发生什么事?

报错,原因是空指针的解引用。

4.4 合并两个有序的链表

这里有点着急,没有移动newTail,大家应该可以理解。

我们有了思路后,就可以实现代码了。

我们按照思路写完了代码,但是是不通过的。当然我们在写代码的过程肯定不是一帆风顺的,有错我们应该兴奋起来,这样可以完善我们的短板,如果每次写的代码都是一遍过我们会觉得非常没意思,有错我们将它修改正确这才是满满的成就感。

我们现在来看一下报的错误:

第57行发生空指针的解引用。

也就是俩者都为空的时候,会发生解引用错误,我们应该特殊处理一下,在开头我们判断list1和list2是否为空。

在开头增加了list1和list2的判别自测示例就可以通过。

同时终端也是通过的。

所以这段代码就算是写完了。但是我们会感觉有点冗余,我们现在来优化。

对比可以看出,代码量缩短了,其中使用到了哨兵位,就是不储存数据,只是占一个位置。

这道题的介绍就到这里结束了。

4.5 链表分割

还是老样子我们先画图找思路

有了思路直接写代码。

我们也是成功提交了,主要要注意大链表的尾要指向空。

4.6 链表的回文结构

这样写代码就简单多了。当然,如果题目中没有最后一句话,我们就老老实实的使用第一种思路。

4.7 相交链表

这样我们就实现了判断两链表的相交节点。

4.8 环形链表

我们在平台上实现思路。

代码就是这么简单,我们现在来看看while循环的条件。

通过我们判别不是环链表,我们就可以得出while循环条件。

接下来我们来证明为什么快慢指针在换链表中一定会相遇,同时证明fast走三步可不可以。

其实我们陷入了思维误区,我们把这个问题抽象为物理问题,就可以看出,无论速度快慢 (走多少步),它俩在环中必然会相遇只是时间问题。

链表题还有很多很多,我们这里就不在进行举例,我们该有的思维,在这些例子中都有涉及。

相关推荐
wabs66622 分钟前
关于图论【卡码网100.最大岛屿的面积的思考】
数据结构·算法·图论
yuannl102 小时前
图的最短路径Dijkstra
数据结构
zander2583 小时前
138. 随机链表的复制
数据结构·算法·链表
过期动态3 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
来一碗刘肉面3 小时前
栈在递归中的应用
数据结构·算法
皓月斯语4 小时前
程序设计语言的特点
开发语言·数据结构·c++
邪修king4 小时前
C++ 进阶终章:异常机制与智能指针全解 —— 从错误处理到 RAII 资源管理,打通现代 C++ 的核心命脉
android·数据结构·c++
Adios79412 小时前
设置交集大小至少为2
数据结构·算法·leetcode
来一碗刘肉面19 小时前
栈的应用(表达式求值)
数据结构·算法
yuannl1021 小时前
图的深度优先遍历DFS
数据结构