C提高——链表话题

链表话题

/*
 *运行平台:Visual Studio 2015
 *参考资料:《C Primer plus 第六版》,传智扫地增C提高课程
*/


一、什么是链表

1、介绍

  链表是一种物理存储单元上非连续的存储结构,由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成,节点与节点之间通过指针链接
  每个结点包括两个部分:一部分是存储数据元素的数据域,另一部分是存储下一个结点地址指针域

2、链表元素(针对单链表)

  链表的元素主要包括:头结点(pHead)、当前结点(pCurrent)、前趋结点(pPrior)、后继结点 (pNext)。
对于头结点的话题,有头结点的链表会比没有头结点的链表在以下操作更具有优势:
1、第1个位置的插入删除更加方便
2、统一空表和非空表的处理
具体可以查看这篇博客。

二、单向链表的经典操作

准备工作,创建结点(元素)类型

typedef struct SLIST
{
	int data;
	struct SLIST *pNext;
}SLIST;

1、创建链表

1.1 要求

①、建立带有头结点的单向链表。
②、循环创建结点,结点数据域中的数值从键盘输入,以-1作为输入结束标志。
③、链表的头结点地址由函数值返回。

1.2 编程思路

采用头插法:先建立头结点,后开辟新结点,赋值后前驱结点指向当前结点,重复上述步骤

1.2 程序

SLIST *SList_Creat(void)
{
	SLIST	*pHead, *pM, *pCur;
	int		data;

	//	1、开辟内存,新建头结点
	pHead = (SLIST *)malloc(sizeof(SLIST));
	if (pHead == NULL)
	{
		printf("func SList_Creat() err:-1 (SLIST *)malloc(sizeof(SLIST))\n");
		return NULL;
	}

	pHead->data = 0;
	pHead->pNext = NULL;
	pM = pHead;

	printf("\nPlease enter your data:");
	scanf("%d", &data);

	//	2、根据所给的数据继续开辟结点并赋值,直到为-1
	while (data != -1)
	{
		pCur = (SLIST *)malloc(sizeof(SLIST));
		if (pCur == NULL)
		{
			printf("func SList_Creat() err: (SLIST *)malloc(sizeof(SLIST))\n");
			return NULL;
		}

		pM->pNext = pCur;	//连接当前结点与前驱结点
		pCur->data = data;	//写入数据
		pM = pCur;			//pM指向当前结点的pNext
		pCur->pNext = NULL;

		printf("\nPlease enter your data:");
		scanf("%d", &data);
	}

	//	4、返回头结点地址
	return pHead;
}

2、输出链表

2.1 要求

①、顺序输出单向链表各项结点数据域中的内容。

2.2 编程思路

在这里插入图片描述

在输出链表数据处理的过程中,分为三个状态:初始、遍历和最终状态,这个三个状态都需要一个辅助指针变量pTmp来实现。

  • 初始状态:辅助指针变量指向第一个结点
    pTmp = pHead->pNext
  • 遍历状态:辅助指针变量指向后续结点
    pTmp = pTmp->pNext
  • 最终状态:辅助指针变量指向NULL用于遍历结束

2.3 程序

/* 打印链表 */
int SList_Print(SLIST *pHead)
{
	SLIST	*pTmp = NULL;
	int		ret = 0;

	if (pHead == NULL)
	{
		printf("func SList_Print() err:-1 pHead == NULL\n");
		return -1;
	}

	pTmp = pHead->pNext;

	/* 打印数据 */
	printf("\n链表数据为:\n");
	while (pTmp)
	{
		printf("%d    ", pTmp->data);
		pTmp = pTmp->pNext;
	}

	printf("\n");
	return 0;
}

3、插入结点

3.1 要求

①、在值为data的结点前,插入值为insertdata的结点。
②、若值为data的结点不存在,则插在表尾。

2.2 编程思路

在这里插入图片描述

在插入结点处理的过程中,分为三个状态:初始、遍历和最终状态,这个三个状态都需要两个辅助指针变量pPre(前驱结点)、pCur(当前结点)来实现。

  • 初始状态:当前结点指向第一个结点,前驱结点指向头结点。
    pCur = pHead->pNext;pPre = pHead
  • 遍历状态:两个辅助指针变量向前推进,在当前结点的位置寻找插入点pPre = pCur;pCur = pCur->pNext;
  • 最终状态:当前结点指向NULL用于遍历结束

3.3 程序

/* 插入结点 */
int SList_NodeInsert(SLIST *pHead,int insertdata,int data)
{
	SLIST	*pPre, *pCur,*pM;
	
	if (pHead == NULL)
	{
		printf("func SList_NodeInsert() err:-1 pHead == NULL\n");
		return -1;
	}

	/* 开辟内存,新建插入结点 */
	pM = (SLIST *)malloc(sizeof(SLIST));
	if (pM == NULL)
	{
		printf("func SList_NodeInsert() err:-2 (SLIST *)malloc(sizeof(SLIST))\n");
		return -2;
	}
	pM->data = insertdata;
	pM->pNext = NULL;

	/* 初始状态 */
	pPre = pHead;
	pCur = pHead->pNext;

	/* 遍历寻找 */
	while (pCur)
	{
		if (pCur->data == data)
		{
			break;
		}
		pPre = pCur;
		pCur = pCur->pNext;
	}

	/* 插入结点(兼容遍历无此结点的情况) */
	pM->pNext = pCur;
	pPre->pNext = pM;

	return 0;
}

4、删除结点

2.1 要求

①、删除值为data的结点

2.2 编程思路

在这里插入图片描述

在删除结点处理的过程中,分为三个状态:初始、遍历和最终状态,这个三个状态都需要个辅助指针变量pPre(前驱结点)、pCur(当前结点)来实现。

  • 初始状态:当前结点指向第一个结点,前驱结点指向头结点
    pCur = pHead->pNext;pPre = pHead
  • 遍历状态:两个辅助指针变量向前推进,在当前结点的位置寻找删除点pPre = pCur;pCur = pCur->pNext;
  • 最终状态:当前结点指向NULL用于遍历结束

删除结点的步骤:
①、利用辅助指针变量存储下一个结点的地址pTmp=pCur->pNext
②、释放当前结点free(pCur)
③、前驱节点指向下一个结点pPre->pNext=pTmp

4.3 程序

/* 删除结点 */
int SList_NodeDel(SLIST *pHead,int data)
{
	SLIST	*pPre, *pCur, *pTmp;

	if (pHead == NULL)
	{
		printf("func SList_NodeDel() err:-1 pHead == NULL\n");
		return -1;
	}

	/* 初始状态 */
	pPre = pHead;
	pCur = pHead->pNext;

	/* 遍历寻找 */
	while (pCur)
	{
		if (pCur->data == data)
		{
			break;
		}
		pPre = pCur;
		pCur = pCur->pNext;
	}

	/* 最终状态 */
	if (pCur == NULL)
	{
		printf("遍历无此结点\n");
		return -1;
	}

	pTmp = pCur->pNext;
	free(pCur);
	pPre->pNext = pTmp;
	
	return 0;
}

5、逆置链表

5.1 要求

①、对建立的链表进行逆序操作

5.2 编程思路

在这里插入图片描述

采用原地逆置,分为三个状态:初始、遍历和最终状态,这个三个状态都需要个三辅助指针变量pPre(前驱结点)、pCur(当前结点)、后继结点(pTmp)来实现。

  • 初始状态:当前结点指向第二个结点,前驱结点指向第一个结点,后继结点指向第三个结点。

pPre = pHead->pNext; pCur = pHead->pNext->pNext; pTmp = pCur->pNext;

  • 遍历状态:每每修改两个相邻结点的前后关系,增加一个辅助指针变量保存下一次的修改目标结点

pTmp = pCur->pNext; pCur->pNext = pPre; pPre = pCur; pCur = pTmp;

  • 最终状态:当前结点指向NULL用于遍历结束,需要头尾相连。

5.3 程序

/* 逆置链表 */
int SList_Reverse(SLIST *pHead)
{
	SLIST	*pPre, *pCur, *pTmp;

	pPre = pCur = pTmp = NULL;

	if (pHead == NULL || pHead->pNext == NULL || pHead->pNext->pNext == NULL)
	{
		printf("不需逆置\n");
		return -1;
	}

	/* 初始状态 */
	pPre = pHead->pNext;
	pCur = pHead->pNext->pNext;

	/* 逆置过程 */
	while (pCur)
	{
		pTmp = pCur->pNext;	//当前结点的下一结点
		pCur->pNext = pPre;	//逆置
		pPre = pCur;		//辅助指针变量往后推进
		pCur = pTmp;
	}

	/* 头尾相连 */
	pHead->pNext->pNext = NULL;
	pHead->pNext = pPre;

	return 0;
}

6、删除链表

6.1 要求

①、删除所建立的链表,释放内存空间

6.2 编程思路

在这里插入图片描述

