【面试刷题】分治法与二叉树

【面试刷题】分治法与二叉树

一、二叉树上的分治法模板

1、模板

public 返回结果类型 divideConquer(TreeNode root) {
    if (root == null) {
        处理空树应该返回的结果
    }
    
    // if (root.left == null && root.right == null) {
    //		处理叶子节点应该返回的结果
    //		如果叶子节点返回的结果可以通过两个空节点的返回结果得到
    //		就可以省略这一段代码
    
	左子树返回结果 = divideConquer(root.left)
    右子树返回结果 = divideConquer(root.right)
    整棵树的结果 = 按照一定方法合并左右子树的结果
    
    return 整棵树的结果
}

2、LeetCode-剑指55-II.平衡二叉树

image-20220117195619470

代码实现

//返回两个值
class Result {
    public boolean isBalanced;
    public int height;
    public Result(boolean isBalanced, int height) {
        this.isBalanced = isBalanced;
        this.height = height;
    }
}
class Solution {
    public boolean isBalanced(TreeNode root) {
        Result res = divideConquer(root);
        return res.isBalanced ? true : false;
    }

    //定义:求出root为根的子树是否为平衡树且返回高度
    private Result divideConquer(TreeNode node) {
        //出口:空树的时候,返回isBalanced = true, height = 0;
        if (node == null) {
            return new Result(true, 0);
        }

        //拆解:拆解到左右两边得到左右子树的平衡信息和高度信息
        Result leftResult = divideConquer(node.left);
        Result rightResult = divideConquer(node.right);

        boolean isBalanced = true;

        //左右子树的公共部分,高度都是一样的,都是其左右子树的最大高度加上1
        int height = Math.max(leftResult.height, rightResult.height) + 1;

        //处理左右子树的结果
        if (!leftResult.isBalanced || !rightResult.isBalanced) {
            isBalanced = false;
        }

        if (Math.abs(leftResult.height - rightResult.height) > 1) {
            isBalanced = false;
        }

        return new Result(isBalanced, height);
    }
}

反思学习:

  • 返回两个值的处理方法
  • 分治法的运用,只关注与当前的子问题,将整个大问题,划分为同样算法处理的子问题,只是递归调用的参数发生改变。
  • 处理左右子树的结果,最后封装在自己的结果当中。

3、LeetCode-257.二叉树的所有路径

image-20220117200134195

代码实现

class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> paths = new ArrayList<>();
		
        //根节点的处理
        if (root == null) {
            return paths;
        }

        //处理叶子结点,这里需要单独处理,因为左右子树的调用过程无法解决
        if (root.left == null && root.right == null) {
            paths.add("" + root.val);
            return paths;
        }
        //从左子树的的所有路径当中加上当前的值,构成一个新的路径,添加到结果当中
        for (String leftPath : binaryTreePaths(root.left)) {
            paths.add(root.val + "->" + leftPath);
        }

        for (String rightPath : binaryTreePaths(root.right)) {
            paths.add(root.val + "->" + rightPath);
        }

        return paths;
    }
 
}

反思学习

  • 在处理问题的时候拆分为,左右子树两个子问题。

二、二叉树上求值、求路径

1、LintCode-596.最小子树

image-20220118090645874

image-20220118091008020

思路分析

  • 保证只有一棵 和最小的子树
  • 并且给出的二叉树不是一棵空树
  • 碰到二叉树的问题,应该思考,整棵树在该问题上的结果和左右孩子在该问题上的结果之间有什么联系
    • 树的和 = 根节点值 + 左子树和 + 右子树和

代码实现---结果封装类

//结果包含三个值,封装在class中
class Result {
    //最小的子树根
    public TreeNode minSubtree;
    //当前树的和,最小的和
    public int sum, minSum;
    public Result(TreeNode minSubtree, int minSum, int sum) {
        this.minSubtree = minSubtree;
        this.minSum = minSum;
        this.sum = sum;
    }
}
public class Solution {
    public TreeNode findSubtree(TreeNode root) {
        // write your code here
        Result res = helper(root);
        return res.minSubtree;
    }

