C语言中单链表的基本操作(创建、销毁、增删查改等)

 更新时间:2023年02月05日 10:20:16   作者:安河桥畔  
这篇文章主要介绍了C语言中单链表的基本操作(创建、销毁、增删查改等),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

链表分类

链表主要有下面三种分类方法:

  • 单向或者双向
  • 带头或者不带头
  • 循环或者非循环综合来看链表有八种类型,本文主要针对的是不带头节点的非循环单链表。

单链表的介绍

typedef struct SListNode
{
	DataType data;//数据域
	struct SListNode *next;//结构体指针,指向下一个节点
}SListNode;//类型别名

如图

链表的每一个节点由数据域和指针域构成,数据域存放数据,指针域中的指针指向下一个节点。

plist表示链表的指针,指向链表的第一个节点,最后一个节点的指针为空。

单链表的基本操作

创建

创建单链表有几点需注意:

  • 链表与顺序表的区别是,顺序表是物理空间上连续的,而链表只在逻辑上连续,所以链表申请空间时是使用一个申请一个,顺序表则是一次申请一段空间,空间不足时进行扩容。
  • 如果在栈上申请空间,在函数调用结束后会释放,所以需要在堆区申请空间。
  • 每次申请一个节点都要存入数据,所以链表总是满的,而顺序表则可能有一段空间没有利用。
  • 函数的返回值是指向节点的结构体类型的指针
SListNode* BuySListNode(DataType x)
{
	SListNode* plist = (SListNode*)malloc(sizeof(SListNode));
	if (plist == NULL)
	{
		return NULL;//判断是否申请成功
	}
	plist->data = x;
	plist->next = NULL;
	return plist;
}

打印

遍历链表,进行打印

void SListPrint(SListNode* plist)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d-->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

尾插

尾插的操作步骤:

  • 若链表为空,*pplist指向新插入的节点
  • 链表不为空则遍历链表,找到最后一个节点
  • 令最后一个节点的next指向新插入的节点
  • 新插入的节点next指向NULL

注意事项:

  • 因为插入元素要改变原链表中指针的指向,故传参时要传入二级指针。
  • assert(pplist)是判断链表是否存在,因为pplist是指向链表的指针的地址,若pplist为空,说明链表的地址不存在,即链表不存在;而如果(*pplist)为空,表示的是该链表是空链表。
void SListPushBack(SListNode** pplist, DataType x)
{
	//改变指针指向,参数传二级指针
	assert(pplist);//判断链表是否存在,与链表是否为空不同
	//1.若链表为空,*pplist指向插入的节点
	if (*pplist == NULL)
	{
		*pplist = BuySListNode(x);
	}
	else {
		//2.链表不为空,指针移动到链表最后一个节点,其next指向插入的节点
		SListNode* cur = *pplist;
		while (cur->next)
		{
			cur = cur->next;//cur的next为空时,cur指向最后一个节点
		}
		cur->next = BuySListNode(x);
	}
}

头插

头插操作顺序:

  • 申请一个新节点
  • 新节点的next指向原来的第一个节点,即*pplist
  • 改变*pplist指向,使其指向新增的节点

进行头插时,要注意各个步骤的顺序,如果直接令*pplist指向了新增的的节点,会导致原有的第一个节点无法找;另外,链表为空时的操作方法与链表非空时代码可以合并,不用再分开写各种情况。

void SListPushFront(SListNode** pplist, DataType x)
{
	assert(pplist);
	//if (NULL == *pplist)
	//{
	//	//链表为空
	//	*pplist = BuySListNode(x);
	//}
	//else
	//{
	//	SListNode* temp = *pplist;//temp指向链表原来的第一个节点
	//	*pplist = BuySListNode(x);//plist指向新增的节点
	//	(*pplist)->next = temp;//新增的节点指向原来的第一个节点
	//}
	//上面两种情况代码可以合并
	SListNode* node = BuySListNode(x);//申请一个新节点
	node->next = *pplist;//新增的节点的next指向原来的第一个节点
	*pplist = node;//*pplist指向新增的节点
}

尾删

尾删步骤:

  • 判断链表是否为空或只有一个结点
  • 遍历找到最后一个节点的前驱结点prev
  • 令prev的next指向NULL
  • 释放原来最后一个节点申请的空间

