手撕无头单链表


  • 博客主页:江池俊的博客
  • ⏩ 收录专栏:数据结构探索
  • 专栏推荐:✅C语言初阶之路 ✅C语言进阶之路
  • 代码仓库:江池俊的代码仓库
  • 编译环境:Visual Studio 2022
  • 欢迎大家点赞评论收藏⭐

在这里插入图片描述

文章目录

  • 一、什么是链表
    • 1.1 链表的概念
    • 1.2 链表的分类
  • 二、单链表的结构
  • 三、单链表的创建
    • 3.1 单链表的存储结构
    • 3.2 单链表的接口
  • 四、接口实现(增、删、查、改)
    • 4.1 单链表的插入
      • 前提:向内存申请节点空间
      • 尾插
      • 头插
      • 在pos节点之前插入x
      • 在pos节点以后插入x
    • 4.2 单链表的删除
      • ♨️尾删
      • ♨️头删
      • ♨️删除pos节点
      • ♨️删除pos的后一个节点
    • 4.3 单链表的其他接口实现
      • 打印单链表
      • 查找
      • 单链表的销毁
  • 五、源码
    • 5.1 `SList.h` 文件
    • 5.2 `SList.c` 文件
    • 5.3 `Test.h` 文件


一、什么是链表

1.1 链表的概念

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

1.2 链表的分类

链表主要有单向、双向、带头节点、不带头节点、循环和非循环这些特点,再经过它们的互相组合就形成了 2 × \times × 2 × \times × 2 = 8种链表的结构。

此处我要讲解的是 不带头单向非循环 链表。


二、单链表的结构

单向链表(又名单链表、线性链表) 是链表的一种,其特点是链表的链接方向是单向的,对链表的访问要通过从头部开始,依序往下读取。

一个单向链表的节点被分成两个部分。第一个部分保存或者显示关于节点的信息,第二个部分存储下一个节点的地址。单向链表只可向一个方向遍历。

手撕无头单链表_第1张图片

前面我们学习了顺序表,这里我们先来分析一下顺序表和单链表的优缺点:

顺序表:

优点:

  1. 访问元素时,顺序表可以在常数时间内完成(O(1)),无论数据量多大。
  2. 顺序表可以利用缓存和预读取技术进行优化,以提高访问速度。
  3. 对于随机访问和快速查找操作,顺序表通常比单链表更高效。

缺点:

  1. 顺序表在插入和删除操作上较慢,因为可能需要移动大量的元素来保持数据的连续性。这些操作的平均时间复杂度为O(n),其中n是元素的数量。
  2. 顺序表通常需要连续的内存空间,因此当数据量非常大时,可能会面临内存分配的问题。
  3. 在顺序表中查找特定元素可能需要遍历整个数组,这在处理大量数据时可能会变得很慢。

单链表:

优点:

  1. 插入和删除操作在链表的前端和后端相对较快,时间复杂度为O(1)。
  2. 内存使用效率较高,因为每个节点只存储一个指向下一个节点的引用。
  3. 对于一些特定的问题,如反转链表或找到链表中倒数第k个元素等,单链表有较好的解决方案。

缺点:

  1. 访问链表的中间元素需要从头部开始遍历,时间复杂度为O(n),其中n是链表的长度。
  2. 在大规模数据中,查找操作可能比其他数据结构(如数组或哈希表)慢。
  3. 由于需要额外的空间来存储指针,所以内存使用量比顺序表大。

三、单链表的创建

3.1 单链表的存储结构

typedef int SLTDataType;//定义数据类型的别名,方便后续存储元素类型改变的修改

//定义结点类型 
typedef struct SListNode
{
	SLTDataType data;//每个节点存放一个数据元素
	struct SListNode* next;//结构体指针,指向下一个节点
}SLTNode;

3.2 单链表的接口

