26

So say I have

List<String> teamList = new LinkedList<String>()
teamList.add("team1");
teamList.add("team2");
teamList.add("team3");
teamList.add("team4");
teamList.add("team5");
teamList.add("team6");

Is there a simple way of picking... say 3 out the 6 elements in this list in a randomized way without picking the same element twice (or more times)?

030
  • 10,842
  • 12
  • 78
  • 123
Ren
  • 4,594
  • 9
  • 33
  • 61

8 Answers8

65

Try this:

public static List<String> pickNRandom(List<String> lst, int n) {
    List<String> copy = new ArrayList<String>(lst);
    Collections.shuffle(copy);
    return n > copy.size() ? copy.subList(0, copy.size()) : copy.subList(0, n);
}

I'm assuming that there are no repeated elements in the input list, also I take the precaution of shuffling a copy for leaving the original list undisturbed. Use it like this:

List<String> randomPicks = pickNRandom(teamList, 3);
Óscar López
  • 232,561
  • 37
  • 312
  • 386
  • 1
    That actually is a pretty good idea! Thank you very much! – Ren Dec 04 '11 at 21:43
  • 2
    usable protection from IndexOutOfBoundsException: return n > copy.size() ? copy.subList(0, copy.size()) : copy.subList(0, n); – pawegio Sep 04 '14 at 12:57
  • 9
    Shuffling the entire list when you only need 3 elements is very wasteful for large lists. – Eyal Sep 30 '14 at 12:49
  • Shuffle breaks if the passed in List is immutable. – Arunav Sanyal Dec 11 '20 at 02:55
  • @ArunavSanyal several methods in `Collections` will break, if you take a look at the documentation it doesn't mention that immutability is supported. The Java API was not built with immutability in mind, you can be certain that any method that modifies a collection expects it to be mutable. – Óscar López Dec 11 '20 at 09:18
  • @ÓscarLópez Yes I am aware of the fact that several methods in collections will break. I have been scarred enough number of times in production that I treat all collections as immutable unless explicitly required/alluded to. Code is significantly easier to argue about and write, despite being less efficient. Please note, this is not a criticism of your answer, its more of a warning, that there are very real scenarios in which it will cause problems. – Arunav Sanyal Dec 15 '20 at 00:54
7

The shuffle approach is the most idiomatic: after that, first K elements are exactly what you need.

If K is much less than the length of the list, you may want to be faster. In this case, iterate through the list, randomly exchanging the current element with itself or any of the elements after it. After the K-th element, stop and return the K-prefix: it will be already perfectly shuffled, and you don't need to care about the rest of the list.

(obviously, you'd like to use ArrayList here)

alf
  • 8,377
  • 24
  • 45
6

Create a set of ints, and put random numbers between 0 and list's length minus one into it in a loop, while the size of the set is not equal the desired number of random elements. Go through the set, and pick list elements as indicated by the numbers in the set. This way would keep your original list intact.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • 1
    Good approach unless you want to pick 999 elements out of a 1000 element list :) – waxwing Dec 04 '11 at 21:44
  • 1
    That's a very good idea :) but keeping track of an alternate list of indexes can get a little tricky. But thank you anyway :) – Ren Dec 04 '11 at 21:47
  • @waxwing Yeah, that would take a while :-) In general, if R, the desired number of random items, is greater than half list's length, it's cheaper to pick N-R elements to remove from the list, and then take all but the ones in the randomly-generated set. – Sergey Kalinichenko Dec 04 '11 at 21:49
  • well..i'm late to the party..but can't help myself to ask..how this solution will prevent duplicate numbers to get added in the set? I might get duplicate numbers in the set of `int` and change same value twice from the main list. – Tarun Chauhan Feb 11 '19 at 10:21
  • @TarunChauhan Set data structure eliminates duplicates. In other words, if you need five elements out of 20, your loop may need to perform addition, say, seven or eight times before set gets to the desired length of 5. – Sergey Kalinichenko Feb 11 '19 at 11:18
  • Alright...so it is just like continuous loop until you get the desired length of random numbers. But i think this solution can be used only when the desired size of random numbers is small. Say i have a range of 0-80 numbers and i want 52 random unique numbers out of it...then i think this solution may end up in more loops..correct me if I'm wrong...but yes for short lenght this solution seems best – Tarun Chauhan Feb 12 '19 at 12:39
5

Here is a way of doing it using Java streams, without having to create a copy of the original list or shuffling it:

