## 数据结构之单向链表的基本操作详细总结 爆肝总结超详细万字长文C语言版

先吐槽一下:前段时间在好兄弟的帮助下,敲完了学生成绩管理系统,当时感觉链表真滴好难啊。
不过敲完这个管理系统,也对链表入了个门。近几天前开始学数据结构,有了前面的基础感觉链表理解起来也不是很难。
我总结了下我目前接触到的关于链表的基本操作,主要的操作其实也就“增删查改”。以后肯定还会再更新的,感觉链表挺灵活的,操作起来会挺多变的。
建议看的每个函数时时候和总体的代码合起来看,这样就不会对那些参数感到混乱,总体的代码放在文末
为了小伙伴们能快速get到每个函数的功能,下面的那些函数名特意起得那么长
OK,那就先上一张自制的思维导图(用Word做的,有点难看,将就着看吧)
在这里插入图片描述

(看到上面的思维导图你应该有一眼就能看出如何去处理的操作吧,我猜你肯定也有一时半会想不出的,可以点击目录直达不会的地方哦。毕竟这是长长长长文,慢慢看过去肯定会看烦了。)
OK,少说废话,直接开始。

一、 建立链表

首先我们先定义下结点(数据域就整这两个简单的吧)

//结点的定义
typedef struct node
{
    char name[10];//姓名
    char num[10];//序号
    struct Node *next;
}Node; 

Node *Head= NULL;//在main函数外,全局变量,代表头指针,不是头结点哈
Node *Rear=NULL;//在main函数外,全局变量,代表尾指针,不是尾节点哈

就我个人而言,我喜欢利用全局变量来建立一个头指针,然后在给链表增加信息时在头结点上慢慢延伸出去。这样就建立起了链表。不过是在增加信息的时候建立的而已,这里先做个铺垫。

题外话本来这里我想写关于链表的初始化的,但我感觉会让很多小伙伴看懵了(其实我也有点懵),因为那要用到二级指针了。其实就我个人而言,我很少对链表进行那样的初始化。所以也就没写(主要是怕写错了,误导了大家),等我真正理解了二级指针初始化链表在更新一下,总感觉少了二级指针初始化链表是不完美的,大家等我更新吧

二、 插入

插入操作这里就是增加链表的结点嘛,情况也比较多,我先把我遇到的情况按下面例子一一阐述。

(1)头插法

用头插法建成的链表,输出时刚好和输入顺序相反,下面用图例形象的说明我所遇到头插法的不同情况。你们看的时候可能会疑惑,那就是为何空链表时我这里为何没有头结点,其实啊,这与我上面定义的全局变量有关,再翻上去看看吧,哈哈哈。
要注意的是头插法的两种情况:空链表和非空链表
OK,话不多说,先来张自制图
在这里插入图片描述
在这里插入图片描述
因为我没在图上画上操作代码,所以看图的时候建议和下面的代码一起看。

下面由上图来编写头插法代码

//头插法函数
void InsertFromHead()
{
	//先赋值 
		char name[10];
		char num[10];
		printf("Please input one name:\n");
		scanf("%s",name);
		printf("Please input one num:\n");
		scanf("%s",num);
	Node *p;
	p = (Node*)malloc(sizeof(Node));
	strcpy(p->name,name);
	strcpy(p->num,num);
	p->next = NULL;//防止指针乱指
	
	//头插法的两种情况,对应了上图的两种情况
		if (NULL == Head)//空链表 
		{
			Head = p;//在这里头指针成了头结点
			Head->next = NULL;
		}	
		else 
		{
			//新节点的下一个指向头 
			p->next = Head;
			//头向前移动一个 
			Head = p;
			
		}	
	
}

我写的代码里面也有注释,别忘了看呀,这里确实有点难理解,不过看我画的图也不会那么难理解的。

(2)尾插法

其实尾插法我觉得是最简单的增加链表的方法了,这里我觉得大家都能顾名思义了,不过呢,为了让大家更好理解(嘿嘿,其实是方便我以后复习),我决定还是画图来说明下。和头插法一样有两种情况。
OK,话不多说,先来张自制图
在这里插入图片描述
上面这张图是空表的情况下,进行尾插入。其实可以类比头插法的空表情况。结合代码看会更加容易理解,这画图的不太好操作,望见谅没把操作代码画上去。
再来一张不是空表情况下的尾插法图示
在这里插入图片描述
上面这张图试着标了下操作序号,以及关键性的代码。结合下面的代码看这张图更容易理解。

