单链表各种操作 C语言 注释详细

  • Post author:
  • Post category:其他

功能:(9个) 创建 测长 查找 遍历 插入 删除 逆置  查找单链表中间元素 判断单链表是否有环

平台:Windows VS13

代码:

#include <stdio.h>
#include <stdlib.h>//使用malloc free要用到该头文件//【拓展3】函数malloc() free() 在stdlib.h中声明,而C++中的new delete是关键字
#include <stdbool.h>
typedef struct _tag_node
{
	int m_data;//数据域 - 节点内容 //【拓展1】这里的int 可换为 #define TYPE int 【疑问1】但是这样在遍历单链表 用printf函数的时候,如何打印TYPE类型?printf("%TYPE",...)???
								   //【拓展2】结构体首元素是指针,以实现底层算法与上层应用的分离-企业级代码规范 
	struct _tag_node *m_next;//指针域 - 指向下一个节点
}node;

/*
	功能     :尾插法实现单链表
	变量解释 :head头指针、p指针 指向开辟的节点、q辅助指针 指向p - 作为尾节点存在
	代码描述 :第一次while循环,头指针head指向第一个p节点,q指向p;
			   然后每while循环一次开辟一个p节点,辅助指针q指向p;
			   最后退出的时候q作为尾节点指向NULL,实现含有头节点的单链表。
			   该单链表特点 - 有头节点(头节点数据域为空,只存在指针域,且没有前驱),尾节点指向NULL;- 即“头节点没前驱,尾节点没后继。”
	返回值   :返回链表的头指针
*/
node *create_list()
{
	int num = 0;//链表中节点的个数
	int data;
	node *head, *p, *q;
	q = NULL;//【注意1】指针q需要初始化一下 不然编译器报错
	head = (node *)malloc(sizeof(node));//头节点

	while (1)
	{
		printf("Please input the data: ");
		scanf_s("%d", &data);
	
		if (data == 0)//如果输入的节点内容为0  则表示退出该子函数
			break;
		p = (node *)malloc(sizeof(node));//指针p指向开辟的节点
		p->m_data = data;

		if (++num == 1)//如果链表里只有一个节点
		{
			head->m_next = p;//头指针指向该唯一的节点
		}
		else
		{
			q->m_next = p;
		}
		q = p;//q指向当前节点 说白了即q就是p,while循环每次p都会重新在堆区开辟空间,而q每次都是被覆盖的,即q指向当前节点(尾差的p)
	}
	q->m_next = NULL;//节点数据域给0退出while循环,则尾节点q指向NULL
	return head;
}

/*
	功能     :测长
	函参解释 :*head 传址 - 传入的是指向链表首地址的头指针
	代码描述 :头节点数据域为空,所以从头节点的下一个节点开始算起
	返回值   :返回单链表长度,为int类型
*/
int length_list(node *head)
{
	int len = 0;
	node *p = NULL;
	p = head->m_next;

	while (p != NULL)
	{
		len++;
		p = p->m_next;
	}
	return len;
}

/*
	功能     :遍历
	函参解释 :*head 传址 - 传入的是指向链表首地址的头指针
	代码描述 :与测长类似,但要多做个空链表的容错
	返回值   :空
*/
void printf_list(node *head)
{
	int index = 0;//用来表示第几个节点
	node *p = NULL;
	p = head->m_next;//【注意3】传指针的话,子函数内最好再声明一个指针,用来指向传递进来的指针,这样做既能操作原数据还能避免对原指针的指向破坏。

	if (NULL == p)//常量写前面的好处:如果手误写成 NULL = p 编译器能检测出来,反之p = null则不行 //【注意2】NULL是指针常量
	{
		printf("The list is empty !\n");
		return;
	}

	while (p != NULL)
	{
		index++;
		printf("printf_list the %dth node data = %d\n", index, p->m_data);
		p = p->m_next;
	}
}

/*
	功能     :查询 
	函参解释 :*head - 传址 - 传入的是指向链表首地址的头指针,pos - 查询单链表中第pos个节点
	代码描述 :(1)多了几个容错,pos<0、pos==0、空链表即 p == NULL 的情况;
			   (2)--pos先减再while()循环判断,p = head->m_next; p即第一个节点,所以pos==1的时候while()不会循环;
			   因此while()从pos==2开始循环,找到第2个节点
	返回值   :指向第pos个节点的指针,类型是 node* 
*/
node *search_node(node *head, int pos)
{
	node *p = head->m_next;

	if (0 > pos)
	{
		printf("incorrect position to search node !\n");
		return NULL;
	}
	if (0 == pos)
	{
		return head;
	}
	if (NULL == p)
	{
		printf("Link is empty !\n");
		return NULL;
	}

	while (--pos)
	{
		if (NULL == (p = p->m_next))
		{
			printf("incorrect position to search node!\n");
			break;
		}
	}
	return p;
}

