2013年2月Google最新面试题: 均匀抽样

       这题为某妞电面的题,我们还讨论了一下,最后确定了一下解法,应该八九不离十。

一,题目描述

       给定一个正整数n,表示范围为(0,,,n-1)的数列。另从这n个数中抽出m个数,组成一个有序数列,需要设计一个算法,从剩下的数中选一个数,但是要求任何数,出现的概率一样。

      比如给定n = 10 ,  Array() = {1, 5, 8},m= 3 那么对剩下的7个数随机抽样,让这7个数每个数出现的概率是1/7。


二,解决思路

       这题其实是个简单题。毕竟是第一个题。

       1, 直接思路

       一种直接的思路很容易想到。顺序扫描一遍,用个数组把没出现的数都记下来。然后对下标抽样,这样就行了。

       比如 剩下的数是remain(7) = {0,2,3,4,6,7,9} ,然后取[0,6] 之间下标的随机数,这样就可以了。

       这个过程主要是要扫描一遍m,把n-m个数记下来,然后再对n-m个抽样。 这样的复杂度是O(n) 。假设取样的复杂度为O(1)。

       但太自然的想法,往往不是Google想考的方法。

       2,二分法

       既然m是有序的,是不是能够采用二分的思路呢?其实也是可以的。

      如果模拟二分搜索的过程,但是向左二分,还是向右二分是有概率的就行了。例如如下所示,要对2,3,4进行抽样,并且要求P(2) = P(3) = P(4)

     

      过程是这样的:

      1)我们先像二分搜索找{1,5,8} 这几个数的中间数,5。

      2)计算5向左走和向右走的概率。这里让每一步走的概率是独立的,即上一步是往左还是往右没有关系。由于还剩下7个数,左边还剩下4个,右边还剩下3个,所以向左走的概率是4/7,向右走的概率为3/7。

      3)抛一个色子,决定。不过这个色子有7个面,如果抛出来的面大于4,那就往右走,否则往左走。不失一般性,假设往左走。5的左边是1。

      4)对于1继续。计算左右随机走的概率,往左是1/4,往右是3/4。 

      5)再抛一次色子,假设往右。但是再往右就又到5了,所以可以抽取样本了。 在1到5之间的2,3,4中间随机选一个。选一个的概率显然是1/3。

      6) 所以总的概率是 4/7 * 3/4 * 1/3 = 1/7 。其他的数的概率也可以这样算得。

      这样算法的复杂度为o(logm),可以看到理论上是比较好的。但是二分法也要更多次的调用生成随机数的函数。而普通法只要一次就行了。

 三, 代码实现

      1,二分法的代码:

/**
  生成一个随机数
  @param data m 个有序的自然数 m<n
  @param left_index 用来二分游走的左下标 初始化为0
  @param right_index 用来二分游走的右下标 初始化为m-1
  @param left_bound 所游走的区域的左边最小值 初始化为0
  @param right_bound 所游走的区域的右边最大值 初始化为n-1
*/
int sample(const vector<int>& data, int left_index, int right_index, int left_bound, int right_bound)
{
	int answer = -1;
	//1, 算区域的中值
	int mid_index = (left_index + right_index) >> 1;
	int mid_value = data[mid_index];
	//2, 算下mid左边剩下多少个未出现的
	int left_missing = (mid_value - left_bound) - (mid_index-left_index);
	int right_missing =(right_bound - mid_value) - (right_index - mid_index);
	//3, 决定往哪边走
	int random_choice = random(left_missing+right_missing);
 
	if(random_choice <= left_missing ){ //4.1 往左走
		if(mid_index-1 < left_index){//往左到头了
			answer = random(left_bound, mid_value-1);
		}else{
			answer = sample(data,left_index, mid_index-1, left_bound,mid_value-1);
		}
	}else{//4.2 往右走
		if(mid_index+1 > right_index){//往右到头了
			answer = random(mid_value+1,right_bound);
		}else{
			answer = sample(data,mid_index+1, right_index, mid_value+1, right_bound);
		}
	}
	return answer;
}
上述还有个两个辅助函数

/**
 生成start到end之间的随机数
 @param start 下限 可以为0
 @param end 上限
*/
int random(int start, int end)
{
	if(start == end)
		return start;
	int delta = end - start + 1;
	return rand()%delta+start;
}
/**
  生成[1,max]之间的随机数
  @param max 随机数的范围
*/
int random(int max)
{
	return rand()%max +1;
}
当然上面是用尾递归实现的,很容易改成循环的形式。代码就是根据上面的过程一步步写的。

为了作为对比,下面也给出普通思路的代码:

/**
 * 从n-m个剩余的数中采样
 */
int sample(const vector<int>& remain_data)
{
	return remain_data[rand()%remain_data.size()];
}
/**
 * 找到未出现的数据
 * @param data m个有序的数列
 * @param remain_data 返回的剩余n-m个数
 * @param n 
 */
void find_remain_data(const vector<int>& data, vector<int>& remain_data,int n)
{
	unsigned int remain_data_index = 0,data_index = 0;
	int prev = 0;
	remain_data.resize(n-data.size());
	//1,5,8
	for(; data_index < data.size() ; data_index++)
	{
		//[prev,data[data_index])
		for(int remain = prev ; remain < data[data_index]; remain++)
				remain_data[remain_data_index++] = remain;
		prev = data[data_index]+1;
	}
	//[prev,n)
	for(int remain = prev ; remain < n; remain++)
			remain_data[remain_data_index++] = remain;
}
四,实验验证

       1,正确性。

       每个实验进行1e5次,比如n = 10 , {1,5,8} ,进行1e5次实验,验证概率其他数出现的概率是否是 1/7.

       

       可以看出出现的次数是比较均匀的,误差也是很小的。

       当然这只是个例子,还做了n比较大的实验,发现都比较符合规律。

      2,两种方法的时间对比。

      n 分别设为1e4,1e5,1e6,1e7,1e8, m 随机选大概n/2数。迭代的次数依然为1e6。

      

时间对比
n普通方法二分法
1e40.030.315
1e50.0520.397
1e60.2380.463
1e72.0070.538
1e819.8080.611

普通法:


二分法:


          这些都是在Debug模式下跑的,release下太快,没啥比较意义了。

          可以看出,当n比较小的时候,普通法比较快,但是当n增长比较大的时候,变化很大。而二分法比较稳定,起伏不是很大。


         原创文章,转载或商用引用本文本博地址


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值