在删除链表处理的过程中,需要一个辅助指针变量pTmp来缓冲。
执行步骤:

  1. 先缓冲第一个结点的位置pTmp=pHead->pNexr
  2. 后释放头结点free(pHead)
  3. 头结点指向下一个结点,重复执行上述步骤pHead=pTmp

6.3 程序

/* 删除链表 */
int SList_Destory(SLIST *pHead)
{
	SLIST *pTmp;

	if (pHead == NULL)
	{
		printf("func SList_Destory() err:-1 pHead == NULL\n");
		return -1;
	}

	/* 建立缓冲变量 */
	while (pHead)
	{
		pTmp = pHead->pNext;
		free(pHead);
		pHead = pTmp;
	}

	return 0;
}

三、话题一:链表操作时的内存泄漏问题

1、问题例子

采用上述创建链表的函数为例子

//	1、开辟内存,新建头结点
	pHead = (SLIST *)malloc(sizeof(SLIST));
	if (pHead == NULL)
	{
		printf("func SList_Creat() err:-1 (SLIST *)malloc(sizeof(SLIST))\n");
		return NULL;
	}
//	2、根据所给的数据继续开辟结点并赋值,直到为-1
	while (data != -1)
	{
		pCur = (SLIST *)malloc(sizeof(SLIST));
		if (pCur == NULL)
		{
			printf("func SList_Creat() err: (SLIST *)malloc(sizeof(SLIST))\n");
			return NULL;
		}

		pM->pNext = pCur;	//连接当前结点与前驱结点
		pCur->data = data;	//写入数据
		pM = pCur;			//pM指向当前结点的pNext
		pCur->pNext = NULL;

		printf("\nPlease enter your data:");
		scanf("%d", &data);
	}

  在这个函数中,一个入口,对应多个出口,并且执行失败退出函数时无释放内存,会导致内存泄露问题

2、解决

对这个函数,我们采用goto语句进行优化,完整代码如下


int SList_Creat_updata(SLIST **mypHead)
{
	SLIST	*pHead, *pM, *pCur;
	int		data;
	int		ret = 0;

	//	1、开辟内存,新建头结点
	pHead = (SLIST *)malloc(sizeof(SLIST));
	if (pHead == NULL)
	{
		ret = -1;
		printf("func SList_Creat() err:%d (SLIST *)malloc(sizeof(SLIST))\n", ret);
		goto END;
	}

	pHead->data = 0;
	pHead->pNext = NULL;
	pM = pHead;

	printf("\nPlease enter your data:");
	scanf("%d", &data);

	//	2、根据所给的数据继续开辟结点并赋值,直到为-1
	while (data != -1)
	{
		pCur = (SLIST *)malloc(sizeof(SLIST));
		if (pCur == NULL)
		{
			ret = -2;
			printf("func SList_Creat() err:%d (SLIST *)malloc(sizeof(SLIST))\n", ret);
			goto END;
		}

		pM->pNext = pCur;	//连接当前结点与前驱结点
		pCur->data = data;	//写入数据
		pM = pCur;			//pM指向当前结点的pNext
		pCur->pNext = NULL;

		printf("\nPlease enter your data:");
		scanf("%d", &data);
	}

END:
	if (ret != 0)
	{
		SList_Destory(pHead);
	}
	else
	{
		*mypHead = pHead;
	}
	//	4、返回头结点地址
	return ret;
}

四、话题二:传统链表、Linux内核链表与通用企业链表

1、传统链表的缺点

  • 和具体结构绑定,不通用
  • 链表逻辑试图包含业务逻辑(业务数据)
  • 业务数据和链表逻辑耦合性太高

举个例子来说明:
如上面的结点的数据类型

typedef struct SLIST
{
	int data;
	struct SLIST *pNext;
}SLIST;

  当我们需要在结点中包含一个新的变量时,上面所写的操作函数部分将不适合使用,缺乏拓展性和可维护性

2、Linux内核链表

   内核链表的结构是个双向循环链表,只有指针域数据域根据使用链表的人的具体需求而定
   内核链表设计哲学:既然链表不能包含万事万物,那么就让万事万物来包含链表。

3、通用企业链表

  所谓通用链表 , 即链表具有与用户自定义的数据类型(节点类型)的无关性.我们可以一次编写处处使用

  • 优点:具有通用性,开销低。
  • 缺点:不负责内存管理,大量采用宏,无类型检查。

  这里只是简单介绍一下传统链表、Linux内核链表与通用企业链表,后续会专门写一篇文章,对三者进行分析。

发布了40 篇原创文章 · 获赞 29 · 访问量 3613

猜你喜欢

转载自blog.csdn.net/weixin_42813232/article/details/105376285