//尾插法函数
void InsertFromRear()
{
	char name[10];
	char num[10];
	printf("Please input one name:\n");
	scanf("%s",name);
	printf("Please input one num:\n");
	scanf("%s",num);
	
	Node *p;
	p = (Node*)malloc(sizeof(Node));
	strcpy(p->data,data);
	strcpy(p->num,num);
	
	//尾插法
	if(NULL == Head)//空表情况下
	{
		Head = p;
		Rear = p;
	}
	else//不是空表时尾插
	{
		Rear->next = p;
		Rear = p;
	}
	Rear->next = NULL;	
 } 

(3)中间插入

这里中间插入,我们要做到就是先找到我们要插入的位置。所以可以先去看看下面的“查找”再来看这里。

(3.1)指定结点前插入

看这个前建议先理解“在指定结点后插入”,因为我接下来要操作的就是,先将新结点接到指定结点后面,然后将他们的数据域的值互换即可达到在“指定节点前插入”的效果。有些小伙伴可能想不太通这里,欢迎私信我,我教你啊,哈哈哈。
OK,话不多说,上代码

//在指定结点前插入 
void InsertBefore(Node *p)//这里的参数是从查找函数中得来的,具体看合并后的代码
{
	//先赋值 
		char name[10];
		char num[10];
		printf("Please input one name:\n");
		scanf("%s",name);
		printf("Please input one num:\n");
		scanf("%s",num);
	//创建新结点信息 
		Node *pNew,*pt;
		pNew = (Node*)malloc(sizeof(Node));
		strcpy(pNew->name,name);
		strcpy(pNew->num,num);
		pNew->next = NULL;
	//先插入到指定节点后面 
			if(Rear == p)//当指定结点是尾结点的时候 
		{
			Rear->next = pNew;
			Rear = pNew;
		}
		else
		{
			pNew->next = p->next;//先连后断 
			p->next = pNew;
		} 
	//再交换数据域中的数据 
		//	pt = (Node*)malloc(sizeof(Node));
	//下面这里特意这样写,直观地感受交换数据	
		strcpy(	pt->name , p->name);		strcpy(	pt->num , p->num);	
		strcpy(p->name , pNew->name);		strcpy(p->num , pNew->num);	
		strcpy(pNew->name , pt->name);		strcpy(pNew->num , pt->num);	
	
}

其实和在指定位置后插入是差不多的,就是在后面多了个数据交换。理解了在指定结点后入,理解这个那肯定是水到渠成。
图解和在指定结点后插入是一样的,我就不画图了。

(3.2)指定结点后插入

首先我们得先找到我们所需操作的结点位置,所以先查找一下,返回一个指定结点的地址。再将这个地址传进插入函数中。OK,先上代码看看
这是在main函数中的操作

//main函数中的大致操作如下
Node *pmain;//用来保存指定节点的地址
pmain = FindListByNum();//当然也可以使用Node *FindListByName()和Node *FindListByRank(),
//在合并后的代码那里我会处理下这里的选择何种查找方法,小伙伴们也可以去看看

InsertAfter(pmain);

这是插入函数

//在指定位置后插入 
void InsertAfter(Node *p)
{
	
		//先赋值 
		char name[10];
		char num[10];
		printf("Please input one name:\n");
		scanf("%s",name);
		printf("Please input one num:\n");
		scanf("%s",num);
	//创建新结点信息 
		Node *pNew;
		pNew = (Node*)malloc(sizeof(Node));
		strcpy(pNew->name,name);
		strcpy(pNew->num,num);
		pNew->next = NULL;
	//分析不同情况	
		if(Rear == p)//当指定结点是尾结点的时候 
		{
			Rear->next = pNew;
			Rear = pNew;
		}
		else
		{
			pNew->next = p->next;//先连后断 
			p->next = pNew;
		} 
		
	//这里可能有小伙伴会有疑惑了,为什么不用判断空表的情况,其实这就要回到我们的查找了,其实在那里就已经将空表的情况给分析到了 
}

