5

I ran into the challenge where I need a function that returns a random number within a given range from 0 - X. Not only that, but I require the number returned to be unique; not duplicating numbers that have already been returned on previous calls to the function.

Optionally, when this is done (e.g. the range has been 'exhausted'), just return a random number within the range.

How would one go about doing this?

Klaas Leussink
  • 2,208
  • 16
  • 23
  • 5
    If you're expecting to do this a lot, and `X` isn't too big, you can build an array with the values `0 ... X` and then shuffle it. You can then just iterate though the array to get the random values, and re-shuffle when you reach the end. – Pointy Aug 04 '12 at 12:50

4 Answers4

6

This should do it:

function makeRandomRange(x) {
    var used = new Array(x),
        exhausted = false;
    return function getRandom() {
        var random = Math.floor(Math.random() * x);
        if (exhausted) {
            return random;
        } else {
            for (var i=0; i<x; i++) {
                random = (random + 1) % x;
                if (random in used)
                    continue;
                used[random] = true;
                return random;
            }
            // no free place found
            exhausted = true;
            used = null; // free memory
            return random;
        }
    };
}

Usage:

var generate = makeRandomRange(20);

var x1 = generate(),
    x2 = generate(),
    ...

Although it works, it has no good performance when the x-th random is generated - it searches the whole list for a free place. This algorithm, a step-by-step Fisher–Yates shuffle, from the question Unique (non-repeating) random numbers in O(1)?, will perform better:

function makeRandomRange(x) {
    var range = new Array(x),
        pointer = x;
    return function getRandom() {
        pointer = (pointer-1+x) % x;
        var random = Math.floor(Math.random() * pointer);
        var num = (random in range) ? range[random] : random;
        range[random] = (pointer in range) ? range[pointer] : pointer;
        return range[pointer] = num;
    };
}

(Demo at jsfiddle.net)

Extended version which does only generate one "group" of unique numbers:

function makeRandomRange(x) {
    var range = new Array(x),
        pointer = x;
    return function getRandom() {
        if (range) {
            pointer--;
            var random = Math.floor(Math.random() * pointer);
            var num = (random in range) ? range[random] : random;
            range[random] = (pointer in range) ? range[pointer] : pointer;
            range[pointer] = num;
            if (pointer <= 0) { // first x numbers had been unique
                range = null; // free memory;
            }
            return num;
        } else {
            return Math.floor(Math.random() * x);
        }
    };
}

(Demo)

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Works great! (and fast for large ranges), however it does not continue to produce random numbers after the initial unique ones have been created. I may be wrong, but your code outputs only a fixed amount. In my specific case I need an unlimited supply of random numbers within range 0-X, whenever the user presses the button and only the first X have to be unique. – Klaas Leussink Aug 04 '12 at 15:18
  • No, it steadily produces random numbers in the range `0-X`. And if the output is grouped by every X numbers, in every of the groups you won't find duplicates. This "overfulfills" your requirement that only the first group (results 0 to X-1) needs to be unique. – Bergi Aug 04 '12 at 15:35
  • Yes, now I see. But wouldn't clearing the 'NumHistory' array in my function do the same? – Klaas Leussink Aug 04 '12 at 15:49
  • Frankly told, I don't really understand your approach. How can you pass in a different range each time the `generate` method is called? Is that a needed feature or a bug? – Bergi Aug 04 '12 at 16:03
  • The amount is influenced by other scripts – Klaas Leussink Aug 04 '12 at 16:11
  • So, if you called `gen(2)` and then `gen(1)`, it is not sure whether there is a duplicate (`0,0`) or not (`1,0`)? Will `gen(3)` need to return `2` after that or can it be random - there already is a duplicate? – Bergi Aug 04 '12 at 16:32
  • No, no, the value will be random on pageload. After that, it won't change. – Klaas Leussink Aug 04 '12 at 19:16
  • Which value, the `x`? Then you would be perfectly fine with my generate-maker. – Bergi Aug 04 '12 at 19:21
3

You got some great programming answer. Here's one with a more theoretical flavor to complete your panorama :-)

Your problem is called "sampling" or "subset sampling" and there are several ways you could do this. Let N be the range you are sampling frame (i.e., N=X+1) and M be the size of your sample (the number of elements you want to pick).

Jérémie
  • 1,353
  • 9
  • 19
2

I wrote this function. It keeps its own array with a history of generated numbers, preventing initial duplicates, continuing to output a random number if all numbers in the range have been outputted once:

