刚刚参加一次面试,跟技术面试官探讨了一个话题。面试官给我出了一个题目,涉及算法。大意是:如果我们要设计一个产品抽奖,每种奖品都有一定概率被抽中,当然也要一定概率什么也抽不中。面试官要考试,对问题的描述半遮半掩,多次对话后我弄清了他的意图,于是稍作思考,给出了两种方案。
1. 把未抽中的机会也虚拟为一种产品,将所有产品的概率计算总和,按照100分配实际概率。再给出一个抽奖总次数,生成一个奖品池,比如一个集合,随机抽取后删除该奖品。不过,如果抽奖机会总数很大,这种方案很耗资源。
2. 同上,给每一种奖品分配一个计数器,被随机抽中就会减掉一个,被抽完就删除或者不再被抽取。这种方案不会消耗太多资源。
作为一个面试者,我所理解的就是对概率的精确控制。就如同发行奖券,总数是一定要有的。可是面试官告诉我,没有总数。
我不知道他想要什么样的答案。
我请教他,他告诉我,为每一种产品被抽中的概率设置一个数据空间,看看随机数飘落在那个空间,就抽中哪一种奖品,落到未抽中的空间,就不被抽中。大意就是,把概率参考数据加起来,最大就是这个总和,最小为0,按照概率参考数据依次排列,每一种奖品被抽中概率拥有一个范围。随机生成一个0到总数的随机数,看看会在哪个范围之内。
奖品被抽中概率参考:
小米:20
华为:40
OPPO:15
未中奖:25
我明白了他的意思,感觉的确是很简洁。不过,随机数会均匀分布吗?在我印象中好象不是。他告诉我目前Java的Random是最均匀的。那好吧。
很明显,我追求的是精确,他的需求是大概率。我还是没有领会到面试官的原意。
各自保留意见之后。我回到家,写了一个demo。来对Random随机数的均匀度做一个测试。
package com.chris.algorithm;
import org.junit.Test;
import java.util.Random;
/**
* @author Chris Chan
* Create on 2021/5/21 0:55
* Use for:
* Explain: 测试随机数发生器的均匀度
*/
public class RandomTest {
//private Random random = new Random();
private Random random = new Random();
@Test
public void test01() {
int count = 10000;
int[] counts = new int[10];
for (int i = 0; i < count; i++) {
double v = random.nextDouble() * 100;
for (int j = 0; j < 10; j++) {
if (v >= j * 10.0D && v < j * 10.0D + 10) {
counts[j]++;
break;
}
}
}
showIntArray(counts);
}
@Test
public void test02() {
int count = 10000;
int[] counts = new int[10];
for (int i = 0; i < count; i++) {
double v = Math.random() * 100;
for (int j = 0; j < 10; j++) {
if (v >= j * 10.0D && v < j * 10.0D + 10.0D) {
counts[j]++;
break;
}
}
}
showIntArray(counts);
}
private void showIntArray(int[] ints) {
for (int i = 0; i < ints.length; i++) {
System.out.printf("%-8d\t", ints[i]);
}
}
}
我分别测试了util的Random和Math.random(),结果都差不多.
多次测试,发现区间分布最高值和最低值有时候相差百分之十以上,这算不算均匀呢?估计得看业务需求的重视程度了。处于严格控制的角度,我觉得可能会有问题,可预测的后果就是很有可能我们准备的奖品严重不足,活动预算超支。但是如果这活动本身设计就有很大的包容性,允许有出入,按照面试官的想法,那当然是可以的,算法也简单。
那,面试官的方案是不是会让随机数飘落均匀分布呢?我也写了一个例子来测试。
package com.chris.algorithm;
import java.util.Random;
/**
* @author Chris Chan
* Create on 2021/5/21 0:02
* Use for:
* Explain:
*/
public class MainTest {
private Random random = new Random();
public static void main(String[] args) {
new MainTest().test01();
}
private void test01() {
//在0~20之间生成6个随机数double中将概率参考数,最后一个为不中奖概率参考数
double[] nums1 = new double[6];
for (int i = 0, len = nums1.length; i < len; i++) {
nums1[i] = random.nextDouble() * 20;
}
System.out.println("原始中奖概率参考数:");
showDoubleArray(nums1);
//计算各自所占的概率
double sum = 0.0D;
for (int i = 0, len = nums1.length; i < len; i++) {
sum += nums1[i];
}
double[] nums2 = new double[6];
for (int i = 0, len = nums2.length; i < len; i++) {
nums2[i] = nums1[i] / sum * 100;
}
System.out.println("\n\n概率参考书总和:");
System.out.printf("%-8.15f\n", sum);
System.out.println("各自所占概率百分数:");
showDoubleArray(nums2);
//拼装为连续的左闭区间
double[] nums3 = new double[7];
double start = 0.0D;
for (int i = 0, len = nums3.length; i < len; i++) {
nums3[i] = start;
if (i < len - 1) {
start += nums1[i];
}
}
System.out.println("\n\n拼装的左闭区间序列:");
showDoubleArray(nums3);
//飘落随机数
int count = 100000;
int[] counts = new int[6];
for (int i = 0; i < count; i++) {
double v = random.nextDouble() * sum;
for (int j = 0, len3 = nums3.length; j < len3 - 1; j++) {
if (v >= nums3[j] && v < nums3[j + 1]) {
counts[j] += 1;
break;
}
}
}
System.out.println("\n\n随机飘落结果统计");
System.out.println(count);
showIntArray(counts);
}
private void showDoubleArray(double[] doubles) {
for (int i = 0; i < doubles.length; i++) {
System.out.printf("%-8.15f\t", doubles[i]);
}
}
private void showIntArray(int[] ints) {
for (int i = 0; i < ints.length; i++) {
System.out.printf("%-17d\t", ints[i]);
}
}
}
测试结果。
多次测试,结果显示,抽奖随机数飘落分布概率和预设的奖品应有的概率十分接近。面试官的话还是没错的,跟预想的结果很相似。具体需要精确的概率,还是大概率,就根据业务需求来选择算法吧。