生成m [0..n-1]范围内的m个不同随机数

时间:2011-08-04 19:42:32

标签: c++ algorithm random performance

我有两种方法可以在[0..n-1]

范围内生成m个不同的随机数

方法1:

//C++-ish pseudocode
int result[m];
for(i = 0; i < m; ++i)
{
   int r;
   do
   {
      r = rand()%n;
   }while(r is found in result array at indices from 0 to i)
   result[i] = r;   
}

方法2:

//C++-ish pseudocode
int arr[n];
for(int i = 0; i < n; ++i)
    arr[i] = i;
random_shuffle(arr, arr+n);
result = first m elements in arr;

当n远大于m时,第一种方法更有效,而第二种方法则更有效。但“更大”并不是一个严格的概念,是吗? :)

问题: 我应该使用什么公式的n和m来确定method1或method2是否会更有效? (根据运行时间的数学期望)

11 个答案:

答案 0 :(得分:15)

纯数学:
让我们计算两种情况下rand()函数调用的数量并比较结果:

案例1: 当你已经选择了k个数字时,让我们看看步骤i = k上的呼叫的数学期望。通过一次rand()调用获得号码的概率等于p = (n-k)/n。我们需要知道这种呼叫数量的数学期望,这导致获得我们还没有的数字。

使用1电话获取电话的可能性为p。使用2来电 - q * p,其中q = 1 - p。一般情况下,在n调用后准确获取它的概率为(q^(n-1))*p。因此,数学期望是
Sum[ n * q^(n-1) * p ], n = 1 --> INF。这个总和等于1/p(由wolfram alpha证明)。

因此,在i = k步骤中,您将对1/p = n/(n-k)函数执行rand()次调用。

现在让我们总结一下:

Sum[ n/(n - k) ], k = 0 --> m - 1 = n * T - 方法1中rand次呼叫的数量 这里T = Sum[ 1/(n - k) ], k = 0 --> m - 1

案例2:

此处rand()random_shuffle n - 1次内被调用(在大多数实现中)。

现在,要选择方法,我们必须比较这两个值:n * T ? n - 1 因此,要选择适当的方法,请按上述方法计算T。如果T < (n - 1)/n,最好使用第一种方法。否则使用第二种方法。

答案 1 :(得分:9)

查看original Fisher-Yates algorithm的维基百科说明。它主张基本上使用方法1最多为n / 2,而方法2则用于其余部分。

答案 2 :(得分:6)

就个人而言,我会使用方法1,然后如果M&gt; N / 2,选择N-M值,然后反转数组(返回未拾取的数字)。因此,例如,如果N为1000并且您想要950个,则使用方法1选择50个值,然后返回其他950个。

编辑:虽然,如果你的目标是一致的性能,我会使用修改后的方法2,它不会完全洗牌,但只会改组N长度数组的前M个元素。

int arr[n];
for(int i = 0; i < n; ++i)
    arr[i] = i;

for (int i =0; i < m; ++i) {
   int j = rand(n-i); // Pick random number from 0 <= r < n-i.  Pick favorite method
   // j == 0 means don't swap, otherwise swap with the element j away
   if (j != 0) { 
      std::swap(arr[i], arr[i+j]);
   }
}
result = first m elements in arr;

答案 3 :(得分:6)

对于任何结果集,这是一个在O(n)内存和O(n)时间(其中n是返回结果的数量,而不是您选择的集合的大小)中工作的算法。它是用Python方便的,因为它使用了哈希表:

def random_elements(num_elements, set_size):
    state = {}
    for i in range(num_elements):
        # Swap state[i] with a random element
        swap_with = random.randint(i, set_size - 1)
        state[i], state[swap_with] = state.get(swap_with, swap_with), state.get(i, i)
    return [state[i] for i in range(num_elements) # effectively state[:num_elements] if it were a list/array.

这只是一个部分渔民洗牌,数组被混洗实现为稀疏散列表 - 任何不存在的元素都等于其索引。我们将第一个num_elements索引洗牌,并返回这些值。如果set_size = 1,这相当于在范围内选择一个随机数,并且在num_elements = set_size的情况下,这相当于标准的渔民洗牌。

观察到这是O(n)时间是微不足道的,并且因为循环的每次迭代在哈希表中最多初始化两个新索引,所以它也是O(n)空间。

答案 4 :(得分:3)

第三种方法怎么样?

int result[m];
for(i = 0; i < m; ++i)
{
   int r;
   r = rand()%(n-i);
   r += (number of items in result <= r)
   result[i] = r;   
}

修改它应该是&lt; =。它实际上是避免碰撞的额外逻辑。

这是一个更好的例子,使用Fisher-Yates的Modern Method

//C++-ish pseudocode
int arr[n];
for(int i = 0; i < n; ++i)
    arr[i] = i;

for(i = 0; i < m; ++i)
    swap(arr, n-i, rand()%(n-i) );

result = last m elements in arr;

答案 5 :(得分:2)

谈论数学期望,这很没用,但无论如何我都会发布:D

随机播放很简单O(m)。

现在另一个算法有点复杂了。生成下一个数字所需的步骤数是试验次数的预期值,试验长度的概率是几何分布。所以......

p=1          E[X1]=1            = 1           = 1
p=1-1/n      E[x2]=1/(1-1/n)    = 1 + 1/(n-1) = 1 + 1/(n-1) 
p=1-2/n      E[x3]=1/(1-1/n)    = 1 + 2/(n-2) = 1 + 1/(n-2) + 1/(n-2)
p=1-3/n      E[X4]=1/(1-2/n)    = 1 + 3/(n-3) = 1 + 1/(n-3) + 1/(n-3) + 1(n-3)
....
p=1-(m-1)/n) E[Xm]=1/(1-(m-1)/n))

请注意,总和可以分成三角形,见右侧。

让我们使用谐波系列的公式:H_n = Sum k = 0-&gt; n(1 / k)=约ln(k)

Sum(E[Xk]) = m + ln(n-1)-ln(n-m-1) + ln(n-2)-ln(n-m-1) + ... = m + ln(n-1) + ln(n-2) + ... - (m-1)*ln(n-m-1) ..

对于谐波系列的总和有一些论坛,如果你仍然感兴趣我会查找它...

更新:实际上这是一个非常好的公式(感谢精彩的混凝土数学书籍)

Sum(H_k) k=0->n = n*H_n - n

所以预期的步骤数:

Sum(E[Xk]) = m + (n-1)*ln(n-1) - (n-1) - (n-m-1)*ln(n-m-1) - (n-m-1)) - (m-1)*ln(n-m-1).

注意:我还没有验证过。

答案 6 :(得分:1)

这是一个很长的镜头,但它可以工作,这取决于你的系统。

  1. 从一些合理的比例开始,比如0.5。
  2. 当请求进入时,请使用从阈值比率的当前值中获得的任何方法进行处理。
  3. 记录所需的时间和“空”时间,使用其他方法执行相同的任务。
  4. 如果替代解决方案比原始解决方案快得多,请向上或向下调整阈值。
  5. 这种方法的明显缺陷是,在高度可变的负载系统中,您的“离线”测试不会太可靠。

答案 7 :(得分:0)

有人建议Fisher-Yates洗牌。不知道下一个代码是否生成了均匀分布的整数,但它至少是紧凑的并且是一遍的:

std::random_device rd;
std::mt19937 g(rd());
for (size_type i = 1; i < std::size(v); ++i) {
    v[i] = std::exchange(v[g() % i], i);
}

答案 8 :(得分:0)

使用set而不是数组呢,我认为它比数组容易得多

set<int> Numbers;
while (Numbers.size() < m) {
   Numbers.insert(rand() % n);
}

答案 9 :(得分:-1)

很可能在调试模式下启动它(并保留一个方法作为注释)几次以获得平均值更简单,然后使用另一种方法从中获得平均值

答案 10 :(得分:-1)

我不建议使用这种方法,但是可以使用

#include <iostream>
#include <random>
#include <ctime>

using namespace std;

int randArray[26];
int index = 0;

bool unique(int rand) {

    for (int i = 0; i < index; i++)
        if (rand == randArray[i])
            return false;
    index++;
    return true;
}


int main()
{
    srand(time(NULL));

    for (int i = 1; i < 26; i++)
        randArray[i] = -1;

    for (int i = 0; i < 26; i++) {

        randArray[i] = rand() % 26;

        while (!unique(randArray[i])) {
            randArray[i] = rand() % 26;
        }
    }

    for (int i = 0; i < 26; i++) {
        cout << randArray[i] << " ";
    }

    cout << "\n" << index << endl;


    return 0;
}