C#生成随机、唯一值

40

我寻找已经有一段时间并且一直在努力寻找这个,我正在尝试在C#中生成几个随机而独特的数字。 我使用 System.Random,并且我使用一个DateTime.Now.Ticks种子:

public Random a = new Random(DateTime.Now.Ticks.GetHashCode());
private void NewNumber()
{
    MyNumber = a.Next(0, 10);
}

我经常调用 NewNumber(),但问题是我经常得到重复的数字。有些人建议是因为每次声明随机数时都会这样,它不会产生随机数,所以我将声明放在函数外面。有什么建议或比使用System.Random更好的方法吗?谢谢


2
http://csharpindepth.com/Articles/Chapter12/Random.aspx - Habib
只要您只创建一次Random对象,就不应该有问题。如果您希望数字是唯一的(尚未使用过该数字),那么您需要添加额外的内容,而不仅仅使用Random。 - RoneRackal
2
你是在寻找“1..10 数字的排列”而不是“1..10 范围内的随机数”吗?(它一定会为您提供一个随机顺序的 10 个唯一数字) - Alexei Levenkov
1
为什么不生成GUID?你需要整数还是任何唯一字符串就足够了? - Burhan Uddin
2
当所有十个唯一值都被生成后,您期望您的代码产生什么?第11个值(第10个)应该是什么? - Andrew Savinykh
18个回答

32

我经常调用NewNumber(),但问题是我经常得到重复的数字。

Random.Next不能保证生成唯一的数字。此外,您的范围是从0到10,很有可能会得到重复的值。也许您可以设置一个int列表,在检查它不包含重复项后将随机数插入列表中。类似这样的:

public Random a = new Random(); // replace from new Random(DateTime.Now.Ticks.GetHashCode());
                                // Since similar code is done in default constructor internally
public List<int> randomList = new List<int>();
int MyNumber = 0;
private void NewNumber()
{
    MyNumber = a.Next(0, 10);
    if (!randomList.Contains(MyNumber))
        randomList.Add(MyNumber);
}

3
对于超过10个元素的列表,使用HashSet会更好。此外,没有必要以这种方式初始化随机数 - 在默认构造函数中已经执行了类似的代码... - Alexei Levenkov
11
算法复杂度高是不好的。这不能被接受作为答案。 - Sergey Kostrukov
这不是一个随机列表,你需要检查重复项,但在现实世界中,重复可能会发生!! - Arsalan
我认为解决方案将取决于所接受的范围和选择数字的数量。如果所选数字的数量很大,那么洗牌Enumerable.Range.ToArray()将效果最佳,而如果您从巨大的范围中选择了少量数字,那么HashSet就是有意义的选择。 - Ferazhu
2
真的吗?这是一个非常糟糕的算法。不要使用它! - Nellymandela

26

如果您的范围仅限于0到9,您可以尝试洗牌一个包含可能整数的数组。这样做的好处是避免了数字生成中的任何冲突。

var nums = Enumerable.Range(0, 10).ToArray();
var rnd = new Random();

// Shuffle the array
for (int i = 0;i < nums.Length;++i)
{
    int randomIndex = rnd.Next(nums.Length);
    int temp = nums[randomIndex];
    nums[randomIndex] = nums[i];
    nums[i] = temp;
}

// Now your array is randomized and you can simply print them in order
for (int i = 0;i < nums.Length;++i)
    Console.WriteLine(nums[i]);

我刚刚测试了一下,也很好用!非常感谢! - Christian Peut
注意!那是一个错误的洗牌实现!我马上会发布一个正确的实现。 - Matthew Watson
请看下面的帖子以获取正确的实现方式,以及一些相关讨论链接。 - Matthew Watson
为什么不直接使用 Enumerable.Range(0, 10).OrderBy(x => rnd.NextDouble()).ToArray(); 呢? - thepirat000
3
唯一真正的原因是速度。对于10个元素,显然没有区别,但对于大型集合来说就很重要了。OrderBy()算法可能是O(N log N),而洗牌是O(N)。 - itsme86
如果你将这种方法与在进行操作时返回随机值的方式相结合(https://dev59.com/l3M_5IYBdhLWcg3wp06l),那么获取单个值的时间复杂度为O(1),获取整个序列的时间复杂度为O(number_of_desired_items)(而不是先对整个范围进行洗牌,时间复杂度为O(range_size))。 - Alexei Levenkov

14

注意,我不建议这样做 :) 这里还有一个“一行代码”:

var result = Enumerable.Range(0,9).OrderBy(g => Guid.NewGuid()).ToArray();

1
喜欢你在这里的想法。但为什么不使用 Enumerable.Range(0, 9).OrderBy(g => rand.NextDouble()).ToList(),然后你就可以得到问题中的范围了。 - SDK
1
如果你想要在1到10000000之间得到两个不同的数字,这将非常缓慢。 - Rob

11
我正在发布一个正确的洗牌算法实现,因为这里发布的另一个算法不能产生均匀的洗牌结果。
正如其他答案所述,对于需要随机化的少量值,您可以简单地用这些值填充一个数组,对该数组进行洗牌,然后使用您想要的任意数量的值。
以下是Fisher-Yates Shuffle(也称为Knuth Shuffle)的实现。 (阅读该链接的“实现错误”部分(搜索“始终在每次迭代中从有效数组索引的整个范围中选择j”)以查看有关此处发布的其他实现存在何种问题的讨论。)
using System;
using System.Collections.Generic;

namespace ConsoleApplication2
{
    static class Program
    {
        static void Main(string[] args)
        {
            Shuffler shuffler = new Shuffler();
            List<int> list = new List<int>{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
            shuffler.Shuffle(list);

            foreach (int value in list)
            {
                Console.WriteLine(value);
            }
        }
    }

    /// <summary>Used to shuffle collections.</summary>

    public class Shuffler
    {
        public Shuffler()
        {
            _rng = new Random();
        }

        /// <summary>Shuffles the specified array.</summary>
        /// <typeparam name="T">The type of the array elements.</typeparam>
        /// <param name="array">The array to shuffle.</param>

        public void Shuffle<T>(IList<T> array)
        {
            for (int n = array.Count; n > 1; )
            {
                int k = _rng.Next(n);
                --n;
                T temp = array[n];
                array[n] = array[k];
                array[k] = temp;
            }
        }

        private System.Random _rng;
    }
}

@downvoters:你们有没有调查一下另一个随机洗牌的问题?问题的答案在被接受的答案下面。它使用了错误的洗牌算法。还请看看我对那个答案的评论。 - Matthew Watson
除了您将其泛化之外,为什么这比其他方案更好? - Mitulát báti
1
@Mitulátbáti,你指的“另一个”答案是哪个?如果你指的是“被接受的答案”,那么这个更好,因为它的复杂度是O(N),而被接受的答案的复杂度是O(N^2) - Matthew Watson
1
即使你多次称其为“另一个”,我指的是那个。 - Mitulát báti
@Mitulátbáti 我特别提到了另一种洗牌算法,所以假设这确实是你的意思,那么这个答案比那个更好,因为这个可以产生均匀的洗牌结果 - 另一个洗牌算法(只有一个其他的洗牌算法作为答案发布)存在一个错误,会产生不均匀的洗牌结果。请注意,我在这里的答案已经提供了一个链接到维基百科文章,其中详细介绍了为什么另一个洗牌算法是有问题的。 - Matthew Watson

4

以下是仅适用于Unity的答案:

请使用这个预制好的方法:输入一个范围和你想要获取的数字的数量。

public static int[] getUniqueRandomArray(int min, int max, int count) {
    int[] result = new int[count];
    List<int> numbersInOrder = new List<int>();
    for (var x = min; x < max; x++) {
        numbersInOrder.Add(x);
    }
    for (var x = 0; x < count; x++) {
        var randomIndex = UnityEngine.Random.Range(0, numbersInOrder.Count);
        result[x] = numbersInOrder[randomIndex];
        numbersInOrder.RemoveAt(randomIndex);
    }

    return result;
}

UnityEngine.Random与C#的等效部分有些不同。当然,你可以轻松地转换为C#。 - Evren Ozturk
第一个for循环应该像这样,否则最大值不会被选中。 for (var x = min; x <= max; x++) - Filip12345

3

@Habib的答案相同,但作为函数:

List<int> randomList = new List<int>();
int UniqueRandomInt(int min, int max)
{
    var rand = new Random();
    int myNumber;
    do
    {
       myNumber = rand.Next(min, max);
    } while (randomList.Contains(myNumber));
    return myNumber;
}

如果randomList是一个类属性,那么UniqueRandomInt将在同一类实例的上下文中返回唯一的整数。如果您希望它在全局范围内是唯一的,您需要将randomList设置为静态。

2

根据你所需要的,你可以像这样做:

using System;
using System.Collections.Generic;
using System.Linq;

namespace SO14473321
{
    class Program
    {
        static void Main()
        {
            UniqueRandom u = new UniqueRandom(Enumerable.Range(1,10));
            for (int i = 0; i < 10; i++)
            {
                Console.Write("{0} ",u.Next());
            }
        }
    }

