17

What is the most efficient way to select 2 unique random elements from an array (ie, make sure the same element is not selected twice).

I have so far:

var elem1;
var elem2;

elem1 = elemList[Math.ceil(Math.random() * elemList.length)];
do {
  elem2 = elemList[Math.ceil(Math.random() * elemList.length)];
} while(elem1 == elem2)

But this often hangs my page load.

Any better solution?

Extra question, how do I extend this to n elements

zsquare
  • 9,916
  • 6
  • 53
  • 87

11 Answers11

38

do NOT use loops and comparisons. Instead

  • shuffle the array
  • take first two elements
Community
  • 1
  • 1
georg
  • 211,518
  • 52
  • 313
  • 390
  • 2
    This would be slower for large input arrays. – Dogbert Mar 15 '12 at 12:27
  • @Dogbert: definitely. On the other side, it would be interesting to know exactly how much slower for how large arrays. Care to jsperf this? – georg Mar 15 '12 at 12:37
  • My array is quite big (500+ objects), as @Dogbert said, wouldnt this approach be less efficient? – zsquare Mar 15 '12 at 12:37
  • 1
    @zsquare: 500 objects isn't big at all. I don't think there will be any noticeable difference. Much more important is that this method allows to pick N elements easily and at constant time. – georg Mar 15 '12 at 13:49
  • 2
    @thg435 Saying "500 objects isn't big at all" is like saying "500 lengths of string aren't very long". – supermasher Feb 20 '14 at 12:45
7

It can be done using built-in functionality (slice and sort),

var n = 2
    randomItems = array.sort(() => .5 - Math.random()).slice(0, n);
Mehdi Dehghani
  • 10,970
  • 6
  • 59
  • 64
5

http://underscorejs.org/#sample

_.sample(list, [n])

Produce a random sample from the list. Pass a number to return n random elements from the list. Otherwise a single random item will be returned.

_.sample([1, 2, 3, 4, 5, 6]);
=> 4

_.sample([1, 2, 3, 4, 5, 6], 3);
=> [1, 6, 2]

Looking at the source it uses shuffle just like @thg435 suggested.

Community
  • 1
  • 1
Mars Robertson
  • 12,673
  • 11
  • 68
  • 89
  • 2
    Alternative is [lodash has a `sampleSize` method](https://lodash.com/docs/4.17.5#sampleSize) – aug Feb 28 '18 at 23:02
4

Your code will hang when the list contains only one item. Instead of using ==, I recommend to use ===, which looks more suitable in this case.

Also, use Math.floor instead of Math.ceil. The length property is equal to <highest index> + 1.

var elem1;
var elem2;
var elemListLength = elemList.length;

elem1 = elemList[Math.floor(Math.random() * elemListLength)];
if (elemListLength > 1) {
    do {
      elem2 = elemList[Math.floor(Math.random() * elemListLength)];
    } while(elem1 == elem2);
}
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • My code hangs when there are more than one element also, will try with your code. – zsquare Mar 15 '12 at 12:21
  • Also, any ideas how to extend it to picking `n` random elements? – zsquare Mar 15 '12 at 12:21
  • 1
    @zsquare The code and algorithm is similar to this one [Generating unique random numbers (integers) between 0 and 'x'](http://stackoverflow.com/a/8378885/938089?generating-unique-random-numbers-integers-between-0-and-x) – Rob W Mar 15 '12 at 12:24
3

On what Rob W told you, I'll add that a different solution would be to find a random point and for the second point find a random offset from the point:

var elem1;
var elem2;
var elemListLength = elemList.length;

var ix = Math.floor(Math.random() * elemListLength);
elem1 = elemList[ix];

if (elemListLength > 1) {
    elem2 = elemList[(ix + 1 + Math.floor(Math.random() * (elemListLength - 1))) % elemListLength];
}

We add 1 because the current element can't be reselected and subtract 1 because one element has already been selected.

For example, an array of three elements (0, 1, 2). We randomly select the element 1. Now the "good" offset value are 0 and 1, with offset 0 giving the element 2 and offset 1 giving the element 0.

Note that this will give you two random elements with different INDEX, not with different VALUE!

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • Any advantages of this method over the orignal? – zsquare Mar 15 '12 at 12:48
  • @zsquare No while cycle. It will give a correct couple of elements in constant deterministic time. And I consider it more elegant :-) :-) But in truth nothing really important. Speed isn't a problem in this case. – xanatos Mar 15 '12 at 12:51
  • there is an error though, `(elem1 + 1 + Math.floor(Math.random() * (elemListLength - 1))) % elemListLength` `elem1` is an object, you need to save the older index instead – zsquare Mar 15 '12 at 13:00
