首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >LeetCode贪心算法全解析:从基础到高级应用

LeetCode贪心算法全解析:从基础到高级应用

作者头像
安全风信子
发布2025-11-13 13:21:23
发布2025-11-13 13:21:23
1790
举报
文章被收录于专栏:AI SPPECHAI SPPECH

一、贪心算法基础概念

1.1 贪心算法的定义

贪心算法(Greedy Algorithm)是一种在解决问题时总是做出在当前看来最好选择的算法。也就是说,贪心算法并不从整体最优上加以考虑,它所做出的选择只是在某种意义上的局部最优选择。

贪心算法的核心思想是:通过局部最优的选择,期望能够导致全局最优的结果。虽然贪心算法不能保证在所有情况下都能得到全局最优解,但在许多问题中,贪心算法能够产生全局最优解,或者在可以接受的时间复杂度内得到一个较好的近似解。

1.2 贪心算法的特点

贪心算法具有以下特点:

  1. 局部最优选择:贪心算法在每一步都做出当前看来最好的选择,即局部最优选择。
  2. 无后效性:贪心算法做出的选择不会影响之前的状态,只与当前状态有关。
  3. 最优子结构:问题的最优解包含子问题的最优解。
1.3 贪心算法的适用场景

贪心算法适用于具有以下性质的问题:

  1. 贪心选择性质:所求问题的整体最优解可以通过一系列局部最优的选择来达到。
  2. 最优子结构性质:问题的最优解包含子问题的最优解。

常见的贪心算法应用场景包括:

  1. 活动选择问题:如会议安排、课程表安排等。
  2. 霍夫曼编码:用于数据压缩。
  3. 最小生成树算法:如Kruskal算法和Prim算法。
  4. 最短路径算法:如Dijkstra算法。
  5. 背包问题:如分数背包问题。
  6. 任务调度问题:如CPU调度、磁盘调度等。
1.4 贪心算法与其他算法的区别

贪心算法与其他算法的主要区别如下:

  1. 与动态规划的区别
    • 贪心算法在每一步都做出局部最优选择,且一旦做出选择就不再回溯。
    • 动态规划则会保存所有子问题的解,并根据子问题的解来做出选择,可能会进行回溯。
  2. 与分治法的区别
    • 贪心算法和分治法都将问题分解为子问题,但贪心算法在分解时会做出局部最优选择,而分治法则不会。
    • 贪心算法的子问题是相互关联的,而分治法的子问题是相互独立的。

二、贪心算法的基本策略

2.1 贪心策略的选择

贪心算法的关键在于如何选择贪心策略,即如何在每一步做出局部最优选择。常见的贪心策略包括:

  1. 排序后选择:将问题中的元素按照某种规则排序,然后依次选择最优的元素。
    • 例如,在活动选择问题中,我们可以按照活动的结束时间排序,然后每次选择结束时间最早的活动。
  2. 优先级队列选择:使用优先级队列来维护候选元素,每次从队列中取出最优的元素。
    • 例如,在Huffman编码中,我们使用最小堆来维护字符的频率,每次取出频率最小的两个字符进行合并。
  3. 贪心条件判断:根据问题的特点,设计一个贪心条件,每次选择满足该条件的元素。
    • 例如,在跳跃游戏中,我们可以维护一个最远可到达的位置,每次选择能够到达最远位置的下一步。
2.2 贪心算法的证明方法

要证明贪心算法能够得到全局最优解,通常需要证明以下两个性质:

  1. 贪心选择性质:证明问题的全局最优解可以通过一系列局部最优选择来达到。
    • 通常采用反证法,假设存在一个全局最优解,但该解不包含贪心选择,然后推导出矛盾。
  2. 最优子结构性质:证明问题的最优解包含子问题的最优解。
    • 通常采用数学归纳法,假设对于规模较小的子问题,贪心算法能够得到最优解,然后证明对于规模较大的问题,贪心算法也能得到最优解。
