规范的链表写法之有空头链表

链表这个东西不是很简单,多次重复学习,看视频敲代码才终于变成自己的知识。

本次讲解的是单向链表,只能通过后面节点找到前面节点,无法通过前面节点找到后面节点。

后续会对双向链表,双向循环链表进行讲解。

Ⅰ.无空头的链表

链 表 添 加 { 头 添 加 + 头 删 除 应 用 场 景 − − − 栈 头 添 加 + 尾 删 除 应 用 场 景 − − − 队 列 尾 添 加 + 头 删 除 应 用 场 景 − − − 队 列 链表添加\left\{ \begin{array}{rcl} 头添加+头删除 & &应用场景---栈 \\ 头添加+尾删除& &应用场景---队列\\ 尾添加+头删除& &应用场景---队列 \end{array} \right. +++

1.添加节点,尾添加

#include<stdio.h>
#include<stdlib.h>
//有空头
//节点结构体
struct Node
{
	int a;
	struct Node* pNext;
};
//一个节点记着头(用来遍历链表),一个记着尾,头尾指针都为空,说明链表里没有节点
struct Node* g_pHead = NULL;
struct Node* g_pEnd = NULL;
//创建链表。在链表中增加一个数据
//尾添加
void AddListToTail(int a)
{
	//创建一个节点
	//在vs里malloc会自动进行数据类型转换,但是在别的编译器里,不写强制类型转换,该malloc会报错。
	struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
	pTemp->a = a;
	pTemp->pNext = NULL;//这里很重要,保证最后一个节点指向空,遍历时作为遍历退出条件
	//将新节点链接到链表上----分两种情况:
	//1.链表里还没有节点,链表添加第一个节点时,头节点和尾节点指向同一位置
	if (NULL == g_pHead || NULL == g_pEnd)//其实这里只需要一个判断就够了,不需要 || 因为其中有一个为空就说明该链表为空 
	{
		g_pHead= pTemp;
		g_pEnd = pTemp;
	}
	//2.链表从第二个节点开始,头节点就不需要再变化了,只需要尾节点添加就行了
	else
	{
		//由于指针的性质,两个指针指向同一位置时,通过其中一个指针就可以改变所指位置的值,自然指向该位置的另一个指针所指的值就变了。
		//这里就是通过g_pEnd->pNext = pTemp;语句改变g_pEnd所指节点成员pNext的值,而g_pEnd是单独的一个节点,它便按照规则也就是下一个节点,指向尾节点。
		g_pEnd->pNext = pTemp;//前一个节点的next连接到新的节点
		g_pEnd = pTemp;
	}
	//由于g_pEnd = pTemp;在两个分支里都有,所以可以写成如下形式
	/*
	if (NULL == g_pHead || NULL == g_pEnd)//其实这里只需要一个判断就够了,不需要 || 因为其中有一个为空就说明该链表为空
	{
		g_pEnd = pTemp;
	}
	else
	{
		g_pEnd->pNext = pTemp;
	}
	g_pEnd = pTemp;
	*/
}

(1)划重点

根据指针的性质,两个指针指向同一位置时,通过其中一个指针就可以改变所指位置的值,自然指向该位置的另一个指针所指的值就变了。
链表尾添加就是通过这一性质完成的。

g_pEnd->pNext = pTemp;
g_pEnd = pTemp;

(2)测试

int main()
{
	AddListToTail(1);
	AddListToTail(2);
	AddListToTail(3);
	AddListToTail(4);
	g_pHead;
	system("pause");
	return 0;
}

打断点查看链表添加情况,可以看到数据逐一添加到头节点后面。最先添加的数据在前面,最后添加的数据在后面,和队列的性质一样。
在这里插入图片描述

(3)使用链表添加数组数据

int a[10] = { 0,1,2,3,4,5,6,7,8,9 };
for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++)
{
	AddListToTail(a[i]);
}

2.添加数据,头添加

//创建链表。在链表中增加一个数据
//头添加
void AddListToHead(int a)
{
	//创建一个节点
	struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
	//节点数据赋值
	pTemp->a = a;
	pTemp->pNext = NULL;//这里很重要,保证第一个点指向空,遍历时作为遍历退出条件
	//将新健节点链接到链表上----分两种情况:
	//1.链表里还没有节点,链表添加第一个节点时,头节点和尾节点指向同一位置
	if (NULL == g_pHead || NULL == g_pEnd)
	{
		g_pEnd = pTemp;
		g_pHead = pTemp;
	}
	//2.链表从第二个节点开始,尾节点就不需要再变化了,只需要新节点pTemp指向头节点,头节点再更新指向新节点
	else
	{
		//注意,下面这样写是错误的,这样写是假的头添加,相当于只是给头尾指针换了个名字
		//尾添加是尾节点的next指向新节点,头添加是新节点的next指向头
		//这是头添加和尾添加最大的区别
		/*
		g_pHead->pNext = pTemp;
		g_pHead = pTemp;
		*/
		//正确写法
		//新节点的next指向头
		pTemp->pNext = g_pHead;
		g_pHead = pTemp;
	}
	//由于g_pHead = pTemp;在两个分支里都有,所以可以写成如下形式
	/*
	if (NULL == g_pHead || NULL == g_pEnd)
	{
		g_pEnd = pTemp;
	}
	else
	{
		pTemp->pNext = g_pHead;
	}
	g_pHead = pTemp;
	*/
}

(1)划重点

注意,下面这样写是错误的,这样写是假的头添加,相当于只是给头尾指针换了个名字。当添加第二个及其后续节点时,尾添加是尾节点的next指向新节点,头添加是新节点的next指向头,这是头添加和尾添加最大的区别。
错误写法

g_pHead->pNext = pTemp;
g_pHead = pTemp;

正确写法

pTemp->pNext = g_pHead;
g_pHead = pTemp;

(2)测试

int a[10] = { 0,1,2,3,4,5,6,7,8,9 };
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		AddListToHead(a[i]);
	}

