11

Is it possible to pick a random number from a given range (1-90), but exclude certain numbers. The excluded numbers are dynamically created but lets say they are 3, 8, and 80.

I have managed to create random number generator but couldn't identify any functions that let me fulfill my requirements.

Random r = new Random();
this.num = r.Next(1, 90);

The numbers which are to be excluded are previously generated numbers. So, if the random number is one, this would then get added to the excluded numbers list.

RSM
  • 14,540
  • 34
  • 97
  • 144
  • http://stackoverflow.com/a/18485399/352101 – Bolu Dec 05 '13 at 14:12
  • If you have K non-excluded numbers, then choose a random number between 1 and K, and then map that number to the actual value. For example, if your valid values are [1,3,5,6], choose a random value between 1 and 4. If you randomly choose 3, then your result is 5, because it is third in the list of valid values. – mbeckish Dec 05 '13 at 14:13
  • @mbeckish mapping is non-trivial if you have a non-contiguous range of exclusions, such as `[1, 27, 29, 35, 76]` – ashes999 Dec 05 '13 at 14:14
  • @ashes999 - It is trivial if your range is small enough to put into an array. The OP has not provided specific details, which is why I didn't get into the specifics of the mapping. – mbeckish Dec 05 '13 at 14:15
  • Thanks to everyone for some great answers. Has been very useful so far and engaging to read. Im so sorry, but the question has an update. – RSM Dec 05 '13 at 14:30
  • @RyanMurphy, your latest update makes this now somehow a completely different question. – Bolu Dec 05 '13 at 14:38

7 Answers7

8

Using some handy extension methods here, you can create a range of numbers and select randomly from that rage. For example, with these extension methods:

public static T RandomElement(this IEnumerable<T> enumerable)
{
    return enumerable.RandomElementUsing(new Random());
}

public static T RandomElementUsing(this IEnumerable<T> enumerable, Random rand)
{
    int index = rand.Next(0, enumerable.Count());
    return enumerable.ElementAt(index);
}

You can apply these to a filtered range of numbers:

var random = Enumerable.Range(1, 90).Except(arrayOfRemovedNumbers).RandomElement();
Community
  • 1
  • 1
David
  • 208,112
  • 36
  • 198
  • 279
  • 2
    Using `ElementAt` with a non-direct IList source is not very effective I think. – King King Dec 05 '13 at 14:24
  • @KingKing: Can you elaborate? Admittedly I haven't tested it, but the MSDN docs seem to indicate that it would work. – David Dec 05 '13 at 14:35
  • 1
    Yes, I agree it works beautifully, I just want to mean the performance of ElementAt is not good if the enumerable source is large, unless we use it on some direct IList. However I think it is OK for the OP. – King King Dec 05 '13 at 14:38
  • 2
    @KingKing: Performance is definitely something the OP would want to test, agreed. For a list of integers in this case, I can't imagine it being a big deal. Certainly better than the answers which suggest picking a new random number in a loop :) – David Dec 05 '13 at 14:39
  • We can always prepare some valid **List** of numbers and randomize out the index and then we don't need ElementAt, however doing so will consume more memory (for the List), hence we can't get both good performance and minimal memory :) – King King Dec 05 '13 at 14:41
  • 1
    Thankyou, everyone had good ideas, but I learnt new things from this answer, so great, thanks. – RSM Dec 05 '13 at 14:45
5

Create a container which holds the numbers you do not want:

var excludedNumbers = new List<int> { 1, 15, 35, 89 };

Then use do something like:

Random random = new Random();

int number;

do
{
   number = r.Next(1, 90);
} while (excludedNumbers.Contains(number));

// number is not in the excluded list now
gleng
  • 6,185
  • 4
  • 21
  • 35
4

Might not be the best choice but you can use a while loop to check the numbers you don't want

Random r = new Random();
this.num = r.Next(1, 90);
do
{
    this.num = r.Next(1, 90);
}  
while (this.num == 3 || this.num == 8 || this.num == 90);

For much numbers you can use an array or a list and loop through them

