以下笔记为jjyaoao本人制作,欢迎借鉴学习和提出相关建议,转载需要标明出处www.jjyaoao.space

应用

image-20220227190959555

有根树

image-20220227191351981

有序树

凡是兄弟之间有明确次序的树,我们把它叫做有序树

任何一颗树中所含的边数 = 顶点数目-1 = 其中所有定点度数之和

这个结论之所以重要,是因为我们可以得到边数和顶点个数是同阶的

也就是说,如果一棵树的总体规模,如果可以度量为顶点数+边数,那么它从渐进的意义来讲,是与顶点个数同阶的,也就是我们后面评判一棵树的规模,可以以顶点的数目n作为参照

image-20220227191606564

路径+环路

这里我们用边的数目来代表它的路径长度

image-20220227192230822

连通+无环

image-20220227192652059

深度+层次

image-20220227193021833

image-20220227193322474

何时取等号?

当节点v是深度最大的叶节点或祖先节点时,取等号

树的表示

ADT接口

image-20220227193655840

父节点

image-20220227193759974

image-20220227193901814

孩子节点

image-20220227194115628

父亲+孩子

整合了父亲 和 孩子的优点

image-20220227194422283

长子+兄弟

反观父亲+孩子出现的问题

根源在于每一个节点的出度是不尽相同的

解决方案:每一个节点均设两个引用

这种方式是 对树本质的深刻理解

image-20220227194730644

二叉树

有根有序树

image-20220227195032678

image-20220227195211681

真二叉树

如果某一棵树原先度数是0,那么我们为他引入两个节点,如果原先度数是1,那么我们为他引入一个节点。

  • 一方面从渐进的意义上讲,阶数相当
  • 另一方面,当我们后续实现就能看出,这种添加完全是假象的,不需要实际的添加节点进入,只是方便理解,实现。
  • 犹如虚拟实现,想象它背后的逻辑是对的,不需要实际的实现它

image-20220227195555277

描述多叉树

表面上是不可能的,竟然用一个特例,来描述整个现象

这也解释了为何树会变成一个小的标题;:因为二叉树就可以代表树

image-20220227200404015

二叉树实现

BinNode类

image-20220227200736089

接口实现

image-20220227201159646

BinTree类

虚方法updateHeight是为了,后面庞大的衍生类能对这个方法进行适当的重写

image-20220227201346494

高度更新

未涉及到变种,仍采用常规定义

image-20220227201850643

节点插入

通过调用binNode类的同名接口insertAsrc() 传入参数e,意味着创建一个e的节点,并且让x指向这个接口所返回的那个地址交给x此前为空的右孩子引用,从而使得引用能指向新晋生成的节点

image-20220227203021139

子树接入

image-20220227203139822

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T> //将S当作节点x的左子树接入二叉树,S本身置空
0002 BinNodePosi<T> BinTree<T>::attach ( BinTree<T>* &S, BinNodePosi<T> x ) { //x->lc == NULL
0003 if ( x->lc = S->_root ) x->lc->parent = x; //接入
0004 _size += S->_size; updateHeightAbove ( x ); //更新全树规模与x所有祖先的高度
0005 S->_root = NULL; S->_size = 0; release ( S ); S = NULL; return x; //释放原树,返回接入位置
0006 }
0007
0008 template <typename T> //将S当作节点x的右子树接入二叉树,S本身置空
0009 BinNodePosi<T> BinTree<T>::attach ( BinNodePosi<T> x, BinTree<T>* &S ) { //x->rc == NULL
0010 if ( x->rc = S->_root ) x->rc->parent = x; //接入
0011 _size += S->_size; updateHeightAbove ( x ); //更新全树规模与x所有祖先的高度
0012 S->_root = NULL; S->_size = 0; release ( S ); S = NULL; return x; //释放原树,返回接入位置
0013 }