2.3 贪心算法的解题步骤

使用贪心算法解决问题的一般步骤如下:

  1. 理解问题:明确问题的目标和约束条件。
  2. 设计贪心策略:根据问题的特点,设计一个贪心策略,即在每一步做出局部最优选择的规则。
  3. 证明贪心策略的正确性:证明该贪心策略能够得到全局最优解,或者在可以接受的时间复杂度内得到一个较好的近似解。
  4. 实现算法:根据贪心策略,编写代码实现算法。
  5. 分析算法的时间复杂度和空间复杂度:评估算法的效率。

三、LeetCode中的贪心算法题目解析

3.1 分发饼干(LeetCode 455)

题目描述:假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例: 输入:g = [1,2,3], s = [1,1] 输出:1 解释: 你有三个孩子和两块饼干,3个孩子的胃口值分别是:1,2,3。 虽然你有两块饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:将饼干和孩子的胃口值都排序,然后每次用最小的能满足孩子胃口的饼干去满足胃口最小的孩子。这样可以保证尽可能多的孩子得到满足。

代码语言:javascript
复制
public class Solution {
    public int findContentChildren(int[] g, int[] s) {
        // 将孩子的胃口值和饼干的尺寸都排序
        Arrays.sort(g);
        Arrays.sort(s);
        int childIndex = 0; // 当前考虑的孩子的索引
        int cookieIndex = 0; // 当前考虑的饼干的索引
        // 遍历所有的饼干,尝试满足尽可能多的孩子
        while (childIndex < g.length && cookieIndex < s.length) {
            // 如果当前饼干可以满足当前孩子的胃口,给这个孩子,并考虑下一个孩子
            if (s[cookieIndex] >= g[childIndex]) {
                childIndex++;
            }
            // 无论是否满足当前孩子,都考虑下一块饼干
            cookieIndex++;
        }
        // 返回满足的孩子的数量
        return childIndex;
    }
}
3.2 买卖股票的最佳时机 II(LeetCode 122)

题目描述:给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例: 输入:prices = [7,1,5,3,6,4] 输出:7 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 总利润为 4 + 3 = 7 。

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:只要今天的价格比昨天高,就进行一次买卖交易。这样可以保证每次交易都能获得正利润,从而最大化总利润。

代码语言:javascript
复制
public class Solution {
    public int maxProfit(int[] prices) {
        int maxProfit = 0;
        // 遍历价格数组,只要今天的价格比昨天高,就进行一次买卖交易
        for (int i = 1; i < prices.length; i++) {
            // 如果今天的价格比昨天高,就可以获利
            if (prices[i] > prices[i - 1]) {
                maxProfit += prices[i] - prices[i - 1];
            }
        }
        return maxProfit;
    }
}
3.3 跳跃游戏(LeetCode 55)

题目描述:给定一个非负整数数组 nums ,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。

示例: 输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:维护一个最远可到达的位置,每次更新这个最远可到达的位置。如果最远可到达的位置大于等于数组的最后一个位置,则说明可以到达最后一个位置。

代码语言:javascript
复制
public class Solution {
    public boolean canJump(int[] nums) {
        int maxReach = 0; // 最远可到达的位置
        // 遍历数组中的每个位置
        for (int i = 0; i < nums.length; i++) {
            // 如果当前位置已经超过了最远可到达的位置,说明无法到达最后一个位置
            if (i > maxReach) {
                return false;
            }
            // 更新最远可到达的位置
            maxReach = Math.max(maxReach, i + nums[i]);
            // 如果最远可到达的位置已经大于等于数组的最后一个位置,说明可以到达最后一个位置
            if (maxReach >= nums.length - 1) {
                return true;
            }
        }
        // 遍历完数组后,如果最远可到达的位置大于等于数组的最后一个位置,说明可以到达最后一个位置
        return maxReach >= nums.length - 1;
    }
}
3.4 跳跃游戏 II(LeetCode 45)