int[] exclude = { 3, 8, 90, 11, 24 };
Random r = new Random();
this.num = r.Next(1, 90);
do
{
    this.num = r.Next(1, 90);
}
while (exclude.Contains(this.num));
  • 1
    I am sure he can do this himself. I guess he was asking for a more elegant solution :). – nphx Dec 05 '13 at 14:08
  • 1
    what if we have many excluded numbers such as about 50 numbers? using a while loop may delay your calculation, for example, if we have 88 excluded numbers, it may have to loop much before finding the non-excluded one. – King King Dec 05 '13 at 14:08
  • 1
    @KingKing Then just add them in an array and check whether the current number is present. – nphx Dec 05 '13 at 14:09
  • @nphx As I said **Not the best choice** –  Dec 05 '13 at 14:09
  • @nphx my comment has been updated, that's not the only issue. – King King Dec 05 '13 at 14:10
  • i think OP is looking for some way how to avoid generating those excluded numbers. – Sudhakar Tillapudi Dec 05 '13 at 14:13
  • @KingKing, `if we have 88 excluded numbers`... shall we just use the other 2 items to return the random then.... – Bolu Dec 05 '13 at 14:13
  • @Bolu the OP said clearly that the excluded numbers are dynamic, they can be changed and he need some general solution. – King King Dec 05 '13 at 14:14
  • @KingKing, but you can still check if you want to loop through numbers or excluding them (if too many) before generating. – Bolu Dec 05 '13 at 14:21
3

Your latest update, which implies that each value can only be selected once, makes the problem easy.

  1. Create a collection of values within the range.
  2. Randomly shuffle the collection.
  3. To"randomly" select an item, just return the first item in the collection, and remove it from the collection.
mbeckish
  • 10,485
  • 5
  • 30
  • 55
2
Random r = new Random();
this.num = r.Next(1, 90);

int excluded[] = new int[] { 3,8,80 }; // list any numbers in this array you want to exclude

for (int i = 0; i < excluded.Length; i++)
{
    if (this.num == excluded[i])
    {
        this.num = r.Next(1, 90); // or you can try doing something else here
    }
}
puretppc
  • 3,232
  • 8
  • 38
  • 65
2

This solution does it in O(n) worst case where n is your list of exclusions, and constant memory. The code is a little longer but might be relevant if you:

  • Possibly have a huge list of exclusions
  • Need to run this many times
  • Have a large range

It preserves the random distribution in the sense that it actually skips the exclusion list and generates a random number within the range excluding the set.

This is the implementation:

private static int RandomInRangeExcludingNumbers(Random random, int min, int max, int[] excluded)
{
    if (excluded.Length == 0) return random.Next(min, max);

    //array should be sorted, remove this check if you
    //can make sure, or sort the array before using it
    //to improve performance. Also no duplicates allowed
    //by this implementation
    int previous = excluded[0];
    for (int i = 1; i < excluded.Length; i++)
    {
        if (previous >= excluded[i])
        {
            throw new ArgumentException("excluded array should be sorted");
        }
    }

    //basic error checking, check that (min - max) > excluded.Length
    if (max - min <= excluded.Length)
        throw new ArgumentException("the range should be larger than the list of exclusions");

    int output = random.Next(min, max - excluded.Length);


    int j = 0;
    //set the start index to be the first element that can fall into the range
    while (j < excluded.Length && excluded[j] < min) j++;

    //skip each number occurring between min and the randomly generated number
    while (j < excluded.Length && excluded[j] <= output)
    {
        j++;
        output++;
        while (excluded.Contains(output))
            output++;
    }

    return output;
}

And a test function to make sure it works (over 100k elements)

private static void Test()
{
    Random random = new Random();
    int[] excluded = new[] { 3, 7, 80 };
    int min = 1, max = 90;

    for (int i = 0; i < 100000; i++)
    {
        int randomValue = RandomInRangeExcludingNumbers(random, min, max, excluded);

        if (randomValue < min || randomValue >= max || excluded.Contains(randomValue))
        {
            Console.WriteLine("Error! {0}", randomValue);
        }
    }
    Console.WriteLine("Done");
}
Bas
  • 26,772
  • 8
  • 53
  • 86
2

Make sure excludedNumbers is a HashSet for best performance.

var random = new Random();
var exludedNumbers = new HashSet<int>(new int[] { 3, 8, 80});
var randomNumber = (from n in Enumerable.Range(int.MinValue, int.MaxValue)
                    let number = random.Next(1, 90)
                    where !exludedNumbers.Contains(number)
                    select number).First();
Andreas Adler
  • 771
  • 1
  • 9
  • 16