现在跟大家分享一下第十二章的心得。
1.问题描述
程序的输入包含两个整数m和n,其中m<n。输出是0~n-1范围内m个随机整数的有序列表,不允许重复。从概率的角度说,我们希望得到没有重复的有序选择,其中每个选择出新的概率相等。
2.程序设想
当m=2,n=5时,首先考虑第一个整数0。选择0的概率是2/5,即m/n。现在开始对第二个整数1进行思考。若已经选择了0,那么剩下的4个数中,选择1个数,所以选择1的概率为1/4;但对于没有选择0的情况进行分析,那么就从剩下4个数中,还可以选择2个数,所以选择1的概率就变为2/4。
当从r个剩余的整数中选出s个,概率就会是s/r。所以可以用以下伪代码来表示:
select = m remaining = n for i= [0,n] if ( rand() % remaining) < select print i select -- remaining --
只要m<n,程序会正确地选出m个整数。不会选择更多的整数,因为当select = 0 时,if的判断语言为false,所以就不会进行选择;也不会选择更少的整数,因为select/remaing = 1时,这个整数一定会被选到。
对于此中,具体java代码实现如下:
import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.Set; import java.util.TreeSet; import ckj.programperl.util.Constants; import ckj.programperl.util.Util; public class RandomSelect { private int m_length; // n private int m_select; // m private int[] m_array_range; private int[] m_select_range; private Random m_rand; // 随机数 public RandomSelect(){ this(Constants._ARRAY_SELECT,Constants._ARRAY_LENGTH); } public RandomSelect(int m, int n){ m_length = n ; m_select = m ; m_rand = new Random(); } public void method1(){ m_select_range = new int[m_select]; int select = m_select; int remaing = m_length; // System.out.println(m_rand.nextInt()); int pos = 0 ; for ( int i = 0 ; i < m_length ; i ++){ if ( m_rand.nextInt(remaing) < select ){ m_select_range[pos] = i ; pos ++ ; select --; } remaing -- ; } System.out.println("method1产生的随机数--->"); Util.print(m_select_range); }
3.其他方法
想到这里,可能很多人都放弃了继续对此问题的思考。这书要教会的是,不要满足与一个解法。
考虑都Set这个数据集合的特殊性,(没有重复性),所以可以叫Set作为我们存储的结构来存储这个随机数。因为当出现了相同的随机数时,SET会自动把它丢弃,直到添加到新的随机数为主。为了保证其有序性,所以使用了TreeSet集合保存随机数。其代码实现如下:
public void method2(){ m_select_range = new int[m_select]; Set<Integer> set = new TreeSet<Integer>(); while(set.size() < m_select){ set.add(m_rand.nextInt(m_length)); } Iterator<Integer> itr = set.iterator(); int i = 0 ; while ( itr.hasNext()){ m_select_range[i] = itr.next(); i++; } System.out.println("\nmethod2产生的随机数--->"); Util.print(m_select_range); }
而第三种方法,也是我自己最初想到的方法。因为记得前面的章节曾提出过随机生成不重复的数,所以我可以直接调用其方法,然后对这些随机数排序即可。而随机生成不重复的数,运用的是互换位置法。如,一个数组:0,1,2,3,4,5。所以任意生成从[0,5]的随机数,然后与第1个位置的数互换。接着,随机生成[1,5]的随机数,与第2个位置互换。这样得到的前2位就是我们想要的不重复的随机数。这种方法的好处,就是空间利用为0(n)。不如第一种方法。具体代码实现如下:
public void mehtod3(){ List<Integer> list = new ArrayList<Integer>(); int range = m_length; int select =m_select; m_array_range = new int[range]; m_select_range = new int[select]; for ( int i = 0 ; i < range;i++){ m_array_range[i] = i; } for (int i = 0 ; i < select ; i ++){ // System.out.println(m_rand.nextInt(range)); Util.swap ( m_array_range,i,i+m_rand.nextInt(range-i) ); list.add(m_array_range[i]); } Collections.sort(list); //Util.print(list.toArray()); for ( int i = 0 ; i < m_select ; i ++){ m_select_range[i] = list.get(i); } System.out.println("\nmethod3产生的随机数--->"); Util.print(m_select_range); }
在util包中的实际的静态方法如下:
package ckj.programperl.util; public class Util { public static void print(int array[]){ for ( int i = 0 ; i < array.length ; i ++){ System.out.print(array[i] + " "); } System.out.println(); } public static void swap ( int array[], int i, int j ){ // System.out.println("i--->"+i+" j--->" + j); int temp = array[i]; array[i] = array[j]; array[j] = temp; } }
参数数值:
package ckj.programperl.util; public class Constants { public static int _ARRAY_LENGTH = 16000000; public static int _ARRAY_SELECT = 20; }
4.评价与小结
主函数的调用:
package ckj.programperl.randomselect.Main; import ckj.programperl.randomselect.RandomSelect; public class Main { public static void main(String[] args) { RandomSelect rs = new RandomSelect(); long time1 = System.currentTimeMillis(); rs.method1(); long time2 = System.currentTimeMillis(); System.out.println("花费时间--->"+(time2-time1)); rs.method2(); long time3 = System.currentTimeMillis(); System.out.println("花费时间--->"+(time3-time2)); rs.mehtod3(); long time4 = System.currentTimeMillis(); System.out.println("花费时间--->"+(time4-time3)); } }
运行效果为:
method1产生的随机数---> 1997491 2750273 3127074 3833180 4427167 4581482 4676440 5215199 6053172 6260700 6342617 6982987 7713904 8902963 9999452 10547400 12117138 12260635 12919556 14783364 花费时间--->258 method2产生的随机数---> 313021 1386478 2009643 2397259 3266888 4292300 5405340 5434602 5577191 8033889 8973808 10698762 11024965 11295896 11568427 12631196 12639134 12675218 12872131 13338840 花费时间--->2 method3产生的随机数---> 2517438 2628671 3355097 3503923 6049574 7359175 7711849 8980609 9106329 9245641 9539006 10240380 11092067 11617640 11854653 11993375 12317527 14661711 15945139 15981795 花费时间--->35
第二种方法,当m接近与n时,SET集合就会丢弃很多重复的随机数,使得效率较慢。而第三种方法,使用O(n)的空间,比第一种方法的效果差。但从结果来看,当n远远大于m时,第二种方法比较适合。当m接近n时,第一种方法比较适合。