【数据结构】单链表的实现

前面解决了顺序表是如何工作的,并用代码实现顺序表。

用顺序表的好处有:

  1. 动态内存管理

  2. 高效随机访问

  3. 简化数据操作
    顺序表搞定后,接下来就开始单链表的运用

    顺序表

一、单链表的定义

单链表是一种线性数据结构 ,它通过指针 将一个个 "节点"(类比火车车厢)串联起来,形成一个有序的序列。每个节点只存储下一个节点的地址 ,因此它的遍历方向是单向的(只能从 "头" 走到 "尾",不能反向)。

二、单链表的结构组成

单链表的节点结构是单链表的核心组成单元:

c 复制代码
typedef int SLTNode; //将用到的数据用SLTNode来定义,方便类型的修改

typedef struct SListNode
{
    SLTNode data;               // 数据域:存储具体数据(类比"车厢里的货物")
    struct SListNode* next; // 指针域:存储下一个节点的地址(类比"车厢之间的连接钩")
}SLTNode; //将修改节点的名称,节省代码量
  • typedef (数据类型) SLTNode:可以直接修改类型,方便数据域的运用。
  • 数据域(data:存储节点的实际数据(比如整数、字符串、对象等),对应 "车厢里的货物"。
  • 指针域(next :存储下一个节点的地址,通过它将所有节点 "链" 在一起,对应 "车厢之间的连接钩"。

三、单链表的核心特性

结合 "火车" 类比,可以更清晰地理解它的特性:

  1. 单向性

    就像火车只能 "从车头到车尾" 单向行驶,单链表的节点只能通过 next 指针从前往后遍历,无法直接从后一个节点跳回前一个节点(除非额外记录前驱节点地址)。

  2. 动态性

    单链表的节点是动态分配内存 的(运行时才创建 / 销毁),不像数组需要预先分配固定大小的空间。这意味着单链表的长度可以灵活增减(类比火车可以随时加车厢、减车厢)。

  3. 头节点与空链表

    • 头节点 :单链表的 "起始节点",我们通常用一个指针(如 struct SListNode* head)指向它,类比 "火车头"。
    • 空链表 :当 head == NULL 时,链表中没有任何节点,类比 "没有车厢的空火车"。
      在链表里,"火车"是这样子的

四、单链表的基本操作(入门必懂)

理解结构后,可以通过这些操作进一步掌握它:

操作 功能类比 实现思路
初始化 准备一列空火车 将头指针 head 置为 NULL
头插法(新增) 在火车头前加一节车厢 新建节点 → 新节点的 next 指向原头节点 → 头指针 head 指向新节点。
尾插法(新增) 在火车尾加一节车厢 遍历到最后一个节点 → 最后一个节点的 next 指向新节点。
遍历 从车头到车尾检查每节车厢 head 开始,循环访问当前节点的 data,再通过 next 跳转到下一个节点,直到 next == NULL
查找 在火车里找某节装特定货物的车厢 遍历链表,逐个对比节点的 data,找到则返回节点地址,否则返回 NULL
删除 移除某节车厢 找到目标节点的前驱节点 → 前驱节点的 next 指向目标节点的 next → 释放目标节点的内存。

五、链表打印(基础)

在一个给定的链表结构中,如何实现节点从头到尾的打印?

1.1头文件

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

typedef int SLTDataType;

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

//打印单链表的头函数
void SLTPrint(SLTNode* Phead);

1.2函数实现

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"

void SLTPrint(SLTNode* Phead)
{
	while (Phead)
	{
		//打印链表中每一个数据
		printf("%d->", Phead->data);
		//遍历每个车厢
		Phead = Phead->Next;
	}
	printf("NULL\n");
}

1.3测试

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"

void SLTTest01()
{
	//手动创建每个节点
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	//将节点中的数据给赋值
	node1->data = 1;

	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;

	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;

	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;

	//让每个节点相互连接
	node1->Next = node2;
	node2->Next = node3;
	node3->Next = node4;
	node4->Next = NULL;

	//创建一个指针,指向列车的开头
	SLTNode* Phead = node1;

	//传入列车的开头,然后打印单链表
	SLTPrint(Phead);
}

int main()
{
	SLTTest01();
	return 0;
}

打印的结果为:

复制代码
1->2->3->4->NULL

六、尾插实现

形式如下:

C 复制代码
void SLTPopBack(SLTNode** PPhead);

SLTPushBack(&Phead, 2);

单链表的尾插,也就是让一个创建节点放在末尾节点的后面,但有两种情况

  • 情况1:链表为空
  • 情况2:链表中有多个节点
  • 要使用头插和尾插,首先要实现节点的创建

1.1创建节点

C 复制代码
//创建节点
SLTNode* SLTByNode(SLTDataType x)
{
	//手动申请空间
	SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));
	if (NewNode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	NewNode->data = x;
	NewNode->Next = NULL;

	return NewNode;
}

1.2情况1:无节点

遇到情况1时,可以先画图进行分析

此时的链表为空,则要给链表增加节点,使新的空间成为首个节点

代码如下:

C 复制代码
//创建节点
SLTNode* NewNode = SLTByNode(x);
//情况1(只有一个节点):若无节点,申请空间给链表
if (*PPhead == NULL)
{
	*PPhead = NewNode;
}

1.3情况2,有节点

情况2如下图所示

代码如下:

C 复制代码
//情况2(有多个节点):创建一个指针指向开头,往下遍历直到空指针停止
else
{
	SLTNode* Ptail = *PPhead;
	while (Ptail->Next)
	{
		Ptail = Ptail->Next;
	}
	Ptail->Next = NewNode;
}

七、头插

1.1形式:

C 复制代码
void SLTPushFront(SLTNode** PPhead, SLTDataType x);

SLTPushFront(&Phead, 2);

1.2头插实现

头文件

C 复制代码
/*头文件*/
void SLTPushFront(SLTNode** PPhead, SLTDataType x);

测试

C 复制代码
/*test文件*/
SLTPushFront(&Phead, 2);

函数实现

C 复制代码
/*函数实现*/
void SLTPushFront(SLTNode** PPhead, SLTDataType x)
{
	//判断是否为空链表
	assert("PPhead");
	assert("*PPhead");

	//创建新节点
	SLTNode* NewNode = SLTByNode(x);

	//将空间置于前面
	NewNode->Next = *PPhead;
	//把指针指向新空间
	*PPhead = NewNode;
}

八、尾删

1.1形式

C 复制代码
void SLTPopBack(SLTNode** PPhead);

SLTPopBack(&Phead);

在进行尾删时会有两种情况

  1. 只有一个节点
  2. 有多个节点

1.2情况1:一个节点

C 复制代码
//情况1:只有一个节点
if ((*PPhead)->Next == NULL)
{
	//把唯一的节点删除
	free(*PPhead);
	*PPhead = NULL;
}
  • ->的优先级比*高,必须用大括号括起来

1.3情况2:多个节点

C 复制代码
//情况2:有多个节点
else
{
	//设立两个指针,一个指向结尾,一个指向结尾前一项
	SLTNode* Ptail = *PPhead;
	SLTNode* Ptailadd = *PPhead;
	//让指针遍历到结尾
	while (Ptail->Next)
	{
		Ptailadd = Ptail;
		Ptail = Ptail->Next;
	}
	//释放结尾
	free(Ptail);
	Ptail = NULL;
	Ptailadd->Next = NULL;
}

1.4总合

C 复制代码
//尾删
void SLTPopBack(SLTNode** PPhead);

//实现函数
//尾删
void SLTPopBack(SLTNode** PPhead)
{
	assert(*PPhead && PPhead);

	//情况1:只有一个节点
	if ((*PPhead)->Next == NULL)
	{
		//把唯一的节点删除
		free(*PPhead);
		*PPhead = NULL;
	}
	//情况2:有多个节点
	else
	{
		//设立两个指针,一个指向结尾,一个指向结尾前一项
		SLTNode* Ptail = *PPhead;
		SLTNode* Ptailadd = *PPhead;
		//让指针遍历到结尾
		while (Ptail->Next)
		{
			Ptailadd = Ptail;
			Ptail = Ptail->Next;
		}
		//释放结尾
		free(Ptail);
		Ptail = NULL;
		Ptailadd->Next = NULL;
	}
}

//test文件
SLTPopBack(&Phead);

九、头删

1.1形式

C 复制代码
void SLTPopFront(SLTNode** PPhead);

SLTPopFront(&Phead)

1.2头删实现

头删相比尾删就非常简洁,且只有一种情况:不仅能判断一个节点,也能判断多个节点

C 复制代码
//头删
void SLTPopFront(SLTNode** PPhead)
{
	//保证传的不能为空
	assert(*PPhead && PPhead);

	//让新的指针指向节点的下一个节点
	SLTNode* Ptail = (*PPhead)->Next;
	//把首元素删除
	free(*PPhead);
	//将头指针指向上面创建好的新指针
	*PPhead = Ptail;
}

十、查找

1.1形式

C 复制代码
SLTNode* SLTFind(SLTNode* Phead, SLTDataType x);

SLTNode* find = SLTFind(Phead, 3);

查找元素时,需要一个变量来接收返回值,思路如下

  1. 只查找不会改变链表中的数据,传变量即可
  2. 建个指针指向形参,若后面指向空时,还可以再建个指针继续查找

1.2查找实现

C 复制代码
//查找
SLTNode* SLTFind(SLTNode* Phead, SLTDataType x)
{
	SLTNode* Ptail = Phead;

	while (Ptail)
	{
		if (Ptail->Next == x)
		{
			//找到返回对应的指针
			return Ptail;
		}
		Ptail = Ptail->Next;
	}
	//没找到返回空
	return NULL;
}

在测试文件时,如下:

C 复制代码
//查找
SLTNode* Find = SLTFind(Phead, 3);
if (Find == NULL)
{
	printf("没找到!!\n");
}
else
{
	printf("找到了!!\n");
}
  • 单链表中有3的话则打印找到,相反就没找到

十一、指定位置头插(配合查找)

1.1形式

C 复制代码
SLTNode* SLTInsert(SLTNode** PPhead, SLTNode* pos, SLTDataType x);

SLTInsert(&Phead, find, 4);
  • 其中pos是查找函数返回的地址,也就是返回的指定位置

1.2指定位置头插实现

头插需要配合查找进行,思路如下:

  1. 创建新的节点
  2. 创建指针指向地址
  3. 遍历链表,当指针指到形参pos(Find)时,则找到
  4. 让指针指向新节点
  5. 新节点的Next指向pos
C 复制代码
//在指定位置之前插入数据
SLTNode* SLTInsert(SLTNode** PPhead, SLTNode* pos, SLTDataType x)
{
	assert(PPhead && *PPhead);

	SLTNode* NewNode = SLTByNode(x);
	
	//情况1:当插到首节点之前
	if (*PPhead == pos)
	{
		SLTPushBack(PPhead, x);
	}
	//情况2:插到其他地方之前
	else
	{
		SLTNode* Ptail = *PPhead;
		while (Ptail->Next != pos)
		{
			Ptail = Ptail->Next;
		}
		NewNode->Next = pos;
		Ptail->Next = NewNode;
	}
}

形参中的pos其实是查找中的Find,使用时两者配合使用

十二、指定位置尾插(配合查找)

1.1形式

与指定位置头插一样,需要配合查找函数

C 复制代码
//在指定位置之后插入数据
SLTNode* SLTInserAfter(SLTNode* pos, SLTDataType x);

//在指定位置之后插入数据
SLTInserAfter(find1, 5);

1.2指定位置尾插实现

  1. 用查找函数,查找想插入的指定位置
  • 不需要传入链表,可直接用查找到的地址
  • 创建节点,节点指向查找的地址的下一个节点
  • 用查找节点指向新建立的节点
C 复制代码
//在指定位置之后插入数据
SLTNode* SLTInserAfter(SLTNode* pos, SLTDataType x)
{
	SLTNode* NewNode = SLTByNode(x);
	
	//新节点指向查找的下一个节点
	NewNode->Next = pos->Next;
	//查找的节点指向新节点
	pos->Next = NewNode;
}

十三、删除指定节点

需要与查找函数一起使用,且有两种情况:头删和删其他

1.1形式

C 复制代码
//删除节点
void SLTErase(SLTNode** PPhead, SLTNode* pos);

SLTErase(&Phead, find2);

1.2实现

C 复制代码
//删除节点
void SLTErase(SLTNode** PPhead, SLTNode* pos)
{
	assert(PPhead && *PPhead);
	assert(pos);

	//情况1:删除头节点
	if (pos == *PPhead)
	{
		//头删函数
		SLTPopFront(PPhead);
	}
	else
	{
		SLTNode* Ptail = *PPhead;
		//情况2:删除其他节点
		while (Ptail->Next != pos)
		{
			Ptail = Ptail->Next;
		}
		Ptail->Next = pos->Next;

		free(pos);
		pos = NULL;
	}
}

十四、删除指定位置之后的节点

删除指定位置之后的节点只需要传查找函数的find即可

1.1形式

C 复制代码
//删除指定位置之后的节点
void SLTEraseAfter(SLTNode* pos);

//删除指定位置之后的节点
SLTEraseAfter(find3);

1.2实现

C 复制代码
//删除指定位置之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->Next);
	//定义一个指针指向要删除的节点
	SLTNode* del = pos->Next;
	//pos del del->next
	//让删除节点的前一个节点指向删除节点的后一个节点
	pos->Next = del->Next;
	//释放删除的节点并指向空
	free(del);
	del = NULL;
}

