算法-递归&分治

一、递归

0、递归概述

为什么要用递归而不用循环:

​ 以n的阶乘为例,确实使用循环会更方便,但是使用递归的场景,一般是比较难以确认推导路径的,例如一棵树,要获取所有节点值的和,这样就不能使用循环了,就需要使用递归。

递归三要素:

​ 定义需要递归的问题(重叠的子问题)

​ 确定递归边界

​ 保护和还原现场

伪代码如下

void doSomething(int level, int param){
	// 具有边界
	if(level > MAX_VALUE){
		return;
	}
	// 每层的处理逻辑是一样的
	process(level, param);
	// 使用新的参数调用该方法
	doSomething(level+1, new_param);
}

1、子集(中等)

题目地址:https://leetcode.cn/problems/subsets/

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

解答

​ 解题思路就是循环 nums 的每一个数据,该数据要或者不要都是一个选项,那么就可以使用递归的模板:

​ 递归边界:当走完nums数组,则已经到了边界,将数组存储

​ 处理逻辑:当前节点不要是一种情况,当前节点要是一种情况

​ 递归:继续向下一个节点移动

​ 根据上面的分析,不要时,直接将index加一递归调用即可,如果要,则先将该节点的数据放入集合,然后再将index 加一递归调用

​ 为了不太多的占用空间,因此可以使用一个共享的集合存储临时放入的值,只有到最后需要放入返回集合时,才新建一个集合并将其放入返回集合中。

​ 由于使用的是共享的集合,因此要还原现场,那么在递归调用后,要把本次的影响清除,即remove掉本次添加的节点。

​ 这里为什么要清除本次的影响,可能不太好理解,举例说明:

​ 以题目中的1,2,3为例,当前共享的集合为[],那么:

​ 递归下标0,选择不要,集合为[]

​ 递归下标1,选择不要,集合为[]

​ 递归下标2,选择不要,集合为[]

​ 递归下标3:到达边界,出现一个结果 []

​ 回归递归2,选择要,集合为[3]

​ 递归下标3:到达边界,出现一个结果[3]

如果不清除当前添加3的影响,即不删除当前添加的3

​ 回归递归1:选择要,集合为[3,2]

​ 递归下标2,选择不要,集合为[3,2]

​ 递归下标3:到达边界,出现一个结果 [3,2]

​ 回归递归2,选择要,集合为[3,2,3]

​ 递归下标3:到达边界,出现一个结果[3,2,3]

这里明显已经错误了

如果清除当前添加3的影响,即不删除当前添加的3

​ 回归下标2时,先清楚了最后一个元素,变为[]

​ 回归递归1:选择要,集合为[2]

​ 递归下标2,选择不要,集合为[2]

​ 递归下标3:到达边界,出现一个结果 [2]

​ 回归递归2,选择要,集合为[2,3]

​ 递归下标3:到达边界,出现一个结果[2,3]

class Solution {
    private List<List<Integer>> ans;
    private List<Integer> choose;
    private int[] nums;

    public List<List<Integer>> subsets(int[] nums) {
        ans = new ArrayList();
        choose = new ArrayList();
        this.nums = nums;
        subsets(0);
        return ans;
    }

    private void subsets(int index){
        if(index == nums.length){
            List<Integer> list = new ArrayList(choose);
            ans.add(list);
            return;
        }
        subsets(index+1);
        choose.add(nums[index]);
        subsets(index+1);
        choose.remove(choose.size()-1);
    }
}

2、组合(中等)

题目地址:https://leetcode.cn/problems/combinations/description/

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

提示:

  • 1 <= n <= 20
  • 1 <= k <= n

解答

这道题的解题思路和上面的一致,区别就是如何判断边界条件,上面是如果超过了nums长度,则到了边界,这道题是长度为k,那么集合的长度到了k,就到了边界,将集合放入结果集中,还有另外一种情况,就是index已经超过了n的边界,但是集合的长度还没到,这种也到了边界,但是不将集合放入结果集中。

class Solution {

    private List<List<Integer>> ans;
    private List<Integer> choose;
    private int n;
    private int k;

    public List<List<Integer>> combine(int n, int k) {
        ans = new ArrayList();
        choose = new ArrayList();
        this.n = n;
        this.k = k;
        combine(1);
        return ans;
    }


    private void combine(int index){
        if(index > n && choose.size() < k){
            return;
        }
        if(choose.size() == k){
            List<Integer> list = new ArrayList(choose);
            ans.add(list);
            return;
        }
        combine(index+1);
        choose.add(index);
        combine(index+1);
        choose.remove(choose.size()-1);
    }
}

3、全排列(中等)

题目地址:https://leetcode.cn/problems/permutations/description/

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

解答

​ 以题目的1,2,3为例,第一位可以是1,2,3的其中一个

​ 如果第一位是1,由于1已经使用,那么第二位可以是2,3

​ 如果第二位是2,由于1,2已经使用,那么第三位只能是3

​ 第三位是3,出一个结果 1,2,3

​ 如果第二位是3,由于1,3已经使用,那么第三位只能是2

