随机数的产生——————摘录

说明:写做本文的出发点是最近和一个有3年开发经验的.NET开发人员聊天,他跟我说常常没有思路,在实际开发中我也见过一个具备45年开发经验的开发人员几乎没有灵活变通的能力,因此打算写一系列文章,在这个系列文章中我会主要讲解解题的思路,而不是讲述什么新技术新特性,借这个系列文章为初中级开发者了解遇到问题别人是如何思考和解决的。固然,若是你的思路比本文提到的更好,欢迎指出来,同时若是你对本系列文章有更好的建议或者有平常中的一些典型问题,请给我联系,咱们共同探讨。目前我暂时能想到的有不重复随机数产生问题、字符串与数值转换的问题、特殊的数据库锁问题、访客来路追踪问题、在线用户统计问题、统计用户访问页面偏好问题。
好了,我如今开始本篇的讲述。本篇的最原始形态是来源于我早年作的一个Java SE应用软件,它是用来模拟彩票投注站的选好软件的。应为在早年Java SE中用swing作界面布局是一件比较痛苦的事情,因此后来我从新用C#作了一个。这个问题的原型就是解决双色球随机选号的问题,咱们知道双色球红色球共包含13333个红色号码球及11616个蓝色号码球,一注双色球号码应包括6个红色球号码和1个蓝色球号码。蓝色号码球很好解决,随机从11616个数字中随机选取一个就好了。可是红色球就存在这样同样问题,每次选取的红色球不能与本注中已经选取的号码重复,这个问题归结为生成不重复随机数问题。
在本篇我就怎么生成不重复的红色球展开讨论。
解题思路一
在早期的Java中不包含泛型,只能使用ArrayList,因此我是用ArrayList来实现的。在Java中的ArrayListC#中的ArrayList在用法上是很类似的(这就是为何高手常常说掌握一门语言以后再去掌握另外一门语言是很容易的事情,应为思想是相通的,呵呵)。在这里我最想一想到的就是使用循环,每次循环中随机生成一个随机数,判断一下这个随机数是否已经在本注中使用,若是没有使用就将这个号码保存到结果中去,反之则进行下一轮循环,循环的结束条件就是生成了知足要求的6个数字。接触到泛型以后,我知道在这里我所使用的数据类型是int类型(固然也可使用byte类型),若是使用ArrayList保存int这样的值类型数据会存在着装箱和拆箱操做,带来没必要要的性能损失,因此针对这种集合中数据类型单一的状况能够考虑泛型集合,因而获得了下面的代码:

 

  1. /// <summary>     
  2. /// 从1到33中任意选取不重复的6个随机数     
  3. /// </summary>     
  4. /// <returns></returns>     
  5. public List<int> GenerateNumber1()     
  6. {     
  7.     //用于保存返回的结果     
  8.     List<int> result = new List<int>(6);     
  9.     Random random = new Random();     
  10.     int temp = 0;     
  11.     //若是返回的结果集合中实际的元素个数小于6个     
  12.     while (result.Count < 6)     
  13.     {     
  14.         //在[1,34)区间任意取一个随机整数     
  15.         temp = random.Next(1, 34);     
  16.         if (!result.Contains(temp))     
  17.         {     
  18.             //若是在结果集合中不存在这个数,则添加这个数     
  19.             result.Add(temp);     
  20.         }     
  21.     }     
  22.     //result.Sort();//对返回结果进行排序     
  23.     return result;     

 

固然,上面这种思路是能够实现的,可是每次随机生成一个随机数都要判断在结果集合中是否已经存在这个数,若是存在还要继续下一个循环,这样一来并非每一轮循环都能生成一个有效(即不重复)的随机数,而且result.Contains(temp)尽管看起来只有一句,但实际在内部仍是要经过循环来判断,效率仍是较低。假若有一天有我的看到这篇文章,他想:很好,我终于能够试试了,我要从110000个数中取出9999个不重复的随机数,用上面的这个方法,可能很长时间都得不到结果(不要觉得没有这样的人,我就碰见多屡次不会变通的人,实际上最好的解决办法就是从10000中随机去掉一个就能够了,而不是照搬上面的套路)。
解题思路二
刚才说到在方法一中并非每一轮循环都能生成一个有效的、不重复的随机数,那么有没有这样的办法,保证每一轮循环都能生成一个有效地、不重复的随机数呢?答案是有的。
具体作法是这样的,咱们将初始化一个容器集合,在这个容器集合中包含了全部可能的值,而后每次随机从这个容器集合中随机选取一个值保存到结果集合中去,以后咱们就从容器集合中将这个已经使用过的值删除掉,而后再进行下一轮的循环。既然都已经从容器集合中删除掉了,天然在下一轮循环中随机从容器集合中取一个值,这个值天然不会重复了。由于这个集合的容量是可变的,那么天然也是使用泛型集合了,代码以下:

 

  1. /// <summary>     
  2. /// 从1到33中任意选取不重复的6个随机数     
  3. /// </summary>     
  4. /// <returns></returns>     
  5. public List<int> GenerateNumber2()     
  6. {     
  7.     //用于存放1到33这33个数     
  8.     List<int> container = new List<int>(33);     
  9.     //用于保存返回结果     
  10.     List<int> result = new List<int>(6);     
  11.     Random random = new Random();     
  12.     for (int i = 1; i <= 33; i++)     
  13.     {     
  14.         container.Add(i);     
  15.     }     
  16.     int index = 0;     
  17.     int value = 0;     
  18.     for (int i = 1; i <= 6; i++)     
  19.     {     
  20.         //从[0,container.Count)中取一个随机值,保证这个值不会超过container的元素个数     
  21.         index = random.Next(0, container.Count);     
  22.         //以随机生成的值做为索引取container中的值     
  23.         value = container[index];     
  24.         //将随机取得值的放到结果集合中     
  25.         result.Add(value);     
  26.         //从容器集合中删除这个值,这样会致使container.Count发生变化     
  27.         container.RemoveAt(index);     
  28.         //注意这一句与上面一句能达到一样效果,可是没有上面一句快     
  29.         //container.Remove(value);     
  30.     }     
  31.     //result.Sort();排序     
  32.     return result;     

 

通过这么一改动,确实能作到每次循环都能生成一个惟一的有效的不重复的数,这样一来就能作到在M个数中选取N个数时只须要循环M×N次就能够了(MN,而且都是正整数)。不过也有人会说,我如今还在维护一个.NET1.1的项目,我这里也有相似的需求,但是在.NET2.0如下版本中是没有泛型的,你能给我想个办法吗?个人答案是能够的。
解题方法三
在这个方法中咱们不适用泛型集合,只使用数组,这样一来这种作法就能够适用于CJavaPHP等语言和.NET1.1中了。
思路以下,首先使用一个数组做为存储全部可能值的容器集合,而后经过循环每次生成一个随机值,这个值未来会做为下标来访问容器集合中的数值。由于数组是不可变集合,咱们不能将已经使用数值从数组中删除,而且它们是简单的数据类型咱们不可能给每一个数值增长一个属性表示数值是否已经被使用过了,那该怎么办呢?办法就是每次从可用的下标集合中随机生成一个值,而后以这个值做为索引从容器集合中获得相应的值保存到结果集合中,除此以外再将这个已经使用过的值与数组中最后一个没有使用到的值互换位置,而后下一轮再在全部没有使用过的值中从新再取一个值。代码以下:

 

  1. public int[] GenerateNumber3()     
  2. {     
  3.     //用于存放1到33这33个数     
  4.     int[] container = new int[33];     
  5.     //用于保存返回结果     
  6.     int[] result = new int[6];     
  7.     Random random = new Random();     
  8.     for (int i = 1; i <= 33; i++)     
  9.     {     
  10.         container[i - 1] = i;     
  11.     }     
  12.     int index = 0;     
  13.     int value = 0;     
  14.     for (int i = 0; i < 6; i++)     
  15.     {     
  16.         //从[1,container.Count + 1)中取一个随机值,保证这个值不会超过container的元素个数     
  17.         index = random.Next(1, container.Length-1-i);     
  18.         //以随机生成的值做为索引取container中的值     
  19.         value = container[index];     
  20.         //将随机取得值的放到结果集合中     
  21.         result[i]=value;     
  22.         //将刚刚使用到的从容器集合中移到末尾去     
  23.         container[index] = container[container.Length - i-1];     
  24.         //将队列对应的值移到队列中     
  25.         container[container.Length - i-1] = value;     
  26.     }     
  27.     //result.Sort();排序     
  28.     return result;     

 

这样一来,问题获得了解决了。这种作法也能够移植到不支持泛型的版本或者语言当中。
 
再来一点变更
上面咱们处理的都是连续的状况,假如万一让咱们在不连续的集合中随机选择5个不重复的,好比在某个班50个学生中随机抽取5个学生来,貌似上面的作法不行了?其实否则,依然能够沿用这种思路,好比使用上面的第三种办法即GenerateNumber3(),无非就是声明一个字符串数组,在这个字符串数组中存放全班全部同窗的姓名,而后按照下标来随机取5个姓名便可(具体代码这里省略)。
不过对这种状况还有不一样的,好比在快女和春晚中都有短信投票的环节,最后会从全部发送短信的手机号中随机抽取几个手机号码做为中奖号码(为了简化,将一号多投的状况视做一次,而且假设没有廉租房“连号”的“公平”状况),可能有人会想到照搬上面的状况。实际上在这种状况下不适合,有可能随机数的集合至关大,这样就不适合在内存中存储了,能够考虑使用数据库存储而后利用数据库的随机函数,在不一样的数据库中取随机记录的函数可能会不一样。好比在MySQL中以下(假设表名为table_name)
select * from `table_name` order by rand() limit 0,10
而在SQL Server中会是以下(假设表名为table_name)
select top 10 * from [table_name] order by newid()
至于在Oracle中如何随机抽取记录,你们google一下吧。
固然,这个状况还能够再复杂一下,可能电视台针对短信参与的用户要分一二三等奖,一等奖1名,二等奖2名,三等奖3名,记念奖50名,那么状况就会稍微再复杂一点,不过也就是再增长一个字段而已,这个字段表示当前的号码是否已经中过奖,每次选取号码时只选择那些不曾中奖过的号码便可。
总结
关于不重复随机数生成的问题我在七八年前就遇到过,四五年前的时候曾经作过总结,最近看到有人在讨论这个问题,因而就又从新捡起这个话题了。如今捡起这个话题的目的不是想再简单介绍可能的几种算法,而是从思路上去说明,而且将状况慢慢复杂化,想要说明的是程序员们(不限于.NET程序员)不要用固定的思路去解决问题,可能一样的要求在不一样的场合下会有不一样的作法。明白了思路才能真正作到以不变应万变,学会一两个控件的用法或者多指导一两个API并不算什么本领,可以在遇到之前没有碰到过的问题时迅速简化解决思路才是本领,另外一种本领就是遇到错误时如何快速根据经验定位错误产生缘由的本领。
 
周公
相关文章
相关标签/搜索