子树删除

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T> //删除二叉树中位置x处的节点及其后代,返回被删除节点的数值
0002 int BinTree<T>::remove ( BinNodePosi<T> x ) { //assert: x为二叉树中的合法位置
0003 FromParentTo ( *x ) = NULL; //切断来自父节点的指针
0004 updateHeightAbove ( x->parent ); //更新祖先高度
0005 int n = removeAt ( x ); _size -= n; return n; //删除子树x,更新规模,返回删除节点总数
0006 }
0007 template <typename T> //删除二叉树中位置x处的节点及其后代,返回被删除节点的数值
0008 static int removeAt ( BinNodePosi<T> x ) { //assert: x为二叉树中的合法位置
0009 if ( !x ) return 0; //递归基:空树
0010 int n = 1 + removeAt ( x->lc ) + removeAt ( x->rc ); //递归释放左、右子树
0011 release ( x->data ); release ( x ); return n; //释放被摘除节点,并返回删除节点总数
0012 } //release()负责释放复杂结构,与算法无直接关系,具体实现详见代码包

子树分离

1
2
3
4
5
6
7
template <typename T> //二叉树子树分离算法:将子树x从当前树中摘除,将其封装为一棵独立子树返回
0002 BinTree<T>* BinTree<T>::secede ( BinNodePosi<T> x ) { //assert: x为二叉树中的合法位置
0003 FromParentTo ( *x ) = NULL; //切断来自父节点的指针
0004 updateHeightAbove ( x->parent ); //更新原树中所有祖先的高度
0005 BinTree<T>* S = new BinTree<T>; S->_root = x; x->parent = NULL; //新树以x为根
0006 S->_size = x->size(); _size -= S->_size; return S; //更新规模,返回分离出来的子树
0007 }

遍历

image-20220227203444357

递归—-》迭代是很有必要的,递归的过程只具有渐进意义的O(n),因为它每一步所装的箱子都是一样的大小,我们完全可以根据实际情况,来把他箱子改小,这在实际应用具有巨大的意义,虽然只是常数的提升!

先中后都属于深度搜索

VST貌似是指针函数的引用

先序遍历

visit里面则放你需要做的事情(取来干嘛)

递归

image-20220227203724142

迭代

为此,我们只需引入栈

版本1
思路

image-20220227204013897

实现

image-20220227204309966

实例

染黑来代表这个节点接受访问

image-20220227204410029

image-20220227204521557

迭代版2

为了推广到后面的中序,后序算法

新思路

image-20220227204631651

image-20220227204742910

实现

最好熟读,并且背下来!

image-20220227204924207

image-20220227205241760

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//从当前节点出发,沿左分支不断深入,直至没有左分支的节点;沿途节点遇到后立即访问
0002 template <typename T, typename VST> //元素类型、操作器
0003 static void visitAlongVine (
BinNodePosi<T> x,
VST& visit,
Stack<BinNodePosi<T>>& S )
{ //这里的visitAlongVine就是上面那张图的....leftbrach
0004 while ( x ) {
0005 visit ( x->data ); //访问当前节点
0006 S.push ( x->rc ); //右孩子入栈暂存(可优化:通过判断,避免空的右孩子入栈)
0007 x = x->lc; //沿左分支深入一层
0008 }
0009 }
0010
0011 template <typename T, typename VST> //元素类型、操作器
0012 void travPre_I2 ( BinNodePosi<T> x, VST& visit ) { //二叉树先序遍历算法(迭代版#2)
0013 Stack<BinNodePosi<T>> S; //辅助栈
0014 while ( true ) {
0015 visitAlongVine ( x, visit, S ); //从当前节点出发,逐批访问
0016 if ( S.empty() ) break; //直到栈空
0017 x = S.pop(); //弹出下一批的起点
0018 }
0019 }
实例

image-20220227205258504

中序遍历

递归

image-20220227205401863

改写迭代难度比先序要大,因为它并不是全是尾递归!!!

难点

image-20220227205513370

观察

image-20220227205808403

思路

image-20220227205928756

实现

与先序遍历的区别——-visit –> go(vine)

