[数据结构——lesson3.单链表]

目录

引言

学习目标:

1.什么是链表

2.链表的分类

[2.1 单向链表和双向链表](#2.1 单向链表和双向链表)

(1)单向链表

(2)双向链表

[2.2 带头结点链表和不带头结点链表](#2.2 带头结点链表和不带头结点链表)

(1)带头结点链表

(2)不带头结点链表

[2.3 循环链表和不循环链表](#2.3 循环链表和不循环链表)

(1)循环链表

(2)非循环链表非循环链表)

3.链表的实现

[3.1 单链表的功能](#3.1 单链表的功能)

3.2单链表的定义

3.3单链表功能实现

1.打印单链表数据

2.单链表头插

(1)创建节点

(2)头插代码

3.单链表头删

4.单链表尾插

代码实现

5.单链表尾删

6.单链表数据查找

7.单链表数据修改

代码实现

8.指定位置数据插入

(1)指定位置之前插入数据

(2)指定位置之后插入数据

9.指定位置数据删除

(1)删除pos节点

(2)删除pos之后的节点删除pos之后的节点)

10.销毁单链表

完整代码SList.h


引言

在上一篇文章中 [数据结构------lesson2.顺序表]我们学习了数据结构中的顺序表,我们知道了顺序表的空间是连续存储的,这与数组非常类似。但是我们也遗留了一些关于顺序表的问题

当我们同时当插入数据时可能存在移动数据与扩容的情况,这大大增加我们的时间与空间成本。因此我们接下来学习新的数据结构------链表

学习目标:

  • 什么是链表?
  • 链表的分类
  • 链表的实现

1.什么是链表

链表(Linked List)是一种常见的数据结构,它通过一系列的节点(Node) 来存储数据元素。这些节点之间通过指针或引用相互连接 ,从而形成一个链状结构。每个节点通常包含两部分:一部分用于存储数据元素,另一部分用于存储指向下一个节点的指针或引用。

注意:

1.从上图可以看出,链式结构在逻辑上是连续的,但在物理上不一定连续;

链表是一种物理存储 结构上非连续、非顺序 的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表中的每个节点通过指针指向下一个节点,从第一个节点开始,依次沿着指针可以访问到后续节点,形成一个逻辑上连续的序列。但在物理内存中,这些节点可能分布在不同的位置,并不要求存储单元是连续的。

2.链表的结点一般都是从堆上申请的;

链表中的结点是独立申请的空间 ,通常是在需要插入数据时才去申请一块节点的空间。操作系统的内存管理中,堆是一块可供程序动态分配的内存区域,它允许程序在运行时根据需要申请和释放内存,适合链表这种动态数据结构,因为链表的长度不固定,需要随时根据数据的插入和删除来分配和释放节点空间。

3.从堆上申请的空间,是按照一定的策略来分配的,两次省请的空间可能连续,可能不连续。

堆内存的分配由操作系统的内存分配器负责,它会根据一定的算法和策略来管理和分配内存,这些算法会根据堆内存的当前使用情况来寻找合适的空闲块分配给程序。由于堆内存的使用是动态变化的,每次申请内存时,内存分配器找到的空闲块位置是不确定的,所以两次申请的空间可能连续,也可能不连续。

2.链表的分类

链表可以分为几种不同的类型,其中最常见的有八种,它们大致可以分为三类:

2.1 单向链表和双向链表

(1)单向链表

在单向链表中,每个节点只包含一个指向下一个节点的指针。

(2)双向链表

在双向链表中,每个节点除了包含数据元素外,还包含两个指针:一个指向下一个节点(称为后继节点),另一个指向前一个节点(称为前驱节点)。

2.2 带头结点链表和不带头结点链表

(1)带头结点链表

带头结点的单链表是在链表的第一个数据元素结点之前附加一个结点,称为头结点。

(2)不带头结点链表

非带头结点的单链表不包含头结点,链表的第一个结点即为数据元素结点,头指针直接指向链表的第一个数据元素结点。

2.3 循环链表和不循环链表

(1)循环链表

循环链表是一种特殊类型的链表,其中最后一个节点指向第一个节点,形成一个循环。这种结构使得链表在逻辑上成为一个环状结构。

(2)非循环链表

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
无头单向非循环链表

带头双向循环链

本篇博客将详细讲解一下无头单向非循环链表。

无头单向非循环链表是一种链表结构,其特点是没有头结点,且链表中的节点单向连接,不形成循环。这种链表结构相对简单,一般不会单独用来存储数据,而是作为其他数据结构的子结构,如哈希桶、图的邻接表等。

3.链表的实现

3.1 单链表的功能

单链表一般要实现如下几个功能:

  1. 打印单链表中的数据。
  2. 对单链表进行头插(开头插入数据)。
  3. 对单链表进行头删(开头删除数据)。
  4. 对单链表进行尾插(末尾插入数据)。
  5. 对单链表进行尾删(末尾删除数据)。
  6. 对单链表进行查找数据。
  7. 对单链表数据进行修改。
  8. 对指定位置的数据删除。
  9. 对指定位置的数据插入。
  10. 销毁单链表。

3.2单链表的定义

单链表的结点定义方式与我们以往定义的方式都不同,它是一个结构体中包含两个成员。一个是存储数值,一个存放下一个结点的地址。

cpp 复制代码
//定义节点的结构
//数据+指向下一节点的指针
typedef int SLTDataType;
 
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

3.3单链表功能实现

1.打印单链表数据

遍历链表并打印每个节点的数据来展示链表的内容,直到遇到链表的末尾。

代码实现

cpp 复制代码
void SLTPrint(SLTNode* phead)
{
	// 初始化一个指针pcur,指向链表的头节点
	SLTNode* pcur = phead;
	// 使用while循环遍历链表,直到pcur指向NULL(即链表末尾)  
	while (pcur != NULL)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	// 循环结束后,打印"NULL"以明确表示链表的结束 
	printf("NULL\n");
}

2.单链表头插

(1)创建节点

由于单链表每次插入都需要插入一个节点,因此我们可以写一个创建节点的函数方便后续调用。

cpp 复制代码
//申请内存
SLTNode* SLTCreateNode(SLTDataType x)
{
	// 为新的单链表节点分配内存空间 
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail:");
		exit(1);
	}
	// 初始化新节点的数据字段  
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
(2)头插代码

在这里我们需要注意的是:单链表传参需要二级指针。因为头插数据需要改变一级指针plist的指向,而形参的改变无法影响实参,所以需要二级指针才能改变一级指针plist的指向。

cpp 复制代码
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	// 断言确保传入的头节点指针的地址非空,避免解引用空指针
	assert(pphead);
	// 创建一个新的节点,并初始化其数据为x  
	SLTNode* newnode = SLTCreateNode(x);
	// 将新节点的next指针指向当前头节点
	newnode->next = *pphead;
	// 更新头节点指针,使其指向新节点
	*pphead = newnode;
}

一、对 assert(pphead) 的理解

核心作用 :确保二级指针 pphead 不为空,避免解引用空指针导致程序崩溃。

  • 逻辑背景
    代码中存在 *pphead(解引用二级指针),而对空指针执行解引用操作(如 *NULL)会触发未定义行为(通常导致程序崩溃)。
    assert(pphead) 会在程序运行时检查 pphead 是否为 NULL
    • ppheadNULL,程序会报错并终止,提示开发者问题所在;
    • pphead 非空,则继续执行后续代码。
  • 本质目的:通过断言提前暴露潜在的空指针风险,提高代码健壮性。

二、为什么需要传递二级指针?

核心原因 :需要通过函数修改原始指针(如链表头节点指针 head)的地址本身。

  • 指针操作的本质
    • 若仅传递一级指针 head(即结构体指针),函数内部只能修改 head 指向的结构体内容,无法改变 head 本身存储的地址值(类似 "传值" 效果)。
    • 若需要修改 head 本身的地址(例如头节点变更、链表初始化等场景),必须传递 head 的地址,即二级指针 pphead(类似 "传址" 效果)。

3.单链表头删

头删与头插类似,都可能需要改变plist的指向,所以也需要二级指针。并且也需要防止链表为空的情况发生

代码实现

cpp 复制代码
//头删
void SLTPopFront(SLTNode** pphead)
{
	// 链表不能为空,确保pphead不是空指针
	// 且*pphead(即头节点)也不是空指针
	assert(pphead && *pphead);
	// 保存头节点的下一个节点的地址,
	// 以便删除头节点后能够更新头指针
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	// 更新头节点
	*pphead = next;
}

4.单链表尾插

代码实现
cpp 复制代码
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//	*pphead就是指向第一个节点的指针
	//	处理空链表
	SLTNode* newnode = SLTCreateNode(x);
	// 判断链表是否为空
	if (*pphead == NULL)
	{
		// 如果链表为空,则新节点即为头节点
		*pphead = newnode;
	}
	else
	{
		// 找尾
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		// 循环结束,ptail指向尾节点
		ptail->next = newnode;
	}
}