// Generates a unique number from a range
// keeps track of generated numbers in a history array
// if all numbers in the range have been returned once, keep outputting random numbers within the range
var UniqueRandom = { NumHistory: new Array(), generate: function(maxNum) {
        var current = Math.round(Math.random()*(maxNum-1));
        if (maxNum > 1 && this.NumHistory.length > 0) {
            if (this.NumHistory.length != maxNum) {
                while($.inArray(current, this.NumHistory) != -1) { current = Math.round(Math.random()*(maxNum-1)); }
                this.NumHistory.push(current);
                return current;
            } else {
                //unique numbers done, continue outputting random numbers, or we could reset the history array (NumHistory = [];)
                return current;
            }
        } else {
            //first time only
            this.NumHistory.push(current);
            return current;
        }
    }
};

Here's a working Fiddle

I hope this is of use to someone!

Edit: as pointed out by Pointy below, it might get slow with a large range (here is a fiddle, going over a range from 0-1000, which seems to run fine). However; I didn't require a very large range, so perhaps this function is indeed not suited if you look to generate and keep track of an enormous range.

Community
  • 1
  • 1
Klaas Leussink
  • 2,208
  • 16
  • 23
  • This probably works fine, but it's not very efficient. If X is large this will get really slow after not too long. – Pointy Aug 04 '12 at 12:56
  • Good point. I really don't know how it will handle large ranges. I have edited my answer with a link to a fiddle that generates unique values within a range from 0-1000 using a setTimeout. It seems to run fine, but that is probably due to the processing power of my machine – Klaas Leussink Aug 04 '12 at 13:09
  • 1
    The problem is not in the range of numbers but the amount of numbers which you will generate. It will take O(n^2) time to generate n numbers. You should use an Object (`{}`) instead of an array and store the already taken numbers as keys. – Vatev Aug 04 '12 at 13:13
  • If I understand you correctly, an object is faster to check (for the presence of a key) than an array? – Klaas Leussink Aug 04 '12 at 13:23
  • 1
    JS Arrays are Objects (hash tables with string keys) and the lookup time by key is O(1) for both (pretty much instant). But in this case you are doing a lookup by value which is a for loop that checks all of the values 1 by 1. – Vatev Aug 04 '12 at 13:44
  • I see, and how would I improve this function, considering this? (I'm not an expert ;-)) – Klaas Leussink Aug 04 '12 at 13:46
  • I made an edit, but then realized that it will become an infinite loop if all the numbers in the interval are already in use... don't approve it yet. – Vatev Aug 04 '12 at 14:03
  • Yep, I noticed :) Thanks for your help so far! – Klaas Leussink Aug 04 '12 at 14:05
  • 1
    According to [this](http://crypto.stackexchange.com/questions/1379/how-to-generate-a-list-of-unique-random-numbers) and [this](http://stackoverflow.com/questions/196017/unique-random-numbers-in-o1) question there doesn't seem to be a way to create unique random numbers in O(1) time per number for variable intervals. So I guess there's not gonna be a better solution unless maxNum is a constant. – Vatev Aug 04 '12 at 14:20
  • @Vatev: See my answer for an implementation of that algorithm – Bergi Aug 04 '12 at 15:09
  • @Bergi This is the same solution as the ones in the other 2 questions I mentioned earlier (it appears to be a classic problem), and it uses a constant maxNum. You can instantiate multiple generators, but you can't use the same list of 'already taken' numbers with different intervals, which is the purpose of c_kick's answer. That doesn't mean it's not working, it's just not the same thing. – Vatev Aug 04 '12 at 16:55
  • @Vatev: Yes, I've realised that now, too. I'm just not sure what the OP tries to achieve with this approach, I can't see a usecase. – Bergi Aug 04 '12 at 17:03
  • @Bergi Yes I too can't imagine why would he need something like that. There might be a solution for his specific case that does not satisfy the general case, but we will have to know what he is trying to do. The O(n) solution will work well for small intervals because it will just overflow quickly and O(n) for small n is not that bad. – Vatev Aug 04 '12 at 17:10
  • I understand my usercase needs some specification: the random number is used in a Quote-generator. This generator produces random phrases, from an x amount of arrays (containing sub arrays). I want to make sure that when a user uses the generator, he/she at least sees all the possible variations once, before continuing to actual random phrases. I have rewritten my original code in the meanwhile, resetting the history array when all random vals have been returned. This way, the generator never repeats a result within the range of given arrays. I hope I made the case clear :) – Klaas Leussink Aug 04 '12 at 19:14
-1

You may try generating the number using the current date and time value which would make it unique. To make it within the range, you may have to use some mathematical function.

dr_mak
  • 62
  • 9