image-20220227210525111

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T> //从当前节点出发,沿左分支不断深入,直至没有左分支的节点
0002 static void goAlongVine ( BinNodePosi<T> x, Stack<BinNodePosi<T>>& S ) {
0003 while ( x ) { S.push ( x ); x = x->lc; } //当前节点入栈后随即向左侧分支深入,迭代直到无左孩子
0004 }
0005
0006 template <typename T, typename VST> //元素类型、操作器
0007 void travIn_I1 ( BinNodePosi<T> x, VST& visit ) { //二叉树中序遍历算法(迭代版#1)
0008 Stack<BinNodePosi<T>> S; //辅助栈
0009 while ( true ) {
0010 goAlongVine ( x, S ); //从当前节点出发,逐批入栈
0011 if ( S.empty() ) break; //直至所有节点处理完毕
0012 x = S.pop(); visit ( x->data ); //弹出栈顶节点并访问之
0013 x = x->rc; //转向右子树
0014 }
0015 }

实例

image-20220227210602115

分摊分析

image-20220227210908305

难道我们的结论是总体需要n²的时间吗???

整个算法的确呈现迭代,而且每一步的goalongleftbranch或长或短,甚至有可能有达到Ω(n)的量级

​ 不过我们不难发现,所有左侧链的长度加起来,也只是n 因此,我们刚才所得到的n²的结果只是一个假象

​ 而我们得到n²的结论只是将估计量括起来当成一个方框,而累计的复杂度则是方框的面积

image-20220227211211051

后序遍历

递归

image-20220227211455758

思路

image-20220227211629424

实现

image-20220227211717915

image-20220227211832376

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename T> //在以S栈顶节点为根的子树中,找到最高左侧可见叶节点
0002 static void gotoLeftmostLeaf ( Stack<BinNodePosi<T>>& S ) { //沿途所遇节点依次入栈
0003 while ( BinNodePosi<T> x = S.top() ) //自顶而下,反复检查当前节点(即栈顶)
0004 if ( HasLChild ( *x ) ) { //尽可能向左
0005 if ( HasRChild ( *x ) ) S.push ( x->rc ); //若有右孩子,优先入栈
0006 S.push ( x->lc ); //然后才转至左孩子
0007 } else //实不得已
0008 S.push ( x->rc ); //才向右
0009 S.pop(); //返回之前,弹出栈顶的空节点
0010 }
0011
0012 template <typename T, typename VST>
0013 void travPost_I ( BinNodePosi<T> x, VST& visit ) { //二叉树的后序遍历(迭代版)
0014 Stack<BinNodePosi<T>> S; //辅助栈
0015 if ( x ) S.push ( x ); //根节点入栈
0016 while ( !S.empty() ) { //x始终为当前节点
0017 if ( S.top() != x->parent ) ////若栈顶非x之父(而为右兄)
0018 gotoLeftmostLeaf ( S ); //则在其右兄子树中找到HLVFL(相当于递归深入)
0019 x = S.pop(); visit ( x->data ); //弹出栈顶(即前一节点之后继),并访问之
0020 }
0021 }

表达式树(RPN)

盯住红色部分

image-20220227212132025

image-20220227212124292

仔细观察,这就是一个后序遍历

也可以理解为不断求值的过程

所以说RPN是不需要括号的,与其说是不需要,不如说是背后隐藏了一棵树在里面

层次遍历

实现

可以看到层次遍历的接口形式和其他遍历完全一致

image-20220228104402805

1
2
3
4
5
6
7
8
9
10
template <typename T> template <typename VST> //元素类型、操作器
0002 void BinNode<T>::travLevel ( VST& visit ) { //二叉树层次遍历算法
0003 Queue<BinNodePosi<T>> Q; //辅助队列
0004 Q.enqueue ( this ); //根节点入队
0005 while ( !Q.empty() ) { //在队列再次变空之前,反复迭代
0006 BinNodePosi<T> x = Q.dequeue(); visit ( x->data ); //取出队首节点并访问之
0007 if ( HasLChild ( *x ) ) Q.enqueue ( x->lc ); //左孩子入队
0008 if ( HasRChild ( *x ) ) Q.enqueue ( x->rc ); //右孩子入队
0009 }
0010 }

