【问题标题】:Performance issue with generation of random unique numbers生成随机唯一数的性能问题
【发布时间】:2011-11-17 15:45:24
【问题描述】:

我有一种情况,我需要创建数以万计的唯一数字。但是,这些数字必须是 9 位数字,并且不能包含任何 0。我目前的方法是生成 9 位数字 (1-9) 并将它们连接在一起,如果该数字不在列表中,则将其添加到其中。例如

public void generateIdentifiers(int quantity)
{
    uniqueIdentifiers = new List<string>(quantity);
    while (this.uniqueIdentifiers.Count < quantity)
    {
        string id = string.Empty;
        id += random.Next(1,10);
        id += random.Next(1,10);
        id += random.Next(1,10);
        id += " ";
        id += random.Next(1,10);
        id += random.Next(1,10);
        id += random.Next(1,10);
        id += " ";
        id += random.Next(1,10);
        id += random.Next(1,10);
        id += random.Next(1,10);
        if (!this.uniqueIdentifiers.Contains(id))
        {
            this.uniqueIdentifiers.Add(id);
        }
    }
}

但是在大​​约 400,000 时,由于越来越多的生成数字是重复的,因此该过程确实变慢了。我正在寻找一种更有效的方法来执行此过程,非常感谢任何帮助。

编辑:-我正在生成这些-http://www.nhs.uk/NHSEngland/thenhs/records/Pages/thenhsnumber.aspx

【问题讨论】:

  • 你可以用HashTable代替List
  • “随机”和“唯一”在同一个句子中会触发“你可能做错了什么”警报。你介意解释一下你在追求什么吗?
  • Describe the goal,不仅仅是一步。不要屈服于the XY problem
  • 关于你的问题的一个有趣的事情是你错误地识别了问题的原因。它不会减慢因为有更多的碰撞随着检测碰撞的成本越来越高,它会减慢速度。有更多碰撞的事实是无关紧要的;这根本不是导致经济放缓的原因。导致速度变慢的原因是您选择了完全不适合检测冲突的数据结构。
  • @Joan:您必须知道 Random 的实现细节,但这很容易从 IL 进行逆向工程。您从输出中按顺序获得的每个数字都会为您提供大约 3 位关于 Random 在生成该序列时的内部状态的信息。例如,如果 Random 具有 64 位的内部状态,那么您只需大约两打输出数字就可以高概率地确定内部状态。一旦你知道了内部状态,你就知道生成的其余数字。

标签: c# performance random uniqueidentifier


【解决方案1】:

使用字符串数组或字符串生成器,wjile 使用字符串添加。

此外,您的代码效率不高,因为在生成许多 id 后,您的列表可能会保存新生成的 id,因此 while 循环将运行得比您需要的多。

使用 for 循环并从此循环生成您的 id,而不进行随机化。如果需要随机 id,请再次使用 for 循环并生成超出您需要的内容并给出生成间隔,然后从该列表中随机选择您需要多少。

使用下面的代码创建一个静态列表并在启动程序时填写它。我稍后会添加第二个代码来生成随机 ID 列表。 [我有点忙]

    public static Random RANDOM = new Random();
    public static List<int> randomNumbers = new List<int>();
    public static List<string> randomStrings = new List<string>();

    private void fillRandomNumbers()
    {
        int i = 100;
        while (i < 1000)
        {
            if (i.ToString().Contains('0') == false)
            {
                randomNumbers.Add(i);
            }
        }
    }