public static List<String> pickRandom(List<String> list, int n) {
    if (n > list.size()) {
        throw new IllegalArgumentException("not enough elements");
    }
    Random random = new Random();
    return IntStream
            .generate(() -> random.nextInt(list.size()))
            .distinct()
            .limit(n)
            .mapToObj(list::get)
            .collect(Collectors.toList());
}

Note: It can become inefficient when n is too close to the list size for huge lists.

Helder Pereira
  • 5,522
  • 2
  • 35
  • 52
5

You can also use reservoir sampling.

It has the advantage that you do not need to know the size of the source list in advance (e.g. if you are given an Iterable instead of a List.) Also it is efficient even when the source list is not random-access, like the LinkedList in your example.

finnw
  • 47,861
  • 24
  • 143
  • 221
  • Thanks for the answer. However could you explain more into detail? I am very unfamiliar with the method you just explained. Thank you. – Ren Dec 05 '11 at 00:48
  • @Yokhen, the idea is that if (for example) you are choosing 3 items, and you have just considered the 40th item in the input list, at that point you have chosen 3 of 40 items so the last item has a 3/40 chance of being in the output array. If you look at the pseudocode in the Wikipedia article, you will see that is what the last operation (`r ← random (0 .. i); if (r < k) then a[r] ← S[i]`) does. – finnw Dec 05 '11 at 02:26
  • This should be the best solution. – Hu Cao Apr 13 '17 at 20:38
2

Use

Collections.shuffle(teamList);

to randomize the list, then remove teams one at a time from the list via teamList.remove(0);

For example:

  List<String> teamList = new LinkedList<String>();
  teamList.add("team1");
  teamList.add("team2");
  teamList.add("team3");
  teamList.add("team4");
  teamList.add("team5");
  teamList.add("team6");

  java.util.Collections.shuffle(teamList);

  String[] chosen3 = new String[3];
  for (int i = 0; i < chosen3.length && teamList.size() > 0; i++) {
     chosen3[i] = teamList.remove(0);
  }
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
  • A really good idea assuming it's okay to change the order. Otherwise you can make yourself a local copy (a bit more expensive, but it works). I personally would use `remove(teamList.size() - 1)` so that if the implementation changes to a different list it has the highest chance of being efficient, but `remove(0)` works too :) – corsiKa Dec 04 '11 at 21:35
  • Thanks, that is helpful, but I am trying to have the original list intact. Just pick the element but not to erase it so that I can have it for later use. I guess a workaround could be to create a second list with all the elements of the original list in it and then do what you suggest to do. Thank you anyway :) – Ren Dec 04 '11 at 21:36
  • 1
    Lol, we just said partly the same thing. Thanks glowcoder. – Ren Dec 04 '11 at 21:37
  • @Yokhen actually you can get a list from 0 to N-1, where `N=teamList.size()`, shuffle it, and use first K numbers as indices for the original list. – alf Dec 04 '11 at 21:40
2

All good ideas but shuffling is expensive. The more efficient method (IMO) would be doing a count controlled loop and picking a random int between 0 and n; where n initially is equal to the length of your list.

In each iteration of the loop you swap the selected item with the item at n-1 on the list and decrement n by one. This way you avoid picking the same element two times and don't have to keep a separate list of selected items.

Mr1159pm
  • 774
  • 1
  • 10
  • 21
  • that sounds like a good alternative, but it will require me a bit more of work. Thanks anyway :) – Ren Dec 04 '11 at 22:06
  • This is equivalent to alf's solution, right? Except he puts the elements at the start. – waxwing Dec 04 '11 at 22:09
0
int[] getRandoms(int[] ranges, int n, int[] excepts) {
    int min = ranges[0];
    int max = ranges[1];

    int[] results = new int[n];
    for (int i = 0; i < n; i++) {
        int randomValue = new Random().nextInt(max - min + 1) + min;
        if (ArrayUtils.contains(results, randomValue) || ArrayUtils.contains(excepts, randomValue)) {
            i--;
        } else {
            results[i] = randomValue;
        }
    }
    return results;
}

util class

public static class ArrayUtils {

    public static boolean contains(int[] array, int elem) {
        return getArrayIndex(array, elem) != -1;
    }

    /** Return the index of {@code needle} in the {@code array}, or else {@code -1} */
    public static int getArrayIndex(int[] array, int needle) {
        if (array == null) {
            return -1;
        }
        for (int i = 0; i < array.length; ++i) {
            if (array[i] == needle) {
                return i;
            }
        }
        return -1;
    }
}

using

int[] randomPositions = getRandoms(new int[]{0,list.size()-1}, 3, new int[]{0,1});

it will random 3 items in your list except item 0 and item 1

Linh
  • 57,942
  • 23
  • 262
  • 279