实例

节点进入后,左顾右盼,先左顾后右判 这里使用的是队列 和之前先序遍历使用的栈不同,先序遍历就是先右后左

image-20220228104622270

image-20220228104829062

遍历序列

我们可以知道,任何一棵树都能有三种遍历序列,

那现在我们试想一下,如果我们知道某棵树的遍历序列是否我们能够还原成那一棵树的初始状态,如果可以,那我们使用什么方法?

先序|后序+中序

首先通过 先/后 序遍历序列知道了根

再通过中序遍历,我们就可以很容易的将左右子树拆分开

为何我们不能通过先/后序列来确定整棵树的位置?

当右子树是空的的时候。我们得到的数据是这样的。

image-20220228105656140

当左子树是空的,我们得到的图是这样的

image-20220228105749570

可以看到,这里出现了歧义——>

也就是,我们无法确定,到底真正的图是缺少了左子树还是右子树

(先序+后续)*真

既然是真二叉树,那么它一定会有左子树和右子树,也就是说,我们先序遍历的第二个位置一定是左子树节点,后序遍历倒数第二个位置一定是右子树节点,我们从而可以快速,相互的定位他们在不同遍历中的位置。

image-20220228110415453

在这里,确实可以进行分而治之,从而通过递归的形式,完全可以重现出二叉树完整的结构

知道序列画树

​ 我们通过观察,可知:

image-20220227204631651

​ 先序遍历的顺序,即为子树向左下方画出一条一条的线,同理可得,后序遍历的访问次序,是向着左上的箭头,而中序遍历是从左往右的结点顺序,因此,可得如下思路:tree

我们可以得到,他的顺序

  • 先序A B D E C F
  • 中序D B E A C F
  • 后序D E B F C A

观察,因为能构建出树,必有中序,所以通过中序和先(后)序得到的根节点,画出基础树,这里,我们用后序+中序来举例

​ A 不知道已经发现了吗,中序中,我们先不考虑根结点,可以发现,我们这里采用的遍历是按照左上

D B E C F 一列一列划线,意味着我们只需要默想后序的顺序,然后看着左边的基础树,脑海中一直想象着划线,如果与脑海中的相违背,就断一下,逐渐就会画好了hhhh,只是突然想到了,记忆下来图一乐,不知道会不会用到哟,估计到时候都忘了tree1

Huffman树

PFC编码

image-20220228110530053

image-20220228110854380

编码成本

image-20220228111104371

其实优化的过程就是把M节点和那个蓝色节点交换,则得到了较优的结果

那么,为什么我们能赚到呢?

是因为我们出现了高度差为2的’’叶子’’节点,3,4更不用说,但这也必然会赚到,因为更多的点深度低了,(相比于深度高了的少量节点)以多博少

频度

image-20220228111635194

带权编码成本

image-20220228111854006

四平八稳的方式不一定是最好的了……

image-20220228112036346

高高 低低,一定是赚的

编码算法

我要构建出理想的树,有两个考虑方式,历史只记住的成功的一个,但我们作为学习,也需要考虑失败的那个,从高处开始挂下去,从低处往高树挂

image-20220228112328510

image-20220228112347632

构造编码树

使用先中后随便一种遍历方式,都可以得到他们的次序(DFS)

访问到以后,会记录绳子,我们把他从底端打印到顶端(栈未完全封装)

image-20220228112450337

image-20220228112501063

image-20220228112513027

解码

从第一个bit开始,最开始位置到根,如果是0就往左,1就往右,找到了叶子,就把叶子的那个字符pop()打印出来,然后自己reset()回到根,随着不断迭代,终止到最后一个叶子,从而完成了解码的工作

image-20220228113234676

image-20220228113305640