手把手教你实现一个C++单链表

 更新时间:2022年11月29日 16:31:05   作者:小林同志_____  
链表是一种数据结构,用于数据的存储。这篇文章主要为大家介绍了如何实现一个C++单链表,文中的示例代码讲解详细,感兴趣的小伙伴可以尝试一下

一. 节点声明

链表是一种数据结构,用于数据的存储。

如图,每一个链表节点所在的内存空间是不延续的,因为不是连续存储,所以需要存入下一个节点的地址,这样方便找到下一个数值,而最后一个链表指向的空间为NULL,因此我们可以声明一个结构体,代表一个节点。

typedef int ListDataType;

typedef struct SListNode
{
	ListDataType  data;
	struct SListNode* Next;

}SLNode;

SListNode 是我们的节点结构体,ListDataType是存储数据的类型。

二. 尾插

链表的节点建立好后,接下来我们来进行尾部插入数据。

那么我们只需要找到尾部节点,把尾部节点指向的NULL值置为新节点。新节点指向NULL

因此我们要新建一个节点,然后找到最后一个节点。

void SListPushBack(SLNode** phead, ListDataType x)
{
	//新开辟一个节点
	SLNode* newNode = (SLNode*)malloc(sizeof(SLNode));
	if (newNode == NULL)
	{
		//空间开辟失败
		printf("SListPushBack::newNode");
		exit(-1);
	}

	//找到最后一个节点
	SLNode* cru = *phead;
	//如果cru指向NULL,说明cru是最后一个节点
	while (cru->Next != NULL)
	{
		cru = cru->Next;
	}
	//cru 指向新节点
	cru->Next = newNode; 
	//新节点指向NULL,存储数据x
	newNode->data = x;
	newNode->Next = NULL;

}

但是这种方法,我们需要注意一种情况,那就是如果链表中本身没有节点,那么cru初始就是一个空指针,而循环条件我们对空指针进行了解引用,所以我们需要判断一下。

//尾部数据插入
void SListPushBack(SLNode** phead, ListDataType x)
{
	//新开辟一个节点
	SLNode* newNode = (SLNode*)malloc(sizeof(SLNode));
	if (newNode == NULL)
	{
		//空间开辟失败
		printf("SListPushBack::newNode");
		exit(-1);
	}

	//新节点指向NULL,存储数据x
	newNode->data = x;
	newNode->Next = NULL;


	if (*phead == NULL)
	{
		//当前链表为空,那么我直接让新节点替代phead
		*phead = newNode;
		return;
	}

	//找到最后一个节点
	SLNode* cru = *phead;

	//如果cru指向NULL,说明cru是最后一个节点
	while (cru->Next != NULL)
	{
		cru = cru->Next;
	}
	//cru 指向新节点
	cru->Next = newNode; 
	

}

然后我们测试一下,发现链表已经链接起来了

三. 链表打印

为了方便后面测试,我们决定先实现打印链表的函数。我们只需要依次查找节点指向的元素,直到最后一个指向NULL时,我们停下来。

//链表打印
void SListPrint(SLNode* head)
{
	SLNode* cru = head;
	while (cru)
	{
		printf("%d->",cru->data);
		cru = cru->Next;
	}
	printf("NULL\n");
}

四. 头插

头部插入和尾部插入差不多,我们只需要把新节点指向链表中的第一个节点,然后把头节点换成新节点。

因为我们尾插的时候创建了节点,头插又创建节点,太麻烦了,所以我们把创建节点封装成一个函数。

//创建节点
SLNode* SListCreateNode(ListDataType x)
{
	//新开辟一个节点
	SLNode* newNode = (SLNode*)malloc(sizeof(SLNode));
	if (newNode == NULL)
	{
		//空间开辟失败
		printf("SListPushBack::newNode");
		exit(-1);
	}

	//新节点指向NULL,存储数据x
	newNode->data = x;
	newNode->Next = NULL; 
	return newNode;
}

尾插函数调整

//尾部数据插入
void SListPushBack(SLNode** phead, ListDataType x)
{
	
	SLNode* newNode = SListCreateNode(x);

	if (*phead == NULL)
	{
		//当前链表为空,那么我直接让新节点替代phead
		*phead = newNode;
		return;
	}

	//找到最后一个节点
	SLNode* cru = *phead;

	//如果cru指向NULL,说明cru是最后一个节点
	while (cru->Next != NULL)
	{
		cru = cru->Next;
	}
	//cru 指向新节点
	cru->Next = newNode; 
}

头插函数

//头部数据插入
void SListPushFront(SLNode** phead, ListDataType x)
{
	//新建节点
	SLNode* newNode = SListCreateNode(x);

	//让节点指向头
	newNode->Next = *phead;
	//头节点更替为新节点
	*phead = newNode;
	
}

头插测试

五. 尾删

