【数据结构初阶】单链表

目录

[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走三步可不可以。

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

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

相关推荐
ZIM学编程6 小时前
「学长有话说」作为一个大三学长,我想对大一计算机专业学生说这些!
java·c语言·数据结构·c++·python·学习·php
子枫秋月8 小时前
单链表实现全解析
c语言·数据结构·c++
刀法自然8 小时前
栈实现表达式求值
数据结构·算法·图论
Yupureki9 小时前
从零开始的C++学习生活 19:C++复习课(5.4w字全解析)
c语言·数据结构·c++·学习·1024程序员节
ゞ 正在缓冲99%…10 小时前
leetcode1312.让字符串成为回文串的最少插入次数
数据结构·算法·leetcode·动态规划·记忆化搜索
laocooon52385788611 小时前
寻找使a×b=c成立的最小进制数(2-16进制)
数据结构·算法
一匹电信狗15 小时前
【牛客CM11】链表分割
c语言·开发语言·数据结构·c++·算法·leetcode·stl
不染尘.15 小时前
图的邻接矩阵实现以及遍历
开发语言·数据结构·vscode·算法·深度优先
xiaoye-duck15 小时前
数据结构之栈和队列-栈
数据结构