5.单链表尾删

与单链表尾插类似,当链表只有一个头结点时需要将plist置为空所以也需要二级指针 。并且也需要防止链表为空的情况发生。

cpp 复制代码
//尾删
void SLTPopBack(SLTNode** pphead)
{
	// 链表不能为空,确保pphead不是空指针,
	// 且*pphead(即头节点)也不是空指针
	assert(pphead && *pphead);
	// 如果链表只有一个节点
	if ((*pphead)->next == NULL)	//->优先级高于*
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		// 链表有多个节点
		SLTNode* prev = *pphead;
		SLTNode* ptail = *pphead;
		// 遍历链表直到找到尾节点
		while (ptail->next)
		{
			prev = ptail;			// 更新prev节点
			ptail = ptail->next;	// 移动到下一个节点
		}
		free(ptail);	// 释放尾节点
 
		prev->next = NULL;
	}
}

6.单链表数据查找

如果找到了就返回这个节点的地址,否则就返回空指针NULL。

代码实现

cpp 复制代码
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	// 遍历链表,直到当前节点为空(即到达链表末尾)
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;	// 找到当前节点的指针
		}
		pcur = pcur->next;
	}
	return NULL;
}

7.单链表数据修改

代码实现
cpp 复制代码
//数据修改 
void SLTModifity(SLTNode* pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos); 
	SLTNode* cur = pphead;
	while (cur)
	{
		if (cur == pos)
		{
			cur->data = x;
			break;
		}
		cur = cur->next;
	}
}

