线性表 :: “无头”单链表的设计与实现
说明:本文属于读书笔记。笔者将以讲述的方式表达全片文章。故文中提到的某些字词是非正式术语,只是笔者本人的理解性词语。
前言:此前,笔者已分享了线性表 :: “有头”单链表的设计与实现(链式存储结构),且提及链表的设计与操作离不开指针的理解与操作,故本文将聊一聊指针在链表中的使用。注意:本文更多会基于有头链表的文章思路来写,故如需学习参考可点击上面链接跳转(如果是 CV 战士,懂得都懂)。
目录
1. 有头链表与无头链表的区别
有头链表:就是问了便于初次学习理解使用链表而设计的,在链表的头部放置一个结点,专门用于 标记头部 使用的,其下一个结点就是链表的第一个结点。(如下图,有头链表的测试代码)我们实际使用不完全不在意在头结点中存放的数据,我们在意的是它的指针域,因为存放了(非空链表)第一个结点的地址信息。
无头链表:就是我们舍去了原来用来“标记”的头!
2. 从有头链表到无头链表(指针操作)
2.1 链表与指针
在上图中可以看见,在此时代码段的第一行就是建立一个指针,有啥用?
起到指向作用。对于有头链表,指向链表头;对于无头链表,指向链表的第一个结点。
在有头链表中,其指针域存储的是第一个结点的地址,其数据类型为 struct Node*,即指针。在对链表的数据操作中我们主要在意的是这个地址!!!就是上一篇文章中提到的门牌号。知道了门牌号才能找到正确的目标。
由于有头链表设置了头结点,我们要找第一个结点,那通过 pHead->next (首结点的地址)就可以找到。如有头链表设计与实现中的尾插法,定义一个游标指针就是为了明确指向对象,当指向头结点时,ptemp->next 即可以表示头结点的指针域,也可以表示第一个结点,它的实质数据就是一个地址信息。如果是空就说明链表为空,非空就说明存在第一个结点。
由上可知,链表操作依赖于地址,设计者操作地址的方式就是使用指针!
2.2 从有头链表到无头链表
掏出此前的有头链表代码(代码地址)。(如下图)对于测试代码中,我们已知创建了指针 plist 指向链表,plist = createNode(0);就是设置头结点。
从有头链表到无头链表,我们直接把 plist = createNode(0);会咋样?
我直接好家伙,程序噶了!!!(下图看问题)
编译器提示:pHead 的两个成员无法读取内存!显然,问题出在了传入的 plist,前文也说了,有头链表传入后,我们是通过 pHead->next 得知第一个结点的地址的,现在头结点没了!!!那地址在哪?
plist 原来指向头结点,现在没了,这不直接指向第一个结点就好了!指针的数据就是一个地址值,同时它也是一个变量,它是不是也有地址,那设计的时候直接把这个指针的地址作为第一个结点的地址不就解决了吗!这就可以找到第一个结点的位置了!
由于 plist 的数据类型是 struct Node* ,&plist 的数据类型是?struct Node** !(如果读者对指针的相关概念或操作不太熟悉,请自行查阅资料,后续笔者可能会写相关文章,再更新内容。)
3. 无头链表的设计与实现
说明:前文已经指出:链表操作依赖于地址,设计者操作地址的方式就是使用指针!,且已进行了关系的简单说明,后文将不做过多解释!
同时,有头链表与无头链表的功能实现相似,无非就是关于无头链表实现中需要注意判断条件的变化!下文会作必要的解释。
3.1 结点设置与创建结点
结点设置与创建结点并不涉及地址的相关操作,故与有头链表的代码相同!(代码如下)
struct Node{
int data; /* 数据域 */
struct Node* next; /* 指针域 */
};
struct Node* CreateNode(int data){
// 1. 动态申请内存空间
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
// 检验申请失败(一般不会)
if(NULL == pnew) return;
// 2. 存储数据
pnew->data = data;
pnew->next = NULL;
// 3. 返回结点
return pnew;
}
3.2 遍历链表
关于遍历,与有头链表遍历基本相同,唯一的区别在于有头链表在验证链表非空之后,我们设置游标指针直接通过头结点取访问了第一个结点。那在无头链表中不跳过头结点即可。具体实现看代码及注释!
void travel(struct Node* pHead){
if (NULL == pHead)
{
printf("链表为空!\n");
return;
}
printf("plist:");
/*
注意此处,有头链表中设置了头结点略过
struct Node* ptemp = pHead->next;(此处不要)
*/
while (pHead){
printf("%d ", pHead->data);
pHead = pHead->next;
}
printf("\n");
}
3.3 尾插法
尾插法即链表尾部插入新结点,与有头链表最大的区别在于传入的参数,前文也提到了,没有了头结点,我们使用指向链表的指针地址作为第一个结点的地址进行链表建立。此处具体实现看代码及注释!
/*
注意参数列表第一项,数据类型为 struct Node** ,
接收指针的地址进行操作 !!!
*/
void appendNode(struct Node** pHead,int data){
// 1. 申请新结点
struct Node* pnew = createNode(data);
if(NULL == pnew) return;
// 2. 判断链表是否为空
/*
参数传入的地址需要解引用,读取到值;
显然,当 plist 为空时,即 *pHead 为空,也就是说链表为空;
反之,链表不为空!!!
*/
if(NULL == *pHead){
// 链表为空 直接放入新结点作为第一个结点!
*pHead = pnew; // 相当于链表指针指向新结点
}
else{
// 链表不为空
// 3. 设置游标指针便于理解操作
struct Node* ptemp = *pHead;
// 4. 遍历查找尾结点
while(ptemp->next){
ptemp = ptemp->next;
}
// 5. 挂载新结点
ptemp->next = pnew;
}
}
int main()
{
/* 设置指针 */
struct Node* plist = NULL;
/* 注意二级指针的使用,我们引用指针的地址 */
appendNode(&plist,123);
}
3.4 头插法
头插法相较于尾插法就是少了寻找尾结点,因为头插法操作就是在原有的第一个结点前加入新结点即可!那不就是指针移动标识新结点为头结点就欧克了。但是注意要先让新结点与原有的第一个结点链接,否则会数据丢失!具体实现看代码及注释!
void addNode(struct Node** pHead,int data){
// 1. 申请新的结点
struct Node* pnew = createNode(data);
if(NULL == pnew) return;
// 2. 判断链表是否为空
if(NULL == *pHead){
// 链表为空,挂载成为新结点(第一个结点)
*pHead = pnew;
}else{
// 链表非空!
// 3. 新结点先于原来的第一个结点链接
pnew->next = *pHead;
// 4. 指针移动到新结点处
*pHead = pnew;
}
}
3.5 指定插入法
此处我们直接设定:具体实现看代码及注释!
假设指定位置尾 i :
i <= 1:即头插法;
i >= 链表长度:即尾插法;
i = 其他:即中间插入
void insertNode(struct Node** pHead, int pos, int insertNodeData){
if (NULL == pHead) return;
// 1. 创建新结点
struct Node* pnew = createNode(insertNodeData);
if (pos <= 1){//头插法
pNew->next = *pHead;
*pHead = pnew;
return;
}
// pTemp指向链表第一个节点
struct Node* ptemp = *pHead;
// 2. 找到插入位置的前一个节点
for (int i = 0; i < pos - 2; i++){
if (NULL == ptemp->next) break;//链表过短
ptemp = ptemp->next;
}
// 3. 新节点连接插入位置之后的节点
pnew->next = ptemp-next;
// 4. 新节点成为插入位置节点的下一个节点
ptemp->next = pnew;
}
3.6 查找与修改
根据前文的几个修改案例,大家应该能发现把!有头链表与无头链表的功能代码修改主要是由于头结点的变动引起的,前面的遍历就是这个道理。在修改代码中原本引入了游标指针略过头结点,此时删除该部分,即不略过就好了。(但是注意,不改是由于笔者的所写的代码在对应功能没有对头结点进行操作故不需要修改。)
struct Node* findNode(struct Node* pHead, int findData){
// 1. 判断链表是否为空
if (NULL == pHead) return NULL;
// 2. 略过头节点(使用游标)(此处删除即可)
// pHead = pHead->pNext;
// 3. 寻找指定值(找到就返回地址,没找到就返回null)
while (pHead){
if (findData == pHead->data) return pHead;
pHead = pHead->pNext;
}
return NULL;
}
void changeNode(struct Node* pHead,int changedata,int aimdata){
// 1. 查找到目标值
struct Node* pChange = findNode(pHead,aimdata);
// 2. 如果返回为 NULL 即操作目标不在链表中
if(NULL == pChange ) return;
// 3. 直接复制覆盖即可
pChange->data = changedata;
printf("修改成功!\n");
}
3.7 删除结点
删除结点是一个难点,不过也不一定,你得转换一个思路,只要我们删除的不是第一个结点,那第一个结点就相当于有头链表的头结点,对后边的操作和有头链表一样。难点就在头结点的删除!!!具体实现看代码及注释!
void deleteNode(struct Node** pHead, int deleteData){
if (pHead == NULL) {
printf("没有找到要删除的节点,删除失败!\n");
return;
}
//1 找到要删除的节点,找不到直接返回
struct Node* pDel = findNode(*pHead, deleteData);
if (NULL == pDel) {
printf("没有找到要删除的节点,删除失败!\n");
return;
}
//如果要删除的节点是头节点
if (pDel == *pHead){
//1 下一个节点成为新的头节点
*pHead = pDel->next;
//2 释放
free(pDel);
printf("删除成功!\n");
return;
}
//2 找到要删除的节点的前一个节点
struct Node* pDelPrev = *pHead;//不能跳过头节点,删第一个!!!
while (pDelPrev->next != pDel){
pDelPrev = pDelPrev->next;
}
//printf("pDelPrev:%d\n", pDelPrev->data);
//3 要删除的节点的后一个节点成为 要删除的节点的前一个节点 的后面节点
pDelPrev->next = pDel->next;
//4 删除
free(pDel); pDel = NULL;
printf("删除成功!\n");
}
4. 结语
本篇内容对分析的内容较少,有了有头链表的阅读与思考,笔者认为稍加思考,作图分析,设计思路是不难的。
如代码有问题可评论或留言,笔者会及时处理!
实操系列与功能补充指引!(待更新)
- 计数功能的实现。相关文章地址 => 双链表的设计与实现
- 无头链表的新设计,及链表与指针关系讲述相关文章。 => 顺序栈与链式栈的设计与实现