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

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

用顺序表的好处有:

  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;
}
相关推荐
Rust研习社6 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒7 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro7 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax8 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH8 小时前
Koa和Express的区别
后端
MariaH8 小时前
Koa框架的使用
后端
luckdewei9 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某10 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy11 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom11 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github