注意事项:

  • 区分链表为空、单个结点、多个结点各种情况
  • 不能找到最后一个节点并将其置空,而是要找到其前驱节点,断开与最后一个节点的连接
  • 删除节点后要释放空间,避免内存泄漏
void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	//1.链表为空
	if (NULL== *pplist)
	{
		return;
	}
	//2.链表只有一个元素
	else if (NULL == (*pplist)->next)
	{
		free(*pplist);
		*pplist = NULL;
	}
	//3.链表有多个元素
	else
	{
		SListNode* prev = NULL; 
		SListNode* cur = *pplist;
		while (cur->next)
		{
			prev = cur;
			cur = cur->next;//循环结束时cur指向最后一个节点
		}
		//cur= NULL;//这里不能写cur=NULL,需要找到cur的前一个节点,将其next置空\
		否则前一个结点的next依然指向原来的最后一个节点
		prev->next = NULL;//prev成为最后一个节点
		free(cur);//释放原来最后一个节点的空间
	}

头删

头删的操作步骤:

  • 保存第一个节点的指针信息
  • 令*pplist指向第二个节点
  • 释放原来的第一个节点的空间

同样的,头删也要注意保存原来第一个节点的位置,否则*pplist指向改变后,原来的第一个节点就找不到了,会无法释放空间造成内存泄漏。

void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	//1.单链表为空
	if (NULL == *pplist)
	{
		return;
	}
	2.单链表有一个节点
	//else if (NULL == (*pplist)->next)
	//{
	//	*pplist = NULL;//删除后链表为空
	//}
	3.单链表有多个节点
	//else
	//{
	//*pplist= (*pplist)->next;
	//}
	
	//两种情况可以合并,只有一个节点时,*pplist的next为空
	else
	{
		SListNode* delNode = *pplist;
		*pplist = delNode->next;
		free(delNode);//释放删除节点的空间
	}
}

查找

用于查找某一元素是否存在于链表中,若存在则返回其第一次出现在链表中的位置,不存在则返回NULL。

遍历时注意循环条件。

SListNode* SListFind(SListNode* plist, DataType x)
{
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return	NULL;
}

任意位置插入

pos节点后插入的步骤:

  • 申请一个新的节点
  • 新增节点的next指向原pos的next
  • pos的next指向新增的节点

注意事项

  • 任意位置的插入操作只能在给定节点的后面插入,前面的节点无法同通过给出的节点找到。
  • 要注意插入的操作顺序,否则原来链表pos后的节点可能会找不到
void SListInsertAfter(SListNode* pos, DataType x)
{
	assert(pos);//指针合法性校验
	SListNode* newNode = BuySListNode(x);
	newNode->next = pos->next;
	pos->next = newNode;
}

任意位置删除

与任意位置的插入相同,只能删除给定节点pos后面的节点

void SListDeleteAfter(SListNode* pos)
{
	assert(pos);
	 //1.链表有一个节点
	if (NULL == pos->next)
	{
		return;
	}
	//2.链表有多个节点
	else
	{
		SListNode* temp = pos->next;
		pos->next = temp->next;
		free(temp);
	}
}

销毁

链表的销毁,遍历一遍,逐个释放空间

void SListDestroy(SListNode** pplist)
{
	assert(pplist);//链表是否存在
	//1.链表为空
	if (NULL == *pplist)
	{
		return;
	}
	else
	{
		SListNode* cur = NULL;
		while (*pplist)
		{
			cur = *pplist;
			*pplist = (*pplist)->next;
			free(cur);
		}
	}
}

完整代码

work.h

头文件包含,函数声明,定义结构体

#pragma once
#include<stdio.h>
#include<Windows.h> 
#include<assert.h>
#include<assert.h>

typedef int DataType;
typedef struct SListNode
{
	DataType data;//数据域
	struct SListNode *next;//结构体指针,指向下一个节点
}SListNode;//类型别名

//函数声明
SListNode* BuySListNode(DataType x);//节点申请
void SListPrint(SListNode* pst);//单链表遍历打印
void SListPushBack(SListNode** pplist, DataType x);//单链表尾插
void SListPushFront(SListNode** pplist, DataType x);//单链表头插
void SListPopBack(SListNode** pplist);//单链表尾删
void SListPopFront(SListNode** pplist);//单链表头删
SListNode* SListFind(SListNode* plist, DataType x);//单链表查找
void SListInsertAfter(SListNode* pos, DataType x);//pos后位置的插入
void SListDeleteAfter(SListNode* pos);//pos后位置的删除
void SListDestroy(SListNode** pplist);//释放链表空间