    private Result helper(TreeNode node) {
        //处理空节点
        if (node == null) {
            //注意这里的空节点初始化,赋了正无穷的处理。
            return new Result(null, Integer.MAX_VALUE, 0);
        }


        //分治
        Result leftRes = helper(node.left);
        Result rightRes = helper(node.right);

        //定义本层的结果
        TreeNode minSubTree = node;
        int minSum = leftRes.sum + right.sum + node.val;
        int sum = minSum;

        //如果左子树最小,返回左子树
        if (leftRes.minSum <= minSum) {
            minSum = leftRes.minSum;
            minSubtree = leftRes.minSubtree;
        }
		//如果右子树最小,返回右子树
        if (rightRes.minSum <= minSum) {
            minSum = rightRes.minSum;
            minSubtree = rightRes.minSubtree;
        }

        return new Result(minSubTree, minSum, sum);
    }
}

代码实现---全局变量

public class Solution {
    //初始化最小的和为正无穷
    public int minSum;
    public TreeNode minRoot;
    public TreeNode findSubtree(TreeNode root) {
        minSum = Integer.MAX_VALUE;
        minRoot = null;
        helper(root);
        return minRoot;
    }

    private int helper(TreeNode node) {
        //处理空节点
        if (node == null) {
            return 0;
        }

        //递归的拆解
        //左子树之和
        int leftSum = helper(node.left);
        //右子树之和
        int rightSum = helper(node.right);
        //当前树的和
        int nodeSum = leftSum + rightSum + node.val;

        if (nodeSum < minSum) {
            minSum = nodeSum;
            minRoot = node;
        }
        //返回当前和
        return nodeSum;
    }
}
  • 特点:好理解,但是不利于多线程。

2、LeetCode-剑指Offer-68.二叉树的最近公共祖先

image-20220118094138201

思路分析

还是分析左右子树和树的最近公共祖先的关系

image-20220118100523399

image-20220118100530847

image-20220118100538601

image-20220118100543890

代码实现

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode A, TreeNode B) {
        if (root == null) {
            return null;
        }

        //如果root为A或者B,立即返回,无需继续向下寻找
        if (root == A || root == B) {
            return root;
        }

        //分别去左右子树寻找A和B
        TreeNode left = lowestCommonAncestor(root.left, A, B);
        TreeNode right = lowestCommonAncestor(root.right, A, B);

        //如果A、B分别存在于两棵子树,root为LCA,返回root
        if (left != null && right!=null) {
            return root;
        }

        //左子树有一个点或者左子树有LCA,说明右子树什么都没有,两个点都在左子树当中
        if (left != null) {
            return left;
        }

        //右子树有一个点或者右子树有LCA,说明左子树什么都没有,两个点都在右子树当中
        if (right != null) {
            return right;
        }

        //左右子树什么都没有
        return null;
    }
}

3、LeetCode-124.二叉树中的最大路径和

image-20220118101815033

image-20220118101857102

思路分析

  • 二叉树的路径:
    • 从任意节点出发,到达任意节点。
    • 该路径至少包含一个节点,且不一定经过根节点。
    • 求所有可能路径和的最大值
  • 路径的三种情况:
    • 途径一个节点只能选择来和去两个方向
    • 右加上的情况:
      • image-20220118102400874
    • 左加上的情况:
      • image-20220118102432335
    • 左加右的情况:
      • image-20220118102508327

解题思路

image-20220118102731145

实现逻辑

image-20220118102831277

  • 因为不一定经过根节点,所以要用一个全局最大和,来作为最后的返回值。

负数的处理

求最大和的时候,尽可能的舍弃负数,使用max(0, x),有点贪心的意味。

image-20220118103104300

代码实现

class Solution {
    int maxSum = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        maxGain(root);
        return maxSum;
    }

    public int maxGain(TreeNode node) {
        //处理空节点
        if (node == null) {
            return 0;
        }

        //递归拆解,左右子树
        //只有在最大贡献值大于0时候,才会选取对应的子节点
        int leftMax = Math.max(maxGain(node.left), 0);
        int rightMax = Math.max(maxGain(node.right), 0);

        //节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
        int pathMax = node.val + leftMax + rightMax;

        //更新答案
        maxSum = Math.max(maxSum, pathMax);

        //返回节点的最大贡献值,这个意思就是继续往上走,从左根,或者右根
        return node.val + Math.max(leftMax, rightMax);
    }
}

