zoukankan      html  css  js  c++  java
  • 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 == '*';
    }
    

    参考连接:讨论版

  • 相关阅读:
    Ubuntu安装teamviewer(附带解决dpkg占用的代码)
    使用print时出错 SyntaxError: Missing parentheses in call to 'print' Did you mean print("Usage....
    非root用户(普通用户)安装CMake
    Permanently added the RSA host key for IP address '13.229.188.59' Permission denied (publickey)fatal
    django6-admin
    django6-信号
    django6-缓存
    django6 -中间件
    天津去哪玩
    django--->form表单
  • 原文地址:https://www.cnblogs.com/guohaoblog/p/13521120.html
Copyright © 2011-2022 走看看