尾删也就是删除链表中的最后一个节点。那么我们需要找到最后一个节点,把它释放,并且要把它的前一个节点置为空指针,否则它会变成一个野指针。

//尾部数据删除
void SListPopBack(SLNode** phead)
{
	SLNode* cru = *phead; //最后一个节点
	SLNode* prve = NULL; //前一个节点

	//最后一个节点指向空
	while (cru->Next)
	{
		prve = cru;
		cru = cru->Next;
	}
	//cru 为最后一个节点。释放掉
	free(cru);
	//前一个节点指向空
	prve->Next = NULL;

}

但是这个尾删是建立在有数据的情况下的,如果没有数据我们进行此操作,那会造成空指针prve访问,所以我们在前面要断言一下

void SListPopBack(SLNode** phead)
{
	//链表为空无法删除
	assert(*phead);

	SLNode* cru = *phead; //最后一个节点
	SLNode* prve = NULL; //前一个节点

	//最后一个节点指向空
	while (cru->Next)
	{
		prve = cru;
		cru = cru->Next;
	}
	//cru 为最后一个节点。释放掉
	free(cru);
	//前一个节点指向空
	prve->Next = NULL;

}

即使这样,以上代码依然有问题,因为当链表只有一个节点时,并不会进入while循环,也就导致 prve对空指针解引用,所以我们还需要判断一下,如果链表只有一个节点,那么就直接释放头节点。

//尾部数据删除
void SListPopBack(SLNode** phead)
{
	//链表为空无法删除
	assert(*phead);

	//只有一个节点时
	if((*phead)->Next == NULL)
	{ 
		//释放头空间
		free(*phead);
		//置为空指针
		*phead = NULL;
		return;
	}

	SLNode* cru = *phead; //最后一个节点
	SLNode* prve = NULL; //前一个节点

	//找到最后一个节点
	while (cru->Next)
	{
		prve = cru;
		cru = cru->Next;
	}
	//cru 为最后一个节点。释放掉
	free(cru);
	//前一个节点指向空
	prve->Next = NULL;
}

代码测试

六. 头删

尾删都会了,头删还远吗?头删我们只需要把头节点指向的空间释放,然后让头节点的指针指向下一个节点。

当然,如果链表为空,我们是无法操作的,我们也要断言或者if判断一下。

//头部数据删除
void SListPopFront(SLNode** phead)
{
	//断言
	assert(*phead);

	//链表不为空,存储下一个位置的地址
	SLNode* NextNode = (*phead)->Next;
	//释放 *phead 
	free(*phead);
	//存储的节点给*phead
	*phead = NextNode;
}

测试一下代码

七. 查找值

在链表里查找值,我们只需要找节点,如果与找的值相等,就返回当前下标或者节点,这里我们用返回节点演示。

SLNode* SListFindNode(SLNode* head, ListDataType x)
{
	SLNode* cru = head;
	//查找值
	while (cru)
	{
		//如果当前节点等于要查找的值
		if (cru->data == x)
		{
			//返回当前节点,也可以返回下标,把函数返回值改成int
			return cru;
		}
		  //下一个节点
		cru = cru->Next;
	}

	//遍历完没有找到, 返回空指针
	return NULL;
}

看看测试结果

链表里我们插入了三个值,1,2,3。然后找1的值并返回当前节点,最终提示我们找到了。

当然,也可以用返回下标的方式,然后利用下标控制查找次数。

八.指定插入

指定插入,我们这里来演示前插,也就是在一个节点的前面进行插入,我们只需要把这个节点前面的节点指向新节点,然后新节点指向这个节点。

所以我们要先创建一个节点,让被插入节点之前的节点指向该节点,让新节点指向被插入的节点。

//指定插入
void SListInsert(SLNode** phead, SLNode* pos, ListDataType x)
{
	//首先插入之前,我们必须保证被插入的pos节点存在,不然无法插入
	assert(pos);
	
	SLNode* cru = *phead; //用来查找被插入的节点

	//查找pos节点
	while (cru->Next != pos)
	{
		cru = cru->Next;
	}

	//找到后,创建一个新节点
	SLNode* newNode = SListCreateNode(x);
	//新节点指向pos
	newNode->Next = pos;
	//pos的前一个节点指向cru
	cru->Next = newNode;
	

}

此时这个代码仍有问题,因为如果 pos是第一个节点时,cru->next无法判断判断第一个节点,所以我们要加个判断,如果是头节点,直接调用头插函数。

