6

I have a Map<String , String> which indicates links from A to B. I want to chain all possible routes. for example :

[A , B]
[B , C]
[C , D]
[E , F]
[F , G]
[H , I]

will output

[A , B , C , D]
[E , F , G]
[H , I]

I found similar question here (but not fully fulfills my requirement) : https://stackoverflow.com/a/10176274/298430

And here is my solution :

public static <T> Set<List<T>> chainLinks(Map<T , T> map) {
    Set<List<T>> resultSet = new HashSet<>();

    map.forEach((from, to) -> {
      if (!map.containsValue(from)) {
        List<T> list = new ArrayList<>();
        list.add(from);
        list.addAll(inner(to, map));
        resultSet.add(list);
      }
    });
    return resultSet;
  }

  private static <T> List<T> inner(T from , Map<T , T> map) {
    if (map.containsKey(from)) {
      List<T> list = new ArrayList<>();
      list.add(from);
      list.addAll(inner(map.get(from), map));
      return list;
    } else {
      List<T> end = new ArrayList<>();
      end.add(from);
      return end;
    }
  }

and the test case :

  @Test
  public void testChainLinks()  {
    Map<String , String> map = new HashMap<String , String>() {{
      put("A" , "B");
      put("B" , "C");
      put("C" , "D");
      put("E" , "F");
      put("F" , "G");
      put("H" , "I");
    }};

    Utils.chainLinks(map).forEach(list -> {
      logger.info("list = {}" , list.stream().collect(Collectors.joining(" -> ")));
    });
  }

It does work correctly :

list = H -> I
list = E -> F -> G
list = A -> B -> C -> D

But I don't like my solution. Because I feel it can be solved in a more functional-style . I can feel the smell of stream.fold() here . I tried but in vain to convert my code to a pure functional style : which means no intermediate objects creation...

Is it possible ? Any hints are grateful !

Community
  • 1
  • 1
smallufo
  • 11,516
  • 20
  • 73
  • 111

3 Answers3

6

Non-recursive solution:

    Set<List<String>> result = map.keySet().stream()
        .filter(k -> !map.containsValue(k))
        .map(e -> new ArrayList<String>() {{
            String x = e;
            add(x);
            while (map.containsKey(x))
                add(x = map.get(x));
        }})
        .collect(Collectors.toSet());
3

EDIT: included filter from David Pérez Cabrera's comment to remove intermediate lists.

Well you can easily recursion:

private static Set<List<String>> chainLinks(Map<String, String> map) {
    return map.keySet().stream().filter(k -> !map.containsValue(k)).map(  (key) ->
                 calc(key, map, new LinkedList<>())

    ).collect(Collectors.toSet());

}
private static List<String> calc(String key,Map<String, String> map,List<String> list){
    list.add(key);
    if (map.containsKey(key))
        return calc(map.get(key),map,list);
    else
        return list;
}
user140547
  • 7,750
  • 3
  • 28
  • 80
  • 3
    Don't forget the filter: return map.keySet().stream() .filter(k -> !map.containsValue(k)) .map((key) -> calc(key, map, new LinkedList<>()) ).collect(Collectors.toSet()); – David Pérez Cabrera Jun 28 '15 at 20:33
  • Well , you code doesnt not filter out intermediate node as starting node. The starting node should not be any linked node. ( in my example , your algorithm shows "C ->D" , which should be avoided. – smallufo Jun 28 '15 at 20:34
  • Thanks @DavidPérezCabrera . it Works. But , is there any non-recursive solution ? (for example : stream.fold ) – smallufo Jun 28 '15 at 20:39
  • 2
    I thought with reduce functions, but the algorithm was O (n^2). In fact, I couldnt improve yours (at recursive part) I prefer to invert map 》invMap to avoid containsValue invocation – David Pérez Cabrera Jun 28 '15 at 20:46
2

There's an alternative solution using the custom collector with close to linear complexity. It's really faster than the solutions proposed before, though looks somewhat uglier.

public static <T> Collector<Entry<T, T>, ?, List<List<T>>> chaining() {
    BiConsumer<Map<T, ArrayDeque<T>>, Entry<T, T>> accumulator = (
            m, entry) -> {
        ArrayDeque<T> k = m.remove(entry.getKey());
        ArrayDeque<T> v = m.remove(entry.getValue());
        if (k == null && v == null) {
            // new pair does not connect to existing chains
            // create a new chain with two elements
            k = new ArrayDeque<>();
            k.addLast(entry.getKey());
            k.addLast(entry.getValue());
            m.put(entry.getKey(), k);
            m.put(entry.getValue(), k);
        } else if (k == null) {
            // new pair prepends an existing chain
            v.addFirst(entry.getKey());
            m.put(entry.getKey(), v);
        } else if (v == null) {
            // new pair appends an existing chain
            k.addLast(entry.getValue());
            m.put(entry.getValue(), k);
        } else {
            // new pair connects two existing chains together
            // reuse the first chain and update the tail marker
            // btw if k == v here, then we found a cycle
            k.addAll(v);
            m.put(k.getLast(), k);
        }
    };
    BinaryOperator<Map<T, ArrayDeque<T>>> combiner = (m1, m2) -> {
        throw new UnsupportedOperationException();
    };
    // our map contains every chain twice: mapped to head and to tail
    // so in finisher we have to leave only half of them 
    // (for example ones connected to the head).
    // The map step can be simplified to Entry::getValue if you fine with
    // List<Collection<T>> result.
    Function<Map<T, ArrayDeque<T>>, List<List<T>>> finisher = m -> m
            .entrySet().stream()
            .filter(e -> e.getValue().getFirst().equals(e.getKey()))
            .map(e -> new ArrayList<>(e.getValue()))
            .collect(Collectors.toList());
    return Collector.of(HashMap::new, accumulator, combiner, finisher);
}

Usage:

List<List<String>> res = map.entrySet().stream().collect(chaining());

(I did not implement the combiner step, thus it cannot be used for parallel streams, but it's not very hard to add it as well). The idea is simple: we track partial chains found so far in the map where keys point to chain starts and ends and the values are ArrayDeque objects containing the chains found so far. Every new entry updates existing deque (if it appends/prepends it) or merges two deques together.

According to my tests this version works 1000x faster than @saka1029 solution for the 50000 element input array with 100 chains.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
  • wow , 1000X is awesome . I'll try it later. BTW how do you generate 50000 elements and make sure it doesn't contain cyclic links ? – smallufo Jun 29 '15 at 07:06
  • 1
    @smallufo: something like this: `for(int i=0; i<100; i++) {for(int j=0; j`). – Tagir Valeev Jun 29 '15 at 07:08
  • After do some minor benchmarks . my computer is 15943ms vs 225ms !!! It is very impressive !!! Kudos to @TagirVallev . (I need some time to digest your algorithm ). – smallufo Jun 29 '15 at 07:25
  • 1
    @smallufo, I added some comments to the code. Hopefully it's helpful. – Tagir Valeev Jun 29 '15 at 08:13