// 动态申请一个节点
SLTNode* BuySListNode(SLTDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//打印单链表
void SLTPrint(SLTNode* phead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
// 在pos节点之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 在pos节点以后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
// 删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
// 删除pos的后一个节点
void SLTEraseAfter(SLTNode* pos);
//单链表的销毁
void SLTDestroy(SLTNode** pphead);

四、接口实现(增、删、查、改)

4.1 单链表的插入

  • 在插入节点之前我们首先需要动态申请一个节点来保存要插入的数据,并将此节点放在相应的位置。
  • 而申请节点的操作在所有插入函数中都需要进行,故我们可以将这个操作写成一个申请节点的函数,方便各种插入函数的调用。

前提:向内存申请节点空间

// 动态申请一个节点
SLTNode* BuySListNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x; 
	newnode->next = NULL;
	return newnode;
}

手撕无头单链表_第2张图片

尾插

注意:

  1. 特殊情况,当链表为空时,则需要更改链表的头指针的地址,使得它的指向不再是 NULL,而是插入的新的节点的地址。
  2. 这里需要修改链表的头指针的地址,所以在传参的时候需要传入链表头指针的地址,即接收它的形式参数为二级指针。
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySListNode(x);
	if (*pphead == NULL) 
	{
		*pphead = newnode;//改变了结构体指针,所以传二级指针
	}
	else
	{
		SLTNode* tail = *pphead; 
		while (tail->next) 
		{ 
			tail = tail->next; 
		}
		tail->next = newnode;//改变的结构体,用结构体的指针即可
	}
}

头插

头插的算法比较简单,只需要将新节点的next指向链表的头,然后修改链表的头为新节点即可。即使链表为空时此算法依然成立。

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySListNode(x);
	
	newnode->next = *pphead; 
	*pphead = newnode; 
}

在pos节点之前插入x

这里我们需要保存 pos 节点之前那个节点的位置,所有使用一个临时结构体指针变量 prev来保存前一个节点的位置,然后将新的节点插入其中即可。

注意:

  • 特殊情况,当 pos 节点等于链表的头节点时,就需要进行头插的操作,将新的节点插入到链表的头节点之前,然后将新节点置为链表的头。
// 在pos节点之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	//要么都是空,要么都不是空 --- 当链表不为空时,pos不能为空
	//当链表为空,则pos必须为空
	//总结;pos一定要为有效节点,NULL不是有效节点
	//assert((!pos && !(*pphead)) || (pos && *pphead));

	assert(pos);//pos不为空
	assert(*pphead);//链表不为空
	SLTNode* newnode = BuySListNode(x); 

	if (*pphead == pos)
	{
		//头插
		newnode->next = *pphead;  
		*pphead = newnode; 
	}
	else
	{
		SLTNode* prev = *pphead;//用来保存pos前面的那个节点
		while (prev->next != pos) 
		{
			prev = prev->next; 
		}
		prev->next = newnode; 
		newnode->next = pos;  
	}
}

在pos节点以后插入x

因为是在 pos 之后插入,所以自动的 pos 极为插入位置的前一个节点,于是只要把新的节点插入到 pos 节点之后即可。

注意:

  • pos 不能为 NULL,因为 NULL 后面无法再插入节点
// 在pos节点以后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySListNode(x); 
	newnode->next = pos->next; 
	pos->next = newnode;
}

4.2 单链表的删除

♨️尾删

删除节点操作,首先要确保链表不为空。其次,就是当链表中只有一个节点时,删除链表后,链表变成空,此时链表的头需要置为空。

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead&&*pphead);//没有节点不需要删除
	//1.一个节点的删除
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else//2.多个节点的删除
	{
	    //第一种删除方式
		//SLTNode* prev = NULL; 
		//SLTNode* tail = *pphead; 
		//while (tail->next != NULL)
		//{
		//	prev = tail;
		//	tail = tail->next;
		//}
		//free(tail);
		//tail = NULL;//tail是局部变量,不置空也可以,因为出了作用域tail就销毁了
		//prev->next = NULL;
		
		//第二种删除方式	
		SLTNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next; 
		}
		free(tail->next);
		tail->next = NULL; 
	}
}