//指定插入
void SListInsert(SLNode** phead, SLNode* pos, ListDataType x)
{
	//首先插入之前,我们必须保证被插入的pos节点存在,不然无法插入
	assert(pos);

	//头节点就是pos
	if (*phead == pos)
	{
		//调用头插
		SListPushFront(phead,x);
		return 0;
	}
	
	SLNode* cru = *phead; //用来查找被插入的节点

	//查找pos节点
	while (cru->Next != pos)
	{
		cru = cru->Next;
	}

	//找到后,创建一个新节点
	SLNode* newNode = SListCreateNode(x);
	//新节点指向pos
	newNode->Next = pos;
	//pos的前一个节点指向cru
	cru->Next = newNode;
}

代码测试

九.指定删除

指定删除和指定插入差不多,我们只需要把被删除节点的前一个节点,指向后一个节点,然后删除节点。如果要删除的是头节点,直接调用头删函数,或者也可以再次写一个头删。

//指定节点删除
void SListEase(SLNode** phead, SLNode* pos)
{
	//pos是空指针,干掉程序
	assert(pos);

	//如果头节点与pos相等,直接调用头删
	if (*phead == pos)
	{
		SListPopFront(phead);
		return;
	}

	//创建一个节点
	SLNode* cru = *phead;  //查找被删除节点的前一个节点
	while (cru->Next != pos)
	{
		cru = cru->Next;
	}
	
	//cru指向 pos 后的位置
	cru->Next = pos->Next;

	//释放pos所在空间
	free(pos);
	pos = NULL;

}

代码测试

十.销毁链表

如果这个链表不想用了,我们想要清空链表。那我们只需要把依次释放内存。

//销毁链表
void SListDestroy(SLNode** phead)
{
	SLNode* cru = NULL;

	while (*phead)
	{
		//存储下一个节点的地址
		cru = (*phead)->Next;+
		//当前地址置空
		*phead = NULL;
		//释放当前空间
		free(*phead);
		//换成下一个节点继续
		*phead = cru;
	}

}

然后我们测试一下,发现所有的节点都置空了

以上代码以上传至git,点这里获取

到此这篇关于手把手教你实现一个C++单链表的文章就介绍到这了,更多相关C++单链表内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C C++算法题解LeetCode1408数组中的字符串匹配

    C C++算法题解LeetCode1408数组中的字符串匹配

    这篇文章主要为大家介绍了C C++算法题解LeetCode1408数组中的字符串匹配示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • C/C++ 活动预处理器详解

    C/C++ 活动预处理器详解

    预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理,预处理的作用就是在代码被编译前对代码做某些替换,这篇文章主要介绍了C/C++ 活动预处理器,需要的朋友可以参考下
    2022-11-11
  • 图解C++的STL之stack和queue,轻松理解数据结构

    图解C++的STL之stack和queue,轻松理解数据结构

    聚焦 C++ 的 STL 中的 stack 和 queue,让数据结构变得简单有趣! 通过图解的方式,我们将轻松理解这两个重要的数据结构,准备好开启 STL 学习之旅了吗?让我们一起探索 stack 和 queue 的奥秘吧!
    2024-03-03
  • C++ Cartographer加载配置文件过程介绍

    C++ Cartographer加载配置文件过程介绍

    这篇文章主要介绍了Cartographer加载配置文件过程,谷歌优秀的激光SLAM开源框架Cartographer算法简单,但是程序部分太多需要学习的地方了,不论是整体框架的结构,还是数据的使用,都是非常优美的
    2023-03-03
  • 浅析C++11新特性的Lambda表达式

    浅析C++11新特性的Lambda表达式

    C++11 新增了很多特性,lambda 表达式是其中之一,本文涉及到C++11这次更新中较为重要的lambda表达式。有需要的朋友们可以参考学习。
    2016-08-08
  • Microsoft Visual C++ 6.0开发环境搭建教程

    Microsoft Visual C++ 6.0开发环境搭建教程

    这篇文章主要为大家详细介绍了Microsoft Visual C++ 6.0开发环境搭建教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-04-04
  • 解析C++中临时对象的产生情况

    解析C++中临时对象的产生情况

    临时对象的产生和销毁都是有成本的,都会影响程序的执行性能和效率,所以如果能了解临时对象产生的原因,就可以提升程序的性能和效率,下面小编就来和大家聊聊临时对象产生的几种情况吧
    2023-06-06
  • visual studio 2019工具里添加开发中命令提示符的方法

    visual studio 2019工具里添加开发中命令提示符的方法

    这篇文章主要介绍了visual studio 2019工具里添加开发中命令提示符的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • 判断给定的图是不是有向无环图实例代码

    判断给定的图是不是有向无环图实例代码

    判断给定的图是不是是有向无环图,方法是应用拓扑排序,代码如下
    2013-05-05
  • Qt连接数据库并实现数据库增删改查的图文教程

    Qt连接数据库并实现数据库增删改查的图文教程

    QT连接数据库是应用开发的常用基础操作,经过实验我总结了一些例程,下面这篇文章主要给大家介绍了关于Qt连接数据库并实现数据库增删改查的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-04-04

最新评论