8.指定位置数据插入

(1)指定位置之前插入数据
cpp 复制代码
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
 
	SLTNode* newnode = SLTCreateNode(x);
	// 如果插入位置是链表的第一个位置(即pos等于头节点)
	// 则调用头插函数
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
}
(2)指定位置之后插入数据
cpp 复制代码
//在指定位置之后插入数据
void SLTnsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTCreateNode(x);
	// 将新节点的next指针指向pos的下一个节点
	newnode->next = pos->next;
	// 将pos的next指针指向新节点,完成插入操作
	pos->next = newnode;
}

9.指定位置数据删除

(1)删除pos节点
cpp 复制代码
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);
	assert(pphead && *pphead);
	// 如果删除的节点是头节点
	if (pos == *pphead)
	{
		SLTPopFront(pphead);	// 头删
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		// 将prev的next指针指向pos的下一个节点
		// 从而绕过pos节点,实现删除
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
(2)删除pos之后的节点
cpp 复制代码
//删除pos之后的节点
void SLTraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	// 创建一个临时指针del,指向pos之后的节点,即要删除的节点
	SLTNode* del = pos->next;
	// 将pos的next指针指向del的下一个节点,从而绕过del节点,实现删除
	pos->next = del->next;
	free(del);
	del = NULL;
}

10.销毁单链表

销毁链表需要依次遍历释放节点。

