leetcode 分治算法

分治算法入门

以下节选自 leetcode上的入门题

分治算法

所谓的分治算法通俗来讲,就是将大的问题拆解成许多单一的子问题,通过解决子问题,并合并子问题结果反推原问题。也就是递归的思想。

169 多数元素

采用暴力算法,依次遍历数组中每个元素出现的次数,时间复杂度为O(n*n),会超时。肯定不是改题目的本意。

方法一:分治思想,递归思路

采用分治思想,递归思路。
     * 1、确定切分的终止条件,直到所有的子问题都是长度为 1 的数组,停止切分。
     * 2、拆分数组,递归地将原数组二分为左区间与右区间,直到最终的数组只剩下一个元素,将其返回
     * 3、处理子问题得到子结果,并合并
     *    3.1 长度为 1 的子数组中唯一的数显然是众数,直接返回即可。
     *    3.2 如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。
     *    3.3 如果他们的众数不同,比较两个众数在整个区间内出现的次数来决定该区间的众数

代码实现:

public class Main {
    /**
     * 采用分治思想,递归思路。
     * 1、确定切分的终止条件,直到所有的子问题都是长度为 1 的数组,停止切分。
     * 2、拆分数组,递归地将原数组二分为左区间与右区间,直到最终的数组只剩下一个元素,将其返回
     * 3、处理子问题得到子结果,并合并
     *    3.1 长度为 1 的子数组中唯一的数显然是众数,直接返回即可。
     *    3.2 如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。
     *    3.3 如果他们的众数不同,比较两个众数在整个区间内出现的次数来决定该区间的众数
     * @param nums
     * @return
     */
    public int majorityElement(int[] nums) {
        if (nums.length < 1) return 0;
        return help(nums, 0, nums.length - 1);
    }

    private int help(int[] nums, int start, int end) {
        // 1、拆分数组,直到剩下最后一个 一定为众数
        if (start == end) return nums[start];
        // 2、处理子问题
        int mid = start + (end - start) / 2;
        int left = help(nums,start,mid);
        int right = help(nums, mid+1,end);
        //  如果它们的众数相同,那么显然这一段区间的众数是它们相同的值。
        if (left == right)
            return left;

        // 统计左右区间的众数
        int leftCount = countElement(nums, left, start, end);
        int rightCount = countElement(nums, right, start, end);

       return leftCount > rightCount ? left : right;
    }

    private int countElement(int[] nums, int num, int start, int end) {
        int count = 0;
        for (int i = start; i <= end; i++) {
            if (num == nums[i])
                count++;
        }
        return count;
    }

    public static void main(String[] args) {
        Main test = new Main();
        int[] nums = {3,3,3,2,1};
        System.out.println(test.majorityElement(nums));
    }
}

复杂度分析

  • 时间复杂度:O($nlogn$)
  • 空间复杂度:O($nlogn$)。用到了递归,需要调用栈,所以复杂度为O($nlong$)

递归讲起来比较抽象,需要debug看看调用栈的信息,下面介绍个好理解的方法

方法二:HashMap

HashMap采用key-value的形式存储数据,刚好可以统计数组中各元素出现的次数。

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class Main {

    public static int majorityElement(int[] nums) {

        HashMap<Integer, Integer> map = new HashMap<>();
        // 统计各元素出现的次数
        for (int i = 0; i < nums.length; i++) {
            map.put(nums[i], map.getOrDefault(nums[i],0) + 1);   
        }

        Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
        for (Map.Entry<Integer,Integer> entry : entries) {
            // 找出满足条件的众数
            if (entry.getValue() > (nums.length / 2)) {
                return entry.getKey();
            }
        }
        return 0;   // 题目描述一定存在,所以这块不会被执行到,返回不报错的整数即可
    }


    public static void main(String[] args) {
        int[] nums = {3,3,3,2,1};
        System.out.println(Main.majorityElement(nums));;
    }
}

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n). 哈希表中最多包含 n - n/2个键值对,所以占用空间为O(n). 题目保证一定有一个众数,而一个长度为n的数组最多只包含n个不同的值,会占用n/2 + 1个数字,所以最多有n-(n/2+1)个不同的其他数字,所以最多有n-n/2(取整)个不同的元素。