打断点查看链表添加情况,可以看到数据逐一添加到链表中,最先添加的数据在底部,而最后添加的数据在顶部,和栈的性质一样。
在这里插入图片描述

3.遍历链表

遍历查询数据

void ScanList()
{
	//操作链表时,头指针不能动,头指针动了,链表的头就找不到了。	
	//建立一个新指针用来遍历
	struct Node* pTemp = g_pHead;
	while (pTemp != NULL)//非空链表不遍历
	{
		printf("%d\n",pTemp->a);
		pTemp = pTemp->pNext;
	}
}

注意这里的判断条件也可以写成while (pTemp),不会出错,最好写成while (pTemp != NULL),一般只有bool型变量这样写—while(b) 以及while(! b)

(1)测试

在这里插入图片描述

(2)划重点

节点数据赋值前的预操作

pTemp->a = a;
pTemp->pNext = NULL;//这里赋值为NULL很重要
  • 对于尾添加,保证最后一个节点指向空,遍历时作为遍历退出条件

  • 对于头添加,保证第一个点指向空,遍历时作为遍历退出条件

如果没有进行上述操作,会出现如下错误,出现了野指针,这是不被允许的。
在这里插入图片描述

4.查询指定节点

(1)代码

struct Node* SelectNode(int a)
{
	struct Node* pTemp = g_pHead;
	while (pTemp != NULL)
	{
		if (a == pTemp->a)
		{
			return  pTemp;
		}
		pTemp = pTemp->pNext;
	}
	return NULL;
}

(2)测试

该节点存在
在这里插入图片描述
该节点不存在
在这里插入图片描述

5.链表释放

(1)基础知识

本例子使用了

struct Node* pTemp = g_pHead;

与之前的

struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));

相比较,提出了本问题:什么时候定义指针需要用malloc分配内存,什么时候不需要。弄清楚这一点对彻底理解清楚程序很有必要。首先需要了解一下几点:

  • C、C++中,内存模型分为栈和堆,这两种模型内存的方式是不同的。
  • 在栈中存放的变量是由系统自动管理的,在函数结束后系统会自动释放,不需要人为的任何操作。
  • 在堆中存放的是用户自己管理的内存,手动分配的,malloc建立,系统不会在函数体执行结束后自动释放,需要用户手动通过free函数释放。
  • 当需要对分配的空间进行自己的管理和释放时,需要实用malloc,或者分配的空间再函数结束后还需要存在。