题目描述:给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。假设你总是可以到达数组的最后一个位置。

示例: 输入:nums = [2,3,1,1,4] 输出:2 解释:跳到最后一个位置的最小跳跃数是 2。 从位置 0 跳到位置 1,跳 1 步,然后跳 3 步到达数组的最后一个位置。

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:在每一步,我们都选择能够到达最远位置的下一步。具体来说,我们维护当前可以到达的最远位置和下一次可以到达的最远位置,当我们到达当前可以到达的最远位置时,跳跃次数加1,并更新当前可以到达的最远位置为下一次可以到达的最远位置。

代码语言:javascript
复制
public class Solution {
    public int jump(int[] nums) {
        int jumps = 0; // 跳跃次数
        int currentEnd = 0; // 当前可以到达的最远位置
        int currentMax = 0; // 下一次可以到达的最远位置
        // 遍历数组中的每个位置(除了最后一个位置,因为一旦到达最后一个位置,就不需要再跳跃了)
        for (int i = 0; i < nums.length - 1; i++) {
            // 更新下一次可以到达的最远位置
            currentMax = Math.max(currentMax, i + nums[i]);
            // 如果到达了当前可以到达的最远位置,跳跃次数加1,并更新当前可以到达的最远位置
            if (i == currentEnd) {
                jumps++;
                currentEnd = currentMax;
                // 如果当前可以到达的最远位置已经大于等于数组的最后一个位置,提前结束循环
                if (currentEnd >= nums.length - 1) {
                    break;
                }
            }
        }
        return jumps;
    }
}

四、贪心算法的中级应用

4.1 分发糖果(LeetCode 135)

题目描述:老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。你需要按照以下要求,帮助老师给这些孩子分发糖果:

  1. 每个孩子至少分配到 1 个糖果。
  2. 评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。 请你给每个孩子分发糖果,计算并返回需要准备的最小糖果数目。

示例: 输入:[1,0,2] 输出:5 解释:你可以分别给这三个孩子分发 2、1、2 颗糖果。

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:分两次遍历数组,第一次从左到右,确保每个孩子的糖果数不小于其左侧孩子的糖果数(如果评分更高);第二次从右到左,确保每个孩子的糖果数不小于其右侧孩子的糖果数(如果评分更高)。这样可以保证所有的约束条件都得到满足。

代码语言:javascript
复制
public class Solution {
    public int candy(int[] ratings) {
        int n = ratings.length;
        int[] candies = new int[n];
        // 第一次遍历,从左到右,确保每个孩子的糖果数不小于其左侧孩子的糖果数(如果评分更高)
        for (int i = 0; i < n; i++) {
            // 每个孩子至少分配到1个糖果
            candies[i] = 1;
            // 如果当前孩子的评分高于左侧孩子的评分,当前孩子的糖果数应比左侧孩子多1
            if (i > 0 && ratings[i] > ratings[i - 1]) {
                candies[i] = candies[i - 1] + 1;
            }
        }
        // 第二次遍历,从右到左,确保每个孩子的糖果数不小于其右侧孩子的糖果数(如果评分更高)
        for (int i = n - 2; i >= 0; i--) {
            // 如果当前孩子的评分高于右侧孩子的评分,当前孩子的糖果数应比右侧孩子多1
            // 注意:这里需要取较大的值,以确保同时满足左侧和右侧的约束条件
            if (ratings[i] > ratings[i + 1]) {
                candies[i] = Math.max(candies[i], candies[i + 1] + 1);
            }
        }
        // 计算需要准备的最小糖果数目
        int total = 0;
        for (int candy : candies) {
            total += candy;
        }
        return total;
    }
}
4.2 无重叠区间(LeetCode 435)

题目描述:给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

