4

i put a random number generator together using Linq. the range of those random numbers needs to be in this case 1-6 inclusive. i want groups of 3 distinct numbers chosen.

i don't understand why this code returns groups that contain only 2 numbers.

        do
        {
            Random rnd = new Random();
            int[] myRndNos = Enumerable
                .Range(1, 6)
                .Select(i => rnd.Next(1, 7))
                .Distinct()
                .Take(3)
                .ToArray();                    

            string test = string.Join(",", myRndNos);

            System.Console.WriteLine(test);
            Console.ReadKey(true);
        } while (true); 

it returns a sample like:

4,6,1
5,2,3
2,4,5
3,2
3,5,1
etc...

why in some cases is it taking only 2 numbers? doesn't make sense to me. tx

user1161137
  • 1,067
  • 1
  • 11
  • 31
  • 2
    Well, one problem might be that you end up with only two unique numbers in your sample. – Asad Saeeduddin Oct 24 '16 at 02:10
  • hmm i suppose that could be the reason? any idea how could i work around that where i can always get a group of three using linq? i suppose i could go and break this up to 1 liners checking for unique numbers and keep looping till it does, but was hoping to get a nice linq way to do it. – user1161137 Oct 24 '16 at 02:30
  • 1
    @user1161137 - `int[] myRndNos = Enumerable.Range(1, 6).OrderBy(i => rnd.Next()).Take(3).ToArray();` works. – Enigmativity Oct 24 '16 at 02:31
  • 1
    @user1161137 - Just another hint - you only want to initialize a single instance of `Random` per thread otherwise you can end up with repeated values in a tight loop due to how the object seeds itself. – Enigmativity Oct 24 '16 at 02:32

6 Answers6

5

You are generating 6 random numbers but if those numbers happen to be duplicates the distinct will eliminate them leaving you with only the list on unique values, if there are only 2 unique items then that's all you will get.

Because you want 3 distinct numbers, I think what you are after is a shuffle.

private static Random rnd = new Random(); 

int[] myRndNos = Enumerable.Range(1, 6).OrderBy(i => rnd.Next()).Take(3).ToArray();

or

private static Random rng = new Random(); 

int[] myRndNos = Enumerable.Range(1, 6).Shuffle();

public static void Shuffle<T>(this IList<T> list)  
{  
    int n = list.Count;  
    while (n > 1) {  
        n--;  
        int k = rng.Next(n + 1);  
        T value = list[k];  
        list[k] = list[n];  
        list[n] = value;  
    }  
}

Source of algorithm: Randomize a List<T>

Community
  • 1
  • 1
Daniel
  • 2,744
  • 1
  • 31
  • 41
  • 2
    You've got the answer nailed, but I might suggest that you do this to shuffle - `int[] myRndNos = Enumerable.Range(1, 6).OrderBy(i => rnd.Next()).Take(3).ToArray();` - it's unbiased and much easier to understand than the traditional shuffle algorithm. – Enigmativity Oct 24 '16 at 02:27
  • thanks @Enigmativity, I've added your suggestion to the answer. – Daniel Oct 24 '16 at 02:37
  • tx! how come orderby resolves this.. not sure i get how it fixes the problem while still keeping it distinct. – user1161137 Oct 24 '16 at 02:39
  • @user1161137, Enumerable.Range(1, 6) creates the 6 distinct numbers, the orderby just changes the order to introduce the randomness. The random number is only used for ordering, in your original question the ransom numbers were being used as output. – Daniel Oct 24 '16 at 02:42
  • ah ok, i get it.. so the order by shuffles them. nice. didn't realize you can seed the order by to do that. thx u both! – user1161137 Oct 24 '16 at 02:52
  • not sure i'm clear actually how the orderby(i => rnd.next()) is working. is it re-ordering the entire range in one shot? doesn't the lambda order it 1 item at a time? but if it's 1 item at a time how is orderby using that. tx – user1161137 Oct 24 '16 at 14:26
  • orderby will sort the entire collection, the lambda is telling the orderby what value to sort on (the value is evaluated per item). if you were sorting a collection of objects and your objects had a name and a SequenceNo property you could say .orderby(i => i.SequenceNo ) to order by the SequenceNo or if you wanted to sort alphabetically by name orderby(i => i.Name). In your case we want a shuffle (random sort) so we are saying sort the collection based on a random number we are generating on the fly for each item in the collection. – Daniel Oct 25 '16 at 00:05