2

If you want to get n random elements you could create a shuffled version of your list and then return the first n elements of the shuffled array as a result.

sietschie
  • 7,425
  • 3
  • 33
  • 54
0

While shuffle the array and pick the first two is correct.
You don't need to shuffle the whole array.

Just shuffle the first two!

var arrElm = [1, 2, 3, 4, 5, 6, 7]

var toTake = 2

var maxToShuffle = Math.min(arrElm.length - 1, toTake)

for (let i = 0; i < maxToShuffle; i++) {
  const toSwap = i + Math.floor(Math.random() * (arrElm.length - i))
  ;[arrElm[i], arrElm[toSwap]] = [arrElm[toSwap], arrElm[i]]
}

console.log(arrElm.slice(0, toTake))

basically the same as https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle

Except you just quit early when you have enough item shuffled.

Jerry
  • 938
  • 7
  • 13
0

You can do something easy like this

const elements = ['indie hackers', 'twitter', 'product hunt', 'linkedIn'];
const randomIndex = Math.floor(Math.random() * elements.length);
const a = elements[randomIndex];

const filteredElements = [...elements].splice(randomIndex, 1);
const b = filteredElements[Math.floor(Math.random() * elements.length)];

a and b will be your random elements.

michelepatrassi
  • 2,016
  • 18
  • 32
0

I find this to be one of the most useful techniques:

var index1 = Math.floor(Math.random() * array.length);
var index2 = Math.floor(Math.random() * (array.length-1));
if index1 == index2 {
   index2 += 1;
}

You cannot go out of bounds as index2 cannot get the last element.

Bob Sfog
  • 105
  • 8
0

The most performant answer, even for very large array (no array shuffle, no loop) :

function pickTwo(arr) {
    let index1 = Math.floor(Math.random() * arr.length)
    let index2 = Math.floor(Math.random() * arr.length)
    
    if (index1 === index2) {
        index2++
        
        if (index2 === arr.length) index2 = 0
    }

    return [arr[index1], arr[index2]]
}

If you don't trust and want to test is statistically :

const arr = [0, 1, 2, 3, 4]

const results = new Array(arr.length).fill(0)

for (let i = 0; i < 200 * 1000; i++) {
    const rands = pickTwo(arr)
    results[rands[0]]++
    results[rands[1]]++
}

console.log(results)

You will get something balanced like that on each run :

[80301, 80173, 80062, 79680, 79784]

If you want a dirty but 3.5% more performant version (inside firefox 113) :

function pickTwo(arr) {
    const index1 = Math.floor(Math.random() * arr.length)
    const index2 = Math.floor(Math.random() * arr.length)

    return [arr[index1], arr[((index1 === index2) ? ((index2+1 === arr.length) ? 0  : index2+1) : index2 )]]
}
epakompri
  • 1
  • 1
0

If you shuffle the array and splice the number of elements you want to return, the return value will contain as many items as it can, if you ask for more items than are in the array. You can shuffle the actual array or a copy, with slice().

Array.prototype.getRandom= function(num, cut){
    var A= cut? this:this.slice(0);
    A.sort(function(){
        return .5-Math.random();
    });
    return A.splice(0, num);
}
var a1= [1, 2, 3, 4, 5];
a1.getRandom(2)
>>[4, 2]

If you want to remove the selected items from the original array, so that a second call will not include the elements the first call returned, pass a second argument: getRandom(3,true);

window.Memry=window.Memry || {};
Memry.a1= [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

Memry.a1.getRandom(3,true);
>>[5,10,7]
Memry.a1.getRandom(3,true);
>>[3,9,6]
Memry.a1.getRandom(3,true);
>>[8,4,1]
Memry.a1.getRandom(3,true);
>>[2]
kennebec
  • 102,654
  • 32
  • 106
  • 127