现在跟大家分享一下第十二章的心得。
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时,第一种方法比较适合。