反思回顾

  • 分治思想与二叉树结构的结合。
  • 分析路径所具有的特点,和不同种的情况。
  • 细节的处理,比如说在最大贡献值,左右子树,贪心取0,即不经过,但是本节点是必须要经过的。
  • 更新答案要全局的最大值,所以不再递归函数当中,定义为全局变量。

三、二叉树结构变化

1、LeetCode-114.将二叉树展开为链表

image-20220119102536412

思路分析

DFS前序遍历这棵树,然后把结果一路向右串联起来。

树的链表 = 树的根节点 + 左子树链表 + 右子树链表

image-20220119103300437

代码实现

class Solution {
    public void flatten(TreeNode root) {
        flattenTree(root);
    }

    //把node这棵树摊平(形成向右的假链表),并返回摊平的数的最尾部节点
    private TreeNode flattenTree(TreeNode node) {
        //如果当前节点为空,无需摊平,直接返回
        if (node == null) {
            return null;
        }

        //分别摊平左右子树,并返回摊平之后的最后一个点
        TreeNode leftLast = flattenTree(node.left);
        TreeNode rightLast = flattenTree(node.right);

        //如果左子树不为空,需要重组node,摊平的左子树和摊平的右子树
        //如果左子树为空,不需要充足,node的右子树已经与摊平的右子树相连
        if (node.left != null) {
            //把摊平的左子树的终点和摊平的右子树的起点连接在一起
            leftLast.right = node.right;
            //把node的右孩子指向摊平的左子树的起点
            node.right = node.left;
            //把node的左孩子置空
            node.left = null;
        }

        //node这棵树被摊平之后,返回这棵树的最尾部节点
        //如果rightLast存在,那么rightLast是最尾部节点
        //否则,如果leftLast存在,那么leftLast是最尾部节点
        //否则,node是最尾部节点
        if (rightLast != null) {
            return rightLast;
        } else if (leftLast != null) {
            return leftLast;
        }
        return node;
    }
}

三、二叉查找树BST

1、LeetCode-108.将有序数组转换为二叉查找树

思路:每次从中间选取,分治法的运用改造左右子树。

class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        if (nums == null) {
            return null;
        }
        return buildBST(nums, 0, nums.length - 1);
    }

    private TreeNode buildBST(int[] nums, int start, int end) {
        if (start > end) {
            return null;
        }

        //每次选取一个区间的中点作为当前区间的根,确保左右子树高度平衡。
        //对左右子树依次改造
        TreeNode node = new TreeNode(nums[(start + end) / 2]);
        node.left = buildBST(nums, start, (start + end) / 2 - 1);
        node.right = buildBST(nums, (start + end) / 2 + 1, end);

        return node;
    }
}

2、LeetCode-701.在二叉查找树中插入节点

在树上定位要插入节点的位置:

  • 如果它大于当前根节点,则应该在右子树当中。
  • 如果没有右子树,则将该点作为右儿子插入
  • 如果存在右子树,则在右子树中继续定位
  • 如果它小于当前根节点,则应该在左子树中,处理同上。
class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        //此处的处理,在递归调用的时候,代表左右子树的左右孩子为空时,返回一个新的结点,插入。
        if (root == null)
            return new TreeNode(val);
        if (root.val > val) {
            root.left = insertIntoBST(root.left, val);
        } else {
            root.right = insertIntoBST(root.right, val);
        }
        return root;
    }
}

3、LeetCode-701.在二叉查找树中搜索

class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        if (root == null)
            return new TreeNode(val);
        if (root.val > val) {
            root.left = insertIntoBST(root.left, val);
        } else {
            root.right = insertIntoBST(root.right, val);
        }
        return root;
    }
}
//递归,分治
class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        if (root == null || root.val == val)
            return root;
        if (val < root.val)
            return searchBST(root.left, val);
        else 
            return searchBST(root.right, val);
    }
}