下面列举一个学习链表初期容易犯的错误:
在释放某一节点前,需要再定义一个节点(不需要malloc)记住要删除的节点,然后pTemp指向pTemp的下一节点,否则会出错,出现访问错误的问题。
在这里插入图片描述

(2)链表释放代码

//链表清空
//数组释放,只需要释放首地址,该数组就全部释放掉了
//链表释放需要循环遍历释放每一个节点
void FreeList()
{
	//记录头
	struct Node* pTemp = g_pHead;
	while (pTemp != NULL)//非空链表不遍历
	{
		struct Node* pt = pTemp;//定义一个节点记住pTemp,待删除
		pTemp = pTemp->pNext;//pTemp指向下一个节点
		free(pt);
	}
	//头尾清空,方便下一次创建
	g_pHead = NULL;
	g_pEnd = NULL;
}

6.插入节点

(1)代码

void AddListRand(int index, int a)
{
	//链表为空
	if (NULL == g_pHead)
	{
		printf("链表没有指定节点");
		return;
	}
	//找位置
	struct Node* pt = SelectNode(index);
	if (NULL == pt)
	{
		printf("没有指定节点\n");
		return;
	}
	//有此节点
	//给a创建节点
	struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
	//节点成员赋值
	pTemp->a = a;
	pTemp->pNext = NULL;
	//链接到链表上
	if (pt == g_pEnd)
	{
		g_pEnd->pNext = pTemp;
		g_pEnd = pTemp;
	}
	else
	{
		//先连
		pTemp->pNext = pt->pNext;
		//后断
		pt->pNext = pTemp;
	}
}

(2)测试

在这里插入图片描述

7.删除节点

1.头删除

代码

//头删除
void DeleteListHead()
{
	//链表检测
	if (NULL == g_pHead)
	{
		printf("链表为NULL,无需释放\n");
		return;
	}
	//记住旧的头
	struct Node* pt = g_pHead;
	//头的下一个节点变成新的头
	g_pHead = g_pHead->pNext;
	//释放旧的头
	free(pt);
}

测试
在这里插入图片描述

2.尾删除

划重点
要先找到尾节点的前一个节点,否则直接删除了尾节点,g_pEnd就无法再容易的找到旧的尾节点的前一个节点。
代码

void DeleteListTail()
{
	//链表检测
	if (NULL == g_pHead)
	{
		printf("链表为NULL,无需释放\n");
		return;
	}
	//链表不为空
	//链表有几个节点,一个节点和多个节点的情况不一样
	//链表只有一个节点
	if (g_pHead == g_pEnd)
	{
		free(g_pEnd);
		//头尾一定要赋空值
		g_pHead = NULL;
		g_pEnd = NULL;
	}
	else
	{
		//找到尾巴前一个节点
		struct Node* pTemp = g_pHead;
		while (pTemp->pNext != g_pEnd)
		{
			pTemp = pTemp->pNext;
		}
		//找到了,删除尾巴
		//释放尾节点
		free(g_pEnd);//释放g_pEnd所指区域的空间
		//尾巴前移
		g_pEnd = pTemp;//g_pEnd作为一个单独的变量还是可以继续使用的
		//尾巴的next指针赋空值NULL
		g_pEnd->pNext = NULL;
	}
}

测试
在这里插入图片描述

3.指定节点删除

注意先判断链表是否为空,还要判断该数据对应的节点是否存在。考虑要周全,要分三种情况,(1)链表有一个节点,(2)两个节点,(3)两个以上节点。两个节点时要判断要删除的节点是头节点还是尾节点;三个以上节点删除时还多一种情况,判断要删除的节点是中间节点的情况。
代码