再来张图帮助你们理解下
其实这种情况和尾插法一样
在这里插入图片描述

三、 输出链表

这个真没啥可说的,就是遍历链表,然后一个一个输出就OK啦。
上代码

//输出链表
void PrintList()
{
	Node *p = Head;//从头结点开始
	if (NULL == p)//先判断是不是空链表
		printf("The list is empty!\n"); //空链表时的提示信息 
	while (p != NULL)
	{
		printf("name is %s,num is %s\n",p->name,p->num);
		p =	p->next;//结点下移一个,遍历链表
	}
}

四、修改

这个又要用到查找函数了,我们先用查找函数找到要修改的结点,然后将他的地址传给修改函数就可以修改了。
OK,话不多说,上代码

//修改指定结点
void ModifyNode(Node *p)//这里的参数是从查找函数中得来的,具体看合并后的代码
{
	//输入想要改成的数据,重新赋给指定结点 
		char name[10];
		char num[10];
		printf("Please input one name:\n");
		scanf("%s",name);
		printf("Please input one num:\n");
		scanf("%s",num);
	strcpy(p->name,name);
	strcpy(p->num,num);
} 

五、查找

我认为查找是个辅助函数,单独用没什么灵魂,一般和中间插入函数、修改函数、删除函数等一起使用才更能凸显这个函数的用处。我们可以按值查找,也可以按序号查找。

(1)按序号查找

下面这里我就先以按序号查找为例给出代码。
注意哈:我这里说的按序号查找不是按我前面定义的num来查找哈,而是第几个的意思。
直接上代码

 //按序号查找某个节点 
 Node *FindListByRank()//用Node *是因为这里要返回一个结构体指针 ,所以是用Node 
 {
 	int i=0;//作为计数器 
 	int n;//需要查找的序号,就第n个结点信息 
 	Node *p;
 	printf("Please input the rank of the node you want to find:\n");
 	scanf("%d",&n);
 	
 	p = Head;
 	while(p!=NULL) 
 	{
 		i++;
 		if(i==n)
 		{
 			return p;
		}	
 		else
 		{
 			p = p->next;
		}
		
	 }
 	
 	printf("No information about the node!\n");
 	return NULL;	//如果没查到,就返回空值,作为main函数中判断结束的条件 
  } 
 

(2)按数据查找

再来按num值查找的和按name值查找的代码

//按num值查找某个节点
 Node  *FindListByNum()//用Node *是因为这里要返回一个结构体指针 ,所以是用Node 
 {
 	Node *p;
 	char num[10];
 	printf("Please input the num of the node you want to find!\n");
 	scanf("%s",num);
 	p = Head;
 	while (p!=NULL)
 	{	
	 	if(0 == strcmp(p->num,num))
 		{
 			return p;
		}
		p = p->next;
	 }
	printf("No information about the node!\n");
	return NULL;	//如果没查到,就返回空值,作为main函数中判断结束的条件 
 }
 
 //按name值查找某个节点
 Node *FindListByName()
  {
 	Node *p;
 	char name[10];
 	printf("Please input the name of the node you want to find!\n");
 	scanf("%s",name);
 	p = Head;
 	while (p!=NULL)
 	{	
	 	if(0 == strcmp(p->name,name)) 
 		{
 			return p;
		}
		p = p->next;
	 }
	printf("No information about the node!\n");
	return NULL;	//如果没查到,就返回空值,作为main函数中判断结束的条件 
 }
 

关于查找就这些东西了,这应该是链表操作里面最简单的了。无非就是设置好判断条件,找到了就返回值,没找到就往下走直到走到尽头。

六、删除

思维导图里关于删除的情况是不是贼多,怂了没?是不是不想学了,哈哈哈,这里刚开始确实会有点难理解。
我看网上好多讲解就是给段代码然后就不管了,那真的不行,因为情况太多了,太难理解了。为了让小伙伴们理解这里我还是画下图来说明。

(1)从头开始删

其实啊,不管是从头开始删,还是从尾开始删,其目的都是将链表清空,释放内存。所以在程序中两个随便用一个就行,看个人喜好吧。
OK,先上张图理解理解
在这里插入图片描述
就是将头结点一步一步往下移,然后释放掉之前的就OK
话不多说,来段代码体会一下

