《编程珠玑》第十二章:取样问题

现在跟大家分享一下第十二章的心得。

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时,第一种方法比较适合。

转载于:https://my.oschina.net/gzckj/blog/123361

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值