示例: 输入:[[1,2],[2,3],[3,4],[1,3]] 输出:1 解释:移除 [1,3] 后,剩下的区间没有重叠。

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:按照区间的结束时间排序,然后每次选择结束时间最早的区间,这样可以给后面的区间留下更多的空间。具体来说,我们维护一个当前选择的区间的结束时间,然后遍历排序后的区间,如果当前区间的开始时间大于等于当前选择的区间的结束时间,说明这两个区间不重叠,我们选择当前区间,并更新当前选择的区间的结束时间。最终,需要移除的区间的最小数量等于总区间数减去选择的区间数。

代码语言:javascript
复制
public class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals == null || intervals.length == 0) {
            return 0;
        }
        // 按照区间的结束时间排序
        Arrays.sort(intervals, (a, b) -> a[1] - b[1]);
        int count = 1; // 选择的区间数,至少有一个区间
        int end = intervals[0][1]; // 当前选择的区间的结束时间
        // 遍历排序后的区间
        for (int i = 1; i < intervals.length; i++) {
            // 如果当前区间的开始时间大于等于当前选择的区间的结束时间,说明这两个区间不重叠
            if (intervals[i][0] >= end) {
                count++;
                end = intervals[i][1];
            }
        }
        // 需要移除的区间的最小数量等于总区间数减去选择的区间数
        return intervals.length - count;
    }
}
4.3 用最少数量的箭引爆气球(LeetCode 452)

题目描述:在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

示例: 输入:points = [[10,16],[2,8],[1,6],[7,12]] 输出:2 解释:对于该样例,x = 6 可以引爆 [2,8],[1,6] 两个气球,以及 x = 11 可以引爆另外两个气球。

解题思路:我们可以使用贪心算法来解决这个问题。这个问题与无重叠区间问题类似,贪心策略是:按照气球的结束坐标排序,然后每次选择结束坐标最小的气球,并在该气球的结束坐标处射出一支箭,这样可以引爆尽可能多的气球。具体来说,我们维护一个当前射出的箭的位置,然后遍历排序后的气球,如果当前气球的开始坐标大于当前射出的箭的位置,说明需要射出一支新的箭,并更新当前射出的箭的位置为当前气球的结束坐标。

代码语言:javascript
复制
public class Solution {
    public int findMinArrowShots(int[][] points) {
        if (points == null || points.length == 0) {
            return 0;
        }
        // 按照气球的结束坐标排序
        // 注意:这里使用Integer.compare来避免溢出
        Arrays.sort(points, (a, b) -> Integer.compare(a[1], b[1]));
        int count = 1; // 射出的箭的数量,至少需要一支箭
        int arrowPos = points[0][1]; // 当前射出的箭的位置
        // 遍历排序后的气球
        for (int i = 1; i < points.length; i++) {
            // 如果当前气球的开始坐标大于当前射出的箭的位置,说明需要射出一支新的箭
            if (points[i][0] > arrowPos) {
                count++;
                arrowPos = points[i][1];
            }
        }
        return count;
    }
}
4.4 加油站(LeetCode 134)

题目描述:在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。如果你可以绕环路行驶一周,则返回出发时的加油站的编号,否则返回 -1。

示例: 输入:gas = [1,2,3,4,5], cost = [3,4,5,1,2] 输出:3 解释: 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 因此,3 可为起始索引。

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:如果总的汽油量大于等于总的消耗量,那么一定存在一个起点,可以绕环路行驶一周。具体来说,我们维护两个变量,一个是当前的剩余油量,另一个是总的剩余油量。如果当前的剩余油量小于0,说明从当前起点到当前位置的路径不可行,我们将起点设置为下一个位置,并重置当前的剩余油量。遍历完数组后,如果总的剩余油量大于等于0,返回起点,否则返回-1。