/*
	功能     :插入
	函参解释 :*head - 传址 - 传入的是指向链表首地址的头指针,pos - 查询单链表中第pos个节点,data是输入的节点的数据
	代码描述 :插入分两种 - 头插、尾插,该函数采用尾差,即插入到第pos个节点的后面。
	返回值   :头指针
*/
node *insert_node(node *head, int pos, int data)
{
	//node *q = head;

	node *item = NULL;
	node *p = NULL;

	item = (node *)malloc(sizeof(node));
	item->m_data = data;
	
	//if (0 == pos)
	//{
	//	item->m_next = q->m_next;//【划重点 造成野指针场景!】【注意5】尾插 新节点插入时 指针指向的顺序一定不能错!!! 
	//							 //书上例子就错了  它没写这句话!!!顺序错误,在插入pos=0 data=5之后,调用测长函数的时候,发生指针错误。
	//							 //因为不写这句话新节点就没有后继,测长函数循环里的p = p->m_next;  新节点没有后继导致野指针!!!
	//	head->m_next = item;	 //...多想了 其实这个情况根本不用考虑 下面的代码对pos==0也符合
	//	return head;
	//}

	p = search_node(head, pos);//找到需要插入的节点位置,并尾插在其后
	if (NULL != p)
	{
		item->m_next = p->m_next;
		p->m_next = item;
	}
	return head;
}

/*
	功能     :删除
	函参解释 :*head - 传址 - 传入的是指向链表首地址的头指针,pos - 查询单链表中第pos个节点
	代码描述 :找到所删除的第pos-1个节点,让该节点的next指针指向第pos个节点的下一个即可删除第pos个节点
	返回值   :只有头指针的空链表时,返回空指针,否则返回头指针
*/
node *delete_node(node *head, int pos)
{
	node *item = NULL;
	node *p = head->m_next;
	if (NULL == p)
	{
		printf("The linklist is empty!\n");
		return NULL;
	}

	p = search_node(head, pos - 1);
	if ((NULL != p) && (NULL != p->m_next))
	{
		item = p->m_next;
		p->m_next = item->m_next;
		free(item);
	}
	return head;
}

/*
	功能     :单链表逆置
	函参解释 :*head - 传址 - 传入的是指向链表首地址的头指针
	代码描述 :遍历链表,利用辅助指针,存储遍历过程中 当前指针指向的下一个元素。然后,将当前节点的指针域反转之后,利用已经存储的指针继续往后遍历
	返回值   :头指针(头节点地址没变 变的是头节点的next指针)
*/
node *reverse(node *head)
{
	node *p, *q, *r;

	if (NULL == head->m_next)
	{
		return head;
	}

	p = head->m_next;
	q = p->m_next;
	p->m_next = NULL;

	while (NULL != q)
	{
		r = q->m_next;//【注意7】链表节点 指针域 怎么保存 指向谁

		q->m_next = p;//
		p = q;
		q = r;
	}
	head->m_next = p;
	return head;
}

/*
	功能     :查找单链表的中间元素
	函参解释 :*head - 传址 - 传入的是指向链表首地址的头指针
	代码描述 :测链表长,表长对2取余得到mid node位置
	返回值   :返回单链表的中间元素的位置
*/
int search_mid_node(node *head)
{
	int ret_lem = 0, mid = 0;
	ret_lem = length_list(head);

	if (0 != (ret_lem % 2))
		mid = (ret_lem + 1) / 2;
	else
		mid = ret_lem / 2;

	return mid;
}

/*
	功能     :判断链表是否存在环
	函参解释 :*head - 传址 - 传入的是指向链表首地址的头指针  **loop_start - 用来存储回环的起始地址 - 主调用函数传参时,仅需传递&loop_start 主调用函数(node *loop_start 一级指针)
	代码描述 :每次循环p1向前走一步,p2向前走两步,直到p2碰到NULL指针或p1==p2的情况时,循环结束。若p1==p2存在环,否则不存在。
	返回值   :bool - false true
*/
bool IsLoop(node *head, node **loop_start)
{
	node *p1 = head, *p2 = head;

	if ((NULL == head) || (NULL == head->m_next))
		return false;

	do
	{
		p1 = p1->m_next;//走一步
		p2 = p2->m_next->m_next;//走两步
	} while ( p2 && p2->m_next && (p1 != p2) );//分别对应 只有一个节点 有两个节点 存在环

	if (p1 == p2)
	{
		*loop_start = p1;//经调试 二级指针loop_start 存储 一级指针p1的地址 //这样主调函数就可以 通过传址(指针)来间接操作环的起始节点p1
						 //二级指针指向一级指针 *二级指针 即取二级指针的值 //https://blog.csdn.net/ye1223/article/details/79674975 二级指针讲得不错
						 //指针只能存储地址,所以主调函数要对 传的一级指针变量 取址//这里的环起始点是人为规定的 - 存储环的起始点(node 1 2 3 4 如果人造环是改变node4为指向node2,则node4为环起始节点)
		return true;
	}
	else
		return false;
}

