2

For example I would like to generate 5 unique numbers between 1 & 10. The results should be 5 numbers from 1 to 10(for example 2 3 4 8 10).

user435216
  • 305
  • 2
  • 5
  • 16

4 Answers4

6
  1. Fill up an array with your range of values
  2. Shuffle the array
  3. Pick the first 5 elements

If the range is very large, and the number of values you want is pretty small (like, 5 distinct values in the range 1 ... 1000000) then you can try generating random numbers in the range and throw away (unlikely) duplicates until you have 5. The problem with the "keep trying" approach is that there's a teeny tiny chance you could spend a lot of time with a string of random values that give you a lot of duplicates. For a big range of values, that's so unlikely that it's probably worth risking it unless the software is giving oxygen to trauma patients or something.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • Very neat, I was thinking about generating a random value each time, and comparing it to the current numbers. But this is much faster. – Lekensteyn Dec 04 '10 at 13:59
  • @Lekensteyn well it all depends on the size of the range and the number of distinct values you want. The nice thing about the shuffle trick is that it guarantees you won't get stuck in an (unlikely, but possible) cycle where you keep getting duplicates from the "random" function. – Pointy Dec 04 '10 at 14:01
  • Really nice idea. However I'm not sure how to shuffle arrays in Javascript, mind providing me a sample? Thanks. – user435216 Dec 04 '10 at 14:04
  • @user435216 sure - give me a sec; I think I have a good shuffle implementation somewhere – Pointy Dec 04 '10 at 15:40
  • @Pointy: with large ranges this will use a lot of memory. Plus shuffling this in a true randow way is also a large operation. You can design an algorithm where you don't get stuck in cycles of finding a unique number. Check my answer for that. – Jan Dec 04 '10 at 16:27
  • Yes, @Jan, I agree - and in fact that's why my answer includes the suggestion it does. Shuffling isn't too bad (linear) if you don't have too many values. I do like your answer, I think. – Pointy Dec 04 '10 at 16:29
3

Another method is to produce random numbers and only add them to the returning array if it does not already include them.

function randomRange(from, to, leng){
    var tem, A= [], L= 0, i= 0;
    randomRangeLoop: 
    while(L< leng){
        tem= Math.floor(Math.random()*to)+from;
        i= 0;
        while(i<L){
            if(A[i++]=== tem) continue randomRangeLoop;
        }
        A[L++]= tem;
    }
    return A;
}

alert(randomRange(1, 10, 5))

/* returned value: (Array) 8,6,1,3,5 */

kennebec
  • 102,654
  • 32
  • 106
  • 127
3
function generateNumbers(resultCount, from, to) {
  var result = []; // holds the final result
  var isAlreadyPicked = []; // quick lookup for the picked numbers
  // holds substitutes for when we pick a number that has been picked before
  var substitutes = []; 

  for (var i = 0; i < resultCount; i++) {
    // pick a random number
    var number = Math.floor(Math.random() * (to - from)) + from;  

    if(isAlreadyPicked[number]) {
      // pick a number from the ones we skipped at the end of the range.
      var j = Math.floor(Math.random() * substitutes.length);
      number = substitutes[j];
      substitutes.splice(j, 1);
    }

    // Save the number and mark it as being picked.
    result.push(number);
    isAlreadyPicked[number] = true;

    // decrease the range. (Because there is 1 less number to choose from)
    to -= 1;

    if(!isAlreadyPicked[to]) {
      // Save a substitute for when we pick the same number in a next iteration.
      substitutes.push(to);
    }    
  }

  return result;
}

It works by decreasing the top of the range with each iteration. If the number that was picked in the previous iteration already exists in the result, just change it to one of the top numbers we left out of the range (There will always be only 1 number in there that wasn't picked before). This is also a true random number because it virtually existed in the place of the number that was picked in one of the previous iterations.

In my opinion this is the best solution because:

  1. It doesn't allocate a lot of memory by assigning all numbers to an array and shuffling that array.

  2. It has only X iterations so hook it up to this breathing machine :)

  3. It is true random, my previous answer where I added 1 if the number was already picked was not. Because the number that is after the number that was picked in the previous iteration had twice the chance of getting picked than the other numbers.

EDIT 1: I was testing this algorithm on a large number of cases and I made a small mistake. There was an edge case where sometimes the numbers were not unique. It happened when one of the top numbers in the range was already picked. I updated my code.

EDIT 2: In case you're interested: here's the code I used to test this:

function test(cases, size) {
  var errors = 0;
  for (var i = 0; i < cases; i++) {      
    var start = Math.floor(Math.random() * size);
    var end = start + Math.floor(Math.random() * size);
    var count = Math.floor(Math.random() * (end - start));

    var nrs = generateNumbers(count, start, end);

    console.log('testing', start, end, count, nrs);

    test:
    for (var j = 0; j < count; j++) {
      var testedNumber = nrs[j];
      if(testedNumber < start || testedNumber >= end) {
        console.error('out of range', testedNumber);
        errors += 1;
        break test;
      }
      for (var k = 0; k < count; k++) {
        if(j !== k && nrs[k] === testedNumber) {
          console.error('picked twice', testedNumber);
          errors += 1;
          break test;
        }
      }
    }
  }

  console.log('all tests finished | errors:', errors)
}
test(1000, 20);

EDIT 3: Pointy suggested to use a faster algorithm to look if a number is unique. I updated the sample with his/her suggestion. Thanks Pointy!

EDIT 4: Made the algorithm a bit more efficient by removing the inner loop and replacing it with a substitutes array. Funny thing is, this algorithm can actually be used as the shuffling algorithm in the previous answer :)

This question intrigues me a lot. I have had the need for an algorithm like this before. This is actually the first time I found a solution that satisfies me. I did some research on this subject and this seems to be a variant of the Fisher-Yates shuffle algorithm.

EDIT 5: Changed the algorithm to pick a random number from the substitutes in stead of just the first.

Jan
  • 8,011
  • 3
  • 38
  • 60
  • 1
    It would probably be *much* faster to make your code implement the "set of numbers so far" as a (sparse) Javascript array. Javascript does **not** allocate space for "empty" array slots, so you can use an array to hold `true` or `false` indexed by the chosen random numbers. Then, your "contains()" test would be constant time. – Pointy Dec 04 '10 at 16:31
  • Wrote a almost similar function [here](https://stackoverflow.com/a/61181788). Only difference is I remember what number was substituted for what number instead of taking a random of substitutes. Wonder if distribution would be uniform, when talking two randoms. Yours is faster though. – TheMaster Apr 13 '20 at 07:51
0

I wrote a similar function as @Jan using a Map collection, In case someone is interested:

const randomUniqueIntegers = (n: number, max: number): number[] => {
    if (n > max) {
        throw new Error('n > max when generating a sample of unique integers');
    }

    const sampleMap = new Map<number, number>();
    while (sampleMap.size < n) {
        const rng = max - sampleMap.size;
        let candidate = randomInt(rng);
        while (sampleMap.has(candidate)) {
            candidate = sampleMap.get(candidate);     
        }
        sampleMap.set(candidate, rng - 1);
    }
    return Array.from(sampleMap.keys());
};