cpp 复制代码
//销毁链表
void SListDesTory(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		// 保存当前节点的下一个节点的指针
		// 以便在释放当前节点后能够继续遍历
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

完整代码

SList.h

cpp 复制代码
#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);
//尾插
void SLTPushBack(SLTNode** phead, SLTDataType);
//头插
void SLTPushFront(SLTNode** phead, SLTDataType);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
 
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
 
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在制定位置之后插入数据
void SLTnsertAfter(SLTNode* pos, SLTDataType x);
 
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTraseAfter(SLTNode* pos);
 
//销毁链表
void SListDesTory(SLTNode** pphead);
 
//数据修改
void SLTModifity(SLTNode* pphead, SLTNode* pos, SLTDataType x);

SList.c

cpp 复制代码
#include"SList.h"
 
void SLTPrint(SLTNode* phead)
{
	// 初始化一个指针pcur,指向链表的头节点
	SLTNode* pcur = phead;
	// 使用while循环遍历链表,直到pcur指向NULL(即链表末尾)  
	while (pcur != NULL)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	// 循环结束后,打印"NULL"以明确表示链表的结束 
	printf("NULL\n");
}
 
//申请内存
SLTNode* SLTCreateNode(SLTDataType x)
{
	// 为新的单链表节点分配内存空间 
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail:");
		exit(1);
	}
	// 初始化新节点的数据字段  
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
 
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//	*pphead就是指向第一个节点的指针
	//	处理空链表
	SLTNode* newnode = SLTCreateNode(x);
	// 判断链表是否为空
	if (*pphead == NULL)
	{
		// 如果链表为空,则新节点即为头节点
		*pphead = newnode;
	}
	else
	{
		// 找尾
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		// 循环结束,ptail指向尾节点
		ptail->next = newnode;
	}
}
 
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	// 断言确保传入的头节点指针的地址非空,避免解引用空指针
	assert(pphead);
	// 创建一个新的节点,并初始化其数据为x  
	SLTNode* newnode = SLTCreateNode(x);
	// 将新节点的next指针指向当前头节点
	newnode->next = *pphead;
	// 更新头节点指针,使其指向新节点
	*pphead = newnode;
}
 
//尾删
void SLTPopBack(SLTNode** pphead)
{
	// 链表不能为空,确保pphead不是空指针,
	// 且*pphead(即头节点)也不是空指针
	assert(pphead && *pphead);
	// 如果链表只有一个节点
	if ((*pphead)->next == NULL)	//->优先级高于*
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		// 链表有多个节点
		SLTNode* prev = *pphead;
		SLTNode* ptail = *pphead;
		// 遍历链表直到找到尾节点
		while (ptail->next)
		{
			prev = ptail;			// 更新prev节点
			ptail = ptail->next;	// 移动到下一个节点
		}
		free(ptail);	// 释放尾节点
 
		prev->next = NULL;
	}
}
 
//头删
void SLTPopFront(SLTNode** pphead)
{
	// 链表不能为空,确保pphead不是空指针
	// 且*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 SLTModifity(SLTNode* pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos); 
	SLTNode* cur = pphead;
	while (cur)
	{
		if (cur == pos)
		{
			cur->data = x;
			break;
		}
		cur = cur->next;
	}
}
 
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
 
	SLTNode* newnode = SLTCreateNode(x);
	// 如果插入位置是链表的第一个位置(即pos等于头节点)
	// 则调用头插函数
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
}
 
//在指定位置之后插入数据
void SLTnsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTCreateNode(x);
	// 将新节点的next指针指向pos的下一个节点
	newnode->next = pos->next;
	// 将pos的next指针指向新节点,完成插入操作
	pos->next = newnode;
}
 
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);
	assert(pphead && *pphead);
	// 如果删除的节点是头节点
	if (pos == *pphead)
	{
		SLTPopFront(pphead);	// 头删
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		// 将prev的next指针指向pos的下一个节点
		// 从而绕过pos节点,实现删除
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
 
//删除pos之后的节点
void SLTraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	//创建临时变量del
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}
 
//销毁链表
void SListDesTory(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		// 保存当前节点的下一个节点的指针
		// 以便在释放当前节点后能够继续遍历
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

结束语

本节内容学习了链表的种类和单链表的实现。

感谢大家三连支持!!!

相关推荐
hsjkdhs7 小时前
数据结构之链表(单向链表与双向链表)
数据结构·链表·指针
Suresoft China7 小时前
软件测试|STATIC 代码静态验证工具 C/C++ 工具链设置指南
c++·单元测试·静态测试·测试覆盖率·static·代码覆盖率·工具链设置
kyle~7 小时前
排序---冒泡排序(Bubble Sort)
c语言·c++·算法
轮到我狗叫了8 小时前
力扣.1054距离相等的条形码力扣767.重构字符串力扣47.全排列II力扣980.不同路径III力扣509.斐波那契数列(记忆化搜索)
java·算法·leetcode
hmcjn(小何同学)8 小时前
轻松Linux-9.进程间通信
linux·运维·服务器·c++·bash
云泽8088 小时前
数据结构之单链表和环形链表的应用(二)-
数据结构
落羽的落羽8 小时前
【C++】C++11的包装器:function与bind简介
c++·学习
打不了嗝 ᥬ᭄8 小时前
【Linux】线程概念与控制
linux·c++
UnnamedOrange9 小时前
ROS1 配置代码覆盖率
c++·cmake