贪心算法 - 2(7000 字详解)

贪心算法

1.1 最优除法

题目链接:最优除法

在这里插入图片描述
在这里插入图片描述

class Solution {
    public String optimalDivision(int[] nums) {
        int n = nums.length; 
        StringBuffer ret = new StringBuffer(); 

        // 先处理只有一个数字和只有两个数字的情况,如果只有一个数字的情况:直接返回该数字作为结果
        // 如果只有两个数字的情况:直接用 "/" 分隔两个数字
        if (n == 1)  return ret.append(nums[0]).toString(); 
        if (n == 2) return ret.append(nums[0]).append("/").append(nums[1]).toString();
        

        // 接下来就是处理多个数字的情况,先拼接第一个数字和第二个数字,第一个数字当作分子,第二个数字当作分母
        ret.append(nums[0]).append("/(").append(nums[1]); 

        // 遍历剩余的数字,从第3个数字开始依次加入除法符号和数字
        for (int i = 2; i < n; i++) {
            ret.append("/").append(nums[i]); // 添加除法符号和当前数字
        }

        // 关闭括号
        ret.append(")");

        // 返回最终结果字符串
        return ret.toString();
    }
}

在这里插入图片描述

1.2 跳跃游戏 II

题目链接:跳跃游戏 II

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int jump(int[] nums) {
        // left 和 right 表示当前层能覆盖的范围,ret 存储跳跃次数,maxPos 表示下一层的最远覆盖范围, n 记录数组的大小
        int left = 0, right = 0, ret = 0, maxPos = 0, n = nums.length;

        while (left <= right) { 
            // 如果下一层的最远覆盖范围已经到达或超过了最后一个位置,直接返回跳跃次数。
            if (maxPos >= n - 1) return ret;
            
            // 遍历当前 left 到 right 的层数,更新下一层的最远覆盖范围
            for (int i = left; i <= right; i++) 
                maxPos = Math.max(maxPos, nums[i] + i);
            

            // 接着更新下一层的范围:下一层的左端点是当前层的右端点的下一个位置,下一层的右端点是当前层能跳到的最远位置 maxPos,接着更新一下跳跃次数
            left = right + 1;
            right = maxPos;
            ret++;
        }

        return -1;
    }
}

在这里插入图片描述

1.3 跳跃游戏

题目链接:跳跃游戏

在这里插入图片描述
在这里插入图片描述

class Solution {
    public boolean canJump(int[] nums) {
        // left 和 right 表示当前层能覆盖的范围,ret 存储跳跃次数,maxPos 表示下一层的最远覆盖范围, n 记录数组的大小
        int left = 0, right = 0, ret = 0, maxPos = 0, n = nums.length;

        while (left <= right) { // 确保当前还在跳跃范围内
            // 如果当前最远范围已经覆盖到最后一个位置,返回 true。
            if (maxPos >= n - 1) return true; 
            
            // 遍历当前范围内所有可能的跳跃位置。
            for (int i = left; i <= right; i++) 
                maxPos = Math.max(maxPos, nums[i] + i);
            
            // 更新下一次跳跃的范围:
            left = right + 1;
            right = maxPos;
        }
        
        return false;
    }
}

1.4 加油站

题目链接:加油站

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length; 

        // 遍历所有可能的起点,尝试找到一个能完成绕行的起点
        for (int i = 0; i < n; i++) {
            int rest = 0; // 记录当前油量余额
            int step = 0; // 记录从当前起点行驶的步数

            // 尝试从第 i 个加油站开始走 n 步,即绕行整个环
            for (; step < n; step++) {
                int index = (i + step) % n; // 当前加油站的下标
                rest = rest + gas[index] - cost[index];
                if (rest < 0) break;  // 如果当前油量余额小于 0,则说明无法从当前起点完成绕行
            }

            if (step == n) return i; 
            
            // 优化:直接跳过已经尝试过的步数
            i = i + step;
        }

        // 如果尝试了所有起点仍然无法绕行,则返回 -1
        return -1;
    }
}

在这里插入图片描述

1.5 单调递增的数字