♨️头删

头删也需要确保链表不为空,其次就是正常的删除节点的操作,当只有一个节点时,仍然满足逻辑。

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

♨️删除pos节点

  1. 首先,确保链表不为空
  2. 其次,判断链表的头节点是否为要删除的节点:
    • 如果是,则将链表的头节点置为此时头节点的 next
    • 如果不是,则用一个 prev 节点来保存 pos 节点的前一个节点的位置,通过 while 循环找到此节点。
// 删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	if (*pphead == pos)
	{
		//头删
		*pphead = pos->next;
		free(pos);
		pos = NULL;
	}
	else
	{
		SLTNode* prev = *pphead;//用来保存pos节点之前的节点地址
		while (prev->next != pos) 
		{
			prev = prev->next; 
		}
		prev->next = pos->next; 
		free(pos); 
		pos = NULL; 
	}
}

♨️删除pos的后一个节点

个操作比较简单,因为要删除的节点的位置的前一个节点即为 pos,所以在进行删除的时候只需要保存要删除的节点,再让 pos
next 指向 posnextnext ,最后 free 掉保存的这个要删除的节点即可。

注意:

  • 这里需要确保 pos 节点不为空且 posnext 也不为空。
// 删除pos的后一个节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* cur = pos->next;
	pos->next = cur->next;
	free(cur);
}

4.3 单链表的其他接口实现

打印单链表

此算法只需要遍历一遍单链表,并将每个节点中存储的值打印出来即可,在循环外最好加上打印 NULL 的语句,便于直观的看出链表的结构。

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

查找

同理,只需要遍历链表即可。

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{//空链表可以查找
	while (phead)
	{
		if (phead->data == x)
		{
			return phead; 
		}
		phead = phead->next; 
	}
	return NULL;//链表为空找不到
}

单链表的销毁

  1. 首先,通过assert(pphead);确认传入的链表头指针不为空,如果为空则程序会中断。
  2. 然后,定义两个指针prevcur,其中prev指向当前节点的前一个节点,cur指向当前节点。初始时,prev为空,cur指向链表的头节点。
  3. 使用一个循环遍历链表。在循环中,首先将prev指向当前节点,然后将cur指向下一个节点。然后释放prev指向的节点的内存空间,并将prev置为空。
  4. 循环直到cur为空,即遍历完整个链表。此时,prev将会指向链表的最后一个节点。
  5. 最后,将链表的头指针设置为空,即*pphead = NULL;,表示链表已经销毁。然后输出"单链表销毁成功"。

整个过程就是从链表的头部开始,逐个释放节点的内存空间,直到链表的尾部,完成整个链表的销毁。

//单链表的销毁
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* prev = NULL;
	SLTNode* cur = *pphead;
	while (cur)
	{
		prev = cur;
		cur = cur->next;
		free(prev);
		prev = NULL;
	}
	*pphead = NULL;
	printf("单链表销毁成功\n");
}

五、源码

5.1 SList.h 文件

#pragma once

#include
#include
#include

typedef int SLTDataType;//定义数据类型的别名,方便后续存储元素类型改变的修改

//定义结点类型 
typedef struct SListNode
{
	SLTDataType data;//每个节点存放一个数据元素
	struct SListNode* next;//结构体指针,指向下一个节点
}SLTNode;

// 动态申请一个节点
SLTNode* BuySListNode(SLTDataType x);

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

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);

//尾删
void SLTPopBack(SLTNode** pphead);

//头删
void SLTPopFront(SLTNode** pphead);

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


//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

// 在pos节点之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

// 在pos节点以后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

// 删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);

// 删除pos的后一个节点
void SLTEraseAfter(SLTNode* pos);

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

5.2 SList.c 文件

#define _CRT_SECURE_NO_WARNINGS 1

#include "SList.h"

