⼆叉树中的深搜
深度优先遍历(DFS,全称为 Depth First Traversal),是我们树或者图这样的数据结构中常⽤的
⼀种遍历算法。这个算法会尽可能深的搜索树或者图的分⽀,直到⼀条路径上的所有节点都被遍历
完毕,然后再回溯到上⼀层,继续找⼀条路遍历。
在⼆叉树中,常⻅的深度优先遍历为:前序遍历、中序遍历以及后序遍历。
因为树的定义本⾝就是递归定义,因此采⽤递归的⽅法去实现树的三种遍历不仅容易理解⽽且代码很简洁。并且前中后序三种遍历的唯⼀区别就是访问根节点的时机不同,在做题的时候,选择⼀个适当的遍历顺序,对于算法的理解是⾮常有帮助的。
例题一
解法(递归):
算法思路:
本题可以被解释为:
1.
对于规模为 n 的问题,需要求得当前节点值。
2.
节点值不为 0 或 1 时,规模为 n 的问题可以被拆分为规模为 n-1 的⼦问题:
a.
所有⼦节点的值;
b.
通过⼦节点的值运算出当前节点值。
3.
当问题的规模变为 n=1 时,即叶⼦节点的值为 0 或 1,我们可以直接获取当前节点值为 0 或 1。
算法流程:
递归函数设计:bool evaluateTree(TreeNode* root)
1.
返回值:当前节点值;
2.
参数:当前节点指针。
3.
函数作⽤:求得当前节点通过逻辑运算符得出的值。
递归函数流程:
1.
当前问题规模为 n=1 时,即叶⼦节点,直接返回当前节点值;
2.
递归求得左右⼦节点的值;
3.
通过判断当前节点的逻辑运算符,计算左右⼦节点值运算得出的结果;
例题二
解法(dfs - 前序遍历):
前序遍历按照根节点、左⼦树、右⼦树的顺序遍历⼆叉树的所有节点,通常⽤于⼦节点的状态依赖于⽗节点状态的题⽬。
算法思路:
在前序遍历的过程中,我们可以往左右⼦树传递信息,并且在回溯时得到左右⼦树的返回值。递归函数可以帮我们完成两件事:
1.
将⽗节点的数字与当前节点的信息整合到⼀起,计算出当前节点的数字,然后传递到下⼀层进⾏递归;
2.
当遇到叶⼦节点的时候,就不再向下传递信息,⽽是将整合的结果向上⼀直回溯到根节点。
在递归结束时,根节点需要返回的值也就被更新为了整棵树的数字和。
算法流程:
递归函数设计:int dfs(TreeNode* root, int num)
1.
返回值:当前⼦树计算的结果(数字和);
2.
参数 num:递归过程中往下传递的信息(⽗节点的数字);
3.
函数作⽤:整合⽗节点的信息与当前节点的信息计算当前节点数字,并向下传递,在回溯时返回当前⼦树(当前节点作为⼦树根节点)数字和。
递归函数流程:
1.
当遇到空节点的时候,说明这条路从根节点开始没有分⽀,返回 0;
2.
结合⽗节点传下的信息以及当前节点的 val,计算出当前节点数字 sum;
3.
如果当前结点是叶⼦节点,直接返回整合后的结果 sum;
4.
如果当前结点不是叶⼦节点,将 sum 传到左右⼦树中去,得到左右⼦树中节点路径的数字和,然后相加后返回结果。
例题三
解法(dfs - 后序遍历):
后序遍历按照左⼦树、右⼦树、根节点的顺序遍历⼆叉树的所有节点,通常⽤于⽗节点的状态依赖于⼦节点状态的题⽬。
算法思路:
如果我们选择从上往下删除,我们需要收集左右⼦树的信息,这可能导致代码编写相对困难。然⽽,通过观察我们可以发现,如果我们先删除最底部的叶⼦节点,然后再处理删除后的节点,最终的结果并不会受到影响。
因此,我们可以采⽤后序遍历的⽅式来解决这个问题。在后序遍历中,我们先处理左⼦树,然后处理右⼦树,最后再处理当前节点。在处理当前节点时,我们可以判断其是否为叶⼦节点且其值是否为 0,如果满⾜条件,我们可以删除当前节点。
•
需要注意的是,在删除叶⼦节点时,其⽗节点很可能会成为新的叶⼦节点。因此,在处理完⼦节点后,我们仍然需要处理当前节点。这也是为什么选择后序遍历的原因(后序遍历⾸先遍历到的⼀定是叶⼦节点)。
•
通过使⽤后序遍历,我们可以逐步删除叶⼦节点,并且保证删除后的节点仍然满⾜删除操作的要
求。这样,我们可以较为⽅便地实现删除操作,⽽不会影响最终的结果。
•
若在处理结束后所有叶⼦节点的值均为 1,则所有⼦树均包含 1,此时可以返回。
算法流程:
递归函数设计:void dfs(TreeNode*& root)
1.
返回值:⽆;
2.
参数 :当前需要处理的节点;
3.
函数作⽤:判断当前节点是否需要删除,若需要删除,则删除当前节点。
后序遍历的主要流程:
1.
递归出⼝:当传⼊节点为空时,不做任何处理;
2.
递归处理左⼦树;
3.
递归处理右⼦树;
4.
处理当前节点:判断该节点是否为叶⼦节点(即左右⼦节点均被删除,当前节点成为叶⼦节点), 并且节点的值为 0:
a.
如果是,就删除掉;
b.
如果不是,就不做任何处理。
例题四
解法(利⽤中序遍历):
后序遍历按照左⼦树、根节点、右⼦树的顺序遍历⼆叉树的所有节点,通常⽤于⼆叉搜索树相关题
⽬。
算法思路:
如果⼀棵树是⼆叉搜索树,那么它的中序遍历的结果⼀定是⼀个严格递增的序列。
因此,我们可以初始化⼀个⽆穷⼩的全区变量,⽤来记录中序遍历过程中的前驱结点。那么就可以在中序遍历的过程中,先判断是否和前驱结点构成递增序列,然后修改前驱结点为当前结点,传⼊下⼀层的递归中。
算法流程:
1.
初始化⼀个全局的变量 prev,⽤来记录中序遍历过程中的前驱结点的 val;
2.
中序遍历的递归函数中:
a.
设置递归出⼝:root == nullptr 的时候,返回 true;
b.
先递归判断左⼦树是否是⼆叉搜索树,⽤ retleft 标记;
c.
然后判断当前结点是否满⾜⼆叉搜索树的性质,⽤ retcur 标记:
▪
如果当前结点的 val ⼤于 prev,说明满⾜条件,retcur 改为 true;
▪
如果当前结点的 val ⼩于等于 prev,说明不满⾜条件,retcur 改为 false;
d.
最后递归判断右⼦树是否是⼆叉搜索树,⽤ retright 标记;
3.
只有当 retleft、 retcur 和 retright 都是 true 的时候,才返回 true。
例题五
解法(中序遍历 + 计数器剪枝):
算法思路:
我们可以根据中序遍历的过程,只需扫描前 k 个结点即可。 因此,我们可以创建⼀个全局的计数器 count,将其初始化为 k,每遍历⼀个节点就将 count--。直到某次递归的时候,count 的值等于 1,说明此时的结点就是我们要找的结果。算法流程:
1.
定义⼀个全局的变量 count,在主函数中初始化为 k 的值(不⽤全局也可以,当成参数传⼊递归过程中);
递归函数的设计:int dfs(TreeNode* root):
•
返回值为第 k 个结点;
递归函数流程(中序遍历):
1.
递归出⼝:空节点直接返回 -1,说明没有找到;
2.
去左⼦树上查找结果,记为 retleft:
a.
如果 retleft == -1,说明没找到,继续执⾏下⾯逻辑;
b.
如果 retleft != -1,说明找到了,直接返回结果,⽆需执⾏下⾯代码(剪枝);
3.
如果左⼦树没找到,判断当前结点是否符合:
a.
如果符合,直接返回结果
4.
如果当前结点不符合,去右⼦树上寻找结果。
例题六
解法(回溯):
算法思路:
使⽤深度优先遍历(DFS)求解。
路径以字符串形式存储,从根节点开始遍历,每次遍历时将当前节点的值加⼊到路径中,如果该节点为叶⼦节点,将路径存储到结果中。否则,将 "->" 加⼊到路径中并递归遍历该节点的左右⼦树。 定义⼀个结果数组,进⾏递归。递归具体实现⽅法如下:
1.
如果当前节点不为空,就将当前节点的值加⼊路径 path 中,否则直接返回;
2.
判断当前节点是否为叶⼦节点,如果是,则将当前路径加⼊到所有路径的存储数组 paths 中;
3.
否则,将当前节点值加上 "->" 作为路径的分隔符,继续递归遍历当前节点的左右⼦节点。
4.
返回结果数组。
•
特别地,我们可以只使⽤⼀个字符串存储每个状态的字符串,在递归回溯的过程中,需要将路径中的当前节点移除,以回到上⼀个节点。
具体实现⽅法如下:
1.
定义⼀个结果数组和⼀个路径数组。
2.
从根节点开始递归,递归函数的参数为当前节点、结果数组和路径数组。
a.
如果当前节点为空,返回。
b.
将当前节点的值加⼊到路径数组中。
c.
如果当前节点为叶⼦节点,将路径数组中的所有元素拼接成字符串,并将该字符串存储到结果
数组中。
d.
递归遍历当前节点的左⼦树。
e.
递归遍历当前节点的右⼦树。
f.
回溯,将路径数组中的最后⼀个元素移除,以返回到上⼀个节点。
3.
返回结果数组。