//从头开始删除
void DeletHead()
{	
//先考虑是不是空表
	if(NULL == Head)
		{
			printf("This list is empty!\n"); 
			return;
		}
	Node *p;//用来做中转站
	while(NULL != Head)
	{
		p = Head;//让头结点的内容先给他 
		Head = Head->next;//头结点下移
		free(p); 
	}
	printf("This list has been clean!\n");
} 

耐心看我写的代码,我都给了敲详细的注释

(2)从尾开始删

我不喜欢从尾部开始删除,因为比从头开始删要麻烦,这个我就当是拓展思路了。这个呢其实和从头开始是差不多的,只不过是将尾结点慢慢往前移。
但是我们的操作就和从头开始删的不一样了。因为我们直接把尾结点移到他前面那个节点上去,貌似很难做到。
这里真的有点难,我想了好久
由于情况复杂所以这里我们要迂回一下,通过另一种方法来达到和尾结点前移一样的效果,
那就是先看图,再看代码

在这里插入图片描述

在这里插入图片描述
看懂我图里所表达的处理方法后,再看我写的代码,注释超多,绝对看得懂
话不多说,上代码

//从尾开始删除 
void DeletRear() 
{
	Node *p;//用来做中转站
	p = Head;//从头开始遍历 
	
	//先考虑是不是空表
	if(NULL == Head)
		{
			printf("This list is empty!\n"); 
			return;
		}	
		
	//下面这个循坏是 链表不是只有单个结点的情况 ,也就是说走完下面这个循坏就还剩一个结点 
	while (NULL != Head && NULL!=p->next) 
	{	
	 
		while (p->next->next != NULL)//这里判断的其实是:p是不是尾结点的前一个结点 
		{
			p = p->next;//p不是尾结点的前一个结点,那就往下走 
		}
		free(p->next);//p->next就是尾结点,这里释放的就是尾结点 
		p->next = NULL;//因为释放了原先的尾结点后,那么现在在p就成了尾结点,所以给p->next赋个NULL 
		p = Head;//再从头开始遍历 
		
	}
	//走完上面的循坏后,还剩一个结点 
	// 下面是只有单个节点的情况 
	if (Head->next == NULL) 
	{
		free(Head);	
	}
	printf("This list has been clean!\n");	
}

题外话:我感觉这是基本操作里最难的,而且最不常用的了。纯当开拓思维了

(3)删除指定节点

(3.1)删除指定节点:前驱结点位置已知

这种情况是常规情况,也是我们在操作时所会采取的的方法。要删除一个指定节点,我们都是先找到指定结点前的结点。假设指定结点的前驱结点为P,那么就可以直接令P->next指向指定结点的后继结点,语句就是 p->next = p->next->next,再释放指定结点就完成删除啦。
但是真滴就这么简单吗?其实这里的情况也有几种。我在这里调试了好久,情况远不是各位想的那么简单,这里真的有点难搞,耐心点, 待我细细道来。
回想下“从尾开始删除”中是不是也出现了p->next->next,所以我们就可以拿来类比下。当只有一个或者两个结点时,p->next->next这个玩意就不太好使了,所以我们要分情况考虑。
看图更容易理解
在这里插入图片描述
上图这两种简单的情况,再去看下代码就更容易理解。
继续看图,下面的图适用于链表有三个及三个以上结点的情况
在这里插入图片描述
上面这张图就是最常规的情况,那就是待删除的结点是中间结点时
下面是待删结点是头结点和尾结点时候的情况
在这里插入图片描述
在这里插入图片描述
以上就是我所想到的所有情况了,看图貌似很简单的样子,对吧?但是代码有点绕哦
ok,话不多说,上代码