代码语言:javascript
复制
public class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        int totalSurplus = 0; // 总的剩余油量
        int currentSurplus = 0; // 当前的剩余油量
        int startStation = 0; // 起始加油站的编号
        // 遍历所有的加油站
        for (int i = 0; i < n; i++) {
            // 计算当前加油站的剩余油量
            int surplus = gas[i] - cost[i];
            // 更新总的剩余油量和当前的剩余油量
            totalSurplus += surplus;
            currentSurplus += surplus;
            // 如果当前的剩余油量小于0,说明从startStation到i的路径不可行
            if (currentSurplus < 0) {
                // 将起点设置为下一个位置
                startStation = i + 1;
                // 重置当前的剩余油量
                currentSurplus = 0;
            }
        }
        // 如果总的剩余油量大于等于0,说明存在一个起点,可以绕环路行驶一周
        return totalSurplus >= 0 ? startStation : -1;
    }
}

五、贪心算法的高级应用

5.1 根据身高重建队列(LeetCode 406)

题目描述:假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

示例: 输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 解释: 编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:按照身高降序排序,如果身高相同,则按照前面的人数升序排序。然后,我们按照排序后的顺序,依次将每个人插入到队列的指定位置。具体来说,对于每个人 [h, k],我们将其插入到队列的第 k 个位置,这样可以保证前面正好有 k 个身高大于或等于 h 的人。

代码语言:javascript
复制
public class Solution {
    public int[][] reconstructQueue(int[][] people) {
        if (people == null || people.length == 0) {
            return new int[0][0];
        }
        // 按照身高降序排序,如果身高相同,则按照前面的人数升序排序
        Arrays.sort(people, (a, b) -> {
            if (a[0] != b[0]) {
                return b[0] - a[0]; // 身高降序
            } else {
                return a[1] - b[1]; // 前面的人数升序
            }
        });
        // 构建队列
        List<int[]> queue = new ArrayList<>();
        for (int[] person : people) {
            // 将每个人插入到队列的第k个位置
            queue.add(person[1], person);
        }
        // 将队列转换为数组并返回
        return queue.toArray(new int[queue.size()][]);
    }
}
5.2 单调递增的数字(LeetCode 738)

题目描述:当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。给定一个非负整数 N,找出小于或等于 N 的最大的整数,且数字呈单调递增。

示例: 输入:N = 10 输出:9

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:从右向左遍历数字的每一位,如果当前位的数字大于下一位的数字,就将当前位的数字减1,并将下一位及之后的所有位的数字都设置为9。这样可以保证得到的数字是小于或等于N的最大的单调递增数字。

代码语言:javascript
复制
public class Solution {
    public int monotoneIncreasingDigits(int N) {
        // 将数字转换为字符数组,方便操作
        char[] digits = String.valueOf(N).toCharArray();
        int n = digits.length;
        int flag = n; // 标记从哪一位开始需要设置为9
        // 从右向左遍历数字的每一位
        for (int i = n - 1; i > 0; i--) {
            // 如果当前位的数字大于下一位的数字,需要调整
            if (digits[i - 1] > digits[i]) {
                digits[i - 1]--; // 当前位的数字减1
                flag = i; // 更新标记,从当前位的下一位开始需要设置为9
            }
        }
        // 将标记位置及之后的所有位的数字都设置为9
        for (int i = flag; i < n; i++) {
            digits[i] = '9';
        }
        // 将字符数组转换为整数并返回
        return Integer.parseInt(new String(digits));
    }
}
5.3 移掉K位数字(LeetCode 402)

题目描述:给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。

示例: 输入:num = “1432219”, k = 3 输出:“1219” 解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219。

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:使用一个栈来维护当前的数字序列,遍历数字的每一位,如果当前位的数字小于栈顶的数字,且还可以移除数字(k > 0),就弹出栈顶的数字,并将k减1。遍历完所有数字后,如果k仍然大于0,就继续弹出栈顶的数字,直到k等于0。最后,将栈中的数字转换为字符串,并移除前导零。

