头指针、头节点、首元结点——《王道数据结构》
一、关于头指针、头节点、首元节点的问题
昨天考研专业课遇到了一个选择题 带头结点的单链表具有什么优点 ,因为平时都是用的带头节点的链表,只是单纯记住了结论。考后我想仔细研究研究这个问题… 来CSDN找点资料,发现越看越模糊,下面我来总结总结。
二、教材说法
1.让我们先来瞅瞅这个《王道考研》辅导书上给的结论:
下图就是一个带头结点的单链表:
上图有一个易错点:很多时候我们容易把L->next看成是头节点,其实是L指向了头节点,而L->next是首元节点。这里我们不妨再剖析下结点的含义:
2.这是《大话数据结构》上给的结论:
3.CSDN上的一些结论:
①.使用头节点,指针域都是指向第一个结点,对于空表和非空表操作都是一致的,对于第一个元素的操作(添加、删除)更加方便。
②使用头节点,则第一个位置的插入和删除都是对 head->next (head是头指针) 操作,而不是head本身,而且减少了算法分支 (if else 分支)。
4.我的理解:
头指针:
①.用来标记链表,做链表的名字。
②.指向链表的第一个结点。
头结点:
①.统一操作(第一个结点的插入、删除)。
②.统一空表和非空表。
首元节点:
①.第一个具有实际意义(存了值)的点。
三、以上只是纸上谈兵,下面我们来实操实操:
记住结论很轻松,但是真考试的时候,要你做选择的时候,往往因为缺少操作而没有底气,是谓“知其然,不知其所以然。” 回头看的时候还真发现自己没有整清楚。
我们来挨个分析,它怎么就有优势了?
定义结点:
typedef struct Node{int data;struct Node* next;
}Node;
优势一:统一插入入操作
①有头节点初始化及插入(头插法)操作
//初始化一个链表,上面说到:“头指针用来标记链表,做链表的名字”操作如下:Node* L=(Node*)malloc(sizeof(Node));
//有了链表头指针我们设置头节点:[头指针就是表示头节点],而头节点的指针域指向的是NULLL->next=NULL;
//插入一个结点,我们省略 申请结点、输入值的操作 直接设这个结点为NewNode,并且用头插法:NewNode->next=L->next;L->next=NewNode;
②无头节点初始化及其头插法(简写)
//同上操作,省请一个头指针,标志一个链表:Node* L=(Node*)malloc(sizeof(Node));//注意这步可以省略head=NULL;
/*!!!注意!!!上面是一个“经典的错误,标准的零分”
这里出现了“内存泄露”:我们再堆区申请了一片内存,但是我们又把它置为空指针,而堆区的那片空间,没有被释放.
相当于:你去领养了一个孩子,反手抛弃了ta,ta不就成了没爹的野孩子。不慌我们看下面的操作*/NewNode->next=L;L=NewNode;
/*上面的操作不就是新结点NewNode指向了NULL吗,所以我们直接让L=NULL就行了!*/
好,那么问题来了?它怎么统一呢?怎么就统一操作了呢?不是各管各的吗?我靠,我们发现头插法确实是各管各的,并没有统一一说,下面我们来试试尾插法。
③有头节点初始化及尾插法
//初始化一个链表,上面说到:“头指针用来标记链表,做链表的名字”操作如下:Node* L=(Node*)malloc(sizeof(Node));
//有了链表头指针我们设置头节点:[头指针就是表示头节点],而头节点的指针域指向的是NULLL->next=NULL;
//插入一个结点,我们省略 申请结点、输入值的操作 直接设这个结点为NewNode,并且用尾插法:
/*用 r 记录head指针,然后让r一直跟随再链表尾部。狼群的狼王负责带路,他要派出一个小弟,去随时检查狼尾部有没有狼掉队,如果亲自去的话,那岂不是群狼无首,到处乱窜?*/Node* r=L;
//假装申请了一个新节点NewNode,省略输入、赋值操作//NewNode->next = r->next;按常理确实应该让NewNode->next指向r->next,或者直接指向NULL。亲测都行。r->next=NewNode;r=NewNode;r->next=NULL;
④无头节点初始化及其尾插法
常规思路(错误示范):
//有了上面的操作,我们直接让head=NULL;
Node* L=NULL;
//派出去一个小弟监查
Node* r=L;
//插入一个结点,我们省略 申请结点、输入值的操作 直接设这个结点为NewNode,并且用尾插法:
NewNode->next=r;
r=NewNode;
乍一看没问题,但是仔细斟酌,你会发现L
已经找不到r
了,L
始终指向空,而r
又一直指向尾结点,导致了一个结果:即使这些结点连在一起,但是我们已经找不到他们了。就好比你只认识狼王和他的小弟,狼王老年痴呆,他小弟又在最后面,你也没有办法挨个遍历整个狼群。那么有头结点的尾插法,为什么L就不会丢失呢:因为L
始终指向的是头结点,这“头节点”好比狼王的亲儿子,虽然狼王老年痴呆但是他肯定记得他的亲儿子,让狼王和他儿子直接对接,那么你仍然可以遍历整个狼群。所以那么我们该怎么写无头节点的尾插法呢?自然就是没有头节点,把第一个结点就设置为头节点。`
(正确示范):
//有了上面的操作,我们直接让head=NULL;
Node* L=NULL;
//派出去一个小弟监查
Node* r=L;
//如果头指针是空,我们让他指向第一个结点(让第一个结点成为狼王儿子),同样省略操作NewNode结点
if(L==NULL)
{Newnode->next=NULL;//操作同设置头节点一样L=NewNode;//让狼王后面更着他儿子r=NewNode;//更新小弟
}
else
{r->next=NewNode;//已经有头结点了,所以操作同有头节点的一样。r=NewNode;
}r->next=NULL;//最后一步置尾结点的next指向NULL。
优势二:统一“删除某个链表结点”操作
①有头节点“从头部”删除整个单链表
我们先理清楚几个结点:L
是头指针,指向头节点,L->next
是首元结点。
再次提醒!!这里一个易错点:很多时候我们容易把L->next看成是头节点,这是我刚开始学习的时候容易犯的错误。
//带头节点的结点
Node* L;//头指针指向头节点,而L->next是首元结点(第一个存数据的结点)
Node* p=L->next;//p是首元结点(第一个具有实际意义的点)
while(p!=NULL)//如果还存在实际意义的点
{Node* q=p->next;//记录下首元结点下一个结点 (皇帝宣布传位给那个儿子) free(p);//释放本结点 (皇帝驾崩)p=q;//让首元结点变成q (继承皇位)
}
这里为什么要多一个q呢?直接free( p ),p=p->next
不香吗?这也是我曾犯过的错误,后头我想了个例子:皇帝驾崩之前,肯定要立遗嘱,记录下把位置传给那个儿子,不可能皇帝死了后诈尸,起来宣布把皇位传给谁。这个p除了有数据域,还有指针域,你在做free( p)
时候,整个结点就没了,它指向谁也不知道了,所以要提前记录下来。
②不带头节点“从头部”删除整个单链表
这里的L直接指向了第一个首元结点:
//不带头节点的链表
Node* L;//头指针
while(L!=NULL)//同上操作
{Node* q=L->next;//记录下一个结点free(L);//释放首元结点L=q;//让首元结点指向它
}
经过比较我们发现:有无头结点对删除整个单链表操作,似乎没有带来便利,那么我们需要进一步探讨:带头结点的单链表是如何统一删除操作的。
下面我们来探讨,带头结点是否对删除单链表单个元素操作,提供了便利:
③带头节点删除单个单链表元素:
为了和删除整个单链表操作区分,我们假定删除单链表中的第二个元素。(实际操作中我们可以根据值、下标等找到需要删除的元素)
Node* L;//头指针指向头节点,而L->next是首元结点(第一个存数据的结点)
Node* aimNode=SearchNode();//假设这是目标结点
Node* p//假设这是目标结点之前的结点
//假设我们删除一个结点
p->next=aimNode->next;
free(aimNode);
④不带头节点“从尾部”删除整个单链表
这里的不带头结点,所以 L
指向首元结点,L->next
是第二个结点。那么我们这样操作:
常规思路(错误示范):
//不带头节点的链表
Node* L;//头指针指向头节点,这里没有头指针所以:L直接指向首元结点
Node* aimNode=SearchNode();//假设这是目标结点
Node* p//假设这是目标结点之前的结点
p->next=aimNode->next;
free(aimNode);
这时候我们发现该代码缺少了一种情况:p
为L
的时候,也就是我们删除第二个点的时候,我们要让第一个结点,指向第三个结点,而这里我们只能操作第二个点。所以我们又需要动用if-else
来完善这种情况:
常规思路(正确示范):
//不带头节点的链表
Node* L;//头指针指向头节点,这里没有头指针所以:L直接指向首元结点
Node* aimNode=SearchNode();//假设这是目标结点
Node* p//假设这是目标结点之前的结点
if(p==L)
{p=aimNode->next;
}
else
{p->next=aimNode->next;
}
free(aimNode);
经过上面一系列的折腾,我们在此发现了带头结点是如何具有统一链表操作。
我觉得我写的太啰嗦并且代码过于简陋,所以需要详细一点的代码大家可以,看看这个小哥的代码:不秃头的小黄人
优势三:统一“空表和非空表控制”操作
①那么显而易见表空和表非空,显然也是在第一个结点做文章。判断是否为空:
带头结点 | 不带头节点 |
---|---|
L->next=NULL | L=NULL |
那么那些情况比较适用呢?
②我们再来回顾下两者之间的区别:
若使用头结点,无论表是否为空,头指针都指向头结点,对于空表和非空表的操作是一致的。
若不使用头结点,当表非空时,头指针指向第1个结点的地址,但是对于空表,头指针指向的是NULL,此时空表和非空表的操作是不一致的。
③我们惊人发现,其实在上面的尾插法中,我们已经体现出了这一特点,不妨回过头来看看:
带头结点:
//初始化一个链表,上面说到:“头指针用来标记链表,做链表的名字”操作如下:Node* L=(Node*)malloc(sizeof(Node));
//有了链表头指针我们设置头节点:[头指针就是表示头节点],而头节点的指针域指向的是NULLL->next=NULL;Node* r=L;r->next=NewNode;r=NewNode;r->next=NULL;
不带头结点:
//有了上面的操作,我们直接让head=NULL;
Node* L=NULL;
//派出去一个小弟监查
Node* r=L;
if(L==NULL)
{Newnode->next=NULL;//操作同设置头节点一样L=NewNode;//让狼王后面更着他儿子r=NewNode;//更新小弟
}
else
{r->next=NewNode;//已经有头结点了,所以操作同有头节点的一样。r=NewNode;
}r->next=NULL;//最后一步置尾结点的next指向NULL。
结果显而易见,不带头结点的确实要多一步判断是否为空操作
四、总结:
话不多说,我们再来看看这个结论:
当然实际情况实际要实际分析。不能关背结论,更不能背代码。面对疑惑希望我们保持刨根问底的精神。
头指针、头节点、首元结点——《王道数据结构》相关推荐
- 【头结点】【头指针】【首元结点】
2022 -1-14 文章目录 2022 -1-14 1. 定义: 2. 哨兵(头结点) 3. 有无头结点的单链表的创建 3.1 有头结点 3.1.1 头插法: 3.1.2 尾插法: 3.2 无头结点 ...
- C++之链表中头指针、头节点、首元结点的
头指针:顾名思义是一个指针,指向链表的开始地址: 头结点:第一个节点,该节点只有地址信息,改地址指向下一个结点,数据域无信息: 首元结点:含第一个元素的结点,为链表的实际开始位置,数据域包含第一个数据 ...
- 头指针,头结点,首元结点的区别,头结点的优点
1.先区分一下概念: 头结点: 在单链表第一个元素结点之前设置的一个结点, 数据域可以不存任何信息,指针域指向单链表第一个元素的结点.对于单链表来说, 头结点可有可无,但为了操作方便,一般情况下单链表 ...
- 头指针为head的带头结点的单链表判空条件head->next==null?
由于考研需求,又乖乖滚回来捧起数据结构了,一年没碰书,忘得都差不多了,还得捡回来,哭死了. 进入正题,为何头指针为head的带头结点的单链表判空条件head->next==null?其实一开始这 ...
- Java中头指针和头结点_. 2 . 【严题集 2.1① 描述以下三个概念的区别:头指针、头结点、首元结点(第一个元素结点)。...
什么是未达账项?包括哪几种情况 2,3,5,8,(),17. (34) 是进度控制的一个重要内容. 光彩夺目的金刚石的化学成分与( )一样. 男,13岁,进食蚕豆后皮肤黏膜发黄,尿呈酱油色.红细胞28 ...
- 软考题目之头结点、头指针和首元节点
遇到一个让人迷惑的题目. 以下关于线性表采用链式存储时删除节点运算的描述,正确的是() A.带头结点的线性链表删除结点时,不需要更改头指针. B.带头结点的线性链表删除第一个结点时,需要更改头指针. ...
- 头指针和头结点意义和区别
1.介绍 链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了.之后的每一个结点,其实就是上一个的后继指针指向的位置.链式存储时只要不是循环链表,就一定存在头指针. 2. ...
- 带头结点单链表、不带头结点单链表(头指针单链表)
1.头结点和头指针的区别 1.1区别: 头指针表明了链表的结点,可以唯一确定一个单链表. 头指针指向链表的第一个结点,其记录第一个存储数据的结点的地址. 头结点是点链表的第一个结点,若单链表有头结点, ...
- 数据结构:头指针,头结点详解(新手.小白)
如果不想看定义的话,可以看我的第二部分,里面有我自己遇到的一些疑难. 下文中出现的有关于java的,也有c语言的指针(可以看成java的引用变量),觉得写得不好,多多包涵 目录 (一)定义详解 (二) ...
- 可由一个尾指针唯一确定的链表有_L2数据结构第08课 单向链表和循环链表
L2-数据结构-第08课 单向链表和循环链表 线性表 线性表是一种常用的数据结构,其中的每一个元素(结点)都有唯一的前驱和唯一的后续.当然,第一个元素只有后续,最后一个元素只有前驱. 线性表一般分为& ...
最新文章
- js正則表達式--验证表单
- 只加皱纹是不行的,教你画出不同年龄段的人物
- 50 【Go版本变化】
- 梦幻模拟战更新服务器正在维护,梦幻模拟战魔之启示录更新维护公告
- 10.傅里叶变换——傅里叶变换、计算傅里叶变换_3
- Spring Security构建Rest服务-0600-SpringSecurity基本原理
- 获取指定存储过程的参数定义
- zabbix源码安装
- C++中指针运算符(*)和数组索引符([])的优先级。指针数组及数组指针
- java写法可能存在的 java.lang.OutOfMemoryError: Java heap space 问题
- 处理word 多级标题编号不联动的问题
- 我们都被监控了?揭秘全球电信网络7号信令(SS7)漏洞
- pc自建服务器传输3ds,新3DS自带的PC管理MicroSD 最强无线传输软件
- CGB2202面向对象第10天
- 纳什均衡定义、举例、分类
- DCOS到底是啥?看完这篇你就懂了
- 介绍几种等待多线程任务执行完毕的方法
- 心率检测实现报告(二)
- 教你如何关闭Win7视频预览节约资源
- Java程序CPU飙升排查,找出死循环代码
热门文章
- java随机生成测试数据
- netkeeper显示651_关于电信Netkeeper客户端升级的通知
- 关于android的外文论文,毕业论文外文翻译-Android开发
- unity商店里下载的资源保存路径
- 练习如下命令的使用: set mset get mget keys type exists ttl expire move 、select del flushdb flusha
- 聊一聊我在腾讯的外包同事
- 基于图书管理系统的需求分析之可行性分析安全需求分析系统需求分析
- 马化腾的马氏建议:“小步快跑 快速迭代”
- matlab脚本栏中间有条线,word中间有一根线 word中间有一条线如何去掉
- 阿里云服务器验证码不能显示解决办法java.lang.Error: Probable fatal error:No fonts found