//删除指定结点
void DeletList(Node *p)
{
	//这里不用判断是不是空表,根据查找函数可知,能进到删除函数里的肯定不是空表 
	//只有一个结点时 
	if (NULL == Head->next)
	{
		free(Head);
		Head = NULL;
		return;
	} 
	//只有两个结点时
	else if ( Head->next == Rear)
	{
			if (Head == p)    //指定结点是头结点时 
			{
				free(Head);
				Head = Rear;
			}
			else 		      //指定结点是尾结点时 
			{
				free(Rear);
				Rear = Head;
				Rear->next = NULL;
			}
	} 
	
	//三个及三个以上的结点
	else
	{
		Node *p1 = Head;
	
		//判断指定结点是不是头节点
		if (Head == p)
		{
			Head = Head->next;
			free(p1);
			p1 = NULL;//free只是将p1指向的内容释放了,但p1这个指针仍然存在,为了不让他变成野指针,所以使他等于NULL
			return ;			
		} 
		
		while (p1 != NULL)
		{	
				if (p1->next == p)	//找到前驱结点后,就可以开始删除操作了 
 				{
				 
					if( p == Rear)//指定结点为尾结点的情况 
					{
						free(p);
						p = NULL;//free只是将p指向的内容释放了,但p这个指针仍然存在,为了不让他变成野指针,所以使他等于NULL
						Rear = p1;//使前驱结点变成尾结点 
						Rear->next = NULL; 
						return ;
					}
					else 		//结点为中间的结点时 
					{
						//先记住要删除的节点 
							Node *p2 = p1->next;														
							p1->next = p1->next->next;
							free(p2);
							p2 = NULL; 
							return ;
					}		
				}
			p1 = p1->next;//当没找到指定结点的前驱结点时就往下继续找	
		}
	} 
	 
} 

(3.2)删除指定节点:只知指定结点位置

其实这种情况是我在刚接触链表删除时,脑子没想到利用前一结点去删除。真正在链表删除的时候也不会不知道前驱结点的位置,所以这个大家就当是拓展下思维吧。
这种情况的处理方法和在指定节点前插入新结点有点类似,要来点迂回战术。
那就是既然我知道指定结点的位置,那我就可以把指定结点的后继结点给删掉,只不过先将后继结点的数据先赋给指定结点,不就成了吗。
OK,看图说话
在这里插入图片描述
这里,我就不写代码了。因为在实际操作中根本不会这样去做,纯当拓展思维咯。

(4)按值删除

这个按值删除其实就是删除指定节点的一种变形,理解了删除指定节点,看懂这个按值删除那肯定是轻轻松松。

(4.1)按值删除:只删遇到的第一个

这里我们可以调用下之前的删除函数,就函数里面调用函数,超级便捷,我把详细的解释都放到代码注释里去了,这里就不啰嗦了。
话不多说,上代码

//删除指定值,只遇到的第一个结点 
void DeletDataNode()
{
	char num[10];//这里设num为指定值,也可以换成数据域其他数据 
	printf("Please input the num you want to delet:\n");
	scanf("%s",num);
	
	Node *p = Head;//建立新节点,让他从头开始去遍历 
	while (p!=NULL)
 	{	
	 	if(0 == strcmp(p->num,num))
 		{
 				DeletList(p);//直接调用我们上面的删除函数 
				return;
		}
		p = p->next;//没找到就继续往下遍历 
	 }
 	printf("The num is not exist!\n");//循坏结束了,还没找到的就输出一个提示信息
} 

(4.2)按值删除:是这个值的全删掉

这个就是在上面的代码上把循坏里的return给去掉了,目的就是让他走完整个循坏,将所有的指定值都找出来,并且删除。代码我就不上了哈,但是在合并代码的那里会有。

写在最后的吐槽:为了使这篇文章更加严谨,看了很多文章,恶补了好多细节知识。我觉得我的逻辑思维在写文章时会慢慢变严谨。比如之前我老是忘了考虑空表的情况,现在第一反应就是先考虑他。 其实刚开始我画的思维导图并没有这么多,但是在写的时候,脑子里会多想一下,就像分类讨论一样,慢慢滴,情况就变多了。
然后特感谢我的好兄弟每次都那么耐心地给我解答疑惑,学习上有一挚友,真的是件很幸运的事。
由于是初学者,尽管写的时候严谨再严谨,查阅再查阅,但是文中肯定还是会存在不严谨的知识漏洞,望小伙伴们多多包涵,欢迎私信和我交流呀

为了不使这篇文章看上去太冗长,所以合并后的代码我放到了另一篇文章。点合并代码查看。
再来几个疑惑解答
Q1:为什么我的函数几乎没有参数传递?
A1:把参数放函数内,便于小伙伴们读懂代码

***(转载请注明出处,谢谢。更新ing…***)

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值