【讨论】:

    【解决方案2】:

    正如其他人所提到的,使用HashSet&lt;T&gt; 而不是List&lt;T&gt;
    此外,使用 StringBuilder 而不是简单的字符串操作将额外获得 25%。如果你能用数字代替字符串,你就赢了,因为它只需要三分之一或四分之一的时间。

    var quantity = 400000;
    var uniqueIdentifiers = new HashSet<int>();
    while (uniqueIdentifiers.Count < quantity)
    {
        int i=0;
        i = i*10 + random.Next(1,10);
        i = i*10 + random.Next(1,10);
        i = i*10 + random.Next(1,10);
        i = i*10 + random.Next(1,10);
        i = i*10 + random.Next(1,10);
        i = i*10 + random.Next(1,10);
        i = i*10 + random.Next(1,10);
        i = i*10 + random.Next(1,10);
        i = i*10 + random.Next(1,10);
        uniqueIdentifiers.Add(i);
    }
    

    在我的机器上处理 400,000 个数字大约需要 270 毫秒,处理 1,000,000 个数字大约需要 700 毫秒。这即使没有任何并行性。 由于使用HashSet&lt;T&gt; 而不是List&lt;T&gt;,该算法在O(n) 中运行,即持续时间将线性增长。因此,10,000,000 个值大约需要 7 秒。

    【讨论】:

    • 不客气。如果您最后仍需要将数字作为字符串,请在 while 循环之后放置 var result = uniqueIdentifiers.Select (i =&gt; i.ToString("### ### ###")).ToList();。这将使执行时间加倍,但即使您不需要它,它也会阻止您创建字符串,因为它是重复的。
    • BTW:如果需要超过 9 位,则需要使用 Int64 而不是 int
    • 我有个问题:为什么HashTable比List快?
    • @vietean:因为在 HashSet/HashTable 中,项目是索引的,所以查找项目是 O(1) 操作。在列表中,您需要搜索整个列表,即它是一个 O(n) 操作。换句话说:无论有多少项,都需要一个逻辑操作才能从 HashSet/HashTable 中取出一项。
    • 只是出于好奇,您有什么理由不使用Random.Next(100000000, 999999999) 之类的方法来生成 9 位数字吗?这完全适合一个整数,我想这会比进行九次调用更快。
    【解决方案3】:

    我认为第一件事是使用 StringBuilder,而不是串联 - 你会感到惊喜。 另一件事 - 使用更高效的数据结构,例如 HashSet 或 HashTable。

    如果您可以放弃不为零的非常奇怪的要求 - 那么您当然可以只使用一个随机操作,然后按照您想要的方式格式化您的结果数字。

    【讨论】:

      【解决方案4】:

      这个建议可能受欢迎也可能不受欢迎......这取决于人们的观点。因为您没有太具体说明您需要它们的用途、频率或确切数量,所以我建议采用蛮力方法。

      我会生成十万个数字 - 根本不需要很长时间,也许几秒钟?然后使用Parallel LINQ 对它们执行 Distinct() 以消除重复项。然后使用另一个 PLINQ 查询对剩余部分运行正则表达式,以消除其中包含零的任何部分。然后取前 x 千。 (PLINQ 非常适合处理此类大型任务)。如果需要,冲洗并重复,直到有足够的量满足您的需要。

      在一台不错的机器上,编写这个简单的函数所花费的时间比运行它所花费的时间要长。我还想问一下,当您说您实际上需要“数万个”时,为什么要测试 40 万个条目?

      【讨论】:

      • 虽然我不完全同意您的方法,但您对并行性的建议是关键点,每个人都忘记了,因为这是并行化的完美候选者。
      【解决方案5】:

      尽量避免检查以确保您始终选择一个唯一的号码:

      static char[] base9 = "123456789".ToCharArray();
      
      static string ConvertToBase9(int value) {
          int num = 9;
          char[] result = new char[9];
          for (int i = 8; i >= 0; --i) { 
              result[i] = base9[value % num];
              value = value / num;
          }
          return new string(result);
      }
      
      public static void generateIdentifiers(int quantity) {
          var uniqueIdentifiers = new List<string>(quantity);
          // we have 387420489 (9^9) possible numbers of 9 digits in base 9.
          // if we choose a number that is prime to that we can easily get always
          // unique numbers
          Random random = new Random();
          int inc = 386000000;
          int seed = random.Next(0, 387420489);
          while (uniqueIdentifiers.Count < quantity) {
              uniqueIdentifiers.Add(ConvertToBase9(seed));
              seed += inc;
              seed %= 387420489;
          }
      }
      

      我会尝试用小数字来解释背后的想法......

      假设您最多有 7 种可能的组合。我们选择一个与 7 互质的数,例如3,和一个随机的起始数字,例如4.

      在每一轮中,我们将当前数字加 3,然后将结果取模 7,因此我们得到以下序列:

      4 -> 4 + 3 % 7 = 0
      0 -> 0 + 3 % 7 = 3
      3 -> 3 + 3 % 7 = 6
      6 -> 6 + 6 % 7 = 5

      这样,我们以非连续的方式生成从 0 到 6 的所有值。在我的示例中,我们正在做同样的事情,但是我们有 9^9 种可能的组合,并且我选择 386000000 作为质数的素数(你只需要避免 3 的倍数)。

      然后,我选取序列中的数字并将其转换为以 9 为底的数字。

      我希望这很清楚:)

      我在我的机器上对其进行了测试,生成 400k 个唯一值大约需要 1 秒。

      【讨论】:

      • 非常聪明的方法。尽管您没有冲突并且只获得唯一值,但它比简单的优化蛮力算法需要更长的时间(请参阅我的答案),因为这种聪明的方法需要计算。 1,000,000 个值需要 2 秒,而优化的蛮力只需要 700 毫秒。
      • @Daniel Hilgarth:感谢“聪明的方法”:) 我不得不说你的版本还有一个额外的优势,那就是真正随机,而我的不是。顺便说一句,我稍微修改了我的版本,现在它在大约 1 秒内运行 1M 值。
      • 不客气。 :-) 我从来没有想过这样的事情,我什至没有尝试去理解它,呵呵......
      【解决方案6】:

      我认为@slugster 大体上是正确的——尽管您可以运行两个并行进程,一个用于生成数字,另一个用于验证它们并在验证后将它们添加到接受的数字列表中。一旦你有足够的,信号原始进程停止。

      将此与其他建议结合起来 - 使用更有效和更合适的数据结构 - 你应该有一些可以接受的东西。

      但是,为什么您需要这些数字的问题也很重要 - 这个要求似乎应该被分析。

      【讨论】:

        【解决方案7】:

        看看已经发布的解决方案,我的似乎相当基本。但是,它可以工作,并在大约 1 秒内生成 100 万个值(11 秒内生成 1000 万个值)。

        public static void generateIdentifiers(int quantity)
        {
            HashSet<int> uniqueIdentifiers = new HashSet<int>();
        
            while (uniqueIdentifiers.Count < quantity)
            {
                int value = random.Next(111111111, 999999999);
                if (!value.ToString().Contains('0') && !uniqueIdentifiers.Contains(value))
                    uniqueIdentifiers.Add(value);
            }
        }
        

        【讨论】:

          【解决方案8】:

          这样的?

          public List<string> generateIdentifiers2(int quantity)
                  {
                      var uniqueIdentifiers = new List<string>(quantity);
                      while (uniqueIdentifiers.Count < quantity)
                      {
                          var sb = new StringBuilder();
                          sb.Append(random.Next(11, 100));
                          sb.Append(" ");
                          sb.Append(random.Next(11, 100));
                          sb.Append(" ");
                          sb.Append(random.Next(11, 100));
          
                          var id = sb.ToString();
                          id = new string(id.ToList().ConvertAll(x => x == '0' ? char.Parse(random.Next(1, 10).ToString()) : x).ToArray());
          
                          if (!uniqueIdentifiers.Contains(id))
                          {
                              uniqueIdentifiers.Add(id);
                          }
                      }
                      return uniqueIdentifiers;
                  }
          

          【讨论】:

          • 400,000 个号码需要多长时间?
          • 是的太长了...我正要删除它:S
          【解决方案9】:

          我可能会更快:

                  //we can generate first number wich in 9 base system will be between 88888888 - 888888888 
                  //we can't start from zero becouse it will couse the great amount of 1 digit at begining
          
                  int randNumber = random.Next((int)Math.Pow(9, 8) - 1, (int)Math.Pow(9, 9));
          
          
                  //no we change our number to 9 base, but we add 1 to each digit in our number
                  StringBuilder builder = new StringBuilder();
          
                  for (int i=(int)Math.Pow(9,8); i>0;i= i/9)
                  {
                      builder.Append(randNumber / i +1);
                      randNumber = randNumber % i;
                  }
          
                  id = builder.ToString();
          

          【讨论】:

            【解决方案10】:

            这里的诀窍是您需要一万个唯一数字。从理论上讲,您几乎可以有 9,0E+08 种可能性,但是如果您需要的可能性更少,为什么要在意呢?

            一旦您意识到您可以减少如此多的组合,那么创建足够多的唯一数字就很容易了:

            long[] numbers = { 1, 3, 5, 7 }; //note that we just take a few numbers, enough to create the number of combinations we might need
            var list = (from i0 in numbers
                        from i1 in numbers
                        from i2 in numbers
                        from i3 in numbers
                        from i4 in numbers
                        from i5 in numbers
                        from i6 in numbers
                        from i7 in numbers
                        from i8 in numbers
                        from i9 in numbers
                        select i0 + i1 * 10 + i2 * 100 + i3 * 1000 + i4 * 10000 + i5 * 100000 + i6 * 1000000 + i7 * 10000000 + i8 * 100000000 + i9 * 1000000000).ToList();
            

            这个 sn-p 几乎可以立即创建一个包含超过 1,000,000 个有效唯一数字的列表。

            【讨论】:

            • 有 3.87 亿种可能性。这9亿的数字是从哪里来的?
            • @Eric Lippert:快速且明显错误的 hack (999999999-111111111)。我低估了零数字的数量:p。真的是 9^9,但我附近没有计算器。不过,我的观点仍然有效。
            • @InBetween:你能解释一下“你只需要一万个唯一数字”吗?
            • @Daniel Hilgarth:我引用:“我有一种情况,我需要创建 数万 个唯一数字......”从这一点我倾向于相信1,000,000 多个唯一 ID 满足他的需求。
            • 顺便说一句:您意识到您正在创建 10 位数字?
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-05-27
            • 1970-01-01
            相关资源
            最近更新 更多