0

I'm trying to have a sorted map Map<Integer,Set<Integer>> which keeps elements sorted based on the size() of the value set.

In practice this is a map of a node to the other nodes connected to that node. I want to quickly (O(logn)) access the node with the most edges without having to sort every time.

For example the order should be:

3 => {1,2,4,5}
12 => {1,2,3}
14 => {3,2,3}
65 => {3,8}
6 => {2}
2 => {5}

Since TreeMap won't do it since I can't sort based on values, I probably need to roll something custom.

EDIT: The size of the Set may indeed change which may over-complicate things even more

What would be the simplest way to achieve this?

ᴘᴀɴᴀʏɪᴏᴛɪs
  • 5,854
  • 7
  • 43
  • 71

2 Answers2

1

Here's a sort example how to use two sets for this. One set is sorted by Set::size, and the other is just a normal Map with an integer index. To use this, you have to keep the same key/value pairs in both maps.

I'm not sure if I'd recommend trying to make a single Map out of this. It's got two lookups, by index and by size, so it doesn't really work like a regular map. It will depend on your use model.

package quicktest;

import static java.util.Comparator.comparing;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeMap;

public class TreeMapTest
{
   public static void main(String[] args) {
      TreeMap<Integer,Set<Integer>> index = new TreeMap<>();
      TreeMap<Set<Integer>,Integer> size = new TreeMap<>( comparing( Set::size ) );

      for( int i = 0; i < 5; i++ ) { 
         Set<Integer> set = new HashSet<>();
         for( int val = 0; val <= i; val++ ) {
            set.add( val );
         }
         index.put( i, set );
         size.put( set, i );
      }
      System.out.println( size.lastEntry() ); // largest set size
      System.out.println( index.get( 2 ) );  // random index
   }
}
markspace
  • 9,246
  • 2
  • 20
  • 35
  • 1
    This would only work if we don't change a Set.size after being inserted, because sorting key (the size) is mutable. You need proper control when adding/removing elements from each Set. – Kostas Chalkias Dec 28 '15 at 18:02
  • Or make a copy of the Sets before you insert them, so that later changes don't affect the Sets already stored. I'd be inclined to go this route. – markspace Dec 28 '15 at 18:26
  • But OP wants to update indexMap after insertion; so get, change (add/remove elements) then store back – Kostas Chalkias Dec 28 '15 at 18:30
  • Which is basically what I said. You mean the get/change/put cycle isn't desired? I don't see a way around it. If you try to add "control" to a HashSet, you change an O(1) algorithm to O(log n) where n is the total number of sets stored in the system. That seems bad, and inflexible. – markspace Dec 28 '15 at 18:32
  • No I say it is desired (I explain what it needs to be done). I don't understand though how a clone before insertion can help – Kostas Chalkias Dec 28 '15 at 18:40
  • If you make a deep copy, then any changes made to the original Set are not reflected in the copy in the TreeSet. Thus, no special controls on the original set are needed. – markspace Dec 28 '15 at 18:43
  • I know what a deep copy is. But it seems OP has a Map, where it requires constant updates. When an update is required, he may retrieve the Key/Value pair, perform changes, then store it back. So, how a deep copy would work here?. I guess the solution is that you have to remove previous key/value pair (Integer,Set), alter it, then add it back. – Kostas Chalkias Dec 28 '15 at 18:47
1

What about this?

public class MapAndPriority {
    Map<Integer, Set<Integer>> sets = new HashMap<Integer, Set<Integer>>();
    PriorityQueue<Set<Integer>> byLength = new PriorityQueue<Set<Integer>>(1, new Comparator<Set<Integer>>() {
        @Override
        public int compare(Set<Integer> o1, Set<Integer> o2) {
            // Compare in the reverse order!
            return o2.size() - o1.size();
        }
    });

    public void add(int i, Set<Integer> set) {
        sets.put(i, set);
        byLength.offer(set); // or Add, depending on the behavior you want
    }

    public Set<Integer> get(int i) {
        return sets.get(i);
    }

    public Set<Integer> mostNodes() {
        return byLength.peek();
    }

    public void remove(int i) {
        // sets.remove will return the removed set, so that will be removed from byLength. 
        // Need to handle case when i does not exist as a key in sets
        byLength.remove(sets.remove(i));

    }
}

If I understand what you want, then this will:

  1. Add new sets in o(nlog(n))
  2. Regular map get()
  3. Will get the largest set (mostNodes()) in o(log(n))

What I did was to place all sets in a priority queue (along side the map) and then give the priority queue a comparator that compares based on the sizes, so that smaller size is "larger". That way when you call peek() it will return the 'minimum' value in the priority queue, which due to our comparator it will be the longest set. I didn't deal with all kinds of edge cases (like removing when empty).

You can take a look at the documentation for more details and about the complexity.

Ginandi
  • 813
  • 8
  • 19
  • Found a small bug in my implementation. If you change the sets after insertion, then calling `mostNodes` won't work. If you want to be able to change sets after insertion, you can add another method to `MapAndPriority` which adds/removes elements from a set. It will then need to remove the set from the priority queue, change it, and then re-add it. – Ginandi Dec 28 '15 at 18:03
  • Your fix suggestion would work only if you are sure that no-one else have access to any of the Sets and change their size without calling you suggested add/remove method. I guess an Observer pattern would provide a more proper solution to this issue. – Kostas Chalkias Dec 28 '15 at 18:13
  • Well, without knowing the bigger picture I cannot defend my suggestion. But in general, you can expose only this class, and have the getters return read only copies. – Ginandi Dec 28 '15 at 18:29