8.13 | 128-144页:树的概念 二叉树概念 性质 |
---|---|
8.14 | 145-153页:二叉树的4种遍历:先中后层次 线索二叉树的构建 |
8.15 | 154-193页:中序线索二叉树的构建、并延伸至先序和后序的...、树与森林与二叉树转换,遍历、哈夫曼编码哈夫曼树193页下方示例不是很清楚 |
8.16-8.22 | 完善课后选择题 |
summary 第四章
[该章围绕KMP算法开展,从朴素字符串匹配到KMP算法再到next的优化(nextval)。]
学习KMP算法时,应从分析暴力法的弊端入手,思考如何去优化它。实际上,已匹配的相等序列就是模式串的某个前缀,因此每次回溯就相当于模式串与模式串的某个前缀比较,这种频繁的重复比较是效率低的原因。此时,可从分析模式串本身的结构入手,以便得知当匹配到某个字符不等时,应该向后滑动到什么位置,即已匹配相等的前缀和模式串若首尾重合,则对齐它们,对齐部分显然无须再比较,下一步则使直接从主串的当前位置继续进行比较。
4章如果出应用题,把上一章内容中的暴力匹配、next数组和nextval数组代码背熟,按照那个来就可以。
第五章 树与二叉树
树的概念、
二叉树的定义及其主要特征、顺序存储结构和链式存储结构、遍历、线索二叉树的基本概念和构造
树的存储结构、森林和二叉树的转换、树和森林的遍历
哈夫曼树和哈夫曼树编码、并查集UFSet(就是一个数组)及其应用。
这章不太熟的是代码还有一些数量对应(二叉树高度和结点数量),并查集在图论中被广泛应用,例如判断图中是否存在环路、动态连通性问题、网络连接问题等。
- 概念能在做题时对应起来就可
- 性质是用来计算的,大多
文字内容
问答题中的公式推导是记不住的公式,就不记结果直接记忆过程了
还有就是代码中的文字内容有一部分也会在问答中出现,我想多打两遍加强记忆
问
- 度为m、具有n个结点的树的最小高度怎么推?
- 二叉树与度为2的有序树有什么区别?
- 非空二叉树上的叶节点树等于度为2的结点数加1怎么推?
- 哪类二叉树适合顺序存储,为什么?
- 一般二叉树适合顺序存储吗,为什么?
- 二叉树的遍历方式
- 中序+先序构造一棵二叉树的过程描述 (力扣105 后序+中序 力扣106)
- 层次序列和中序 构造二叉树的过程描述
- 什么是线索二叉树?
- 为什么会有额外的空指针,有多少?如何利用空指针?
- 引入线索二叉树的目的
- 中序线索二叉树如何创建?前序和后序的建立有什么区别?
- 如何对中序线索二叉树进行遍历?
- 如何在先序线索二叉树中找结点的后继?–>服务于对线索二叉树的先序遍历
- 如何在后序线索二叉树找节点的后继?
- 用双亲表示法是怎么存储树的?
- 双亲表示法作为树的顺序存储结构与二叉树的顺序存储能划等号吗?
- 树的孩子表示法是怎么存储树的?
- 树的孩子兄弟表示法是怎么存储树的?
- 树的孩子兄弟表示法优缺点?
- 可变长度编码比固定长度编码有什么好处?<=> 前者有什么特点?
答
- 为使树的高度最小,在前 h − 1 h-1 h−1层中,每层的结点数都要达到最大,前 h − 1 h-1 h−1层中最多有 ( m h − 1 − 1 ) / ( m − 1 ) (m^{h-1}-1)/(m-1) (mh−1−1)/(m−1)个结点,前 h h h层最多有 ( m h − 1 ) / ( m − 1 ) (m^{h}-1)/(m-1) (mh−1)/(m−1)个结点。因此 ( m h − 1 − 1 ) / ( m − 1 ) < n < = ( m h − 1 ) / ( m − 1 ) (m^{h-1}-1)/(m-1)<n<=(m^{h}-1)/(m-1) (mh−1−1)/(m−1)<n<=(mh−1)/(m−1),即 h − 1 < l o g m ( n ( m − 1 ) + 1 ) < = h h-1<log_m(n(m-1)+1)<=h h−1<logm(n(m−1)+1)<=h,解得 h m i n = ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ h_{min}=\lceil log_m(n(m-1)+1)\rceil hmin=⌈logm(n(m−1)+1)⌉ 。
- 度为2的树最起码有三个结点,但二叉树可以为空; 度为2的有序树的孩子的左右次序是相对另一个孩子而言的,若某个结点只有一个孩子,则这个孩子就无须区分其左右次序,而二叉树无论其孩子是否为2,均需确定其左右次序,即二叉树的结点次序不是相对于另一结点而言的,而是确定的。
- 设度为0,1和2的结点个数分别为 n 0 , n 1 , n 2 n_0,n_1,n_2 n0,n1,n2,结点总数为 n = n 0 + n 1 + n 2 n = n_0+n_1+n_2 n=n0+n1+n2。二叉树的分支树,除了根节点外,其余结点都有一个分支进入,设 B B B 为分支总数,则 n = B + 1 n=B+1 n=B+1,而分支树是由度为1或2的结点射出的,因此 B = n 1 + 2 ∗ n 2 B=n_1+2*n_2 B=n1+2∗n2,联立两式可以知道 n 0 = n 2 + 1 n_0 = n_2+1 n0=n2+1。
- 二叉树的顺序存储是指用一组连续的存储单元自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在一维数组下表为i-1的分量中。
根据二叉树的性质,完全二叉树和满二叉树适合顺序存储,树中结点的序号可以唯一地反映结点之间的逻辑关系,既能最大可能节省空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。 - 一般二叉树,为了让数组下标能反映二叉树中结点之间的逻辑关系,只能添加一些并不存在的空结点,让其每个结点与完全二叉树上的结点相对照,再存储到一维数组的相应分量中。然而,最坏情况下,一个高度为h且只有h个结点的单支树却需要占据将近 2 h − 1 2^h-1 2h−1个存储单元,空间利用效率较低。
- 由二叉树的递归定义可知,遍历一棵二叉树便要决定对根节点N、左子树L和右子树R的访问顺序。按照先遍历左子树,再遍历右子树的原则,常见的遍历次序中序LNR,先序NLR,后序LRN三种遍历算法,其中序指的是根节点在何时被访问。
- 先找到根节点,并确定其中的左右子树区间;先序序列中的第一个结点一定是二叉树的根节点,中序序列中由根节点将序列分割为左子序列和右子序列;左子树的中序序列和先序序列的长度是相等的,右子树同理;根据这两个序列,可以在先序序列中找到左子树的先序序列和右子树的先序序列,如此递归的分解下去,便能唯一确定这棵二叉树。
- 在层次序列中,第一个结点时二叉树的根节点,这样就将中序序列分割成了左子树的中序序列和右子树的中序序列。若存在左子树,则层次序列的第二个结点一定是左子树的根,可进一步胡啊分左子树;若存在右子树,则层次序列中紧接着的下一个结点一定是右子树的根,可进一步划分右子树。采用这种方法继续分解,就能唯一确定这棵二叉树。【这里注意加粗语句,书中写错了,写成了中序序列了】
- 遍历二叉树是以一定规则将二叉树中的结点排列称一个线性序列,从而得到几种遍历序列,使得该序列中的每个结点(第一个和最后一个)都有一个直接前驱和直接后继。
- 传统的二叉链表存储仅能体现一种父子关系,不能直接得到结点在遍历中的前驱和后继。在含n个结点的二叉树中,由n+1个空指针。这是因为每个叶节点都有2个空指针,每个度为1的结点都有1个空指针,空指针总数为 2 n 0 + n 1 2n_0+n_1 2n0+n1 ,又 n 1 + 2 n 2 + 1 = n 0 + n 1 + n 2 n_1+2n_2+1=n_0+n_1+n_2 n1+2n2+1=n0+n1+n2,所以空指针总数为 n 0 + n 1 + n 2 + 1 = n + 1 n_0+n_1+n_2+1=n+1 n0+n1+n2+1=n+1,可以考虑利用这些空指针来存储指向节点在遍历中的前驱或后继的指针,从而优化二叉树的遍历。
- 引入线索二叉树的目的正是为了利用这些空指针来存储前驱和后继指针,从而使得二叉树的遍历(如中序遍历)能够像遍历单链表那样方便,显著提高了查找节点前驱和后继的速度。
12主要是对【通过中序遍历对二叉树线索化的递归算法】这部分代码思路理清,只要能说清楚就好,主要是想加强对线索二叉树的创建代码的记忆。
- 初始化:设定一个前驱指针 pre,初始时为 nullptr。
中序遍历:在中序遍历二叉树的过程中,针对每个节点 p:
如果节点 p 的左孩子为空,则将其左指针指向 pre,并将左标志位 ltag 置为 1。
如果前驱节点 pre 的右孩子为空,则将 pre 的右指针指向当前节点 p,并将右标志位 rtag 置为 1。
更新 pre 指针为当前节点 p。
收尾工作:遍历结束后,将最后一个节点 pre 的右指针置空,并将其右标志位 rtag 置为 0。
对于前序和后序线索二叉树的建立过程,区别在于线索化的顺序和递归函数调用的位置不同:
前序线索二叉树:线索化过程的顺序与前序遍历一致,即先处理当前节点,再处理左子树,最后处理右子树。因此,pre 和 p 指针的处理顺序需要调整到前序遍历的逻辑。
后序线索二叉树:线索化过程的顺序与后序遍历一致,即先处理左子树,再处理右子树,最后处理当前节点。相应地,递归函数调用的位置也需要按照后序遍历的顺序进行调整。
后继结点,需要按照前中后序的规则,对每个节点是否有左右孩子,以及标识域是否为1进行判断
- 中序线索二叉树的结点中隐含了线索二叉树的前驱和后继信息。在对其遍历的时候,只要先找到序列中的第一个结点,然后依次找结点的后继,直至其后继为空。在中序线索二叉树中找结点后继的规律是:若其右标志为1,则右链为线索,指示其后继,否则遍历右子树中第一个访问的结点(右子树最左下的结点)为其后继。
- 先序线索二叉树中结点如果存在左孩子,左孩子就是其后继,若存在右孩子,右孩子就是其后继,若为叶节点,其右链域直接指示了结点的后继。
- 后序线索二叉树中结点后继分为三种情况:①若结点x是二叉树的根,则其后继为空;②若结点x是其双亲的右孩子,或者是左孩子但没有右子树,则其后继即双亲;③若结点x是其双亲的左孩子,且其双亲有右子树,则其后继为其双亲右子树上按后序遍历的第一个结点。
- 是使用一段连续的存储空间存储的,同时还需要增设一个伪指针,用于指向每个结点的双亲在数组中的位置。
17补充:二叉树属于树,因此二叉树可以用树的顺序存储结构来存储,但树不能使用二叉树的存储结构来存储。
- 不完全相同。虽然双亲表示法和二叉树的顺序存储都是树的顺序存储形式,但二者的存储方式和应用不同。二叉树的顺序存储通过数组下标直接表示节点之间的父子关系,如第 i 个节点与其左右孩子 2i 和 2i+1 之间的关系,而双亲表示法则使用数组存储每个节点的父节点指针,并不直接存储节点之间的层次结构。因此,双亲表示法不能完全等同于二叉树的顺序存储。
- 孩子表示法是将每个结点的孩子结点视为一个线性表,且以单链表作为存储结构,则n个结点就有n个孩子链表(叶节点的孩子链表是空表)。而n个头指针又组成一个线性表,为便于查找,可采用顺序存储结构。
- 孩子兄弟表示法是以二叉链表作为树的存储结构,使每个结点都包含三部分内容:结点值、指向结点第一个孩子结点的指针,以及指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)。
- 树的孩子兄弟表示法比较灵活,其最大的优点是可以方便地实现树转换为二叉树的操作,易于查找结点的孩子,但缺点是从当前结点查找其双亲结点比较麻烦。若为每个结点增设一个parent域指向其父结点,则查找结点的父结点也很方便。
- 可变长度编码特点是对频率高的字符赋以短编码,对频率较低的字符则赋以较长一些的编码,从而可以使字符的平均编码长度简短,起到压缩数据的效果。
挖
概念我感觉只要能在做题的时候想起来对应的条件就好,不用一字不差
- 树是n(n>=0)个结点的【】。当n=0时,称为【】。
- 在任意一棵非空树中应满足【】。
- 树作为一种逻辑结构,同时也是一种分层结构,具有两个特点【】、【】。
- 在树的定义中又用到了其自身,故树的定义是【】的。
- 树适用于表示具有【】结构的数据,树中的某个结点最多只与【】有直接关系,根节点没有【】,因此n个结点的树中有【】条边。而树中每个结点与其下一层的零个或多个结点,即【】都有直接关系。
- 考虑K结点,K的祖先是指【】,双亲是【】,有【】的结点称为兄弟,故K的兄弟有【】,而【】的结点互为堂兄弟,K的堂兄弟有【】;考虑D结点,它的子孙有【】,孩子包括【】。
- 度是指树中一个结点的【】,树中结点的最大度数称为【】,结点B 的度为【】,图中树的度为【】。
- 分支结点是指【】的结点,又称为非终端结点;叶节点是指【】的结点,它没有【】
- 结点的层次是从【】 开始定义的,结点L位于【】,结点的深度就是【】,上图中的树的高度为【】,结点的高度为【】,例如图中B的高度为【】。
- 有序树中结点的各子树【】都是有次序的,不能【】。
- 同一双亲的两个孩子之间不存在路径,因为【】。
- 森林是【】的集合。
- 所有结点的度数加1等于【】。
- 度为m中的树中第i层上至多有【】个结点。
- 高度为h的m叉树至多有【】个结点。
- 二叉树的特点【】,并且其次序【】,所以将其左右子树颠倒,则成为另一棵不同的二叉树。
- 满二叉树的高度为h则其有【】个结点,可以对满二叉树按次序编号,根节点为1,自上而下,自左至右,对于编号为i的编号,若有双亲,其编号为【】,若有左孩子,其编号为【】,若有右孩子,则其编号为【】。
- 完全二叉树是指当且仅当其每个结点都与高度为h的满二叉树中【】的结点一一对应的二叉树。
- 完全二叉树中,若 i < = ⌊ n / 2 ⌋ i<=\lfloor n/2 \rfloor i<=⌊n/2⌋,则结点i为【】,若 i > ⌊ n / 2 ⌋ i>\lfloor n/2 \rfloor i>⌊n/2⌋,则为【】,最大分支结点的编号为【】。
- 完全二叉树中,叶节点只可能在【】层次上出现。
- 完全二叉树中,树的度为1,则结点数为【】。
- 完全二叉树中,一旦出现某个结点(编号为i)为叶节点或只有左孩子,则编号大于i的结点【】。
- 完全二叉树中,结点i所在的层次为【】。
- 完全二叉树中,具有n个结点,则高度为【】 或 【】。
- 若n为奇数,则每个分支结点【】;若n为偶数,则编号最大的分支结点(编号为n/2)【】,其余结点【】。
二叉排序树、平衡二叉树后面复习 - 树中每个分支结点都有2个孩子,树中的度只有0或2的结点,这样的树为【】。
- 非空二叉树的第k层最多有【】个结点。
- 高度为h的二叉树至多有【】个结点。
- 在含有n个结点的二叉链表中,含有【】个空链域。
- 已知【】序列,再给出其他三种遍历序列中的任意一种,就可以唯一确定一棵二叉树。
- 结点中除了含有左右孩子和data以外,增加了两个标志域,以标识指针域指向左/右孩子或前驱/后继,这样的结点构成的二叉链表作为二叉树的存储结构,称为【】,其中指向结点前驱和后继的指针称为【】,加上这种结构的二叉树称为【】。
- 二叉树的线索化是将二叉链表中的【】改为指向【】的线索,线索化的实质就是【】,是因为【】。
- 带头节点的中序线索二叉树,其头结点的lchild域的指针指向【】,rchild域指针指向【】;令二叉树中序序列中的【】和【】均指向头结点。就相当于建立了一个双向线索链表,方便从前往后或者从后往前对线索二叉树进行遍历。
- 如上图所示,B的后继无法通过链域找到,可见在后序线索二叉树上找后继时需要直到结点双亲,即需采用【】作为存储结构。
- 树的存储方式有多种,即可采用【】,又可以采用【】,但无论采用何种存储方式,都要求能唯一地反映【】的逻辑关系,其有三种常见的存储结构分别为【】、【】和【】。
- 树的孩子表示法和双亲表示法各有优点,前者寻找【】的操作非常方便,后者则是寻找【】,如果这两个方法找另一方都需要【】。
- 树的孩子兄弟表示法又称【】,即以【】作为树的存储结构。
- 当森林转换为二叉树时,其第一棵树的子树森林转换为【】,剩余树的森林转换为【】,可知森林的先序遍历和中序遍历即为其对应二叉树的【】和【】遍历。
- 从树中的一个节点到另一个结点之间的分支branch构成这两个结点之间的【】,其上的branch数目称为【】,如果结点被赋予一个表示某种意义的数值,称为该结点的【】,从树的根到一个结点的路径长度与该结点上权值的成乘积称为该结点的【】,当WPL最小时,它恰好是【】。
哈夫曼编码是后两种编码方式,且第一种对应的二叉树就是一个满二叉树 or 缺最后一个结点就可以是满二叉树的完全二叉树;第三种对应的二叉树特点就是所有要被编码的结点均为叶节点。
- 在数据通信中,若对每个字符使用相等长度的二进制表示,称这种编码方式为【】;若允许对不同字符用不等长的二进制位表示,则这种编码方式称为【】;若没有一个编码是另一个编码的前缀,则称之为【】。
- 并查集的存储结构通常用【】
空
- 有限集、空树
- 有且仅有一个特定的称为根的结点;当n>1时,其余结点可分为m(m>0)个互不相交的有限集 T 1 , T 2 , ⋅ ⋅ ⋅ , T m T_1,T_2,···,T_m T1,T2,⋅⋅⋅,Tm,其中每个集合又是一棵树,并成为根的子树。
- 树的根节点没有前驱,除根节点外的所有结点有且只有一个前驱、树中所有结点都可以有零个或多个后继。
- 递归
- 层次、上一层的一个结点即父节点、直接上层节点(父节点)、n-1、子节点
- 从根A到结点K的唯一路径上的所有其他结点、路径上最接近K的结点E、相同双亲、L、双亲在同一层、M和L、H I J M、H I J
- 孩子个数、树的度、2、3
- 度大于0、度为零、孩子结点
- 根节点、第4层、结点所在的层次、4、该节点为根的子树的高度、2
- 从左到右、互换
- 树的分支是有向的,从上至下,从双亲到孩子
- m(m>=0)棵互不相交的树
- 结点的个数n
- m i − 1 ( i > = 1 ) m^{i-1}(i>=1) mi−1(i>=1)
- ( m h − 1 ) / ( m − 1 ) < = > ( 1 + m 2 + m 3 + ⋅ ⋅ ⋅ + m h − 1 ) (m^h-1)/(m-1) <=>( 1+m^2+m^3+···+m^{h-1}) (mh−1)/(m−1)<=>(1+m2+m3+⋅⋅⋅+mh−1)
- 至多只有两棵子树(度最多为2)、不能颠倒
- 2 h − 1 2^h-1 2h−1、 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋、2i、2i+1
- 编号1~n
- 分支节点、叶节点、 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor ⌊n/2⌋
- 层次最大的两个
- 2
- 均为叶节点(叶节点为在倒数第二大的层次时)
- 2 h − 1 < = i n d e x < = 2 h − 1 = = > h = ⌊ l o g 2 i ⌋ + 1 2^{h-1}<= index <=2^{h}-1 ==> h = \lfloor log_2 i \rfloor + 1 2h−1<=index<=2h−1==>h=⌊log2i⌋+1
高度为 2 h − 1 − 1 < n < = 2 h − 1 或 2 h − 1 < = n < 2 h 2^{h-1} - 1< n<=2^{h}-1 或 2^{h-1} <= n<2^{h} 2h−1−1<n<=2h−1或2h−1<=n<2h根据其中闭区间可得 h = ⌈ l o g 2 ( n + 1 ) ⌉ 或 ⌊ l o g 2 ( n ) ⌋ + 1 h = \lceil log_2(n+1) \rceil 或 \lfloor log_2(n)\rfloor+1 h=⌈log2(n+1)⌉或⌊log2(n)⌋+1【记忆:取对数后如果n大于等于向下取整,否则向上取整】
- 2 h − 1 < = n < = 2 h − 1 = = > h = ⌊ l o g 2 n ⌋ + 1 2^{h-1}<= n <=2^{h}-1 ==> h = \lfloor log_2 n \rfloor + 1 2h−1<=n<=2h−1==>h=⌊log2n⌋+1 、 2 h − 1 < = n < 2 h = = > h = ⌈ l o g 2 ( n + 1 ) ⌉ 2^{h-1} <= n<2^{h} ==> h = \lceil log_2(n+1) \rceil 2h−1<=n<2h==>h=⌈log2(n+1)⌉
- 均有左孩子和右孩子、只有左孩子没有右孩子、左右孩子均有
- 正则二叉树
- 2 k − 1 2^{k-1} 2k−1
- 1 + 2 + 2 2 + ⋅ ⋅ ⋅ + 2 ( h − 1 ) = ( 2 h − 1 ) / ( 2 − 1 ) 1+2+2^2+···+2^(h-1) = (2^{h}-1)/(2-1) 1+2+22+⋅⋅⋅+2(h−1)=(2h−1)/(2−1)
- n+1
- 中序
- 线索链表、线索、线索二叉树
- 空指针、前驱或后继、遍历一次二叉树、前驱后继的信息只有在遍历时才能得到
- 根结点、中序遍历的最后一个结点、第一个结点的lchild域指针、最后一个结点的rchild域指针
- 带标志域的三叉链表
- 顺序存储结构、链式存储结构、树中各结点之间 、双亲表示法、孩子表示法、孩子兄弟表示法
- 孩子结点、唯一双亲、遍历整个结构
- 二叉树表示法、二叉链表
- 左子树、右子树、先序、中序
- 路径、路径长度、权值、带权路径长度WPL、哈夫曼树
- 固定长度编码、可变长度编码、前缀编码
- 树的双亲表示
选择(仅错题)+ 一些做题总结
5.1.4的第7道选择题应该选D 但答案有问题,解析没问题
【错误选项】C
【思路】🌳的路径长度是从根到每个结点的路径长度的总和;根到每个结点的路径长度的最大值是树的高度-1。
【区分】哈夫曼树的WLP
【错误选项】B
【思路】最小高度–>只有一个结点的度为2 or 1或者全为度3–>
(
3
h
−
1
)
/
2
>
=
50
(3^h-1)/2 >= 50
(3h−1)/2>=50–>h>=5选C
【勘误】D
【错误选项】D
【思路】A对,叶节点的双亲的左兄弟肯定有左右孩子;B
n
0
=
n
2
+
1
n_0 = n_2 +1
n0=n2+1,B错;C双亲表示法为顺序存储,C错;D错在左孩子不一定存在,所以选A
【错误选项】C
【思路】Ⅰ中空指针数量 =
n
0
∗
2
+
n
1
+
1
=
n
0
+
n
1
+
n
2
+
1
+
1
=
n
+
2
n_0*2+n_1+1 = n_0 + n_1 + n_2 +1 +1= n+2
n0∗2+n1+1=n0+n1+n2+1+1=n+2;Ⅱ明显错,根节点只有2个指针;Ⅲ特殊情况,整棵树只有一个结点的情况时,Ⅲ错。所以选A。
【错误原因】三叉链表包含了双亲,忽略了双亲,所以以为是n+1(二刷)
【错误选项】A
【思路】从完全二叉树中,叶节点只能存在于最大的两层这个知识点来看,第六层最后8个结点为叶节点,还有第七层,所以最多结点数
n
m
a
x
=
2
6
−
1
+
(
2
5
−
8
)
∗
2
=
111
n_{max} = 2^6-1+(2^5-8)*2 = 111
nmax=26−1+(25−8)∗2=111所以选C。
【错误选项】B
【思路】二叉树采用顺序存储时,用数组下标来表示结点之间的父子关系。对于一棵高为5的二叉树,为满足任意性,其1-5层的所有节点都要被存储起来,即考虑为满二叉树的结点个数为2^5-1 = 31,所以选A。
【错误思路】存储单元数量最少–>第五层只有一个节点
n
u
m
m
i
n
=
2
4
−
1
+
1
=
16
num_{min} = 2^4 -1 + 1 = 16
nummin=24−1+1=16,他不是说这个:有10个结点且高度为5的 情况下,存储单元最低可以是多少;而是问最大值,这样分配最小存储单元数量,保证无论在什么情况下,这个最小分配数都可以满足这棵树的存储。
4 5 都记一下结论
【错误选项】C
【思路】后序–>[left][right]root;主辨析C选项,其需要增加条件:两个结点在同一层的情况才可以满足;排除法,选D
【错误选项】B
【思路】中序肯定不行,如果n在m的左子树上,那就找不到路径了。
【Answer】在后序遍历退回时访问根节点,就可以从下向上把从n到m的路径上的节点输出,全都倒着来。
【勘误】A
【错误 正确思路】首先明确越后遍历值越大:A中先b后a;B中先b后a;C中可以是先a后b也可以是先b后a;D中先a后b
我不是很确定,如果有朋友做到了这道题,帮忙看看呢
记23的结论
【错误选项】A
【思路】二叉树是一种逻辑结构,线索二叉树是加上线索后的链表结构,所以是一种存储结构也就是一种物理结构,所以选C。
【错误选项】B
【思路】线索数 = 空指针数 =
2
∗
n
0
+
n
1
=
n
+
1
2*n_0+n_1 = n+1
2∗n0+n1=n+1选C,这咋能错了呢
记28结论;
所以每个结点通过线索都可以直接找到它的前驱和后继这个论述是错误的。27
【错误选项】?
【思路】后序线索二叉树不能有效解决后序后继问题,如下图所示,结点E的右指针指向右孩子,而在后序序列中E的后继结点为B,在查找E的后继时仍然只能按常规方法来寻找。选D
在
5.4.4中的一个重要结论:森林/树转二叉树时,原集合中的叶节点数 == 二叉树中没有左孩子的结点数,这些错题也不亏,都是卡了半天的题
【错误选项】C
【思路】在树的兄弟孩子表示法中,如果一个结点没有孩子,则表现为该结点的左指针域为空,因此本题答案为6选B。是读题有偏差??
15有点难顶,后面刷到这里再重新做一遍
【错误选项】?
【思路】树转换为二叉树时,树的每个分支结点的所有子结点中的最右结点无右孩子,根节点转换后也没有右孩子,因此对应二叉树中无右孩子节点个数 = 分支结点数 + 1 = 2011-116+1 = 1896
【错误选项】B
【思路】选C,只是说一下错误思路,认为这里所有叶节点都尽可能在同一层,这个思路是错的
【错误选项】D
【思路】比较迷惑的就是1-2 1-8 1-9:其实就是{12}与{789}合并了,所以这里肯定是C不是D,我原本以为是把1单独切到{789}的集合中,2又单独一个集合,这里是错误的。
记这里的加权平均长度的计算
【错误选项】C
【思路】先构建哈夫曼树,4个长度为3 2个长度为2,加权平均长度 = WLP/(sum of 权值) 所以这里是[(3+4+5+6)*3 + (8+10)*2 ] /(3+4+5+6+8+10) = 90/36 = 2.5选B.
应用题summary
只写一些容易忘记的应用题思路
- 前中后序遍历
- 线索二叉树的创建:先列对应顺序的遍历序列,找哪个结点无左/右孩子,左孩子指向前驱结点,右孩子指向后继结点,为空就画个箭头指出来就好。
- 孩子兄弟表示法中树转二叉树,二叉树转树:左孩子右兄弟
- 森林转二叉树:每棵树:左孩子右兄弟;每棵树的根也视为兄弟关系;注意角度!需要以第一棵树的根作为轴心顺时针旋转 4 5 。 45^。 45。。
- 二叉树转森林:把上面过程反过来(。・・)ノ
- 树的先根遍历:读根、读子树从左至右 <=> 森林的先序 、 二叉树的先序
- 树的后根遍历:读子树从左至右、读根 <=> 森林的中序 、 二叉树的中序
- 森林的先序遍历:读第一棵树的根、读其子树、按前面的顺序读其他树
- 森林的中序遍历:读第一棵树的子树、读其根、按前面的顺序读其他树
出现次数<=>结点权值
- 哈夫曼树的创建:已知结点集中最小的两个结点按升序分别左右子树、父结点权值二者相加、比较父结点和其余结点、反复
- 哈夫曼编码:构造哈夫曼树,左分支0右分支1,按照结点的路径写出对应结点的哈夫曼编码。但左0右1还是左1右0看题干信息
代码内容
二叉树
和二叉树的先中后序遍历相关的算法设计,围绕root的位置 left的区间范围 right的区间范围为核心展开 leetcode 105 106、非递归程序的实现
链式存储结构
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
先序遍历
递归程序
void PreOrder(BiTree T){
if(T!=NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
非递归程序,使用栈存储结点以完成root[left][right]的形式
首先,将根节点压入栈中,然后进入一个循环,直到栈为空。在每次循环中,我们从栈顶弹出一个节点并访问它,然后依次检查它的右孩子和左孩子,如果存在右孩子,先将其压入栈中,再将左孩子压入栈中。由于栈是后进先出,这样在下一次循环中,左孩子会先被处理,保证了先序遍历的顺序,即根节点先访问,然后是左子树,最后是右子树。
void PreOrder(BiTree T){
InitSatck(S); BiTree p = T;
while(p || !IsEmpty(S)){
if(p){
visit(p);
Push(S,p);
p = p->lchild;
}else{
Pop(S,p);
p = p->rchild;
}
}
}
中序遍历
递归程序
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
非递归程序,使用栈存储结点以完成[left]root[right]的形式
与先序类似,只需把访问结点操作放在出栈操作的后面
void InOrder(BiTree T){
InitStack(s);BiTree p = T;
while(p || !IsEmpty(s)){
if(p){
Push(s,p);
p=p->lchild;
}else{
Pop(s,p);
visit(p);
p = p->rchild;
}
}
}
后序遍历
递归程序
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
非递归程序,使用栈存储结点以完成[left][right]root的形式:
从根节点开始,将其入栈,然后沿其左子树一直往下搜索,知道搜索到没有左孩子的结点;但是此时不能出栈访问,因为若其有右子树,则还需按照相同的规则对右子树进行处理。直至上述操作进行不下去,若栈顶元素想要出栈被访问,要么右子树为空,要么右子树刚被访问完,这样就保证了正确的访问顺序。
void PostOrder(BiTree t){
InitStack(s);
BiTree p = t;//遍历指针
BiTNode* lastVisited = NULL;
while(p || !IsEmpty(s)){
if(p){
Push(s,p);
p = p->lchild;
}else{
GetTop(s,p);
//要对右孩子做相同的处理直至没有右孩子才能visit
if(p->right != NULL && lastVisited != p->right){
p = p->right;
}else{
visit(p);
lastVisited = p;
Pop(s,p);
p = NULL;//防止再次进入左子树的循环
}
}
}
}
层次遍历
void LevelOrder(BiTree t){
InitQueue(q);
BiTree p = t;
//根节点入队
EnQueue(q , t);
while(!IsEmpty(q)){
DeQueue(q,p);
visit(p);
if(p->lchild!=NULL){
EnQueue(q,p->lchild);
}
if(p->rchild!=NULL){
EnQueue(q,p->rchild);
}
}
}
线索二叉树的存储结构
若无左子树,令lchild指向其前驱结点;若无右子树,令rchild指向其后继结点,除此之外,还需要增加两个标志域,以标识指针域指向左/右孩子或前驱/后继。
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag , rtag;
}ThreadNode , *ThreadTree;
//ltag = 1 指向前驱 rtag = 1 指向后继
//ltag = 0 指向左孩子 rtag = 0 指向右孩子
通过中序遍历对二叉树线索化的递归算法
pre指针指向刚刚访问过的结点,指针p指向正在访问的结点,即pre指向p的前驱。
先回忆递归的中序遍历算法代码
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->left,pre);
//将visit(p);替换为将p和pre的空指针域填满
if(p->lchild ==NULL){
//指向前驱
p->lchild = pre;
//标志域置1
p->ltag = 1;
}
//后继是对pre做处理
if(pre->rchild ==NULL){
//指向后继
pre->rchild = p;
//标志域置1
pre->rtag = 1;
}
pre = p;//对p处理完毕后替换pre;
InThread(p->right,pre);
}
}
//调用上述函数的过程
void CreateInThread(ThreadTree T){
ThreadTree pre = NULL;
if(T!=NULL){
InThread(T,pre);
pre->rchild = NULL;
pre->rtag = 1;
}
}
中序线索二叉树的遍历
中序线索二叉树的结点中隐含了线索二叉树的前驱和后继信息。在对其遍历的时候,只要先找到序列中的第一个结点,然后依次找结点的后继,直至其后继为空。for循环
在中序线索二叉树中找结点后继的规律是:若其右标志为1,则右链为线索,指示其后继,否则遍历右子树中第一个访问的结点(右子树最左下的结点)为其后继。
【不含头结点】
//找最左下的结点
ThreadNode *Firstnode(ThreadNode *p){
while(p->ltag == 0) p = p->lchild;//最左下的结点
return p;
}
//求中序线索二叉树中结点p在中序序列下的后继:上述第二段
ThreadNode *Nextnode(ThreadNode *p){
if(p->rtag == 0) return Firstnode(p->rchild);
else return p->rchild;
}
void Inorder(ThreadNode *T){
for(ThreadNode *p = Firstnode(T);p!=NULL;p = Nextnode(p)){
visit(p);
}
}
中序线索二叉树的最后一个结点和结点p前驱的运算
ThreadNode *Lastnode(ThreadNode *p){
//直至右指针域指向空--->rtag == 1
while(p->rtag == 0) p = p->rchild;
return p;
}
ThreadNode *Priornode(ThreadNode *p){
//判断p的ltag标识域,如果1 直接返回p ->lchild如果为0返回左子树的最后一个
if(p->ltag == 0) return Lastnode(p->lchild);
else return p->lchild;
}
树、森林
双亲表示法
这种存储结构采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲在数组中的位置。
#define MAX_TREE_SIZE 100
typedef struct{
ElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MAX_TREE_SIZE];//双亲表示
int n;//结点数
}PTree;
孩子兄弟表示法(Child-Sibling)
“Sibling” 是一个性别中立的词,泛指兄弟或姐妹,不区分性别。它可以指代任何一个兄弟姐妹,或者是兄弟姐妹的集合。
知识又从奇怪的地方进入了🧠
这种存储结构采用二叉链表存储每个结点,包含内容:结点值、指向第一个孩子结点的指针、指向下一个兄弟结点的指针
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
并查集
并查集的三个基本操作是:Initial Union Find 初始化 合并 查找
并查集的结构定义
#define SIZE 100
int UFSets[SIZE];
初始化
将集合S中的每个元素都视为只有一个单元素的子集合
void Initial(int S[]){
for(int i = 0 ; i < SIZE ; i++){
S[i] = -1;
}
}
查找
查找集合S中单元素x所在的子集合,并返回该子集合的根节点。
int Find(int S[] , int x){
while(S[x] >= 0){ // 寻找x的根
x = S[x];
}
return x; //根的S[]小于0
}
合并
将集合S中的子集合Root2并入子集合Root1,要求二者并不🍌,否则不执行合并
void Union(int S[] , int Root1 ,int Root2){
if(Root1 == Root2) return; //要求1 2是不同的集合
S[Root2] = Root1; //将根2连接到根1中
}
并查集实现的优化
在极端情况下,n个元素构成的集合树的深度为n,则Find操作的最坏时间复杂度为O(n)。改进方式:在做Union之前,首先判别子集中的成员数量,然后令成员少的根指向成员多的根,即把小树合并到大树,为此可令根节点的绝对值保存集合树中的成员数量。
void Union(int S[], int Root1, int Root2){
if(Root1 == Root2) return ;
if(S[Root2]>S[Root1]){//2为小树
S[Root1] += S[Root2];
S[Root2] = Root1;
}else{
S[Root2] += S[Root1];
S[Root1] = Root2;
}
}
随着子集逐对合并,集合树的深度越来越大,为了进一步减少确定元素所在集合的时间,优化Find操作:当所查元素x不在树的第二层时,在算法中增加一个“压缩路径”的功能,即将从根到元素x路径上的所有元素都变成为根的孩子。
void Find(int S[], int x){
int root = x;
while(S[root] >= 0){ //循环找到根S[root] < 0
root = s[root];
}
while(x != root){ //将从根到元素x路径上的所有元素都变成为根的孩子。
int t = S[x];
S[x] = root;
x = t;
}
return root;
}