【LeetCode】56.合并区间(贪心算法,java实现)

题目

链接

image-20200718122101135

分析

方法一:贪心算法

分析:

  • 首先画图理解题意;

image.png

image.png

经验:区间类的问题,一般而言是需要画图思考的。因为只有建立直观的感觉,才能更有效的去思考解决问题的方案。

还有需要画图思考的相关算法问题有(其实绝大部分都需要打草稿,大神除外):

  • 和物理现象相关的:第 42 题:接雨水问题、第 11 题:盛最多水的容器、第 218 题:天际线问题;
  • 本身问题描述就和图形相关的问题:第 84 题:柱状图中最大的矩形;
  • 链表问题:穿针引线如果不画图容易把自己绕晕;
  • 回溯算法问题:根据示例画图发现每一步的选择和剪枝的条件;
  • 动态规划问题:画示意图发现最优子结构。

得出结论:可以被合并的区间一定是有交集的区间,前提是区间按照左端点排好序,这里的交集可以是一个点(例如例 2)。

至于为什么按照左端点升序排序,这里要靠一点直觉猜想,我没有办法说清楚是怎么想到的,有些问题的策略是按照右端点升序排序(也有可能是降序排序,具体问题具体分析)。

接着说,直觉上,只需要对所有的区间按照左端点升序排序,然后遍历。

  • 如果当前遍历到的区间的左端点 > 结果集中最后一个区间的右端点,说明它们没有交集,此时把区间添加到结果集;
  • 如果当前遍历到的区间的左端点 <= 结果集中最后一个区间的右端点,说明它们有交集,此时产生合并操作,即:对结果集中最后一个区间的右端点更新(取两个区间的最大值)。

参考代码

  • Java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Stack;


public class Solution {

    public int[][] merge(int[][] intervals) {
        int len = intervals.length;
        if (len < 2) {
            return intervals;
        }

        // 按照起点排序
        Arrays.sort(intervals, Comparator.comparingInt(o -> o[0]));

        // 也可以使用 Stack,因为我们只关心结果集的最后一个区间
        List<int[]> res = new ArrayList<>();
        res.add(intervals[0]);

        for (int i = 1; i < len; i++) {
            int[] curInterval = intervals[i];

            // 每次新遍历到的列表与当前结果集中的最后一个区间的末尾端点进行比较
            int[] peek = res.get(res.size() - 1);

            if (curInterval[0] > peek[1]) {
                res.add(curInterval);
            } else {
                // 注意,这里应该取最大
                peek[1] = Math.max(curInterval[1], peek[1]);
            }
        }
        return res.toArray(new int[res.size()][]);
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        int[][] intervals = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
        int[][] res = solution.merge(intervals);
        for (int i = 0; i < res.length; i++) {
            System.out.println(Arrays.toString(res[i]));
        }
    }
}

复杂度分析

  • 时间复杂度:O*(Nlog*N),这里 N是区间的长度;
  • 空间复杂度:O*(*N),保存结果集需要的空间,这里计算的是最坏情况,也就是所有的区间都没有交点的时候。

说明:Arrays.sort(intervals, Comparator.comparingInt(o -> o[0])); 是 Java8 以后提供的一种函数式编程语法,在这里就不展开叙述了。


这里用到的算法思想是:贪心算法。

在具体的算法描述中:

  • 前提:区间按照左端点排序;
  • 贪心策略:在右端点的选择中,如果产生交集,总是将右端点的数值更新成为最大的,这样就可以合并更多的区间,这种做法是符合题意的。

这道题的证明请见 「官方题解」

这里用到的算法是「贪心算法」,「贪心算法」是在基础算法领域真正很「玄」的算法。很难也很简单。它简单在只要能想到,就不难写出来,且代码一般来说逻辑都比较简单,难在证明的算法的合理性,好在绝大多数情况下不要求证明。

贪心算法就像这个世界上的很多定理和猜想一样,需要人先猜测,然后做实验去验证。贪心这个名字有一定「短视」的意味在里面,就是只注重眼前利益,就能达到全局最优。如果用于形容人的话,有一定讽刺意味,但是在算法领域里面,这种思路的确可以解决特定的一类问题。

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

贪心策略适用的前提是:局部最优策略能导致产生全局最优解。

可以适用贪心的问题就是每一步局部最优,最后导致结果全局最优。

重点:贪心策略可以使用的前提是和要解决的问题相关的。不是所有的问题都适合使用贪心算法。而判断一个问题是否可以应用贪心算法,可以从以下两个角度:

  • 直觉,根据直觉描述出来的算法,具备「只考虑当前,不考虑全局」的特点,那可能就是「贪心算法」;
  • 如果不能举出反例,那多半这个问题就具有「贪心算法性质」,可以使用贪心算法去做。

要严格证明「贪心算法」有效,必须使用数学相关的理论,常见的方法有:

  • 数学归纳法;
  • 反证法。

贪心算法的证明比较难,并且就算看证明也会给人一头雾水的感觉,就像是让你证明 \sqrt{2}2 是无理数一样,但是推翻「贪心算法」很简单。在这里不展开。

经验:由于贪心算法适用的场景一般都是在一组决策里选择最大或者最小值,因此常常在使用贪心算法之前,需要先对数据按照某种规则排序。

一个最简单的理解贪心算法的例子就是「选择排序」,算法描述是:每一轮选择未排定部分里最小的元素交换到未排定部分的开头。