void main()
{
	int mid = 0;//用于接收寻找出的单链表中间元素的位置
	int pos = 1;//用于单链表查询 pos=0表示查询头节点 pos=1表示查询第一个节点 //含头节点的单链表 查询的时候正好pos=2和第2个在一般认识上匹配
	int len = 0;//用于接收length_list()的返回值
	//node *t;//用于删除所有链表里的节点
	node *ret_head = NULL;//用于接收create_list()返回的指针
	node *ret_search = NULL;//用于接收printf_list()返回的指针
	node *ret_insert = NULL;//用于接收insert_node()返回的指针

	//【1】根据输入的data创建单链表
	ret_head = create_list();

	//【2】测链表长度
	printf("length_list = %d\n", length_list(ret_head));

	//【3】遍历整个单链表
	printf_list(ret_head);//【注意4】比如这里,联系一下【注意3】就可以知道,如果ret_head传参的时候,length_list()子函数直接使用ret_head会导致ret_head指向改变,这样printf_list()就会得到错误的头指针!

	//【4】查找单链表中的某个节点
	ret_search = search_node(ret_head, pos);
	printf("search_node pos = %d  data = %d\n", pos, ret_search->m_data);

	//【5】插入节点
	insert_node(ret_head, 4, 5);
	printf("\n");
	printf("length_list = %d\n", length_list(ret_head));
	printf_list(ret_head);

	//【6】单链表逆置
	reverse(ret_head);
	printf("\n");
	printf_list(ret_head);

	//【7】查找到单链表的中间元素并打印
	mid = search_mid_node(ret_head);
	ret_search = search_node(ret_head, mid);
	printf("\n");
	printf("search_mid_node mid = %d  data = %d\n", mid, ret_search->m_data);

	//【8】判断链表是否存在环
	//先人为构造一个环 - 所谓环,起实质是改变某一节点指针域指向,使其指向前面的节点。
	//这里构造环:节点node 1 2 3 4,node4指向node2 数据依次是:头节点 5 4 3 2 1 
	//即 输入4个数据 依次为1 2 3 4 再输入0结束链表创建 然后再上面的插入和逆置操作之后 变成 头节点 5 4 3 2 1
	//node **loop_start = NULL;//用来存储环的起始节点的地址 //【注意8】这里对二级指针和传递指针的概念的理解错误!!!
	node *loop_start = NULL;//用来存储环的开始节点//环是没有开始和结束的准确节点的,这里先当于规定的一个开始节点吧(即快慢指针的相遇节点)
	node *loop_node_start = search_node(ret_head, 5);
	node *node = search_node(ret_head, 1);//node1
	loop_node_start->m_next = node->m_next;
	//printf("\n");
	//printf_list(ret_head);//经验证 存在环 //最方便的方式:在IsLoop(ret_head, loop_start);处打断点,进去看链表的存储结构

	bool ret_IsLoop = IsLoop(ret_head, &loop_start);//【注意9】这里传递的是 &loop_start 而不是二级指针**loop_start
	printf("\n");
	printf("loop_start data = %d\n", loop_start->m_data);//IsLoop()子函数已经实现对环起始节点p1的指向,那么直接对主函数的结构体指针操作即可
	
	//想要恢复单链表 只需要将尾节点指针域重新指向NULL即可
	ret_search = search_node(ret_head, 5);
	ret_search->m_next = NULL;
	printf("\n");
	printf_list(ret_head);

	//【9】删除全部节点
	len = length_list(ret_head);
	while (len)
	{
		delete_node(ret_head, len);
		len = len - 1;
	}
	printf("\n");
	printf("length_list = %d\n", len);
	printf_list(ret_head);

	//删除时 出的问题!
	//释放堆区所有开辟的空间 - 即删除所有节点 - 有malloc必有free 以防导致野指针
	//t = ret_head;
	//while (NULL != t)
	//{
	//	t = t->m_next;
	//	
	//	//free(t);//【注意6】这里就犯傻了,free()函数可以释放指向 开辟的堆区空间的 指针,但是节点的释放有delete_node()函数啊
	//			  //这里free()导致的问题是:释放掉第一个指针后,释放第二个时出现野指针
	//	printf("\n");
	//	printf("length_list = %d\n", length_list(ret_head));
	//	printf_list(ret_head);
	//}

	system("pause");
}

结果:


版权声明:本文为qq_35479392原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。