我正在尝试了解以下任务的解决方案: 从大小为N的数组中随机生成一组M个元素。每个元素的选择概率必须相同。
我找到了以下解决方案(我已经读过this question,但没有回答我的问题):
int rand(Random random, int min, int max) {
return random.nextInt(1 + max - min) + min;
}
char[] generateArray(char[] original, int subsetSize) {
char[] subset = new char[subsetSize];
Random random = new Random();
for (int i = 0; i < subsetSize; i++) {
subset[i] = original[i];
}
for (int i = subsetSize; i < original.length; i++) {
int r = rand(random,0, i);
boolean takeIthElement = r < subsetSize;
if (takeIthElement) {
subset[r] = original[i];
}
}
return subset;
}
// rand() function returns inclusive value
// i.e. rand(0, 5) will return from 0 to 5
此代码可在“破解编码访谈”一书中找到(硬性部分,任务3)。 作者解释如下:
假设我们有一个算法可以从大小为
m
的数组中提取n - 1
个元素的随机集合。我们如何使用该算法从大小为m
的数组中提取n
个元素的随机集合?我们首先可以从前n - 1
个元素中提取大小为m的随机集。然后,我们只需要确定是否应将array[n]
插入子集(这将需要从中抽出一个随机元素)。一种简单的方法是从0到n中选择一个随机数k。如果为k < m
,则将array[n]
插入subset[k]
中。这样既可以“公平地”(即,具有成比例的概率)将array[n]
插入子集中,也可以“公平地”从子集中删除随机元素。 迭代编写甚至更干净。在这种方法中,我们将数组子集初始化为原始数组中的前m
个元素。然后,我们遍历数组,从元素m
开始,每当array[i]
时将k
插入(随机)位置k < m
的子集中。
我认为作者想说的是,我们需要生成未设置,而是生成数组。因此,我认为正确的任务描述应该是: 从大小为N的数组中随机生成一个由M个元素组成的数组。每个元素的选择概率必须相同。
如果为true,则上述代码无法正常工作。原因:
{'1', '2', 'a', 'b'}
和m = 2
{1, 2}; {2, 1}; {1, a}; {a, 1}; {1, b}; {b, 1}; {a, 2}; {2, a}; {b, 2}; {2, b}; {a, b}; {b, a}
我在这里担心的是,该函数永远不会生成以下集合:
{2, 1}; {2, a}; {2, b}
所以,这意味着它是不正确的。
答案 0 :(得分:0)
我认为作者想说我们需要生成的不是 set ,而是生成的 array 。
不,作者真正的意思是 set ,但碰巧将结果的 set 存储在 array 中。通过说结果是一个 set ,这意味着值的顺序无关紧要,这意味着{1, 2}
和{2, 1}
是相同的 set < / em>。
鉴于此,只要结果值为{2, 1}
和1
的概率为1/6,即无序(集合),结果永远不会为2
是可以的可能性。
如果您想要一个有序的结果,即列出它们时有12个不同的结果,那么最简单的解决方案是将原始数组改组并采用第一个M
值。这样可以保证所有结果的概率均等,并且不会重复。
通常使用Fisher–Yates shuffle对数组进行改组,这是对数组进行迭代并将该元素与上一个元素随机交换。
问题中的算法是该算法的一种变体。如果跳过前M个值的随机改组,则顺序无关紧要。然后,它会随机地将后续元素与随机元素交换,但是如果随机位置> M不会发生交换,并且交换掉的值将被丢弃,因为它最终会出现在结果集之外。
因此,这是经过修改的Fisher-Yates随机播放,可以在原始数组的副本中生成随机子集,但是经过优化可以跳过不必要的随机播放,因为我们需要一个集合,而不是一个有序的列表/数组,我们只需要一个子集,而不是所有值。
答案 1 :(得分:0)
首先,从解释和代码中可以很清楚地看出作者所设定的含义,就像他们写的一样。可以在实际实现中将集合建模为数组,这并不意味着任何事情。在编程挑战中,人们经常使用相当简单的结构-例如数组而不是java.util.Set
。
所以任务基本上是:
从大小为
M
的数组中随机选择一组N
个元素。
假设N >= M
。
现在最困难的部分:为什么该算法会产生正确的结果?
仅查看算法,很难理解它的工作原理和原因。我认为这是因为该算法实际上是递归构造的,而递归finall在迭代中没有展开。
让我们从递归开始。
假设我们能够从大小为M
的数组中随机选择N - 1
个元素。我们如何从大小为M
的数组中选择N
个元素?
由于数组中有一个“新”元素,因此我们可以用其中的一个替换选定的元素-或保留原样。但是我们必须保留随机属性。
可以用M
的方式选择N-1
中的一组(N-1)! / M!*(N-1 - M)!
元素。
可以以M
的方式选择N
中的一组N! / M!*(N - M)!
元素。
这意味着我们应保持(N-M)/N
概率的集合,并用M/N
概率替换元素之一。我们还必须选择要用1/M
概率替换的元素。
让我们看看它在代码中的外观。假设subset
是我们从M
中随机选择的N-1
元素集。
首先,我们应该确定是否替换其中一个元素。我们需要(N-M)/N
的概率。为此,我们只需要生成0
和N
之间的随机数即可。如果该数字小于M
,我们将替换。
boolean replace = rand(random, 0, N) < M;
if (replace) {
// then replace
}
现在,我们必须选择要替换的元素之一。由于我们将数组建模为一组,因此我们可以简单地在0
和M - 1
(包括)之间随机选择一个索引。这样我们得到:
boolean replace = rand(random, 0, N) < M;
if (replace) {
subset[rand(random, 0, M - 1)] = original[N];
}
在这里我们可以注意到,如果我们的第一个随机值(rand(random, 0, N)
)小于M
,则它 是0
和{{ 1}}。因此,我们不需要第二个M-1
:
rand
其余的应该是微不足道的。
递归的基本情况是int r = rand(random, 0, N);
boolean replace = r < M;
if (replace) {
subset[r] = original[N];
}
。在这种情况下,我们什么也不会替换,因此所选元素的集合就是原始数组的简单形式。
之后,可以将递归简单地编码为一个循环。 M == N
代表i
的每一步-这使您拥有自己的代码。
答案 2 :(得分:0)
如何用数学证明呢?
您的第二个for
循环运行两次,首先是i
等于2,然后是i
等于3。
当i
为2时,r
变为0、1或2,每个概率为1/3。因此,char a
会以索引0或1或根本不索引的形式移到您的结果中,每个概率为1/3。现在它是[a,2],[1,a]或[1,2]。
当i
为3时,r
为0、1、2或3。b
以1/4的概率移至索引0,以1/4的概率移至索引1并且不会以1/2的概率移动到任何地方。
在下表中,我给出了所有可能情况下的结果。 r
,0、1和2下的值是第一次迭代中的可能值(i
= 2)。右边或r
是第二次迭代中的可能值。
r 0 1 2 3
0 [b, 2] [a, b] [a, 2] [a, 2]
1 [b, a] [1, b] [1, a] [1, a]
2 [b, 2] [1, b] [1, 2] [1, 2]
因此在表中您可以看到,如果两次r
均为0,则您的方法将返回[b, 2]
,依此类推。
表中的12个单元格中的每个单元具有相等的概率,即1/12。让我们检查一下:[1,2],[1,a],[1,b],[a,2]和[b,2]分别存在两次。 [a,b]和[b,a]各自出现一次,但是它们是同一集合,因此该集合也出现两次。这涵盖了所有可能的子集,因此它们的可能性也相同。