3

IS there any way to emulate the behavior of Collections.shuffle without a comparator being vulnerable to the sorting algorithm implementation for the result to be safe?

I mean not breaking the comparable contract etc..

shmosel
  • 49,289
  • 6
  • 73
  • 138
Whimusical
  • 6,401
  • 11
  • 62
  • 105
  • Collections.shuffle has nothing to do with Comparators or sorting. – lbalazscs Mar 24 '15 at 21:28
  • 5
    @lbalazscs I think the OP means to shuffle a list by calling `Collections.sort(comparator)` but having the comparator return something random instead of doing an actual comparison. This probably used to work but fails now that sort() does consistency checking on the comparator. – Stuart Marks Mar 24 '15 at 22:11
  • Yes, I mean using a comparator for sorting randomly – Whimusical Mar 24 '15 at 22:36

3 Answers3

13

It is impossible to implement a real “shuffling comparator” without breaking the contract. One fundamental aspect of the Comparator contract is that the results are reproducible so the ordering of a particular Comparator instance must be fixed.

Of course, you could pre-initialize that fixed ordering using a shuffling operation and create a comparator which will establish exactly this ordering. E.g.

List<ElementType> ordering=new ArrayList<>(list);
Collections.shuffle(ordering);

list.sort(Comparator.comparingInt(ordering::indexOf));

though it is a bit pointless. It’s clear that this comparator must not be used for collections containing elements not being in the ordering list.


Alternatively you can use a stable property of the values which hasn’t an ordering in the first place as sort criteria, e.g. the hash code. This can be augmented by a stable but randomizable transformation, e.g.

public static Comparator<String> randomOrder() {
    ThreadLocalRandom r = ThreadLocalRandom.current();
    int x = r.nextInt(), y = r.nextInt(), z = r.nextInt();
    boolean b = r.nextBoolean();
    return Comparator.comparingInt((String s) -> Integer.reverse((s.hashCode()&x)^y))
      .thenComparingInt(s -> s.length()^z)
      .thenComparing(b? Comparator.naturalOrder(): Comparator.reverseOrder());
}
List<String> list=Arrays.asList("hello", "now", "shuffle", "this", "!");
list.sort(randomOrder());
System.out.println(list);
list.sort(randomOrder());
System.out.println(list);

The key point is that each Comparator instance represents a randomly chosen but fixed ordering and we create a new Comparator instance to request a different ordering. Therefore, no Comparator violates the contract.

Note that this Comparator looks a bit complicated as it has to care about possible hash collisions. It will resort to the length property (also randomized) then and for Strings having the same hash code and length it will simply fall back to either natural or reversed order which is unlikely to be noticeable as it only affects the relation of these uncommon pairs.

If you create such a comparator for values without collisions (e.g. Integer instances) or covering all properties of the values which define the equality (e.g. both, x and y, of a Point), the comparator will look much simpler.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Why caring about collisions? Because compare(e1, e2)==0 if and only iff e1.equals(e2). Using only hash would return 0 in case of collision. In general, for other objects, falling back to the natural order seems reasonable unless other easy-to-scramble property, like the string length. – hectorpal Jul 03 '18 at 18:06
  • Note that this has poor permutation coverage compared to a real shuffle: https://ideone.com/ZXonxI – shmosel Aug 10 '22 at 22:38
  • 3
    @shmosel good point. Abusing a comparator for shuffling will always have serious limitations (e.g. never handle duplicates). I took the opportunity to improve the comparator, see also https://ideone.com/96TWyv It now produces all permutations, but is still inferior to a real shuffle algorithm, as the permutations do not have equal likelihood, so it can take a really large number of shuffles before all have been seen for larger lists. For a quick’n’dirty one time operation it may be sufficient, but generally, I’d never recommend abusing a sort for shuffling. – Holger Aug 22 '22 at 08:28
10

A bit more generic to the previous answer, when the type of element is not known:

public static <T> Comparator<T> shuffle() {
    final Map<Object, UUID> uniqueIds = new IdentityHashMap<>();
    return Comparator.comparing(e -> uniqueIds.computeIfAbsent(e, k -> UUID.randomUUID()));
}

can be used in streams as well:

list.stream().sorted(Streams.shuffle()).collect(Collectors.toList())

there might be collisions somehow, so it can be extended using a HashSet for the UUID to check this case

benez
  • 1,856
  • 22
  • 28
  • 2
    Nice. Note that you can simplify this to `Map uniqueIds = new IdentityHashMap<>(); return Comparator.comparing(e -> uniqueIds.computeIfAbsent(e, k -> UUID.randomUUID()));`. Even solving collisions is not so hard, as it can be done similarly: `Map randomIds = new IdentityHashMap<>(); Map uniqueIds = new IdentityHashMap<>(); return Comparator.comparing((T e) -> randomIds.computeIfAbsent(e, k -> UUID.randomUUID())) .thenComparing(e -> uniqueIds.computeIfAbsent(e, k -> uniqueIds.size()));` – Holger Nov 29 '17 at 17:30
-3

This is my solution:

List<String> st = Arrays.asList("aaaa","bbbb","cccc");
System.err.println(st.stream().sorted((o1, o2) -> RandomUtils.nextInt(0, 2)-1).findFirst().get());
kozla13
  • 1,854
  • 3
  • 23
  • 35