2

For example, if I have a map consists of {"A", 0.0}, {"B", 3.14}, {"C", 3.14}, {"D", 8.8}, {"E", 2.1}, {"F", 1.01} and the top 3 keys would be {"D", "B", "C"}.

I know the procedural way to do this but is there a smarter/functional way to do it in Java 8?

Edit: note that we can put each element of the map into a priority queue of size N, so the time complexity should be Mlog(N), faster than sorting all M elements which is Mlog(M).

Edit 2: As requested, this is what I've got:

 public static void main(String args[]) {
    final Map<String, Double> map = new HashMap<String, Double>() {{
      put("A", 0.0);
      put("B", 3.14);
      put("C", 3.14);
      put("D", 8.8);
      put("E", 2.1);
      put("F", 1.01);
    }};
    System.out.println("answer= " + getTopN(map, 3).toString());
  }

  static List<String> getTopN(final Map<String, Double> map, int n) {
    // Creating priority queue with limit size n
    PriorityQueue<Entry<String, Double>> pq = new PriorityQueue<>(n, Entry.comparingByValue());
    for (Entry<String, Double> entry : map.entrySet()) {
      pq.add(entry);
      if (pq.size() > n) {
        pq.poll();
      }
    }
    Stack<String> stack = new Stack<>();
    while (!pq.isEmpty()) {
      stack.add(pq.poll().getKey());
    }
    final ArrayList<String> answer = new ArrayList<>();
    while (!stack.isEmpty() && n-- > 0) {
      answer.add(stack.pop());
    }
    return answer;
  }
ijklr
  • 135
  • 6
  • Possible duplicate: https://stackoverflow.com/questions/109383/sort-a-mapkey-value-by-values ... I think you just are looking for a Java 8 way to sort/iterate a map based on values. Several of the answers in the above link show you how to do this. – Tim Biegeleisen Sep 04 '18 at 05:38
  • Possible duplicate of [Sort a Map by values](https://stackoverflow.com/questions/109383/sort-a-mapkey-value-by-values) – pkpnd Sep 04 '18 at 05:44
  • 1
    It's not duplicate. Because to sort the whole map sized M, time complexity is M log (M), whereas just to get top N, it's M log(N). – ijklr Sep 04 '18 at 10:18
  • Priority queues in Java cannot be iterated. Best you can do is peek the min/max element in `O(1)` and pop/pull the min/max in `O(logN)`. Is this enough for your case? – fps Sep 04 '18 at 13:25
  • It would also be useful if you could provide the imperative way you're doing this now (a simplified version, if possible) – fps Sep 04 '18 at 13:30
  • 1
    @FedericoPeraltaSchaffner I've edited my post to include what I had. Is it too ugly? I really can't think of a way to make it elegant. – ijklr Sep 05 '18 at 01:13

2 Answers2

3

Here's a way that uses a reverse Double comparator by entry value:

Map<String, Double> map = new HashMap<>();
map.put("A", 0.0);
map.put("B", 3.14);
map.put("C", 3.14);
map.put("D", 8.8);
map.put("E", 2.1);
map.put("F", 1.01);

List<String> topKeys = map.entrySet().stream()
        .sorted(Comparator.<Entry<String, Double>>comparingDouble(Entry::getValue)
                 .reversed())
        .limit(3) //limit to 3
        .map(Entry::getKey)
        .collect(Collectors.toList());

The returned list contains [D, B, C]

ernest_k
  • 39,584
  • 5
  • 45
  • 86
  • Looks nice! Can we do it functionally without sorting the whole map?like using a priority queue, Only need top N. – ijklr Sep 04 '18 at 10:22
  • @ijklr You can replace that with a collection of pair objects... right? – ernest_k Sep 04 '18 at 10:24
  • Not sure what you meant by replacing, can you elaborate the question? – ijklr Sep 04 '18 at 10:29
  • @ijklr instead of `map.entrySet().stream()`, use something like `Arrays.asList(Pair.of("A", 0.0), Pair.of("B", 3.14)...).stream()`. I'm referring to commons-lang's Pair class – ernest_k Sep 04 '18 at 10:35
  • I see. If we are sorting the whole M elements, then sorting array is probably more efficient than sorting a map?. But I don't think sorting the whole thing is necessary to get top N. – ijklr Sep 04 '18 at 10:41
  • After running your code, I think it does not return C as the answer, because it would have a duplicate key of 3.14, same as B's key. – ijklr Sep 04 '18 at 13:22
  • You could change `Comparator.>comparingDouble(Entry::getValue).reversed()` by `Map.Entry.comparingByValue().reversed()` . See [`Map.Entry` docs](https://docs.oracle.com/javase/8/docs/api/java/util/Map.Entry.html#comparingByValue--) – fps Sep 04 '18 at 13:35
  • @ijklr You can always do a partial sort instead of a full sort. For example by inserting all values into a `PriorityQueue` and then polling `N` elements out of it. – Zabuzard Sep 05 '18 at 00:33
  • 1
    @ernest_k sorry, your code actually worked and I just interpreted the result wrong.(I was testing on a more complicated test case and confused myself..) I ran on my example and it worked. – ijklr Sep 05 '18 at 01:12
1

Your code can be improved by making use of TreeSet (instead of PriorityQueue and Stack):

static List<String> getTopN(Map<String, Double> map, int n) {

    TreeSet<Map.Entry<String, Double>> topN = new TreeSet<>(
        Map.Entry.<String, Double>comparingByValue()
            .reversed()                         // by value descending, then by key
            .thenComparing(Map.Entry::getKey)); // to allow entries with repeated values

    map.entrySet().forEach(e -> {
      topN.add(e);
      if (topN.size() > n) topN.pollLast();
    });

    return topN.stream()
        .map(Map.Entry::getKey)
        .collect(Collectors.toList());
}

Note that I'm using a comparator that sorts the TreeSet of entries by value in descending order and then by key. This is to make it possible for the set to contain entries with equal values.

The TreeSet.pollLast() method is the equivalent of PriorityQueue.poll() method.

fps
  • 30,932
  • 7
  • 51
  • 98
  • intellij IDE is saying the type of topN is TreeSet> – ijklr Sep 05 '18 at 19:28
  • how do I hint the compiler? this is the error msg: Error:(68, 54) java: incompatible types: cannot infer type arguments for java.util.TreeSet<> reason: inference variable E has incompatible bounds equality constraints: java.util.Map.Entry upper bounds: java.util.Map.Entry,java.lang.Object – ijklr Sep 05 '18 at 19:30
  • @ijklr Already fixed the compilation errors, see [ideone demo](https://ideone.com/mID8su) – fps Sep 05 '18 at 19:52
  • 1
    yeah it works. I did some benchmark and it performs similarly to what I have. https://github.com/ijklr/stackoverflow – ijklr Sep 06 '18 at 15:12
  • Maybe there's still room for some extra improvement (not sure if there will be some performance gain, though)... Instead of using a `TreeSet` for `topN`, you could use a `TreeMap, String>`. Then, just return the values as a `Collection`, or `new ArrayList<>(topN.values())` if you actually need a list. – fps Sep 06 '18 at 15:30