代码语言:javascript
复制
public class Solution {
    public String removeKdigits(String num, int k) {
        // 特殊情况处理
        if (num.length() == k) {
            return "0";
        }
        // 使用栈来维护当前的数字序列
        Stack<Character> stack = new Stack<>();
        // 遍历数字的每一位
        for (char digit : num.toCharArray()) {
            // 如果当前位的数字小于栈顶的数字,且还可以移除数字,就弹出栈顶的数字
            while (!stack.isEmpty() && stack.peek() > digit && k > 0) {
                stack.pop();
                k--;
            }
            // 将当前位的数字压入栈中
            stack.push(digit);
        }
        // 如果k仍然大于0,继续弹出栈顶的数字
        while (k > 0) {
            stack.pop();
            k--;
        }
        // 将栈中的数字转换为字符串
        StringBuilder result = new StringBuilder();
        while (!stack.isEmpty()) {
            result.append(stack.pop());
        }
        // 反转字符串,因为栈是后进先出的
        result.reverse();
        // 移除前导零
        while (result.length() > 1 && result.charAt(0) == '0') {
            result.deleteCharAt(0);
        }
        return result.toString();
    }
}

// 更高效的实现,使用StringBuilder代替Stack
public class Solution {
    public String removeKdigits(String num, int k) {
        // 特殊情况处理
        if (num.length() == k) {
            return "0";
        }
        // 使用StringBuilder来模拟栈,维护当前的数字序列
        StringBuilder stack = new StringBuilder();
        // 遍历数字的每一位
        for (char digit : num.toCharArray()) {
            // 如果当前位的数字小于栈顶的数字,且还可以移除数字,就删除栈顶的数字
            while (stack.length() > 0 && stack.charAt(stack.length() - 1) > digit && k > 0) {
                stack.deleteCharAt(stack.length() - 1);
                k--;
            }
            // 将当前位的数字添加到栈中
            stack.append(digit);
        }
        // 如果k仍然大于0,继续删除栈顶的数字
        while (k > 0) {
            stack.deleteCharAt(stack.length() - 1);
            k--;
        }
        // 移除前导零
        int start = 0;
        while (start < stack.length() && stack.charAt(start) == '0') {
            start++;
        }
        // 如果所有数字都是零,返回"0"
        if (start == stack.length()) {
            return "0";
        }
        return stack.substring(start);
    }
}
5.4 拼接最大数(LeetCode 321)

题目描述:给定长度分别为 m 和 n 的两个数组,其元素由 0-9 构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n) 个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。求满足该条件的最大数。结果返回一个表示该最大数的长度为 k 的数组。

示例: 输入: nums1 = [3, 4, 6, 5] nums2 = [9, 1, 2, 5, 8, 3] k = 5 输出:[9, 8, 6, 5, 3]

解题思路:我们可以使用贪心算法来解决这个问题。贪心策略是:枚举从第一个数组中选取的数字的个数 i(i的取值范围是 max(0, k - n) 到 min(m, k)),然后从第二个数组中选取的数字的个数是 k - i。对于每个 i,我们从第一个数组中选取 i 个数字,保持相对顺序,使得拼接后的数字最大;从第二个数组中选取 k - i 个数字,保持相对顺序,使得拼接后的数字最大。然后,将这两个子数组合并成一个最大的数组。最后,在所有可能的 i 中,选择合并后的最大的数组作为答案。

代码语言:javascript
复制
public class Solution {
    public int[] maxNumber(int[] nums1, int[] nums2, int k) {
        int m = nums1.length;
        int n = nums2.length;
        int[] result = new int[0];
        // 枚举从第一个数组中选取的数字的个数i
        for (int i = Math.max(0, k - n); i <= Math.min(m, k); i++) {
            int j = k - i; // 从第二个数组中选取的数字的个数
            // 从第一个数组中选取i个数字,保持相对顺序,使得拼接后的数字最大
            int[] subsequence1 = maxSubsequence(nums1, i);
            // 从第二个数组中选取j个数字,保持相对顺序,使得拼接后的数字最大
            int[] subsequence2 = maxSubsequence(nums2, j);
            // 合并这两个子数组成一个最大的数组
            int[] merged = merge(subsequence1, subsequence2);
            // 更新结果,选择合并后的最大的数组
            if (compare(merged, 0, result, 0) > 0) {
                result = merged;
            }
        }
        return result;
    }

    // 从数组nums中选取k个数字,保持相对顺序,使得拼接后的数字最大
    private int[] maxSubsequence(int[] nums, int k) {
        if (k == 0) {
            return new int[0];
        }
        int n = nums.length;
        int[] stack = new int[k];
        int top = -1;
        int remain = n - k; // 可以删除的数字的个数
        for (int i = 0; i < n; i++) {
            // 如果当前数字大于栈顶的数字,且还可以删除数字,就弹出栈顶的数字
            while (top >= 0 && stack[top] < nums[i] && remain > 0) {
                top--;
                remain--;
            }
            // 如果栈还没有满,将当前数字压入栈中
            if (top < k - 1) {
                stack[++top] = nums[i];
            } else {
                remain--;
            }
        }
        return stack;
    }

    // 合并两个子数组成一个最大的数组,保持各自的相对顺序
    private int[] merge(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;
        int[] result = new int[m + n];
        int i = 0, j = 0, k = 0;
        while (i < m && j < n) {
            // 比较两个子数组的剩余部分,选择较大的那个的当前数字
            if (compare(nums1, i, nums2, j) > 0) {
                result[k++] = nums1[i++];
            } else {
                result[k++] = nums2[j++];
            }
        }
        // 处理剩余的数字
        while (i < m) {
            result[k++] = nums1[i++];
        }
        while (j < n) {
            result[k++] = nums2[j++];
        }
        return result;
    }

    // 比较两个子数组的剩余部分,从索引i和j开始
    private int compare(int[] nums1, int i, int[] nums2, int j) {
        int m = nums1.length;
        int n = nums2.length;
        while (i < m && j < n) {
            if (nums1[i] != nums2[j]) {
                return nums1[i] - nums2[j];
            }
            i++;
            j++;
        }
        // 如果一个子数组是另一个子数组的前缀,较长的子数组更大
        return (m - i) - (n - j);
    }
}

六、贪心算法的优化技巧总结

6.1 贪心策略的设计技巧

设计贪心策略是解决贪心问题的关键,以下是一些常用的设计技巧:

  1. 排序:将问题中的元素按照某种规则排序,然后依次选择最优的元素。
    • 常见的排序规则包括:按结束时间排序、按开始时间排序、按权重排序、按优先级排序等。
  2. 优先级队列:使用优先级队列来维护候选元素,每次从队列中取出最优的元素。
    • 常见的优先级队列包括:最大堆、最小堆、自定义优先级队列等。
  3. 双指针:使用两个指针来维护当前的状态,每次移动指针来做出最优选择。
    • 常见的双指针技巧包括:快慢指针、左右指针、滑动窗口等。
  4. 局部最优选择:在每一步,都做出当前看来最好的选择,即局部最优选择。
    • 常见的局部最优选择包括:选择结束时间最早的活动、选择能够到达最远位置的下一步、选择最小的能满足孩子胃口的饼干等。
6.2 贪心算法的证明技巧

证明贪心算法能够得到全局最优解是使用贪心算法的重要步骤,以下是一些常用的证明技巧:

  1. 反证法:假设存在一个全局最优解,但该解不包含贪心选择,然后推导出矛盾。
    • 例如,在活动选择问题中,假设存在一个全局最优解,该解不包含结束时间最早的活动,然后证明可以将该解中的一个活动替换为结束时间最早的活动,得到一个更优的解,从而推导出矛盾。
  2. 数学归纳法:假设对于规模较小的子问题,贪心算法能够得到最优解,然后证明对于规模较大的问题,贪心算法也能得到最优解。
    • 例如,在霍夫曼编码问题中,假设对于n个字符,贪心算法能够得到最优解,然后证明对于n+1个字符,贪心算法也能得到最优解。
  3. 交换论证:通过交换贪心解和最优解中的某些元素,证明贪心解不劣于最优解。
    • 例如,在任务调度问题中,通过交换贪心解和最优解中的某些任务的顺序,证明贪心解的总完成时间不大于最优解的总完成时间。
