链表话题
/*
*运行平台: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
来缓冲。
执行步骤:
- 先缓冲第一个结点的位置
pTmp=pHead->pNexr
- 后释放头结点
free(pHead)
- 头结点指向下一个结点,重复执行上述步骤
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内核链表与通用企业链表,后续会专门写一篇文章,对三者进行分析。