​ 第三位是2,出一个结果 1,3,2

​ 如果第一位是2....

​ 可以发现,每一个位置的值都是从第一位开始判断,如果当前节点已经被使用了,那么就不能用

​ 因此可以定义一个共享的used数组,长度与nums一致,用以表示nums中的数值哪一个被使用过了

​ 根据以上分析:

​ 边界:当下标已经超过nums的下标时,表示已经得到了一个值,进行处理

​ 具体的处理:

​ 由于每个节点都是循环nums,从中取一个没有用过的值,因此循环nums,根据 used 判断当前节点是否已使用,如果没有使用,则当前节点使用该值,如果已经使用,则当前节点不使用该值,继续向后递归。

​ 如果使用该值,就将used的当前节点置位true,表示已使用

​ 处理完毕后,需要还原现场,因此需要将used当前节点改为false;

class Solution {

    private List<List<Integer>> ans;
    private List<Integer> choose;
    private boolean[] used;
    private int[] nums;

    public List<List<Integer>> permute(int[] nums) {
        ans = new ArrayList();
        choose = new ArrayList();
        used = new boolean[nums.length];
        this.nums = nums;
        permute(0);
        return ans;
    }


    private void permute(int index){
        if(index == nums.length){
            List<Integer> list = new ArrayList(choose);
            ans.add(list);
            return;
        }
        for(int i=0; i<nums.length; i++){
            if(!used[i]){
                choose.add(nums[i]);
                used[i] = true;
                permute(index + 1);
                used[i] = false;
                choose.remove(choose.size()-1);
            }
        }
    }
}

4、全排列 II(中等)

题目地址:https://leetcode.cn/problems/permutations-ii/description/

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10

解答

​ 与上一道题一样,添加了一个 哈希表去重。

class Solution {
    private List<List<Integer>> ans;
    private List<Integer> choose;
    private int[] nums;
    private boolean[] used;
    private Map<String, String> map;
    public List<List<Integer>> permuteUnique(int[] nums) {
        ans = new ArrayList();
        choose = new ArrayList();
        this.nums = nums;
        used = new boolean[nums.length];
        map = new HashMap();
        permuteUnique(0);
        return ans;
    }

    private void permuteUnique(int index){
        if(index == nums.length){
            if(!map.containsKey(geneKey())){
                List<Integer> list = new ArrayList(choose);
                ans.add(list);
                map.put(geneKey(), "");
            }
            return;
        }

        for(int i=0; i<nums.length; i++){
            if(!used[i]){
                used[i] = true;
                choose.add(nums[i]);
                permuteUnique(index + 1);
                used[i] = false;
                choose.remove(choose.size()-1);
            }
        }
    }

    private String geneKey(){
        String key = "";
        for(int str : choose){
            key += "-" + str;
        }
        return key;
    }
}

5、反转二叉树(简单)

题目地址:https://leetcode.cn/problems/invert-binary-tree/description/

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例 1:

img

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

示例 2:

img

输入:root = [2,1,3]
输出:[2,3,1]

示例 3:

输入:root = []
输出:[]

提示:

  • 树中节点数目范围在 [0, 100]
  • -100 <= Node.val <= 100

解答

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode invertTree(TreeNode root) {
        if(root == null){
            return root;
        }
        TreeNode left = root.left;
        root.left = root.right;
        root.right = left;
        invertTree(root.left);
        invertTree(root.right);
        return root;
    }
}

6、验证二叉搜索树(中等)

题目地址:https://leetcode.cn/problems/validate-binary-search-tree/

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

img

输入:root = [2,1,3]
输出:true

示例 2:

img

输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。

解答

​ 定义一个最大值一个最小值,那么当前节点应该在这个区间内,左子节点应该在最小值与当前节点值的区间内,右子节点应该在当前节点与最大值的区间内。

​ 这里主要是节点值的区间是从int的最小值到最大值,因此定义范围时就不能用int,应该用long

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public boolean isValidBST(TreeNode root) {
        return isValidBST(root, Long.valueOf(Integer.MIN_VALUE) -1, Long.valueOf(Integer.MAX_VALUE) +1);
    }

    public boolean isValidBST(TreeNode root, long min, long max) {
        if(root == null){
            return true;
        }
        if(root.val<=min || root.val>=max){
            return false;
        }
        return isValidBST(root.left, min, root.val)
            && isValidBST(root.right, root.val, max);
    }
}

7、二叉树的最大深度(简单)

题目地址:https://leetcode.cn/problems/maximum-depth-of-binary-tree/description/

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 3 。

解答

​ 返回左子节点和右子节点的最大值即可

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int maxDepth(TreeNode root) {
        return maxDepth(root, 0);
    }

    private int maxDepth(TreeNode node, int depth){
        if(node == null){
            return depth;
        }
        return Math.max(maxDepth(node.left, depth + 1), maxDepth(node.right, depth + 1));
    }
}

8、二叉树的最小深度(简单)

题目地址:https://leetcode.cn/problems/minimum-depth-of-binary-tree/description/

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明:叶子节点是指没有子节点的节点。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:2

示例 2:

输入:root = [2,null,3,null,4,null,5,null,6]
输出:5

解答

​ 返回左子节点和右子节点的最小值

​ 题目中定义了叶子节点是指没有子节点的节点,因此要处理只有一个子节点的情况,这种情况就不能到此节点位置,应该返回另一侧的值。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int minDepth(TreeNode root) {
        if(root == null){
            return 0;
        }
        return minDepth(root, 0);
    }

    private int minDepth(TreeNode node, int depth){
        if(node.left == null && node.right == null){
            return depth+1;
        }
        if(node.left == null){
            return minDepth(node.right, depth+1);
        }
        if(node.right == null){
            return minDepth(node.left, depth + 1);
        }
        return Math.min(minDepth(node.left, depth+1), minDepth(node.right, depth+1));
    }
}

二、分治

0、分治概述

分治,即“分而治之”,就是把原问题划分成若干个同类子问题,分别解决后,再把结果合并起来

关键点:原问题和各个子问题都是重复的(同类的)————递归定义

除了向下递归“问题”,还要向上合并“结果”

分治算法一般用递归实现

1、括号生成(中等)

题目地址:https://leetcode.cn/problems/generate-parentheses/description/

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1
输出:["()"]

提示:

  • 1 <= n <= 8

解答

​ 按照左右分为两部分,分别计算,然后拼装

​ 使用分治,如果是3,可以分为 0-3,1-2,2-1,3-0

​ 对于 1-2,1只能是 只能是 (),对于2,可以是 ()(),也可以是 (()),那么就可以合并为 ()()() 和 ()(()) 两种情况

​ 对于 2-1, 2可以是 ()(),也可以是 (()),1只能是 () ,那么就可以合并为 ()()() 和 (()) () 两种情况

​ 对于 3-0,就是 () 里面套了2个,即 ((())) 或者 (()()),0-3也是一样的效果

​ 对于上述分析,可以发现,出现了重复项,为了保证不出重复项,就可以把左侧看为一个完整的整体,即肯定是使用 () 包括起来的,就不会出现重复了,例如 1 必须是 (),2 必须是 (()),3必须是 (()()) 或 (()()) ,这样左侧肯定是一个不可分割的整体,就不会出现重复项

​ 那如何保证他是一个整体呢,就是一定让其是被括号包括起来的,如果左侧的长度为 left,右侧长度为 right,那么就可以使用 "(" + lStr + ")" + rStr 来计算,其中 lStr 是左侧的括号,rStr 是右侧的括号,但是左侧括号因为外部又包了一个括号,因此是使用的 left -1 来计算的。

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> list = new ArrayList();
        if(n == 0){
            list.add("");
            return list;
        }
        for(int i=1;i<=n;i++){
            List<String> left = generateParenthesis(i-1);
            List<String> right = generateParenthesis(n-i);
            for(String lStr : left){
                for(String rStr : right){
                    list.add("(" + lStr + ")" + rStr);
                }
            }
        }
        return list;   
    }
}

2、合并K个升序链表(困难)

题目地址:https://leetcode.cn/problems/merge-k-sorted-lists/description/

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例 2:

输入:lists = []
输出:[]

示例 3:

输入:lists = [[]]
输出:[]

解答

​ 进行分治处理,每次取中间节点,将数组分为左右两个数组,将左右两个数组合并为两个链表,再对这两个链表进行合并,这部分使用递归

​ 在递归方法中,首先,如果数组长度为0,返回null,如果为1,返回当前链表,如果为2,返回这两个链表的合并结果,否则,继续拆分为左右数组进行合并。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0){
            return null;
        }
        if(lists.length == 1){
            return lists[0];
        }
        return mergeKLists(lists, 0, lists.length-1);
    }


    private ListNode mergeKLists(ListNode[] lists, int left, int right) {
        if(right - left < 0){
            return null;
        }
        if(right - left == 0){
            return lists[left];
        }
        if(right - left == 1){
            return mergeTwo(lists[left], lists[right]);
        }
        int mid = (right - left)/2 + left;
        ListNode leftNode = mergeKLists(lists, left, mid); // 0,1
        ListNode rightNode = mergeKLists(lists, mid+1, right); // 2,2
        return mergeTwo(leftNode, rightNode);
    }
    

    private ListNode mergeTwo(ListNode leftNode, ListNode rightNode){
        ListNode headNode = new ListNode();
        ListNode tempNode = headNode;
        while(leftNode != null || rightNode != null){
            if(leftNode == null){
                tempNode.next = rightNode;
                break;
            }
            if(rightNode == null){
                tempNode.next = leftNode;
                break;
            }
            if(leftNode.val < rightNode.val){
                tempNode.next = leftNode;
                tempNode = tempNode.next;
                leftNode = leftNode.next;
            }else{
                tempNode.next = rightNode;
                tempNode = tempNode.next;
                rightNode = rightNode.next;
            }
        }
        tempNode = headNode;
        return headNode.next;
    }
}
posted @ 2022-12-08 11:40  李聪龙  阅读(48)  评论(0编辑  收藏  举报