十五、销毁链表

在使用完链表时,就要销毁链表

1.1形式

C 复制代码
//销毁链表(销毁一个一个的节点)
void SLTDesTroy(SLTNode** PPhead);

1.2实现

C 复制代码
//销毁链表(销毁一个一个的节点)
void SLTDesTroy(SLTNode** PPhead)
{
	assert(PPhead && *PPhead);

	SLTNode* Pcur = *PPhead;
	while (Pcur)
	{
		SLTNode* next = Pcur->Next;
		free(Pcur);
		//让销毁节点的指针指向要销毁下一个的节点
		Pcur = next;
		//当Pcur为空,就跳出循环
	}
	*PPhead = NULL;
}
相关推荐
刘一说6 小时前
深入理解 Spring Boot Web 开发中的全局异常统一处理机制
前端·spring boot·后端
塔能物联运维6 小时前
物联网边缘节点数据缓存优化与一致性保障技术
java·后端·物联网·spring·缓存
吴名氏.6 小时前
细数Java中List的10个坑
java·开发语言·数据结构·list
IT_陈寒7 小时前
Vite 5震撼发布!10个新特性让你的开发效率飙升200% 🚀
前端·人工智能·后端
JohnYan7 小时前
工作笔记 - 记一次PG数据导入和清理
后端·postgresql
_dindong7 小时前
牛客101:递归/回溯
数据结构·c++·笔记·学习·算法·leetcode·深度优先
刃神太酷啦7 小时前
力扣校招算法通关:双指针技巧全场景拆解 —— 从数组操作到环检测的高效解题范式
java·c语言·数据结构·c++·算法·leetcode·职场和发展