dabadaba
dabadaba

Reputation: 9532

Quick way of creating a random subset of an array using IntStream

I am wondering if IntStream and lambdas can be used somehow to quickly (one-line) create an array containing a random subset of an existing array of elements.

For example, say I have a pool of players:

Player[] allPlayers;

And I want to get a random subset of those players, given the dimension of the required subset. Traditionally I would do something like:

List<Player> players = new ArrayList<Player>(Arrays.asList(allPlayers));

int subsetSize = 8;
Player[] subset = new Player[subsetSize];
for (int i = 0; i < subsetSize; i++) {
  int randIndex = new Random().nextInt(players.size());
  subset[i] = players[randIndex];

  players.remove(randIndex);
}

return subset;

But can this process be done with Java 8 features? Which I assume would make it more condensed, which is what I am trying to achieve. I am still getting the hang of these new Java 8 features, like IntStream and lambdas and I wouldn't know how to use them for this particular case.

Upvotes: 4

Views: 949

Answers (3)

Paul Boddington
Paul Boddington

Reputation: 37655

An efficient way to do this is:

List<String> players = Arrays.asList("a", "b", "c", "d", "e");
int subsetSize = 3;
for (int i = 0; i < subsetSize; i++)
    Collections.swap(players, i, ThreadLocalRandom.current().nextInt(i, players.size()));
System.out.println(players.subList(0, subsetSize)); 

This doesn't require an inefficient search for unused indices if subsetSize is large. It doesn't require calling shuffle on the entire list, so is more appropriate if subsetSize is small. It also avoids calling ArrayList.remove which has linear time complexity.

This code does not use Stream operations, but I do not think it is possible to write a simple, efficient solution using Stream without using a lambda that modifies state (considered bad practice).

Upvotes: 2

Tunaki
Tunaki

Reputation: 137229

In this case, you want to select streamSize distinct elements from an input array.

You can use the Random#ints(randomNumberOrigin, randomNumberBound) method:

Returns an effectively unlimited stream of pseudorandom int values, each conforming to the given origin (inclusive) and bound (exclusive).

This returns a stream of random integers in the specified range. To ensure distinct values, distinct() is called and limit(...) allows to only keep the number of elements we want.

Random random = new Random();
Player[] subset = random.ints(0, allPlayers.length)
                        .distinct()
                        .limit(subsetSize)
                        .mapToObj(i -> allPlayers[i])
                        .toArray(Player[]::new);

I would note however, that, even if this is a one-liner, it is not as efficient as JB Nizet's solution since this continues generating integers until a distinct one is found.

Upvotes: 4

JB Nizet
JB Nizet

Reputation: 692073

You could use the following, which doesn't use a Stream, isn't a one-liner, but is already more condensed.

List<Player> copy = new ArrayList<>(Arrays.asList(allPlayers));
Collections.shuffle(copy);
return copy.subList(0, subsetSize).toArray(new Player[0]);

Upvotes: 4

Related Questions