work.c

各操作函数的具体实现

#include"work.h"

//链表与顺序表的区别是,顺序表是物理空间上连续的
//而链表只在逻辑上连续,所以链表申请空间时是使用一个申请一个
//顺序表则是一次申请一段空间
SListNode* BuySListNode(DataType x)
{
	//若在栈申请内存函数调用结束后会释放,所以使用动态申请
	SListNode* plist = (SListNode*)malloc(sizeof(SListNode));
	if (plist == NULL)
	{
		return NULL;//判断是否申请成功
	}
	plist->data = x;
	plist->next = NULL;
	return plist;
}

void SListPrint(SListNode* plist)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d-->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

//尾插法
void SListPushBack(SListNode** pplist, DataType x)
{
	//改变指针指向,参数传二级指针
	assert(pplist);//判断链表是否存在,与链表是否为空不同

	//1.若链表为空,*pplist指向插入的节点
	if (*pplist == NULL)
	{
		*pplist = BuySListNode(x);
	}
	else {
		//2.链表不为空,指针移动到链表最后一个节点,其next指向插入的节点
		SListNode* cur = *pplist;
		while (cur->next)
		{
			cur = cur->next;//cur的next为空时,cur指向最后一个节点
		}
		cur->next = BuySListNode(x);
	}
}

//头插法
void SListPushFront(SListNode** pplist, DataType x)
{
	assert(pplist);
	//if (NULL == *pplist)
	//{
	//	//链表为空
	//	*pplist = BuySListNode(x);
	//}
	//else
	//{
	//	SListNode* temp = *pplist;//temp指向链表原来的第一个节点
	//	*pplist = BuySListNode(x);//plist指向新增的节点
	//	(*pplist)->next = temp;//新增的节点指向原来的第一个节点
	//}

	//链表为空的情况可以和不为空合并
	SListNode* node = BuySListNode(x);//申请一个新节点
	node->next = *pplist;//新增的节点的next指向原来的第一个节点
	*pplist = node;//*pplist指向新增的节点

}

//尾删法?
void SListPopBack(SListNode** pplist)
{
	assert(pplist);

	//1.链表为空
	if (NULL== *pplist)
	{
		return;
	}
	//2.链表只有一个元素
	else if (NULL == (*pplist)->next)
	{
		free(*pplist);
		*pplist = NULL;
	}
	//3.链表有多个元素
	else
	{
		SListNode* prev = NULL; 
		SListNode* cur = *pplist;
		while (cur->next)
		{
			prev = cur;
			cur = cur->next;//循环结束时cur指向最后一个节点
		}
		//cur= NULL;//这里不能写cur=NULL,需要找到cur的前一个节点,将其next置空\
		否则前一个结点的next依然指向原来的最后一个节点
		prev->next = NULL;//prev最后一个节点
		free(cur);//释放原来最后一个节点的空间
	}
}

//头删
void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	//1.单链表为空
	if (NULL == *pplist)
	{
		return;
	}
	2.单链表有一个节点
	//else if (NULL == (*pplist)->next)
	//{
	//	*pplist = NULL;//删除后链表为空
	//}
	3.单链表有多个节点
	//else
	//{
	//*pplist= (*pplist)->next;
	//}
	
	//两种情况可以合并,只有一个节点时,*pplist的next为空
	else
	{
		SListNode* delNode = *pplist;
		*pplist = delNode->next;
		free(delNode);//释放删除节点的空间
	}
}

//单链表查找
SListNode* SListFind(SListNode* plist, DataType x)
{
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return	NULL;
	
}
//任意位置的插入
//只能在pos的后面插入
void SListInsertAfter(SListNode* pos, DataType x)
{
	assert(pos);//指针合法性校验
	SListNode* newNode = BuySListNode(x);
	newNode->next = pos->next;
	pos->next = newNode;
}
		

//任意位置的删除
//只能删除给定的pos后面的节点
void SListDeleteAfter(SListNode* pos)
{
	assert(pos);
	 //1.链表有一个节点
	if (NULL == pos->next)
	{
		return;
	}
	//2.链表有多个节点
	else
	{
		SListNode* temp = pos->next;
		pos->next = temp->next;
		free(temp);
	}
}