// 动态申请一个节点空间
SLTNode* BuySListNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x; 
	newnode->next = NULL;
	return newnode;
}

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySListNode(x);
	if (*pphead == NULL) 
	{
		*pphead = newnode;//改变了结构体指针,所以传二级指针
	}
	else
	{
		SLTNode* tail = *pphead; 
		while (tail->next) 
		{ 
			tail = tail->next; 
		}
		tail->next = newnode;//改变的结构体,用结构体的指针即可
	}
}

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySListNode(x);
	
	newnode->next = *pphead; 
	*pphead = newnode; 
}


//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead&&*pphead);//没有节点不需要删除
	//1.一个节点的删除
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else//2.多个节点的删除
	{
		//第一种删除方式
		//SLTNode* prev = NULL; 
		//SLTNode* tail = *pphead; 
		//while (tail->next != NULL)
		//{
		//	prev = tail;
		//	tail = tail->next;
		//}
		//free(tail);
		//tail = NULL;//tail是局部变量,不置空也可以,因为出了作用域tail就销毁了
		//prev->next = NULL;

		//第二种删除方式
		SLTNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next; 
		}
		free(tail->next);
		tail->next = NULL; 
	}
}

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

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

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{//空链表可以查找
	while (phead)
	{
		if (phead->data == x)
		{
			return phead; 
		}
		phead = phead->next; 
	}
	return NULL;//链表为空找不到
}

// 在pos节点之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	//要么都是空,要么都不是空 --- 当链表不为空时,pos不能为空
	//当链表为空,则pos必须为空
	//总结;pos一定要为有效节点,NULL不是有效节点
	//assert((!pos && !(*pphead)) || (pos && *pphead));

	assert(pos);//pos不为空
	assert(*pphead);//链表不为空
	SLTNode* newnode = BuySListNode(x); 

	if (*pphead == pos)
	{
		//头插
		newnode->next = *pphead;   
		*pphead = newnode;
	}
	else
	{
		SLTNode* prev = *pphead;//用来保存pos前面的那个节点
		while (prev->next != pos) 
		{
			prev = prev->next;
		}
 		prev->next = newnode;
		newnode->next = pos;  
	}
}

// 在pos节点以后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySListNode(x); 
	newnode->next = pos->next; 
	pos->next = newnode;
}

// 删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	if (*pphead == pos)
	{
		//头删
		*pphead = pos->next;
		free(pos);
		pos = NULL;
	}
	else
	{
		SLTNode* prev = *pphead;//用来保存pos节点之前的节点地址
		while (prev->next != pos) 
		{
			prev = prev->next; 
		}
		prev->next = pos->next; 
		free(pos); 
		pos = NULL; 
	}
}

// 删除pos的后一个节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* cur = pos->next;
	pos->next = cur->next;
	free(cur);
}

//单链表的销毁
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* prev = NULL;
	SLTNode* cur = *pphead;
	while (cur)
	{
		prev = cur;
		cur = cur->next;
		free(prev);
		prev = NULL;
	}
	*pphead = NULL;
	printf("单链表销毁成功\n");
}

5.3 Test.h 文件

#define _CRT_SECURE_NO_WARNINGS 1

#include "SList.h"


void TestSLT1()
{
	SLTNode* plist = NULL; 
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushFront(&plist, 1); 
	SLTPrint(plist);

	SLTNode* pos = SLTFind(plist, 3);//找到3的节点并返回其地址 
	SLTInsert(&plist, pos, 30);//在3前面插入30 
	SLTPrint(plist); 
	 
	SLTInsertAfter(pos, 40);//在3后面插入40 
	SLTPrint(plist); 

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

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

	SLTEraseAfter(pos);//删除3后面的那个节点 
	SLTPrint(plist);  

	SLTErase(&plist, pos);//删除节点3 
	SLTPrint(plist); 

	SLTDestroy(&plist);//销毁单链表 
	SLTPrint(plist); 
}

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

手撕无头单链表_第3张图片


今天的内容就到这里了,后续我会给大家带来一些链表的 oj 题目,大家敬请期待

你可能感兴趣的:(数据结构探索,数据结构,链表,单链表)