二叉查找树与树的递归
引言
在上一篇文章中,主要介绍了二叉树的遍历相关以及相关联的算法
这一篇也是系列的第二篇:二叉查找树
相关的一些较为经典的力扣问题。在算法第四版中,对于树的介绍将其二叉查找树,与平衡查找树归于查询
一章节中。对于平衡二叉树其查找的效率也是显而易见的,这里就不再过多展开。主要系列主线集中在对力扣的算法上面,具体的算法思想以及基础内容移步算法图书电子版。下面开始系列的问题的解决。
96-Unique Binary Search Trees
题目:
给定一个值n,能构建出多少不同的值包含1…n的二叉搜索树(BST:就是对于一个节点来说,其左子树都会小于根节点,右子树都会大于根节点)?
例如给出n=3;有以下题解:
此时我们将其变换一个位置得到以下情况:
此时我们可以看到的是对于根节点是1的时候:等于其左子树的排列组合的个数与右子树排列组合的个数进行相乘,对于1来说,2,3都大于其,所以其右子树的排列组合为全零–也就是说只有一种情况,但是对于右边来说,右子树有2,3。但是对于2,3 来说此时又是分为两种情况,要么是2是根节点,要么3是根节点,所以最后规划来得到的是可以使用动态规划来进行题解:以此类推得到:当一个数组为1,2,3,……n
时候,基于以下原则进行构建的BST树具有唯一性:以 i为根节点,其左子树由[1,i-1] 构成,右子树由[i+1,n] 构成。
此时进行如下分析:
- 如果数组是空的,也就是只能有一种的情况,所以 得到
f(0)=1
; - 如果数组的元素只有一个时候,就是单节点情况,
f(1)=1
; - 如果有两个元素:
1,2
。就会有两种情况,所以可以得到:
f(2)=f(0) * f(1)+ f(1) * f(0)。1是根的情况与2是根的情况。
- 此时若是有三个元素 1,2,3.
f(3) =f(0) ∗ f(2) +f(1) ∗ f(1) +f(2) ∗ f(0)。表示的是为1为根,2为根,3为根的情况。于是我们可以根据以上的推敲得到以下公式:也推敲出可以是动态规划的思想来解决这道题目
代码:
public class Solution {
public int numTrees(int n) {
int [] f=new int[n+1];
f[0]=1;
f[1]=1;
for(int i=2;i<=n;i++){
for(int k=1;k<=i;k++){
f[i]+=f[k-1]*f[i-k];
}
}
return f[n];
}
}
95-Unique Binary Search Trees-II
在完成了上一题的计算以后,我们知道了对于上一题的解放是,对于 1--n
之间的某一个树来说 其左子树就是 1--i-1
; 右子树是i+1--n
其中的i就是那个根。此时时候,将左右两边的相乘即可得到我们的个数,但是现在而言是当我们完成所有的输出思想还是相同的:
对于 i节点来说 我们使用递归来创建其左子树,然后递归创建右子树,最后将其左右子树在以i为根节点的左右子树上合并起来,就是我们要得到的数。
如下图完美诠释了我们的思路与思想。
代码:
import java.util.*;
public class Solution {
public ArrayList<TreeNode> generateTrees(int n) {
return unique(1,n);
}
private ArrayList<TreeNode> unique(int low,int high){
ArrayList<TreeNode> array=new ArrayList<TreeNode>();
// 在不能满足条件时候进行退出
if(low>high){
array.add(null);
return array;
}
for(int i=low;i<=high;i++){
// 构建左右子树
ArrayList<TreeNode> left=unique(low,i-1);
ArrayList<TreeNode> right=unique(i+1,high);
for(int j=0;j<left.size();j++){
for(int k=0;k<right.size();k++){
// 对左右子树进行合并
TreeNode root=new TreeNode(i);
root.left=left.get(j);
root.right=right.get(k);
array.add(root);
}
}
}
return array;
}
}
98-Validate Binary Search Tree
思路: 在解决这道题目的时候想到的是对于任意的一棵树来说都是其左子树要小于根节点,然后右子树大于根节点,前面我们讲过,对于树类型的题目而言,使用栈的收益最高,然后特定的层序是使用队列实现。在特定的时候 例如使用到的循环每个小的部分使用递归也是可以做较好的实现。首先想到的是栈的实现,就是先,中,后序实现,但是我们发现,对于平衡二叉树来说,以上的三种方法遍历完成以后并没有特定的规律。然后使用到层序遍历,发现对于层序遍历来说,由于对于一棵树的左右子树来说,其在层序遍历的时候是完完全全邻接在一起的,这个时候就可以判断两两邻接的是不是递增即可。
然后考虑到的是递归来解决这类的问题,在下面的一个系列三——递归中会进行较为细致的讲解。下面我们来进行两个方法的具体编程。
方法一 中序遍历:
使用中序遍历以后,两两判断其是否是递增的
import java.util.*;
public class Solution {
public boolean isValidBST(TreeNode root) {
ArrayList<Integer> array=new ArrayList<Integer>();
Stack<TreeNode> stack=new Stack<TreeNode>();
TreeNode node=root;
while(node!=null || !stack.isEmpty()){
while(node!=null){
stack.push(node);
node=node.left;
}
TreeNode temp=stack.pop();
array.add(temp.val);
node=temp.right;
}int j=0;
for(int i=0;i<array.size()-1;i++){
if(array.get(i+1)<=array.get(i))
j=1;
}
if(j==1)
return false;
else return true;
}
}
方法二 递归
使用递归来不断深入左右子树,并进行特殊情况的判断,也是对于递归来说最常用的方法。
import java.util.*;
public class Solution {
public boolean isValidBST(TreeNode root) {
if(root==null)
return true;
return isBST(root,null,null);
}
private boolean isBST(TreeNode mid,Integer left,Integer right){
if(mid==null)
return true;
return (left==null || left<mid.val)&&(right==null || mid.val<right)
&&isBST(mid.left,left,mid.val)&& isBST(mid.right,mid.val,right);
}
}
Convert Sorted List to Binary Search Tree
表示给定一个升序单链表,将其转换成为一个二叉平衡树:
思路:对于平衡二叉树来说,其左子树都是小于根节点,右子树大于根节点,所以可以对于每一个范围内的节点,找出中间节点,然后使用递归算法,分别进行平衡树的搭建。思路和算法都不算事很难,主要是对于中间节点的查找使用到了快慢指针的思想以及在递归的正确使用。
public class Solution {
public TreeNode sortedListToBST(ListNode head) {
return BST(head,null);
}
private TreeNode BST(ListNode head,ListNode tail){
if(head==tail) return null;
ListNode fast=head;
ListNode slow=head;
while(fast!=tail && fast.next!=tail){
slow=slow.next;
fast=fast.next.next;
}
TreeNode t=new TreeNode(slow.val);
t.left=BST(head,slow);
t.right=BST(slow.next,tail);
return t;
}
}
Convert Sorted Array to Binary Search Tree
在上一题中我们遇到的是使用升序链表来实现要求,在这个题目中使用到的是一个升序的数组来对我们的平衡二叉树进行转换。
public class Solution {
public TreeNode sortedArrayToBST(int[] num) {
if(num == null || num.length == 0)
return null;
return bst(num ,0, num.length - 1);
}
public TreeNode bst(int[] num, int left, int right){
if(left > right)
return null;
if(left == right)
return new TreeNode(num[left]);
int mid = left + (right - left + 1) / 2;
TreeNode root = new TreeNode(num[mid]);
root.left = bst(num, left ,mid-1);
root.right = bst(num, mid+1, right);
return root;
}
}
引言
在系列一
和系列二
中,我们介绍到了对于二叉树
的基础遍历以及对于平衡二叉树的各种问题,本系列是树系列的第三系列——树的递归。主要解决着在我们所遇到的问题中使用递归思想来解决的问题。也会慢慢通过这一系列的学习,总结出在树的递归中所遇到的问题。
Minimum Depth of Binary Tree
求给定二叉树的最小深度。最小深度是指树的根结点到最近叶子结点的最短路径上结点的数量。
思路:1.首先要明白的是题目中的要求是最小的深度是说 根节点到叶子节点的最短路径的节点数目,所以结束点或者说是特殊情况点就是其左右子树都为空的情况下。
2. 对于此类的问题,因为是对于全部的数来说找寻最短的那个路径,所以在整体上可以使用到递归算法来解决,对左子树进行深度判断,然后再使用递归进入右子树进行判断,对于情况进行一个记录即可。
方法一:递归实现:
在对于一棵树根,其左子树为空表示没有左子树的时候,我们就进入到右子树进行查看,此时加1
表示加上当前的一层的根节点。同理对于右子树。
但是对于左右子树都不是空的时候,我们就进行一个计算,计算除对于左右子树而言,其哪个的值更小,再次加一表示加上自己这一层的。所以就有了以下的答案。
import java.util.LinkedList;
import java.util.Queue;
public class Solution {
public int run(TreeNode root) {
return min(root);
}
private int min(TreeNode root){
if(root==null) return 0;
if(root.left==null){
return min(root.right)+1;
}
if(root.right==null)
return min(root.left)+1;
int l=min(root.left);
int r=min(root.right);
return Math.min(l+1,r+1);
}
}
方法二:遍历实现。
此时想到了我们在之前判断一棵二叉树是不是平衡二叉树时候使用到的层序遍历。对于这道题目来说很是适用,因为对于最短的路径一定是对于某一层的左右子树都是空,当我们在层序遍历时候发现哪一个节点的左右子树都是空,就可以判定其是最短值。
import java.util.LinkedList;
import java.util.Queue;
public class Solution {
public int run(TreeNode root) {
if(root == null)
return 0;
if(root.left == null && root.right == null)
return 1;
int depth = 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
int len = queue.size();
depth++;
for(int i = 0; i < len; i++){
TreeNode cur = queue.poll();
if(cur.left == null && cur.right == null)
return depth;
if(cur.left != null)
queue.offer(cur.left);
if(cur.right != null)
queue.offer(cur.right);
}
}
return 0;
}
}
Maximum Depth of Binary Tree
求给定二叉树的最大深度。最小深度是指树的根结点到最近叶子结点的最短路径上结点的数量。
思路:与上面一题正好相反,在上一题中我们对于最短路径的寻找有使用到层序遍历,是因为最短路径而言,其无左右子树正好是最先出现,但是对于最深子树而言,我们无法使用层序来得知最深的深度,所以这里的层序使用起来并不是那么合适,对于递归的思想总是很奇妙,同上一题类似,但是这里我们也可以进行左右存在的判断,然后将min转变成为max即可,但是这里我们有更加简洁的算法,如下:在节点为空时候,表示再无下一个节点,返回0,返回到上一个层次中,进入另外一边,使用max函数来找出最大值。
public class Solution {
public int maxDepth(TreeNode root) {
if(root==null) return 0;
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}
}
Path Sum
思路:我们继续来进行思考,首先在什么地方判断是真还是假?当然是在一进入到一个节点的时候判断其是不是叶子节点,因为路径不可能说是在中途等于某一个值就判断是正确的,需要进入叶子节点以后,可以使用一个减法,每遍历一个节点的时候就将sum值减去当前节点的值,在无左右子树时候,判断值是否为零,为零表示成功,不为零表示失败继续执行。当然最后对于左右子树来说,有任何一个节点其依次向上到根节点加起来与sum值相等,就是我们想要的结果。
public class Solution {
public boolean hasPathSum(TreeNode root, int sum) {
if(root==null) return false;
if(root.left==null && root.right==null){
if(sum-root.val==0)
return true;
else return false;
}
return hasPathSum(root.left,sum-root.val)||hasPathSum(root.right,sum-root.val);
}
}
Path Sum II
如图所示,描述的是我们若是可以对一个二叉树进行从根节点到叶子节点找到一条路径,其上的值加起来与我们的sum值相等,我们就将其记录下来。
思路:
- 根据输出的结果,会是一个以数组为泛型的数组,所以要明白如何才能够不重复的进行放入。
- 做减法,要知道答案可能不止一个,所以在完成了一次的搜索到叶子节点以后,要对我们定义的array进行remove,以便在加入新的数据时候,不会受到前面数据的影响。
- 什么时候判断是否成功。
还是和前面的一样对于一个叶子节点其左右子树都为空的时候,进行判断加的值是否等于 sum 还是减的值是否等于0。
有了以上的问题我们就可以进行正常的思路编程。
import java.util.*;
public class Solution {
public ArrayList<ArrayList<Integer>> pathSum(TreeNode root, int sum) {
ArrayList<ArrayList<Integer>> arrays=new ArrayList<ArrayList<Integer>>();
ArrayList<Integer> array =new ArrayList<Integer>();
if(root==null) return arrays;
sum(root,sum,arrays,array,0);
return arrays;
}
private void sum(TreeNode t,int sum,ArrayList<ArrayList<Integer>> arrays,ArrayList<Integer> array,int now){
if(t==null)return ;
now+=t.val;
if(t.left==null && t.right== null){
if(sum==now){
array.add(t.val);
arrays.add(new ArrayList<Integer>(array));
array.remove(array.size()-1);
}
return ;
}
array.add(t.val);
sum(t.left,sum,arrays,array,now);
sum(t.right,sum,arrays,array,now);
array.remove(array.size()-1);
}
}
Binary Tree Maximum Path Sum
思路:
- 对于这道题目来说,由于其开始和结束的地方都是不确定的,而且很有可能回事出现负数来在路径中降低总值。所以第一个要解决的问题就是如何判断哪些是可以加入总和的,哪些是不能加入
- 此时我们就想到了在之前的经典的动态规划题目
最大连续子序列和
(也会在后期出一篇关于所有的对于力扣上动态规划专题的博客)这里就先进行一个简单的介绍:“
请计算给出的数组(至少含有一个数字)中具有最大和的子数组(子数组要求在原数组中连续)
例如:给出的数组为[−2,1,−3,4,−1,2,1,−5,4],
子数组[4,−1,2,1],具有最大的和:6.
此时我们可以看到的是 对于这个数组我们可能开始时无从下手,但是对于一个我们拿到的算法题目来说,首先思考的是我想要什么,以及我如何得到。对于这个数组来说,想要值最大,就需要正数才能得到较大的值,所以说 对于我们来说若是一个数是负数就不在我们的范围之内。这是最初的思想,但是问题又来啦,若是一个数加上一个负数组成了一个正数,那没组合起来对于后面的数还是有帮助的,但是问题是一个负数加一个正数就会小于当前的正数值,若是当前的正数就已经足够大了呢, 此时就需要我们进行判断更大值,利用到一个中间变量来存储较大值,再与我们的最终值进行比较,判断出谁会更大。中间值得到的是对于当前进行的比较,最终值得到的值从开始到结束总的最大值。至此我们可以得出一下的程序。
”
public class Solution {
public int maxSubArray(int[] A) {
int result=Integer.MIN_VALUE;
int f=0;
for(int i=0;i<A.length;i++)
{
f=Math.max(f+A[i],A[i]);
result=Math.max(result,f);
}
return result;
}
}
以上是我们解决在数组中的情况,只需要当方面的判断。但是对于我们本题来说,我们需要的左右子树的判断,对于熟悉了树的递归操作而言,我们可以使用DFS来解决树的遍历问题。对于计算出来的左右子树的值,若是大于零 表示此方向上的值对于我们后来的计算是有利的(就像在数组中,判断当前所加起来的值是否是大于零的一样)就加入到中间值中,最后判断较大值计算到最终值中。如下是代码段:
public class Solution {
int max_m=Integer.MIN_VALUE;
public int maxPathSum(TreeNode root)
{
getMax(root);
return max_m;
}
private int getMax(TreeNode root){
if(root==null)
return 0;
int sum=root.val;
int left=getMax(root.left);
int right=getMax(root.right);
if(left>0){
sum+=left;
}
if(right>0){
sum+=right;
}
max_m=Math.max(max_m,sum);
return Math.max(left,right)>0?root.val+Math.max(left,right): root.val;
}
}
// 注意最后我们返回的是root方向上的值,因为这里对于一个最后值的序列来说 不可能是 L>root->R,只能是L->root 或者是 root->R。最后的值是左子树的值+ 根节点+ 右子树。
Sum Root to Leaf Numbers
思路:还是熟悉的对于每个根的左右子树都要记录。这里我们判断一下程序想要的信息。对左右子树进行遍历与记录,当在左右子树都不存在的时候,对我们想要的sum 进行返回。如何获取到sum的值:对于每一个根节点都是叶子节点的10倍,依次类推,得到公式: sum=sum * 10 + root.val;得到如下:
public class Solution {
public int sumNumbers(TreeNode root) {
return sum(root,0);
}
private int sum(TreeNode root,int sum){
if(root==null)
return 0;
sum=sum*10+root.val;
if(root.right==null &&root.left==null)
return sum;
int left=sum(root.left,sum);
int right=sum(root.right,sum);
return left+right;
}
}
总结
从上可以看出对于树的递归,我们总少不了对于左右子树进行递归,无论返回值是boolean 类型,判断是否存在问题, 或是int类型,对什么值进行记录,亦或是 数组类型,对于路径进行记录,总是在一个情况下对返回的情况进行一次返回。对于返回的结果若是需要判断两则都成立,使用&&,判断两则有一个成立即可使用 ||。当然这些也都是个人的拙见,在不断的刷题过程中,也会慢慢形成肌肉记忆,练就自己的一套体系。