6.3 贪心算法的常见陷阱

在使用贪心算法时,需要注意以下常见的陷阱:

  1. 局部最优不一定导致全局最优:贪心算法在每一步都做出局部最优选择,但这并不一定能导致全局最优解。在使用贪心算法之前,需要证明该问题具有贪心选择性质和最优子结构性质。
    • 例如,0-1背包问题不具有贪心选择性质,因此不能使用贪心算法来解决。
  2. 贪心策略的选择:不同的贪心策略可能会导致不同的结果,需要根据问题的特点选择合适的贪心策略。
    • 例如,在活动选择问题中,按照结束时间排序比按照开始时间排序更有效。
  3. 边界条件处理:在实现贪心算法时,需要注意边界条件的处理,避免出现错误。
    • 例如,在跳跃游戏中,需要处理数组长度为1的情况。
  4. 数据类型溢出:在处理大整数时,需要注意数据类型的溢出问题。
    • 例如,在拼接最大数问题中,需要使用数组来存储结果,而不是直接使用整数。

七、总结与展望

贪心算法是一种重要的算法思想,它通过在每一步做出局部最优选择,期望能够导致全局最优的结果。贪心算法具有简单、高效的特点,在许多问题中都有广泛的应用。

在本文中,我们介绍了贪心算法的基本概念、特点、适用场景,以及与其他算法的区别。我们还介绍了贪心策略的选择、贪心算法的证明方法和解题步骤,并通过LeetCode中的题目详细解析了贪心算法的应用。

贪心算法的关键在于如何设计贪心策略,以及如何证明该贪心策略能够得到全局最优解。常见的贪心策略包括排序后选择、优先级队列选择和贪心条件判断等。常见的证明方法包括反证法、数学归纳法和交换论证等。

随着计算机科学的发展,贪心算法的应用也在不断扩展。例如,在人工智能领域,贪心算法被用于启发式搜索;在网络领域,贪心算法被用于路由选择;在调度领域,贪心算法被用于任务调度等。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、贪心算法基础概念
    • 1.1 贪心算法的定义
    • 1.2 贪心算法的特点
    • 1.3 贪心算法的适用场景
    • 1.4 贪心算法与其他算法的区别
  • 二、贪心算法的基本策略
    • 2.1 贪心策略的选择
    • 2.2 贪心算法的证明方法
    • 2.3 贪心算法的解题步骤
  • 三、LeetCode中的贪心算法题目解析
    • 3.1 分发饼干(LeetCode 455)
    • 3.2 买卖股票的最佳时机 II(LeetCode 122)
    • 3.3 跳跃游戏(LeetCode 55)
    • 3.4 跳跃游戏 II(LeetCode 45)
  • 四、贪心算法的中级应用
    • 4.1 分发糖果(LeetCode 135)
    • 4.2 无重叠区间(LeetCode 435)
    • 4.3 用最少数量的箭引爆气球(LeetCode 452)
    • 4.4 加油站(LeetCode 134)
  • 五、贪心算法的高级应用
    • 5.1 根据身高重建队列(LeetCode 406)
    • 5.2 单调递增的数字(LeetCode 738)
    • 5.3 移掉K位数字(LeetCode 402)
    • 5.4 拼接最大数(LeetCode 321)
  • 六、贪心算法的优化技巧总结
    • 6.1 贪心策略的设计技巧
    • 6.2 贪心算法的证明技巧
    • 6.3 贪心算法的常见陷阱
  • 七、总结与展望
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档