4、LeetCode-669.修剪二叉树

  • 若根节点的值小于最小值,则递归调用右子树并返回右子树;
  • 若根节点的值大于最大值,则递归调用左子树并返回左子树;
  • 否则修剪左子树,右子树并返回根节点。
class Solution {
    //修剪完之后返回新的根
    public TreeNode trimBST(TreeNode root, int low, int high) {
        //空节点的处理
        if (root == null)
            return root;
        
       if (root.val < low) {
           return trimBST(root.right, low, high);
       } else if (root.val > high) {
           return trimBST(root.left, low, high);
       } else {
           root.left = trimBST(root.left, low, high);
           root.right = trimBST(root.right, low, high);
       }

        return root;
    }
}

5、LeetCode-230.BST中第k小的元素

image-20220119150540383

image-20220119150622560

利用非递归实现中序遍历

class Solution {
    public int kthSmallest(TreeNode root, int k) {
        //使用stack进行非递归算法的数据存取
        Stack<TreeNode> stack = new Stack<>();

        //一路向左,把树的左边缘的点全部入栈
        while (root != null) {
            stack.push(root);
            root = root.left;
        }

        //0到K-2总共包括k-1次操作
        //经历k-1次才做,可以把第k个数调整到栈顶
        for (int i = 0; i < k - 1; i++) {
            //前一个元素出栈
            TreeNode node = stack.pop();
            //如果出栈的元素有右子树,把右子树的左边缘的点全部入栈
            if (node.right != null) {
                node = node.right;
                //一路向左,把树的左边缘的点全部入栈
                while (node != null) {
                    stack.push(node);
                    node = node.left;
                }
            }
        }

        //当前栈顶就是第K个元素
        return stack.peek().val;
    }

}

四、构建二叉树

1、LeetCode-剑指Offer-07.从前序后中序遍历构建二叉树

典型的分治,树 = 左子树 + 右子树

class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return buildSubTree(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
    }

    private TreeNode buildSubTree(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {
        //递归终止条件
        if (inStart > inEnd || preStart > preEnd)
            return null;

        //val为前序遍历的第一个值,也就是根节点的值
        //idx为根据根节点的值来找中序遍历的下标
        int idxInOrder = 0;
        int val = preorder[preStart];
        TreeNode root = new TreeNode(val);
        //找到在中序遍历中的下标
        for (int i = inStart; i<= inEnd; i++) {
            if (inorder[i] == val){
                idxInOrder = i;
                break;
            }
        }

        //根据idxInorder来递归找左右子树,分治法!
        int leftSubTreeLength = idxInOrder - inStart;
        root.left = buildSubTree(preorder, preStart + 1, preStart + leftSubTreeLength, inorder, inStart, idxInOrder -1);
        root.right = buildSubTree(preorder, preStart + leftSubTreeLength + 1, preEnd, inorder, idxInOrder + 1, inEnd);
        
        return root;
    }
}

2、LeetCode-106.从中序和后序遍历建立二叉树

class Solution {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        return buildSubTree(postorder, 0, postorder.length, inorder, 0, inorder.length);
    }

    public TreeNode buildSubTree(int[] postorder, int postStart, int postEnd, int[] inorder, int inStart, int inEnd) {
        if (inEnd == inStart)
            return null;

        //如果左子树剩下一个元素
        if (inEnd - inStart == 1)
            return new TreeNode(inorder[inStart]);
        
        int idxInorder = 0;
        int val = postorder[postEnd - 1];
        TreeNode root = new TreeNode(val);
        for (int i = 0; i < inEnd; i++) {
            if (inorder[i] == val) {
                idxInorder = i;
                break;
            }
        }

        int leftSubTreeLength = idxInorder - inStart;
        root.left = buildSubTree(postorder, postStart,  postStart + leftSubTreeLength, inorder, inStart, idxInorder);
        root.right = buildSubTree(postorder, postStart + leftSubTreeLength, postEnd - 1, inorder, inStart + leftSubTreeLength + 1, inEnd);
       
        return root;
    }
}
posted @ 2022-01-19 15:30  DarkerG  阅读(49)  评论(0编辑  收藏  举报