0

This is because the range that you are generating is not unique numbers

if the range is 2,2,2,6,6,6 the distinct will result as 2 and 6 as you can see in this example

enter image description here

Hope this helps you

MrVoid
  • 709
  • 5
  • 19
0

Try something like this

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Take3Distinct take3Distinct = new Take3Distinct();
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(string.Join(",", take3Distinct.Take3().ToArray()));
            }
            Console.ReadLine();

        }
    }
    public class Take3Distinct
    {

        const int SIZE = 6;
        Random rand = new Random();
        public List<List<int>> pairs = new List<List<int>>();
        public Take3Distinct()
        {
            for(int i = 1; i <= SIZE; i++)
            {
                List<int> newPair = new List<int>{i, 0};
                pairs.Add(newPair);
            }
        }
        public List<int> Take3()
        {
            foreach (List<int> pair in pairs)
            {
                pair[1] = rand.Next();
            }
            pairs = pairs.OrderBy(x => x[1]).ToList();
            return pairs.Select(x => x[0]).Take(3).ToList();
        }
    }
}
jdweng
  • 33,250
  • 2
  • 15
  • 20
0

Another option without shuffle is with HashSet

var rnd = new Random();
var myRndNos = new HashSet<int>();
while (myRndNos.Count < 3)
    myRndNos.Add(rnd.Next(1, 7));

Debug.Print(string.Join(",", myRndNos));
Slai
  • 22,144
  • 5
  • 45
  • 53
0

This is my shuffle extension. It's basically identical in concept to @Daniel's shuffle algorithm, but it has some additional features:

  1. It will yield return an enumerable, which means you actually only need to randomize as many elements as you actually use (e.g. if you do Enumerable.Range(0,100).ToList().Shuffle().Take(5) it actually only randomizes the 5 elements you need, without shuffling the whole array.
  2. Minor optimization to ignore the element swap if the source and target elements are the same.
  3. Option to do either a destructive (i.e. a shuffle modifying the source list) or non-destructive shuffle. The non-destructive shuffle basically generates a list of indices and destructively shuffles that.
  4. Returns IEnumerable<T> so it can be used in a longer expression, such as with .Take() as mentioned.

My extension class for shuffling:

public static class ListShuffle
{
    private static Random _random = new Random();

    public static IEnumerable<T> ShuffleInPlace<T>(this IList<T> list)
    {
        for (int n = list.Count - 1; n >= 0; n--)
        {
            int randIx = _random.Next(n + 1); // Random int from 0 to n inclusive
            if (randIx != n)
            {
                T temp = list[randIx];
                list[randIx] = list[n];
                list[n] = temp;
            }
            yield return list[n];
        }
    }

    public static IEnumerable<T> Shuffle<T>(this IList<T> list)
    {
        List<int> indices = Enumerable.Range(0, list.Count).ToList();
        foreach (int index in indices.ShuffleInPlace())
        {
            yield return list[index];
        }
    }
}
PMV
  • 2,058
  • 1
  • 10
  • 15
  • You can avoid the swap when using yield https://stackoverflow.com/questions/39235803/array-contains-performance/39237785#39237785 – Slai Oct 24 '16 at 03:12
-1

You are literarily doing rnd.Next(1, 7) 6 times and then DISTINCT and TOP(3); however, it could be "3,3,3,2,2,2" and it will return "3,2" based your codes.

You could use an array (rnd.Next(1, 7).ToArray()) and do random on indexes. When 1 number got hit then replace it with an un-hit number.

for example,

[1,2,3,4,5,6]

Run random and get 3

update array to [1,2,2,4,5,6]

Run random again... ...

you will get 3 distinct numbers with only 3 hits.

Hui Li
  • 1
  • 1