//指定节点删除
void DeleteListRand(int a)
{
	//链表检测
	if (NULL == g_pHead)
	{
		printf("链表为NULL,无需释放\n");
		return;
	}
	//链表有该元素
	struct Node* pTemp = SelectNode(a);
	if (NULL == pTemp)
	{
		printf("查无此节点\n");
		return;
	}
	//找到了该节点,分为三种情况
	//1.链表中只有一个节点
	if (g_pHead == g_pEnd)
	{
		DeleteListHead();
	}
	//2.链表中有两个节点
	else if (g_pHead->pNext == g_pEnd)
	{
		//分为两种情况
		//1.要删除的是头节点
		if (g_pHead == pTemp)
		{
			DeleteListHead();
		}
		//2.要删除的是尾节点
		else
		{
			DeleteListTail();
		}
	}
	//3.链表中有两个以上的节点
	else
	{
		//分为三种情况
		//1.要删除的是头节点
		if (g_pHead == pTemp)
		{
			DeleteListHead();
		}
		//2.要删除的是尾节点
		else if(g_pEnd == pTemp)
		{
			DeleteListTail();
		}
		//删除除了头尾节点以外的节点
		else
		{
			//找到要删除节点Ptemp的前一个节点
			struct Node* pt = g_pHead;
			while (pt->pNext != pTemp)
			{
				pt = pt->pNext;
			}
			//找到了
			pt->pNext = pTemp->pNext;
			free(pTemp);
		}
	}
}

测试
在这里插入图片描述

Ⅱ.有空头的链表

1.尾添加

(1)代码

#include <stdio.h>
#include <stdlib.h>

//节点结构体
struct Node
{
	int a;
	struct Node* pNext;
};
//头尾指针定义
struct Node* g_pHead = NULL;
struct Node* g_pEnd = NULL;
//尾添加
void AddListTail(int a)
{
	//创建一个节点
	struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
	//节点成员赋值
	pTemp->a = a;
	pTemp->pNext = NULL;
	//链接
	//与无空头的链表相比,不需要再单独考虑头节点了
	g_pEnd->pNext = pTemp;
	g_pEnd = pTemp;
}

(2)测试

int main(void)
{
	//链表空头
	//有空头初始化,也简化了链表操作时对头节点的单独考虑,也就不需要再单独考虑头节点了
	g_pHead = (struct Node*)malloc(sizeof(struct Node));
	g_pHead->pNext = NULL;//空头成员不用管
	g_pEnd = g_pHead;
	
	AddListTail(1);
	AddListTail(2);
	AddListTail(3);
	AddListTail(4);
	return 0;
}

划重点
有空头初始化,也简化了链表操作时对头节点的单独考虑,也就不需要再单独考虑头节点了

g_pHead = (struct Node*)malloc(sizeof(struct Node));
g_pHead->pNext = NULL;//空头成员不用管
g_pEnd = g_pHead;

测试结果
在这里插入图片描述

2.头添加

(1)将空头链表初始化和创建节点进行封装

这样做是为了简化代码,看起来更加简洁

//空头链表初始化
void InitListHead()
{
	//链表空头
	//有空头初始化,也简化了链表操作时对头节点的单独考虑,也就不需要再单独考虑头节点了
	g_pHead = (struct Node*)malloc(sizeof(struct Node));
	g_pHead->pNext = NULL;//空头成员不用管
	g_pEnd = g_pHead;
}
//创建节点
struct Node* CreatNode(int a)
{
	//创建一个节点
	struct Node* pTemp = (struct Node*)malloc(sizeof(struct Node));
	if (NULL == pTemp)
	{
		printf("内存不足\n");
		return NULL;
	}
	//节点成员赋值
	pTemp->a = a;
	pTemp->pNext = NULL;
	return pTemp;
}

(2)头添加代码

//头添加
void AddListHead(int a)
{
	struct Node* pTemp = CreatNode(a);
	if (NULL == pTemp)
	{
		return;
	}
	//链接
	pTemp->pNext = g_pHead->pNext;
	g_pHead->pNext = pTemp;
}

(3)测试

代码

int main(void)
{
	//空头链表初始化
	InitListHead();
	AddListHead(0);
	AddListHead(1);
	AddListHead(2);
	AddListHead(3);
	g_pHead;
	return 0;
}

测试结果
在这里插入图片描述

本内容来源于b站C3程序猿的视频讲解,这是我见过最好的视频讲解

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙叙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值