    class UniqueRandom
    {
        private readonly List<int> _currentList;
        private readonly Random _random = new Random();

        public UniqueRandom(IEnumerable<int> seed)
        {
            _currentList = new List<int>(seed);
        }

        public int Next()
        {
            if (_currentList.Count == 0)
            {
                throw new ApplicationException("No more numbers");
            }

            int i = _random.Next(_currentList.Count);
            int result = _currentList[i];
            _currentList.RemoveAt(i);
            return result;
        }
    }
}

1

这是使用 HashSet 查找N个随机且唯一数字的示例。 看起来非常简单,因为 HashSet 只能包含不同的项。 有趣的是 - 它是否比使用 List 或 Shuffler 更快?

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
    class RnDHash
    {
        static void Main()
        {
            HashSet<int> rndIndexes = new HashSet<int>();
            Random rng = new Random();
            int maxNumber;
            Console.Write("Please input Max number: ");
            maxNumber = int.Parse(Console.ReadLine());
            int iter = 0;
            while (rndIndexes.Count != maxNumber)
            {
                int index = rng.Next(maxNumber);
                rndIndexes.Add(index);
                iter++;
            }
            Console.WriteLine("Random numbers were found in {0} iterations: ", iter);
            foreach (int num in rndIndexes)
            {
                Console.WriteLine(num);
            }
            Console.ReadKey();
        }
    }
}

这是最好的答案。这是HashSet的完美使用案例。 - pjpscriv

1

我注意到被接受的答案一直将int添加到列表中,并且一直使用if(!randomList.Contains(MyNumber))来检查它们,我认为这样做在规模扩大时不可取,特别是当您需要请求新数字时。

我会相反地处理。

  1. 在启动时线性生成列表
  2. 从列表中获取随机索引
  3. 从列表中删除找到的int

这将在启动时需要稍微多一点时间,但规模扩大时将更加可扩展。

public class RandomIntGenerator
{
    public Random a = new Random();
    private List<int> _validNumbers;

    private RandomIntGenerator(int desiredAmount, int start = 0)
    {
        _validNumbers = new List<int>();
        for (int i = 0; i < desiredAmount; i++)
            _validNumbers.Add(i + start);
    }

    private int GetRandomInt()
    {
        if (_validNumbers.Count == 0)
        {
            //you could throw an exception here
            return -1;
        }
        else
        {
            var nextIndex = a.Next(0, _validNumbers.Count - 1);
            var number    = _validNumbers[nextIndex];
            _validNumbers.RemoveAt(nextIndex);
            return number;
        }
    }
}

1
但是从列表中删除一个元素不需要O(n)的时间吗?索引方法很好,但是从.NET数组构建的列表中删除一个元素会消耗时间,特别是当列表非常大时! - Artavazd
是的,你说得对。在这种情况下,我只是把消耗任务提前了,而不是推迟。RemoveAt 的成本更早,后期成本更少,而 Contains 则相反(它在内存方面也更好)。我需要优化这个。 - Jack Mariani
我很乐意看到你优化的方法。谢谢分享! - Artavazd
1
为了获得更好的性能,您可以使用哈希集而不是list.Contains(),然后在最后使用ToArray()。 - Mark Lauter

0
  • 采用函数式编程方式*
        static Func<int> GetNextUniqueIntegerFunc(int min, int max)
        {
            var list = new List<int>();

            var random = new Random();

            int getNextValue()
            {
                while (true)
                {
                    var random_number = random.Next(min, max);

                    if (!list.Contains(random_number))
                    {
                        list.Add(random_number);

                        return random_number;
                    }
                }
            }

            return getNextValue;
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接