题目链接:单调递增的数字

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int monotoneIncreasingDigits(int n) {
        // 采用贪心的策略解决问题
        // 先把 n 转为字符串,再由字符串转为字符数组
        char[] s = Integer.toString(n).toCharArray();
        int i = 0, m = s.length; // i 用来遍历字符数组 s,m 记录字符数组的长度

        // 接着从左往右找到第一个递减的数字
        while(i + 1 < m && s[i] <= s[i + 1]) i++;

        // 如果没找到,说明到最后一个数字都还是递增的,那么直接返回这个数字即可,如果找到了就看看是否需要回退
        if(i == m - 1) return n;

        while (i - 1 >= 0 && s[i] == s[i - 1]) i--;

        // 接着把 i 位置上的值减一,其他位置变成 9
        s[i]--;

        for(int j = i + 1; j < m; j++)
            s[j] = '9';
        

        return Integer.parseInt(new String(s));
    }
}

在这里插入图片描述

1.6 环形的计算器

题目链接:环形的计算器
在这里插入图片描述
在这里插入图片描述

class Solution {
    public int brokenCalc(int startValue, int target) {
        // 反向思维 + 贪心算法,用 ret 记录最小操作数
        int ret = 0;
        while(target > startValue){
            if(target % 2 == 1) target++;
            else target /= 2;
            ret++; // 判断完后要让操作次数加一
        }

        // 因为 target 小于 startValue 的时候是一直加一的,我们没必要再写一个循环,直接加上他们的差值即可
        return ret + (startValue - target);
    }
}

在这里插入图片描述

1.7 合并区间

题目链接:合并区间

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int[][] merge(int[][] intervals) {
        // 1. 按照区间的左端点升序排序
        Arrays.sort(intervals, (v1, v2) -> {
            return v1[0] - v2[0]; 
        });

        // 初始化左右边界,设置 left 和 right 为第一个区间的左右端点
        int left = intervals[0][0];  
        int right = intervals[0][1]; 
        
        // ret 用于存储合并后的区间结果
        List<int[]> ret = new ArrayList<>();
        
        // 从第二个区间开始合并
        for (int i = 1; i < intervals.length; i++) { 
            // a 为左端点,b 为右端点
            int a = intervals[i][0]; 
            int b = intervals[i][1]; 
            
            if (a <= right) right = Math.max(right, b); 
            else {
                ret.add(new int[]{left, right}); 
                left = a; right = b; 
            }
        }

        // 别忘了处理最后一个区间,将其加入结果列表,最后把 ret 转为二维数组返回
        ret.add(new int[]{left, right});
        return ret.toArray(new int[0][]);
    }
}

在这里插入图片描述

1.8 无重叠区间

题目链接:无重叠区间
在这里插入图片描述
在这里插入图片描述

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        // 按照区间的左端点升序排序
        Arrays.sort(intervals, (v1, v2) -> {
            return v1[0] - v2[0]; 
        });

        // ret 存储要移除区间的数量
        int ret = 0;

        // 初始化 left 和 right 为第一个区间的左右端点
        int left = intervals[0][0];  // 左端点用不到,可删除
        int right = intervals[0][1]; 

        // 开始从第二个区间检查重叠并移除
        for (int i = 1; i < intervals.length; i++) { 
            // 初始化 a 为左端点,b 为右端点
            int a = intervals[i][0]; 
            int b = intervals[i][1]; 

            if (a < right) { 
                right = Math.min(right, b); // 贪心策略:保留右端点较小的区间,减小后续重叠可能性
                ret++; 
            } else right = b; 
            
        }

        return ret;
    }
}

在这里插入图片描述

1.9 用最少数量的箭引爆气球

题目链接:用最少数量的箭引爆气球

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int findMinArrowShots(int[][] points) {
        // 按照气球区间的左端点进行升序排序
        Arrays.sort(points, (v1, v2) -> {
            return Integer.compare(v1[0], v2[0]); // 直接相减会溢出,所以采用 compare 的方式来比较
        });

        // 初始化 right 为第一个区间的右端点,用 ret 存储最终结果,ret 初始化为 1,因为至少需要一支箭
        int right = points[0][1],  ret = 1;

        // 接着从第二个区间开始遍历所有气球的区间
        for (int i = 1; i < points.length; i++) { 
            // a 为左端点,b 为右端点
            int a = points[i][0]; 
            int b = points[i][1]; 

            if (a <= right)  right = Math.min(right, b);
            else {              
                right = b;
                ret++;
            }
        }

        return ret;
    }
}

