Woodyiiiiiii / LeetCode

My private record of Leetcode solution

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

LeetCode 347. Top K Frequent Elements

Woodyiiiiiii opened this issue · comments

Given a non-empty array of integers, return the k most frequent elements.

Example 1:

Input: nums = [1,1,1,2,2,3], k = 2
Output: [1,2]

Example 2:

Input: nums = [1], k = 1
Output: [1]

Note:

  • You may assume k is always valid, 1 ≤ k ≤ number of unique elements.
  • Your algorithm's time complexity must be better than O(n log n), where n is the array's size.
  • It's guaranteed that the answer is unique, in other words the set of the top k frequent elements is * unique.
  • You can return the answer in any order.

题目要求的是返回前K个经常出现的数,是求前K个最大的数的升级版(书上的例题)。
这类问题可以创建一个最小(大)堆,达到O(nlogn)的时间复杂度。我们可以利用HashMap保存数组中元素和它出现次数的映射,作为比较关系,建立堆。

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        int n = nums.length;
        int[] res = new int[k];
        int i = 0, j = 0;
        HashMap<Integer, Integer> map = new HashMap();
        // (nums, times) put into map
        for (i = 0; i < n; ++i) {
            if (!map.containsKey(nums[i]))
                map.put(nums[i], 1);
            else {
                int t = map.get(nums[i]);
                map.replace(nums[i], t + 1);
            }
        }
        // initalize array res 
        HashSet<Integer> kNums = new HashSet();
        for (i = 0; i < n && j < k; ++i) {
            if (!kNums.contains(nums[i])) {
                kNums.add(nums[i]);
                // res[j++] = nums[i];
                heapInsert(res, nums[i], map, j);
                ++j;
            }
        }
        // min heap
        for (; i < n; ++i) {
            if (!kNums.contains(nums[i]) && map.get(nums[i]) > map.get(res[0])) {
                kNums.add(nums[i]);
                res[0] = nums[i];
                heapify(res, 0, k, map);
            }
        }
        return res;
    }
    public void heapInsert(int[] heap, int value, 
            HashMap<Integer, Integer> map, int index) {
        heap[index] = value;
        while (index != 0) {
            int parent = (index - 1) / 2;
            if (map.get(heap[parent]) > map.get(heap[index])) {
                int t = heap[parent];
                heap[parent] = heap[index];
                heap[index] = t;
                index = parent;
            }
            else break;
        }
    }
    public void heapify(int[] heap, int index, int k, HashMap<Integer, Integer> map) {
        int left = index * 2 + 1, right = index * 2 + 2;
        int smallest = index;
        while (left < k) {
            if (map.get(heap[left]) < map.get(heap[index]))
                smallest = left;
            if (right < k && map.get(heap[smallest]) > map.get(heap[right]))
                smallest = right;
            if (smallest != index) {
                int t = heap[smallest];
                heap[smallest] = heap[index];
                heap[index] = t;
            }else {
                break;
            }
            index = smallest;
            left = index * 2 + 1;
            right = index * 2 + 2;
        }
    }
}

当然还可以使用Java集合类中的优先队列PriorityQueue,实际上就是最大(小)堆。
我卡在了如何定义构造器创建合适的优先队列,然后看了如下大佬的做法:

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        //check input
        if(nums == null || nums.length == 0 || k < 1) return new int[1];
       
   	    //HashMap to count frequency of the elements
   	    //Key as number and values as frequency or counts
        Map<Integer, Integer> map = new HashMap<>();
       
   	    //count number of frequencies for given array
        for(int num: nums) {
            map.put(num, map.getOrDefault(num,0) + 1);
        }

   	    //priority Queue(Max heap) to with custome comparator (Descending order)
        Queue <Integer> queue = new PriorityQueue<>((e1,e2) -> map.get(e2) - map.get(e1));
       
   	    //Add all elements(which will sort via their frequency or counts)
        queue.addAll(map.keySet());
       
        int[] result = new int[k];
        int i = 0;
        while(!queue.isEmpty()) {
            result[i++] = queue.poll();
            if(i==k) break;
        }
        return result;
    }
}