更多方法参考:leetcode讨论区

53. 最大子序和

方法一:暴力算法

public int maxSubArray(int[] nums) {
    int res = Integer.MIN_VALUE;   // 每次遍历寻找最大子序和
    for (int i = 0; i < nums.length; i++) {
        int sum = 0;  // 用来保存子序列的和
        for (int j = i; j < nums.length; j++) {
            sum += nums[j];
            res = Math.max(res, sum);
        }
    }
    return res;
}

复杂度分析:

  • 时间复杂度:O(N*N)
  • 空间复杂度:O(1)

方法二:动态规划

第 i 个子组合的最大值可以通过第i-1个子组合的最大值和第 i 个数字获得,如果第 i-1 个子组合的最大值没法给第 i 个数字带来正增益,我们就抛弃掉前面的子组合,自己就是最大的了。

public int maxSubArray(int[] nums) {
    int[] dp = new int[nums.length];
    dp[0] = nums[0]; // 初始化
    for (int i = 1; i < nums.length; i++) {
        // 状态转移方程
        if (dp[i-1] >= 0) {
            dp[i] = dp[i-1] + nums[i];
        } else {
            dp[i] = nums[i];
        }
    }

    // dp数组中记录了所有的子序列的和,找出最大的即可
    int res = Integer.MIN_VALUE;
    for (int i = 0; i < dp.length; i++) {
        res = Math.max(res, dp[i]);
    }
    return res;
}

结合本题,可以进一步优化,具体如下:

public int maxSubArray(int[] nums) {
        int res = nums[0];
        int sum = nums[0];

        for (int i = 1; i < nums.length; i++) {
            if (sum >= 0) {
                sum += nums[i];
            } else {
                sum = nums[i];
            }
            res = Math.max(res, sum);
        }
        return res;
    }

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

方法三:分治法

类似于归并排序,先切分后合并

public int maxSubArray(int[] nums) {
    return maxSubArrayDivideWithBorder(nums, 0, nums.length-1);
}

private int maxSubArrayDivideWithBorder(int[] nums, int start, int end) {
    // 递归终止条件,只有一个元素的时候
    if (start == end) return nums[start];

    int mid = start + (end - start) / 2;
    int leftMax = maxSubArrayDivideWithBorder(nums, start, mid);
    int rightMax = maxSubArrayDivideWithBorder(nums, mid + 1, end);

    // 下面计算横跨两个子序列的最大值
    // 计算包含左侧子序列最后一个元素的子序列最大值
    int leftCrossMax = Integer.MIN_VALUE;
    int leftCrossSum = 0;
    for (int i = mid; i >= start; --i) {
        leftCrossSum += nums[i];
        leftCrossMax = Math.max(leftCrossMax, leftCrossSum);
    }

    // 计算包含右侧子序列最后一个元素的子序列最大值
    int rightCrossMax = nums[mid + 1];
    int rightCrossSum = 0;
    for (int i = mid + 1; i <= end; i++) {
        rightCrossSum += nums[i];
        rightCrossMax = Math.max(rightCrossMax, rightCrossSum);
    }

    // 计算跨中心的子序列的最大值
    int crossMax = leftCrossMax + rightCrossMax;

    return Math.max(crossMax, Math.max(leftMax, rightMax));
}

复杂度分析:

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(logn)

参考:leetcode讨论区

50、Pow(x, n)

实现 [pow(x, n)],即计算 x 的 n 次幂函数。

思路:

public double myPow(double x, int n) {
    if (x == 0.0) return 0.0;
    double res = 1.0;
    boolean isP = true;  // 判断是否为正数
    if (n < 0) {
        isP = false;
        n = -n;
    }

    while (n != 0) {
        if ((n&1) == 1) {  // 等价于 n % 2 == 1 奇数
            res *= x;
        }
        x *= x;  // 等价于 x = x^2;
        n /= 2;  // 减半
    }
    return isP?res:1/res;
}

复杂度分析:

  • 时间复杂度:O($logn$)
  • 空间复杂度:O(1)

套路:

1) 拆分   
	对于数组元素,一般分解到不能拆分的 单个元素位置  直接return
2)解决子问题
	int mid = start + (end-start)/2
	int left = Recursion(start,mid)
	int right = Recursion(mid+1,end)
3)合并
   根据题意要求编写逻辑

(前两步都是固定的,最后一步需要根据题意灵活处理)

拓展

241. 为运算表达式设计优先级

给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。

示例 1:

输入: "2-1-1"
输出: [0, 2]
解释:
((2-1)-1) = 0
(2-(1-1)) = 2

示例 2:

输入: "2*3-4*5"
输出: [-34, -14, -10, -10, 10]
解释:
(2*(3-(4*5))) = -34
((2*3)-(4*5)) = -14
((2*(3-4))*5) = -10
(2*((3-4)*5)) = -10
(((2*3)-4)*5) = 10

采用分治算法,套模板
比如:2*3-4*5
经过递归拆分成 2  3  4  5
之后再从4 * 5 依次往上推


1)拆分  去除运算符,拆解到只剩下单个元素为止
		具体步骤:将输入的字符串中的运算符去除,转化为整数形式
2)解决子问题    // 通过运算符将字符串分为左右两部分 递归运算得到两个集合结果,接着运算子序列

3)合并  将得到的子序列运算结果存在result中,并在map中存一份,目的是为了避免重复运算子序列提高效率

具体实现:

HashMap<String,List<Integer>> map = new HashMap<>();

public List<Integer> diffWaysToCompute(String input) {
    // 边界处理
    if (input == null || input.length() < 1) return null;

    // 1)拆分  除去运算符,拆解到只剩下一个单个元素为止
    //如果已经有当前解了,直接返回
    if(map.containsKey(input)){
        return map.get(input);
    }

    // 将单个元素转换为整数形式
    List<Integer> result = new ArrayList<>();
    int num = 0;
    int i;
    for (i = 0; i < input.length() && !isOperation(input.charAt(i)); i++) {
        num = num * 10 + input.charAt(i) - '0';
    }
    //将全数字的情况直接返回
    if (i == input.length()){
        result.add(num);
        // 存到map
        map.put(input, result);
        return result;
    }

    // 2)解决子问题
    for (int j = 0; j < input.length(); j++) {
        // 通过运算符将字符串分为两部分
        if (isOperation(input.charAt(j))) {
            List<Integer> result1 = diffWaysToCompute(input.substring(0,j)); // 左闭右开区间
            List<Integer> result2 = diffWaysToCompute(input.substring(j+1));  // 省略最右边界
            // 将两个结果依次运算
            for (int k = 0; k < result1.size(); k++) {
                for (int l = 0; l < result2.size(); l++) {
                    char op = input.charAt(j);
                    result.add(calculate(result1.get(k), op, result2.get(l)));
                }
            }
        }
    }

    //存到 map
    map.put(input, result);
    Collections.reverse(result);   // 因为是递归,不加这句得到的答案逆序的,提交过不了
    return result;
}

private int calculate(int num1, char c, int num2) {
    switch (c) {
        case '+':
            return num1 + num2;
        case '-':
            return num1 - num2;
        case '*':
            return num1 * num2;
    }
    return -1;
}

private boolean isOperation(char c) {
    return c == '+' || c == '-' || c == '*';
}

参考连接:讨论版

posted @ 2020-08-18 00:29  BMDACM  阅读(407)  评论(0编辑  收藏  举报