在这里插入图片描述

1.10 整数替换

题目链接:整数替换
在这里插入图片描述
在这里插入图片描述

class Solution {
    public int integerReplacement(int n) {
        // 采用贪心策略解决这个问题, 用 ret 存储最终的结果
        int ret = 0;

        // 只要当 n 大于 1 的时候就一直执行循环
        while(n > 1){
            // 如果 n 是偶数,没得贪,只能除 2
            if(n % 2 == 0){
                n /= 2; ret++;
            }else{
                // 如果 n 是奇数,要以 3 为条件分三个情况讨论
                if(n == 3){
                    ret += 2;
                    n = 1;
                }else if(n % 4 == 1){
                    // 贪心策略,这个情况减一除二就可以了,但是因为 n 的数据范围太大,我们直接除 2,因为是奇数会自动把 1 消除
                    n /= 2;
                    ret += 2;
                }else{
                    // 贪心策略,这个情况加一除二,同样的,n 的范围太大,所以我们先除 2 再加一
                    n /= 2; n += 1;
                    ret += 2;
                }
            }
        }

        return ret;
    }
}

在这里插入图片描述

1.11 俄罗斯套娃信封问题

题目链接:俄罗斯套娃信封问题

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int maxEnvelopes(int[][] e) {
        // 采用贪心的策略解决问题首先按照第一个维度(宽度)升序排序,如果宽度相同,则按照第二个维度(高度)降序排序
        Arrays.sort(e, (v1, v2) -> {
            if (v1[0] != v2[0]) return v1[0] - v2[0]; // 如果宽度不同,按宽度升序排列
            else return v2[1] - v1[1]; // 如果宽度相同,按高度降序排列
        });

        // 用 ret 存储递增的高度列表,首先把第一个信封的高度加入到 ret 中
        ArrayList<Integer> ret = new ArrayList<>();
        ret.add(e[0][1]);

        // 接着遍历所有的信封,尝试构建递增子序列
        for (int i = 1; i < e.length; i++) {
            int b = e[i][1];  // 当前信封的高度
            
            // 如果当前信封的高度大于 ret 中最后一个高度,说明可以继续扩展递增子序列
            if (b > ret.get(ret.size() - 1)) ret.add(b);  
            else {
                // 如果当前信封的高度小于或等于 ret 中最后一个高度,我们就需要通过二分查找找到合适的位置来替换掉原有的元素
                int left = 0, right = ret.size() - 1;
                
                while (left < right) {
                    int mid = (left + right) / 2;
                    
                    if (ret.get(mid) >= b) {
                        right = mid;  
                    } else {
                        left = mid + 1;  
                    }
                }
                
                ret.set(left, b);
            }
        }

        return ret.size();
    }
}

在这里插入图片描述

1.12 可被三整数的最大和

题目链接:可被三整数的最大和

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int maxSumDivThree(int[] nums) {
        // 利用贪心的策略解决这个问题。因为数据过大,所以我们用 0x3f3f3f3f 表示正无穷大,sum 用于记录整个数组的和
        int INF = 0x3f3f3f3f, sum = 0;
        // 用 x1, x2 表示余数为一时的最小值和次小值,y1,y2 表示余数为 2 时的最小值和次小值
        int x1 = INF, x2 = INF, y1 = INF, y2 = INF;

        // 接着我们在遍历数组的同时更新 sum 和 x1,x2,y1,y2 的值
        for(int i = 0; i < nums.length; i++){
            // 首先更新 sum
            sum += nums[i];

            // 接着更新 x1,x2,y1,y2,接着分情况更新他们的值
            // 当余数为 1 的时候更新 x1 和 x2 的值
            if(nums[i] % 3 == 1){
                if(nums[i] < x1){
                    x2 = x1;
                    x1 = nums[i];
                }else if(nums[i] >= x1 && nums[i] < x2){
                    x2 = nums[i];
                }
            }

            // 当余数为 2 的时候,更新 y1 和 y2 的值
            if(nums[i] % 3 == 2){
                if(nums[i] < y1){
                    y2 = y1;
                    y1 = nums[i];
                }else if(nums[i] >= y1 && nums[i] < y2){
                    y2 = nums[i];
                }
            }
        }

        // 接着可以开始正式操作了
        if(sum % 3 == 0) return sum;
        else if(sum % 3 == 1) return Math.max(sum - x1, sum - y1 - y2);
        else return Math.max(sum - y1, sum - x1 - x2);

    }
}