这段代码里有很多我没见过的并且很有意思的写法:

  1. map.getOrDefault(Object key, V defaultValue)方法官方讲解是"Returns the value to which the specified key is mapped, or defaultValue if this map contains no mapping for the key.",即如果Map中有Key,则返回配对的Value,否则返回参数defalutValue,而加上map.put()方法,意味着如果Map中有Key,则取出Value然后加一再更新(put方法有更新的作用:"If the map previously contained a mapping for the key, the old value is replaced"),如果没有Key,则将(Key, 1)加入Map中。
  2. new PriorityQueue<>((e1,e2) -> map.get(e2) - map.get(e1)),这里设计如何定义Map的键值对的构造器,有三种写法,可以看参考资料3。这里不同的是,Queue的类型是Integer类的,加入了Map来实现compare方法,并不是常规的Map.Entry<>。
  3. queue.addAll(map.keySet());参数是Collection类,可以一次性加入Collection类或实现了Collection类的子类。

2020/09/09:我又来重写了一遍:

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        if(nums == null || nums.length == 0 || k < 1) {
            return new int[1];
        }

        HashMap<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }

        PriorityQueue<Map.Entry<Integer, Integer>> queue =
                new PriorityQueue<>(new Comparator<Map.Entry<Integer, Integer>>() {
                    @Override
                    public int compare(Map.Entry<Integer, Integer> o1, Map.Entry<Integer, Integer> o2) {
                        if (o1.getValue().equals(o2.getValue())) {
                            return o1.getKey() - o2.getKey();
                        }
                        return o2.getValue() - o1.getValue();
                    }
                });

        queue.addAll(map.entrySet());

        int[] res = new int[k];
        int i = 0;
        while ((k--) != 0 && !queue.isEmpty()) {
            res[i++] = queue.poll().getKey();
        }
        
        return res;
    }
}

既然可以用优先队列,那也可以使用TreeMap,TreeMap的按键排序简单,但按值排序需要将其转化为list,再使用Collection.sort进行排序:

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        //check input
        if(nums == null || nums.length == 0 || k < 1) return new int[1];
       
   	    //HashMap to count frequency of the elements
   	    //Key as number and values as frequency or counts
        Map<Integer, Integer> map = new TreeMap<Integer, Integer>();
        Map<Integer, Integer> sortedMap = new LinkedHashMap<Integer, Integer>();
       
   	    //count number of frequencies for given array
        for(int num: nums) {
            map.put(num, map.getOrDefault(num,0) + 1);
        }
        
        // map converted to list and sort
        List<Map.Entry<Integer, Integer>> list = new ArrayList<Map.Entry<Integer, Integer>> (map.entrySet());
		Collections.sort(list,new Comparator<Map.Entry<Integer,Integer>>() {
            @Override
            public int compare(Map.Entry<Integer, Integer> o1, 
                               Map.Entry<Integer, Integer> o2) {
                // return o1.getValue().compareTo(o2.getValue());
                return o2.getValue() - (o1.getValue());
            }
        });
       
        // 使用LinkedHashMap确保真正排好序了
        Iterator<Map.Entry<Integer, Integer>> iter = list.iterator();
        Map.Entry<Integer, Integer> tmpEntry = null;
        while (iter.hasNext()) {
            tmpEntry = iter.next();
            sortedMap.put(tmpEntry.getKey(), tmpEntry.getValue());
        }
        
        int[] result = new int[k];
        int i = 0;
        for (Map.Entry<Integer, Integer> entry : sortedMap.entrySet()) {
            result[i++] = entry.getKey();
            if (i == k) break;
        }
        return result;
    }
}

这段代码有两个Map,TreeMap和LinkedHashMap,因为Collections.sort排好序后TreeMap顺序仍然不变,所以需要将排好序的list中元素放进LinkedHashMap中确保排好序。LinkedHashMap的优点在于,它不同于HashMap根据HashCode值存储数据,它保持了插入顺序,但同时速度慢,相应知识可以查看参考资料5。这样平均运行时间增大了很多,所以还不如用PriorityQueue。

还有一种方法是利用桶排序的方法,详细做法可以看下方参考资料6。


参考资料: