有几种方法可以解决您的问题,每种方法都有自己的优点和缺点。
首先我想指出,您已经收到不少响应,它们执行以下操作:它们生成一个随机数,然后以某种方式检查它是否已在数组中使用,如果已使用,它们只需生成另一个号码,直到他们找到未使用的号码。
这是一种幼稚的方法,说实话,存在严重缺陷。问题在于数字生成的循环试错性质(“如果已使用,请再试一次”)。如果数字范围(例如,[1..N])接近所需数组的长度(例如,M),那么在接近尾声时,算法可能会花费大量时间来尝试找到下一个数字。如果随机数生成器有一点点损坏(例如,从不生成某个数字,或者很少生成),那么使用 N == M 算法可以保证永远循环(或很长时间)。一般来说,这种反复试验的方法是无用的,或者充其量是有缺陷的。
这里已经提出的另一种方法是在大小为 N 的数组中生成随机排列。随机排列的想法很有前途,但是在大小为 N 的数组上(当 M
可以在 Bentley 的“Programming Pearls”(其中一些摘自 Knuth)中找到该问题的良好解决方案。
-
Knuth 算法。 这是一个非常简单的算法,复杂度为 O(N)(即数值范围),这意味着当 M 接近 N 时它最有用。但是,该算法没有除了您的
vektor 数组之外,不需要任何额外的内存,而不是已经提供的具有排列的变体(这意味着它需要 O(M) 内存,而不是 O(N),因为这里建议的其他基于排列的算法)。后者使它成为一个可行的算法,即使对于 M
算法的工作原理如下:遍历从1到N的所有数字,并以rm / rn的概率选择当前数字,其中rm是我们还需要找到多少个数字,rn是多少个数字我们仍然需要迭代。这是您的案例的可能实现
#define M 10
#define N 100
int in, im;
im = 0;
for (in = 0; in < N && im < M; ++in) {
int rn = N - in;
int rm = M - im;
if (rand() % rn < rm)
/* Take it */
vektor[im++] = in + 1; /* +1 since your range begins from 1 */
}
assert(im == M);
在这个循环之后,我们得到一个数组vektor,其中填充了随机选择的数字升序。 “升序”位是我们在这里不需要的。因此,为了“修复”我们只需对vektor 的元素进行随机排列,我们就完成了。请注意,这是一个 O(M) 排列,不需要额外的内存。 (我省略了置换算法的实现。这里已经给出了很多链接。)。
如果您仔细查看此处提出的对长度为 N 的数组进行操作的基于排列的算法,您会发现它们中的大多数几乎都是相同的 Knuth 算法,但为 M == N 重新制定。在这种情况下,上述选择循环将以概率 1 选择 [1..N] 范围内的每个数字,有效地转化为数字 1 到 N 的 N 数组的初始化。考虑到这一点,我认为它变得相当很明显,为M == N 运行该算法然后截断结果(可能丢弃大部分结果)比仅以原始形式运行该算法以获取 M 的原始值并立即获得结果而没有任何截断的意义要小得多.
-
弗洛伊德算法(参见here)。这种方法的复杂度约为 O(M)(取决于所使用的搜索结构),因此在 M 不会进行任何可恶的试错迭代,试图找到一个未使用的随机数。该算法保证在每次调用随机数生成器后生成一个唯一的随机数。
对于您的情况,这是一个可能的实现。 (有不同的方法可以跟踪已使用的数字。假设 N 不是太大,我将只使用一组标志)
#define M 10
#define N 100
unsigned char is_used[N] = { 0 }; /* flags */
int in, im;
im = 0;
for (in = N - M; in < N && im < M; ++in) {
int r = rand() % (in + 1); /* generate a random number 'r' */
if (is_used[r])
/* we already have 'r' */
r = in; /* use 'in' instead of the generated number */
assert(!is_used[r]);
vektor[im++] = r + 1; /* +1 since your range begins from 1 */
is_used[r] = 1;
}
assert(im == M);
上述工作的原因并不是很明显。但它有效。 [1..N] 范围内的恰好 M 个数字将均匀分布。
注意,对于较大的 N,您可以使用基于搜索的结构来存储“已使用”的数字,从而获得一个 O(M log M) 且内存要求为 O(M) 的不错的算法。
(虽然这个算法有一点:虽然结果数组不会被排序,但原始 1..N 排序的某种“影响”仍然会出现在结果中。例如,很明显数字N,如果被选中,只能是结果数组的最后一个成员。如果这种意外排序对结果的“污染”是不可接受的,那么结果vektor 数组可以随机打乱,就像在 Khuth 中一样算法)。
请注意在这两种算法的设计中观察到的非常关键的一点:它们从不循环,试图找到一个新的未使用的随机数。从实际的角度来看,任何使用随机数进行试错迭代的算法都是有缺陷的。此外,这些算法的内存消耗与 M 相关,而不是 N
对于 OP,我会推荐 Floyd 算法,因为在他的应用程序中,M 似乎远小于 N,并且它不需要(或可能不需要)额外的置换传递。然而,对于如此小的 N 值,差异可能可以忽略不计。