在这里插入图片描述

1.13 距离相等的条形码

题目链接:距离相等的条形码
在这里插入图片描述
在这里插入图片描述

class Solution {
    public int[] rearrangeBarcodes(int[] barcodes) {
        // 采用贪心的策略解决这个问题
        // 首先统计每个数字的出现次数,此处采用哈希表统计
        Map<Integer, Integer> hash = new HashMap<>();

        // 接着把 barcodes 中的元素全丢入哈希表中,用 maxValue 标记出现最多的那个数字,用 maxCount 标记出现最多的次数
        int maxValue = 0, maxCount = 0;
        for(int x : barcodes){
            hash.put(x, hash.getOrDefault(x, 0) + 1);
            // 把 x 丢入哈希表中看看 x 数字的出现次数有没有超过出现最多的那个数字
            if(hash.get(x) > maxCount){
                maxValue = x;
                maxCount = hash.get(x);
            }
        }

        // 统计好后先把出现次数最多的那个数字先填了
        int index = 0; // index 用于标记正在填写的位置
        int[] ret = new int[barcodes.length];
        for(int i = 0; i < maxCount; i++){
            ret[index] = maxValue;
            index += 2;
        }

        // 把出现最多次数最多的数字填完后把这个数字移除哈希表
        hash.remove(maxValue);

        // 接着开始遍历哈希表中 keySet 剩下的数字
        for(int x : hash.keySet()){
            for (int i = 0; i < hash.get(x); i++){
                // 如果 index 超出数组范围,从索引 1 开始填充奇数位置
                if (index >= barcodes.length) index = 1;
                ret[index] = x; 
                index += 2;
            }
        }

        return ret;
    }
}

在这里插入图片描述

1.14 重构字符串

题目链接:重构字符串
在这里插入图片描述
在这里插入图片描述

class Solution {
    public String reorganizeString(String s) {
        // 首先统计一下每个字符出现的次数,此处用数字模拟哈希表,maxChar 用来记录出现次数最多的字符,maxCount 记录其出现次数
        int[] hash = new int[26];
        char maxChar = ' ';
        int maxCount = 0;

        // 统计每个字符出现的次数,同时记录出现次数最多的字符
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i); 
            hash[ch - 'a']++; 

            // 如果当前字符的计数超过 maxCount,则更新 maxChar 和 maxCount
            if (maxCount < hash[ch - 'a']) {
                maxChar = ch; 
                maxCount = hash[ch - 'a']; 
            }
        }

        // 处理一下特殊情况,当 maxCount 大于 数组长度的一半,那么这个字符串是不能重构的,返回空字符串即可
        int n = s.length(); 
        if (maxCount > (n + 1) / 2) return "";

        // ret 用于存储最终结果
        char[] ret = new char[n];
        int index = 0; // index 代表目前填充的位置

        // 首先处理出现次数最多的字符
        for (int i = 0; i < maxCount; i++) {
            ret[index] = maxChar; 
            index += 2; 
        }

        // 将 maxChar 的计数清零,因为它已经全部填入结果数组
        hash[maxChar - 'a'] = 0;

        // 接着处理剩下的字符
        for (int i = 0; i < 26; i++) { 
            for (int j = 0; j < hash[i]; j++) { 
                if (index >= n) index = 1; // 如果偶数位置已满,从索引 1 开始填充奇数位置
                ret[index] = (char)(i + 'a'); 
                index += 2; 
            }
        }

        return new String(ret);
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ice___Cpu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值