// 链表空间释放
void SListDestroy(SListNode** pplist)
{
	assert(pplist);//链表是否存在
	//1.链表为空
	if (NULL == *pplist)
	{
		return;
	}
	else
	{
		SListNode* cur = NULL;
		while (*pplist)
		{
			cur = *pplist;
			*pplist = (*pplist)->next;
			free(cur);
		}
	}
}

main.c

程序入口,测试用例

#include"work.h"
void Test()
{
	SListNode* node = NULL;//定义一个结构体指针
	//尾插法插入五个节点
	SListPushBack(&node, 1);
	SListPushBack(&node, 2);
	SListPushBack(&node, 3);
	SListPushBack(&node, 4);
	SListPushBack(&node, 5);
	SListPrint(node);//遍历打印
	SListPushFront(&node, 0);//头插一个节点
	SListPrint(node);//遍历打印
	SListPopBack(&node);//尾删最后一个节点
	SListPrint(node);//遍历打印
	SListPopFront(&node);//头删第一个节点
	SListPrint(node);//遍历打印
	printf("%p\n",  SListFind(node, 4));//查找3在链表中的位置
	printf("%p\n",  SListFind(node, 99));//查找99在链表中的位置
	SListInsertAfter(SListFind(node, 4), 99);//4后面插入一个节点99
	SListPrint(node);//遍历打印
	SListDeleteAfter(SListFind(node, 4));//删除4的下一个节点
	SListPrint(node);//遍历打印
}

int main()
{
	Test();
	system("pause");
	return 0;
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • C语言中的线程信号控制详解

    C语言中的线程信号控制详解

    这篇文章主要通过一些示例为大家详细介绍一下C语言中的线程信号控制,文中的示例代码讲解详细,对我们深入了解C语言有一定的帮助,感兴趣的可以学习一下
    2023-02-02
  • C++阻止类被实例化详解

    C++阻止类被实例化详解

    下面小编就为大家带来一篇浅谈C++阻止类被实例化详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2021-09-09
  • C语言深入探索递归的特点

    C语言深入探索递归的特点

    程序调⽤⾃⾝的编程技巧称为递归 recursion)函数⾃⼰调⽤⾃⼰就是递归,你也可以理解成是⼀种嵌套结构,但递归分为俩部分,第⼀是“递”,进⼊嵌套结构。第⼆是”归“,最终会⼀步⼀步返回。第⼀次接触递归都会很懵,慢慢理解这个过程就明⽩了
    2022-06-06
  • C语言菜鸟基础教程之条件判断

    C语言菜鸟基础教程之条件判断

    本文给大家简单介绍了下C语言中的条件判断语句的语法和用法示例,非常简洁实用,有需要的小伙伴可以参考下
    2017-10-10
  • Qt 信号自定义槽函数的实现

    Qt 信号自定义槽函数的实现

    Qt中实现自定义信号与槽函数,信号用于发送并触发槽函数,槽函数则是具体的功能实现,本文就详细的介绍一下如何使用,感兴趣的可以了解一下
    2021-11-11
  • 深入了解C语言中的动态内存分配

    深入了解C语言中的动态内存分配

    这篇文章主要为大家详细介绍了C语言中的动态内存分配,文中的示例代码讲解详细,对我们学习C语言有一定的帮助,需要的可以参考一下
    2022-06-06
  • C++ 命名空间避免命名冲突的实现

    C++ 命名空间避免命名冲突的实现

    命名空间是C++中用来避免命名冲突的一种机制,本文主要介绍了C++ 命名空间避免命名冲突的实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03
  • C语言模拟实现atoi函数的实例详解

    C语言模拟实现atoi函数的实例详解

    这篇文章主要介绍了C语言模拟实现atoi函数的实例详解的相关资料,atoi函数,主要功能是将一个字符串转变为整数,这里就实现这样的函数,需要的朋友可以参考下
    2017-08-08
  • C++多线程编程超详解

    C++多线程编程超详解

    本文给大家介绍的是C++多线程编程,由于C++本身没有多线程机制,在windows下我们使用调用SDK win32 api来实现,示例都很简单,讲解的也很详细,推荐给大家
    2021-09-09
  • C++获取类的成员函数的函数指针详解及实例代码

    C++获取类的成员函数的函数指针详解及实例代码

    这篇文章主要介绍了C++获取类的成员函数的函数指针详解及实例代码的相关资料,需要的朋友可以参考下
    2017-02-02

最新评论