说明:对于「选择排序」是否是贪心算法,我查过资料,这一点有争议。我个人认为「选择排序」的算法描述符合「局部最优,则整体最优」,即每一步的决策并不考虑全局,只考虑当下,选这个例子的愿意只是因为它足够简单。
证明「贪心算法」在「选择排序」上有效需要使用「循环不变量」,在这里不展开。

贪心算法不是对所有问题都能够每一步只看当下,选择最好的策略,就得到整体最优解,关键是贪心策略的选择。选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

具备「无后效性」其实在「动态规划」这一类问题里体现得特别明显,大家可以通过贪心算法的学习在具体去理解「无后效性」的意思。

  • 当前决策对后面的决策不产生影响;
  • 当前决策只需要记录一个结果,而这个决策是怎么来的不重要。

一旦贪心选择性质不成立,可以考虑的另一种算法思想就是「动态规划」。「动态规划」在每一步做决策的时候,就不只考虑当前步骤的最优解。

贪心算法的应用

  • 对数据压缩编码的霍夫曼编码(Huffman Coding)
  • 求最小生成树的 Prim 算法和 Kruskal 算法
  • 求单源最短路径的Dijkstra算法

贪心算法典型问题

说明:如果是准备普通公司算法面试的朋友,不建议画太多时间去研究「贪心算法」有效性的证明,有可以使用「贪心算法」的直觉,举不出反例,并且编码可以通过所有测试用例即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 可以使用Java中的排序功能来实现。可以使用Arrays.sort()函数,将列表中的元素按照字母顺序排序,或者使用Collections.sort()函数,将列表中的元素按用户指定的排序规则排序。 ### 回答2: 为了实现LeetCode 2561题(Rearranging Fruits)的要求,需要使用Java编程语言。主要思路是遍历给定的水果数组,同时用一个哈希表来记录每个水果出现的次数。然后根据题目要求,重新排列水果使得相同类型的水果尽可能接近,并且按照出现次数的非递增顺序排序。 具体实现步骤如下: 1. 创建一个HashMap来存储每个水果的出现次数。遍历给定的水果数组,如果该水果已经存在于HashMap中,则将其出现次数加1;否则,将该水果添加到HashMap,并将其出现次数初始化为1。 2. 创建一个ArrayList来存储已经排列好的水果。通过HashMap的entrySet方法获取到每种水果和它的出现次数,然后将这些entry按照出现次数的非递增顺序进行排序。 3. 遍历排序好的entry集合,根据每个水果的出现次数,在ArrayList中连续添加相应数量的水果。 4. 返回排列好的水果数组。 以下是Java代码的示例实现: ```java import java.util.*; class Solution { public String[] rearrange(String[] fruits) { HashMap<String, Integer> fruitCountMap = new HashMap<>(); // 统计每个水果的出现次数 for (String fruit : fruits) { if (fruitCountMap.containsKey(fruit)) { fruitCountMap.put(fruit, fruitCountMap.get(fruit) + 1); } else { fruitCountMap.put(fruit, 1); } } ArrayList<Map.Entry<String, Integer>> sortedEntries = new ArrayList<>(fruitCountMap.entrySet()); // 根据出现次数进行非递增排序 Collections.sort(sortedEntries, new Comparator<Map.Entry<String, Integer>>() { public int compare(Map.Entry<String, Integer> entry1, Map.Entry<String, Integer> entry2) { return entry2.getValue().compareTo(entry1.getValue()); } }); ArrayList<String> rearrangedFruits = new ArrayList<>(); // 根据出现次数连续添加水果 for (Map.Entry<String, Integer> entry : sortedEntries) { String fruit = entry.getKey(); int count = entry.getValue(); for (int i = 0; i < count; i++) { rearrangedFruits.add(fruit); } } return rearrangedFruits.toArray(new String[0]); } } ``` 使用以上代码,可以对给定的水果数组进行重新排列,使得相同类型的水果尽可能接近,并且按照出现次数的非递增顺序进行排序。返回的结果就是排列好的水果数组。 ### 回答3: 题目要求将一个字符串中的水果按照特定规则重新排列。我们可以使用Java实现这个问题。 首先,我们需要定义一个函数来解决这个问题。 ```java public static String rearrangeFruits(String fruits) { // 将字符串转换为字符数组方便处理 char[] fruitArray = fruits.toCharArray(); // 统计每种水果的数量 int[] fruitCount = new int[26]; for (char fruit : fruitArray) { fruitCount[fruit - 'a']++; } // 创建一个新的字符数组来存储重新排列后的结果 char[] rearrangedFruitArray = new char[fruitArray.length]; // 逐个将水果按照规则放入新数组中 int index = 0; for (int i = 0; i < 26; i++) { while (fruitCount[i] > 0) { rearrangedFruitArray[index++] = (char) ('a' + i); fruitCount[i]--; } } // 将字符数组转换为字符串并返回 return new String(rearrangedFruitArray); } ``` 上述代码中,我们首先将字符串转换为字符数组,并使用一个长度为26的数组来统计每一种水果的数量。然后,我们创建一个新的字符数组来存储重新排列后的结果。 接下来,我们利用双重循环将每一种水果按照规则放入新数组中。最后,我们将字符数组转换为字符串并返回。 例如,如果输入字符串为`"acbba"`,则经过重新排列后,输出结果为`"aabbc"`。 这样,我们就用Java实现了题目要求的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值