Java 中 Comparable 和 Comparator 接口及其简单应用(二)
好久没写博客了,10月忙着给祖国妈妈过生日去啦(我知道这不是不写博客的理由),11月上旬为了实验室的口粮真是东奔西跑,出了趟差,也是忙的焦头烂额,但还不知道后面有没有具体的结果。可怜的双十一想买的东西都没有买,他们说双十一销售额数字造假,其实我觉得这就是大公司内部定的 KPI 吧,也许双十一之前就已经根据历史销售情况将今年的销售额定成了指标分配下去了,然后大家的剁手能力没有让人失望,刚好达到了需求,皆大欢喜而已。我等等双十二吧emmmm,套路少点,简单点。扯远了,回到正题吧,今天把之前利用 Comparator 接口 做的几个题来分享下。
问题一描述
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]
说明:你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
题解
题目描述相当简单,只需要将每个元素出现的次数统计出来,再取次数出现前 k 的元素即可。
方法一:使用 Hash + 根据 Map 的 value 对 key 进行降序排序
1 /** 2 * hash + 对Map进行Key排序 20ms 3 * @param nums int数组 4 * @param k 频率前k大 5 * @return k个元素 6 */ 7 public List<Integer> topKFrequent(int[] nums, int k) { 8 Map<Integer, Integer> hash = new HashMap<>(); 9 for (int a : nums) { 10 hash.put(a, hash.getOrDefault(a, 0) + 1); 11 } 12 13 List<Map.Entry<Integer, Integer>> list = new ArrayList<>(hash.entrySet()); 14 Collections.sort(list, new Comparator<Map.Entry<Integer, Integer>>() { 15 public int compare(Map.Entry<Integer, Integer> o1, 16 Map.Entry<Integer, Integer> o2) { 17 return o2.getValue().compareTo(o1.getValue()); 18 } 19 }); 20 21 List<Integer> ret = new ArrayList<>(); 22 ListIterator<Map.Entry<Integer, Integer>> it = list.listIterator(); 23 while (it.hasNext() && k > 0) { 24 Map.Entry<Integer, Integer> entry = it.next(); 25 ret.add(entry.getKey()); 26 k --; 27 } 28 return ret; 29 }
由于排序的时间复杂度是 O(nlgn),n 为 map 的大小,满足条件。
方法二:利用小顶堆存放 key,根据 key 对应的 value 进行升序排序
1 /** 2 * hash映射 + 堆排序 19ms 3 * 4 * Java中默认的是 小顶堆,入堆元素是 key,但是比较的是对应的 value,所以还是要传入比较器并实现Comparator 5 */ 6 public static List<Integer> topKFrequent2(int[] nums, int k) { 7 Map<Integer, Integer> map = new HashMap<>(); 8 9 // key:nums元素,value:key出现的次数 10 for (int a : nums) { 11 map.put(a, map.getOrDefault(a, 0) + 1); 12 } 13 14 PriorityQueue<Integer> minHeap = new PriorityQueue<>(new Comparator<Integer>() { 15 @Override 16 public int compare(Integer a, Integer b) { 17 // 升序 18 return map.get(a) - map.get(b); 19 } 20 }); 21 22 for (int key : map.keySet()) { 23 if (minHeap.size() < k) { 24 minHeap.offer(key); 25 } else if (map.get(key) > map.get(minHeap.peek())) { 26 minHeap.poll(); 27 minHeap.offer(key); 28 } 29 } 30 return new ArrayList<>(minHeap); 31 }
由于建堆的时间复杂度为O(klgk),每次插入新元素的时候,维护操作最大时间复杂度为堆的高度O(lgk),n 次维护最大时间复杂度为O(nlgk),故满足条件。
问题二描述
给一非空的单词列表,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字母顺序排序。
输入: ["i", "love", "leetcode", "i", "love", "coding"], k = 2
输出: ["i", "love"]
解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次。
注意,按字母顺序 "i" 在 "love" 之前。
输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4
输出: ["the", "is", "sunny", "day"]
解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词,
出现次数依次为 4, 3, 2 和 1 次。
题解
这个题和上一个题的思路一致,只不过是针对字符串进行统计频数进而排序,还要注意如果两个字符串出现次数一致,则根据字符串的字典序进行排序。相当于在输出结果中大前提是按照字符串出现顺序降序输出,在字符串出现次数相同时,按照字符串字典序升序输出。所以是两个不同的排序规则,在引入比较器的时候,需要多个判断条件。
方法一:hash + 对 Map 排序
hash存储采用TreeMap,默认按照 key 的字典序进行存储
1 /** 2 * hash + 对Map排序 11ms,时间复杂度是O(mlgm),m 代表 m 种不同的字符串 3 * @param s 4 * @param k 5 * @return 6 */ 7 public List<String> topKFrequent(String[] s, int k) { 8 Map<String, Integer> hash = new TreeMap<>(); 9 10 for (String str : s) { 11 hash.put(str, hash.getOrDefault(str, 0) + 1); 12 } 13 14 List<Map.Entry<String, Integer>> list = new ArrayList<>(hash.entrySet()); 15 16 Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() { 17 public int compare(Map.Entry<String, Integer> o1, 18 Map.Entry<String, Integer> o2) { 19 // list 会降序 20 return o2.getValue().compareTo(o1.getValue()); 21 } 22 }); 23 24 ListIterator<Map.Entry<String, Integer>> it = list.listIterator(); 25 List<String> ret = new ArrayList<>(); 26 while (it.hasNext() && k > 0) { 27 Map.Entry<String, Integer> entry = it.next(); 28 ret.add(entry.getKey()); 29 k --; 30 } 31 return ret; 32 }
方法二:hash + 堆排序
由于答案输出是按照次数逆序输出字符串,次数相同的时候按照字符串字典序顺序输出。所以堆中保存的时候,按照出现次数顺序排序,次数出现相同时,按照字典序降序排序。最后在捣进结果的时候再反转一下,就得到了题目要求的结果。
1 /** 2 * hash + 堆排序 9 ms,总体复杂度达到了O(nlgk),n 为字符串个数,k 为堆的大小。 3 * @param s strings 4 * @param k 个数 5 * @return strings 6 */ 7 public List<String> topKFrequent2(String[] s, int k) { 8 Map<String, Integer> hash = new HashMap<>(); 9 10 for (String str : s) { 11 hash.put(str, hash.getOrDefault(str, 0) + 1); 12 } 13 14 PriorityQueue<String> h = new PriorityQueue<>(new Comparator<String>() { 15 @Override 16 public int compare(String o1, String o2) { 17 return hash.get(o1).equals(hash.get(o2)) ? 18 // 按照 次数 升序 19 // o2.compareTo(o1) : hash.get(o1).compareTo(hash.get(o2)); 20 o2.compareTo(o1) : (hash.get(o1) - hash.get(o2)); 21 } 22 }); 23 24 // 将key存放进去,存放的规则是 先按照 key 出现的次数进行升序,如果遇到次数相同的,按照key字典序降序 25 for (String key : hash.keySet()) { 26 h.offer(key); 27 if (h.size() > k) h.poll(); 28 } 29 30 // 保存结果,最后还需要反转一下,这样实现的就是按照次数进行降序排序,次数一样的话,按照字典序升序 31 List<String> ret = new ArrayList<>(); 32 while (!h.isEmpty()) ret.add(h.poll()); 33 34 Collections.reverse(ret); 35 return ret; 36 }