O(1)中的唯一(非重复)随机数?
Posted
技术标签:
【中文标题】O(1)中的唯一(非重复)随机数?【英文标题】:Unique (non-repeating) random numbers in O(1)? 【发布时间】:2010-09-16 19:08:15 【问题描述】:我想生成介于 0 和 1000 之间且永不重复的唯一随机数(即 6 不会出现两次),但这不会诉诸于对先前值进行 O(N) 搜索之类的事情它。这可能吗?
【问题讨论】:
这和***.com/questions/158716/…不是同一个问题 0 是否介于 0 和 1000 之间? 如果您在一定时间内禁止任何事情(例如时间或记忆中的O(n)
),那么下面的许多答案都是错误的,包括已接受的答案。
你会如何洗牌?
警告! 下面给出的许多答案都不会产生真正随机的序列,比 O(n) 慢或有其他缺陷! codinghorror.com/blog/archives/001015.html 是您在使用其中任何一个或尝试自己编造之前的必备读物!
【参考方案1】:
有人发布了“在 excel 中创建随机数”。我正在使用这个理想。 创建一个包含 2 个部分的结构,str.index 和 str.ran; 对于 10 个随机数,创建一个包含 10 个结构的数组。 将 str.index 设置为 0 到 9 并将 str.ran 设置为不同的随机数。
for(i=0;i<10; ++i)
arr[i].index = i;
arr[i].ran = rand();
根据 arr[i].ran 中的值对数组进行排序。 str.index 现在是随机顺序。 下面是c代码:
#include <stdio.h>
#include <stdlib.h>
struct RanStr int index; int ran;;
struct RanStr arr[10];
int sort_function(const void *a, const void *b);
int main(int argc, char *argv[])
int cnt, i;
//seed(125);
for(i=0;i<10; ++i)
arr[i].ran = rand();
arr[i].index = i;
printf("arr[%d] Initial Order=%2d, random=%d\n", i, arr[i].index, arr[i].ran);
qsort( (void *)arr, 10, sizeof(arr[0]), sort_function);
printf("\n===================\n");
for(i=0;i<10; ++i)
printf("arr[%d] Random Order=%2d, random=%d\n", i, arr[i].index, arr[i].ran);
return 0;
int sort_function(const void *a, const void *b)
struct RanStr *a1, *b1;
a1=(struct RanStr *) a;
b1=(struct RanStr *) b;
return( a1->ran - b1->ran );
【讨论】:
【参考方案2】:请在https://***.com/a/46807110/8794687查看我的回答
它是最简单的算法之一,平均时间复杂度O(s log s), s 表示样本量。那里也有一些链接到哈希表算法,据称其复杂度为 O(s)。
【讨论】:
【参考方案3】:您可以使用Format-Preserving Encryption 来加密计数器。您的计数器只是从 0 向上,加密使用您选择的密钥将其转换为您想要的任何基数和宽度的看似随机的值。例如。对于此问题中的示例:基数 10,宽度 3。
块密码通常具有固定的块大小,例如64 位或 128 位。但是格式保留加密允许您采用像 AES 这样的标准密码,并使用一种仍然具有加密稳健性的算法来制作宽度更小的密码,无论您想要什么基数和宽度。
保证永远不会发生冲突(因为加密算法会创建 1:1 映射)。它也是可逆的(2 路映射),因此您可以获取结果数字并返回您开始使用的计数器值。
这种技术不需要内存来存储洗牌数组等,这在内存有限的系统上可能是一个优势。
AES-FFX 是一种建议的标准方法来实现这一点。我已经尝试了一些基于 AES-FFX 思想的基本 Python 代码,虽然不完全符合 --see Python code here。它可以例如将计数器加密为看起来随机的 7 位十进制数或 16 位数字。这是一个基数为 10、宽度为 3 的示例(给出一个介于 0 和 999 之间的数字),如问题所述:
000 733
001 374
002 882
003 684
004 593
005 578
006 233
007 811
008 072
009 337
010 119
011 103
012 797
013 257
014 932
015 433
... ...
要获得不同的非重复伪随机序列,请更改加密密钥。每个加密密钥都会产生一个不同的非重复伪随机序列。
【讨论】:
这本质上是一个简单的映射,因此与 LCG 和 LFSR 没有任何不同,具有所有相关的扭结(例如,序列中超过k
的值永远不会同时出现)。
@ivan_pozdeev:我很难理解你评论的意思。你能解释一下这个映射有什么问题,什么是“所有相关的问题”,什么是k
?
这里所有的“加密”实际上都是用一个相同的数字序列替换序列1,2,...,N
,但仍然是不变的。然后从这个序列中一个一个地提取数字。 k
是选择的值的数量(OP 没有为它指定一个字母,所以我不得不介绍一个)。
@ivan_pozdeev FPE 不是必须实现特定的静态映射,或者“返回的组合完全由第一个数字定义”。由于配置参数比第一个数字的大小(只有一千个状态)大得多,因此应该有多个序列以相同的初始值开始,然后继续到不同的后续值。任何现实的生成器都将无法覆盖整个可能的排列空间;当 OP 没有要求时,不值得提高该故障模式。
+1。如果正确实施,使用具有随机统一选择的密钥的安全分组密码,使用这种方法生成的序列在计算上将与真正的随机洗牌无法区分。也就是说,没有办法比通过测试所有可能的分组密码密钥并查看它们中的任何一个是否生成相同的输出更快地将这种方法的输出与真正的随机洗牌区分开来。对于具有 128 位密钥空间的密码,这可能超出了人类目前可用的计算能力;使用 256 位密钥,它可能会永远如此。【参考方案4】:
How do you efficiently generate a list of K non-repeating integers between 0 and an upper bound N 的问题被链接为重复项 - 如果您想要每个生成的随机数为 O(1) 的东西(没有 O(n) 启动成本)),可以对已接受的答案进行简单的调整。
从整数到整数创建一个空的无序映射(一个空的有序映射将花费 O(log k) 每个元素) - 而不是使用初始化数组。 如果最大值为 1000,则将最大值设置为 1000,
-
选择一个介于 0 和最大值之间的随机数 r。
确保映射元素 r 和 max 都存在于无序映射中。如果它们不存在,则使用等于其索引的值创建它们。
交换元素 r 和 max
返回元素最大值并将最大值减 1(如果最大值变为负数
你完成了)。
返回步骤 1。
与使用初始化数组的唯一区别是元素的初始化被推迟/跳过 - 但它会从同一个 PRNG 生成完全相同的数字。
【讨论】:
【参考方案5】:Fisher Yates
for i from n−1 downto 1 do
j ← random integer such that 0 ≤ j ≤ i
exchange a[j] and a[i]
实际上是 O(n-1),因为你只需要一个交换最后两个 这是 C#
public static List<int> FisherYates(int n)
List<int> list = new List<int>(Enumerable.Range(0, n));
Random rand = new Random();
int swap;
int temp;
for (int i = n - 1; i > 0; i--)
swap = rand.Next(i + 1); //.net rand is not inclusive
if(swap != i) // it can stay in place - if you force a move it is not a uniform shuffle
temp = list[i];
list[i] = list[swap];
list[swap] = temp;
return list;
【讨论】:
已经有一个答案,但它相当冗长,不知道你可以停在 1(不是 0)【参考方案6】:我认为Linear congruential generator 将是最简单的解决方案。
a、c和m只有3个限制价值观
-
m 和 c 是互质的,
a-1 能被 m 的所有素因数整除
a-1 可以被 4 整除,如果 m > 可以被 4 整除
PS 已经提到了该方法,但该帖子对常量值有错误的假设。下面的常量应该适用于您的情况
在您的情况下,您可以使用a = 1002
、c = 757
、m = 1001
X = (1002 * X + 757) mod 1001
【讨论】:
【参考方案7】:当 N 大于 1000 并且您需要抽取 K 个随机样本时,您可以使用包含迄今为止样本的集合。对于每次抽奖,您使用rejection sampling,这将是一个“几乎”O(1) 的操作,因此总运行时间接近 O(K),存储空间为 O(N)。
当 K “接近” N 时,该算法会发生冲突。这意味着运行时间将比 O(K) 差很多。一个简单的解决方法是颠倒逻辑,以便在 K > N/2 时,记录所有尚未抽取的样本。每次抽取都会从拒绝集中删除一个样本。
拒绝抽样的另一个明显问题是它是 O(N) 存储,如果 N 达到数十亿或更多,这是个坏消息。但是,有一种算法可以解决这个问题。这个算法在它的发明者之后被称为维特算法。该算法描述为here。 Vitter 算法的要点是,在每次抽奖后,您使用确保均匀采样的特定分布计算随机跳跃。
【讨论】:
伙计们,拜托了! Fisher-Yates 方法被破坏了。您选择第一个概率为 1/N,第二个概率为 1/(N-1) != 1/N。这是一种有偏差的抽样方法!您确实需要 Vittter 算法来解决偏差。【参考方案8】:此方法适用于限制高且您只想生成几个随机数的情况。
#!/usr/bin/perl
($top, $n) = @ARGV; # generate $n integer numbers in [0, $top)
$last = -1;
for $i (0 .. $n-1)
$range = $top - $n + $i - $last;
$r = 1 - rand(1.0)**(1 / ($n - $i));
$last += int($r * $range + 1);
print "$last ($r)\n";
请注意,数字是按升序生成的,但您可以在之后随机播放。
【讨论】:
由于这会生成组合而不是排列,因此更适合***.com/questions/2394246/… 测试表明这对较小的数字有偏见:(top,n)=(100,10)
的 2M 样本的测量概率为:(0.01047705, 0.01044825, 0.01041225, ..., 0.0088324, 0.008723, 0.00863635)
。我在 Python 中进行了测试,因此数学上的细微差别可能在这里起作用(我确实确保计算 r
的所有操作都是浮点数)。
是的,为了使该方法正常工作,上限必须远大于要提取的值的数量。
即使 “上限 [is] 远大于值的数量”,它也不会“正确”工作。概率仍然是不均匀的,只是幅度较小。【参考方案9】:
假设您想一遍又一遍地检查已洗牌的列表,而不会在每次重新开始洗牌时都出现O(n)
延迟,在这种情况下,我们可以这样做:
创建2个列表A和B,从0到1000,占用2n
空间。
使用 Fisher-Yates 随机播放列表 A,耗时 n
。
绘制数字时,在另一个列表上执行 1 步 Fisher-Yates 洗牌。
当光标在列表末尾时,切换到另一个列表。
预处理
cursor = 0
selector = A
other = B
shuffle(A)
绘画
temp = selector[cursor]
swap(other[cursor], other[random])
if cursor == N
then swap(selector, other); cursor = 0
else cursor = cursor + 1
return temp
【讨论】:
不必保留 2 个列表 - 或在查看之前用完一个列表。 Fisher-Yates 给出了任何初始状态的均匀随机结果。有关说明,请参阅***.com/a/158742/648265。 @ivan_pozdeev 是的,结果相同,但我的想法是通过将随机播放作为绘图动作的一部分来使其摊销 O(1)。 你不明白。在再次随机播放之前,您根本不需要重置列表。改组[1,3,4,5,2]
将产生与改组[1,2,3,4,5]
相同的结果。【参考方案10】:
这里的大多数答案都无法保证它们不会两次返回相同的数字。这是一个正确的解决方案:
int nrrand(void)
static int s = 1;
static int start = -1;
do
s = (s * 1103515245 + 12345) & 1023;
while (s >= 1001);
if (start < 0) start = s;
else if (s == start) abort();
return s;
我不确定约束是否明确指定。假设在 1000 个其他输出之后允许重复一个值,但是天真地允许 0 在 0 之后立即跟随,只要它们都出现在 1000 组的末尾和开头。相反,虽然可以保持距离重复之间有 1000 个其他值,这样做会导致序列每次都以完全相同的方式重播自身,因为没有其他值超出该限制。
这是一个始终保证至少有 500 个其他值的方法,然后才能重复一个值:
int nrrand(void)
static int h[1001];
static int n = -1;
if (n < 0)
int s = 1;
for (int i = 0; i < 1001; i++)
do
s = (s * 1103515245 + 12345) & 1023;
while (s >= 1001);
/* If we used `i` rather than `s` then our early results would be poorly distributed. */
h[i] = s;
n = 0;
int i = rand(500);
if (i != 0)
i = (n + i) % 1001;
int t = h[i];
h[i] = h[n];
h[n] = t;
i = h[n];
n = (n + 1) % 1001;
return i;
【讨论】:
这是一个 LCG,就像 ***.com/a/196164/648265,对于序列以及其他相关的扭结是非随机的。 @ivan_pozdeev mine 比 LCG 更好,因为它确保它不会在第 1001 次调用时返回重复。【参考方案11】:这里有一些示例 COBOL 代码,您可以尝试一下。 我可以将 RANDGEN.exe 文件发送给您,您可以使用它来查看它是否需要您想要的。
IDENTIFICATION DIVISION.
PROGRAM-ID. RANDGEN as "ConsoleApplication2.RANDGEN".
AUTHOR. Myron D Denson.
DATE-COMPILED.
* **************************************************************
* SUBROUTINE TO GENERATE RANDOM NUMBERS THAT ARE GREATER THAN
* ZERO AND LESS OR EQUAL TO THE RANDOM NUMBERS NEEDED WITH NO
* DUPLICATIONS. (CALL "RANDGEN" USING RANDGEN-AREA.)
*
* CALLING PROGRAM MUST HAVE A COMPARABLE LINKAGE SECTION
* AND SET 3 VARIABLES PRIOR TO THE FIRST CALL IN RANDGEN-AREA
*
* FORMULA CYCLES THROUGH EVERY NUMBER OF 2X2 ONLY ONCE.
* RANDOM-NUMBERS FROM 1 TO RANDOM-NUMBERS-NEEDED ARE CREATED
* AND PASSED BACK TO YOU.
*
* RULES TO USE RANDGEN:
*
* RANDOM-NUMBERS-NEEDED > ZERO
*
* COUNT-OF-ACCESSES MUST = ZERO FIRST TIME CALLED.
*
* RANDOM-NUMBER = ZERO, WILL BUILD A SEED FOR YOU
* WHEN COUNT-OF-ACCESSES IS ALSO = 0
*
* RANDOM-NUMBER NOT = ZERO, WILL BE NEXT SEED FOR RANDGEN
* (RANDOM-NUMBER MUST BE <= RANDOM-NUMBERS-NEEDED)
*
* YOU CAN PASS RANDGEN YOUR OWN RANDOM-NUMBER SEED
* THE FIRST TIME YOU USE RANDGEN.
*
* BY PLACING A NUMBER IN RANDOM-NUMBER FIELD
* THAT FOLLOWES THESE SIMPLE RULES:
* IF COUNT-OF-ACCESSES = ZERO AND
* RANDOM-NUMBER > ZERO AND
* RANDOM-NUMBER <= RANDOM-NUMBERS-NEEDED
*
* YOU CAN LET RANDGEN BUILD A SEED FOR YOU
*
* THAT FOLLOWES THESE SIMPLE RULES:
* IF COUNT-OF-ACCESSES = ZERO AND
* RANDOM-NUMBER = ZERO AND
* RANDOM-NUMBER-NEEDED > ZERO
*
* TO INSURING A DIFFERENT PATTERN OF RANDOM NUMBERS
* A LOW-RANGE AND HIGH-RANGE IS USED TO BUILD
* RANDOM NUMBERS.
* COMPUTE LOW-RANGE =
* ((SECONDS * HOURS * MINUTES * MS) / 3).
* A HIGH-RANGE = RANDOM-NUMBERS-NEEDED + LOW-RANGE
* AFTER RANDOM-NUMBER-BUILT IS CREATED
* AND IS BETWEEN LOW AND HIGH RANGE
* RANDUM-NUMBER = RANDOM-NUMBER-BUILT - LOW-RANGE
*
* **************************************************************
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
DATA DIVISION.
FILE SECTION.
WORKING-STORAGE SECTION.
01 WORK-AREA.
05 X2-POWER PIC 9 VALUE 2.
05 2X2 PIC 9(12) VALUE 2 COMP-3.
05 RANDOM-NUMBER-BUILT PIC 9(12) COMP.
05 FIRST-PART PIC 9(12) COMP.
05 WORKING-NUMBER PIC 9(12) COMP.
05 LOW-RANGE PIC 9(12) VALUE ZERO.
05 HIGH-RANGE PIC 9(12) VALUE ZERO.
05 YOU-PROVIDE-SEED PIC X VALUE SPACE.
05 RUN-AGAIN PIC X VALUE SPACE.
05 PAUSE-FOR-A-SECOND PIC X VALUE SPACE.
01 SEED-TIME.
05 HOURS PIC 99.
05 MINUTES PIC 99.
05 SECONDS PIC 99.
05 MS PIC 99.
*
* LINKAGE SECTION.
* Not used during testing
01 RANDGEN-AREA.
05 COUNT-OF-ACCESSES PIC 9(12) VALUE ZERO.
05 RANDOM-NUMBERS-NEEDED PIC 9(12) VALUE ZERO.
05 RANDOM-NUMBER PIC 9(12) VALUE ZERO.
05 RANDOM-MSG PIC X(60) VALUE SPACE.
*
* PROCEDURE DIVISION USING RANDGEN-AREA.
* Not used during testing
*
PROCEDURE DIVISION.
100-RANDGEN-EDIT-HOUSEKEEPING.
MOVE SPACE TO RANDOM-MSG.
IF RANDOM-NUMBERS-NEEDED = ZERO
DISPLAY 'RANDOM-NUMBERS-NEEDED ' NO ADVANCING
ACCEPT RANDOM-NUMBERS-NEEDED.
IF RANDOM-NUMBERS-NEEDED NOT NUMERIC
MOVE 'RANDOM-NUMBERS-NEEDED NOT NUMERIC' TO RANDOM-MSG
GO TO 900-EXIT-RANDGEN.
IF RANDOM-NUMBERS-NEEDED = ZERO
MOVE 'RANDOM-NUMBERS-NEEDED = ZERO' TO RANDOM-MSG
GO TO 900-EXIT-RANDGEN.
IF COUNT-OF-ACCESSES NOT NUMERIC
MOVE 'COUNT-OF-ACCESSES NOT NUMERIC' TO RANDOM-MSG
GO TO 900-EXIT-RANDGEN.
IF COUNT-OF-ACCESSES GREATER THAN RANDOM-NUMBERS-NEEDED
MOVE 'COUNT-OF-ACCESSES > THAT RANDOM-NUMBERS-NEEDED'
TO RANDOM-MSG
GO TO 900-EXIT-RANDGEN.
IF YOU-PROVIDE-SEED = SPACE AND RANDOM-NUMBER = ZERO
DISPLAY 'DO YOU WANT TO PROVIDE SEED Y OR N: '
NO ADVANCING
ACCEPT YOU-PROVIDE-SEED.
IF RANDOM-NUMBER = ZERO AND
(YOU-PROVIDE-SEED = 'Y' OR 'y')
DISPLAY 'ENTER SEED ' NO ADVANCING
ACCEPT RANDOM-NUMBER.
IF RANDOM-NUMBER NOT NUMERIC
MOVE 'RANDOM-NUMBER NOT NUMERIC' TO RANDOM-MSG
GO TO 900-EXIT-RANDGEN.
200-RANDGEN-DATA-HOUSEKEEPING.
MOVE FUNCTION CURRENT-DATE (9:8) TO SEED-TIME.
IF COUNT-OF-ACCESSES = ZERO
COMPUTE LOW-RANGE =
((SECONDS * HOURS * MINUTES * MS) / 3).
COMPUTE RANDOM-NUMBER-BUILT = RANDOM-NUMBER + LOW-RANGE.
COMPUTE HIGH-RANGE = RANDOM-NUMBERS-NEEDED + LOW-RANGE.
MOVE X2-POWER TO 2X2.
300-SET-2X2-DIVISOR.
IF 2X2 < (HIGH-RANGE + 1)
COMPUTE 2X2 = 2X2 * X2-POWER
GO TO 300-SET-2X2-DIVISOR.
* *********************************************************
* IF FIRST TIME THROUGH AND YOU WANT TO BUILD A SEED. *
* *********************************************************
IF COUNT-OF-ACCESSES = ZERO AND RANDOM-NUMBER = ZERO
COMPUTE RANDOM-NUMBER-BUILT =
((SECONDS * HOURS * MINUTES * MS) + HIGH-RANGE).
IF COUNT-OF-ACCESSES = ZERO
DISPLAY 'SEED TIME ' SEED-TIME
' RANDOM-NUMBER-BUILT ' RANDOM-NUMBER-BUILT
' LOW-RANGE ' LOW-RANGE.
* *********************************************
* END OF BUILDING A SEED IF YOU WANTED TO *
* *********************************************
* ***************************************************
* THIS PROCESS IS WHERE THE RANDOM-NUMBER IS BUILT *
* ***************************************************
400-RANDGEN-FORMULA.
COMPUTE FIRST-PART = (5 * RANDOM-NUMBER-BUILT) + 7.
DIVIDE FIRST-PART BY 2X2 GIVING WORKING-NUMBER
REMAINDER RANDOM-NUMBER-BUILT.
IF RANDOM-NUMBER-BUILT > LOW-RANGE AND
RANDOM-NUMBER-BUILT < (HIGH-RANGE + 1)
GO TO 600-RANDGEN-CLEANUP.
GO TO 400-RANDGEN-FORMULA.
* *********************************************
* GOOD RANDOM NUMBER HAS BEEN BUILT *
* *********************************************
600-RANDGEN-CLEANUP.
ADD 1 TO COUNT-OF-ACCESSES.
COMPUTE RANDOM-NUMBER =
RANDOM-NUMBER-BUILT - LOW-RANGE.
* *******************************************************
* THE NEXT 3 LINE OF CODE ARE FOR TESTING ON CONSOLE *
* *******************************************************
DISPLAY RANDOM-NUMBER.
IF COUNT-OF-ACCESSES < RANDOM-NUMBERS-NEEDED
GO TO 100-RANDGEN-EDIT-HOUSEKEEPING.
900-EXIT-RANDGEN.
IF RANDOM-MSG NOT = SPACE
DISPLAY 'RANDOM-MSG: ' RANDOM-MSG.
MOVE ZERO TO COUNT-OF-ACCESSES RANDOM-NUMBERS-NEEDED RANDOM-NUMBER.
MOVE SPACE TO YOU-PROVIDE-SEED RUN-AGAIN.
DISPLAY 'RUN AGAIN Y OR N '
NO ADVANCING.
ACCEPT RUN-AGAIN.
IF (RUN-AGAIN = 'Y' OR 'y')
GO TO 100-RANDGEN-EDIT-HOUSEKEEPING.
ACCEPT PAUSE-FOR-A-SECOND.
GOBACK.
【讨论】:
我不知道这是否真的可以满足 OPs 的需求,但是支持 COBOL 贡献!【参考方案12】:你可以这样做:
-
创建一个列表,0..1000。
随机播放列表。 (请参阅Fisher-Yates shuffle 了解执行此操作的好方法。)
从打乱的列表中按顺序返回数字。
所以这不需要每次都搜索旧值,但它仍然需要 O(N) 进行初始洗牌。但正如 Nils 在 cmets 中指出的那样,这是摊销 O(1)。
【讨论】:
@Just Some Guy N = 1000,所以你说它是 O(N/N),即 O(1) 如果每次插入洗牌后的数组都是一次操作,那么插入1个值后,可以得到1个随机值。 2 代表 2 个值,以此类推,n 代表 n 个值。生成列表需要 n 次操作,所以整个算法是 O(n)。如果需要 1,000,000 个随机值,则需要 1,000,000 个操作 这样想,如果是常数时间,10 个随机数和 100 亿个随机数所用的时间是一样的。但是由于洗牌需要 O(n),我们知道这不是真的。 这实际上需要分摊时间 O(log n),因为您需要生成 n lg n 个随机位。 现在,我有理由这样做了! meta.***.com/q/252503/13【参考方案13】:public static int[] randN(int n, int min, int max)
if (max <= min)
throw new ArgumentException("Max need to be greater than Min");
if (max - min < n)
throw new ArgumentException("Range needs to be longer than N");
var r = new Random();
HashSet<int> set = new HashSet<int>();
while (set.Count < n)
var i = r.Next(max - min) + min;
if (!set.Contains(i))
set.Add(i);
return set.ToArray();
根据需要,N 个非重复随机数的复杂度为 O(n)。 注意:Random 应该是静态的,并且应用了线程安全。
【讨论】:
O(n^2),因为重试次数平均与目前选择的元素数量成正比。 想一想,如果选择 min=0 max=10000000 和 N=5,无论选择多少都重试~=0。但是,是的,您有一个观点,如果 max-min 很小,o(N) 就会崩溃。 如果 N 这不是 O(n)。每次集合都包含值 this is 和额外的循环。【参考方案14】:这是我使用第一个解决方案的逻辑键入的一些代码。我知道这是“语言不可知论”,但只是想在 C# 中将其作为示例呈现,以防有人正在寻找快速实用的解决方案。
// Initialize variables
Random RandomClass = new Random();
int RandArrayNum;
int MaxNumber = 10;
int LastNumInArray;
int PickedNumInArray;
int[] OrderedArray = new int[MaxNumber]; // Ordered Array - set
int[] ShuffledArray = new int[MaxNumber]; // Shuffled Array - not set
// Populate the Ordered Array
for (int i = 0; i < MaxNumber; i++)
OrderedArray[i] = i;
listBox1.Items.Add(OrderedArray[i]);
// Execute the Shuffle
for (int i = MaxNumber - 1; i > 0; i--)
RandArrayNum = RandomClass.Next(i + 1); // Save random #
ShuffledArray[i] = OrderedArray[RandArrayNum]; // Populting the array in reverse
LastNumInArray = OrderedArray[i]; // Save Last Number in Test array
PickedNumInArray = OrderedArray[RandArrayNum]; // Save Picked Random #
OrderedArray[i] = PickedNumInArray; // The number is now moved to the back end
OrderedArray[RandArrayNum] = LastNumInArray; // The picked number is moved into position
for (int i = 0; i < MaxNumber; i++)
listBox2.Items.Add(ShuffledArray[i]);
【讨论】:
【参考方案15】:你可以使用我这里描述的Xincrol算法:
http://openpatent.blogspot.co.il/2013/04/xincrol-unique-and-random-number.html
这是一种纯算法方法,无需数组、列表、排列或繁重的 CPU 负载即可生成随机但唯一的数字。
最新版本还允许设置数字范围,例如,如果我想要 0-1073741821 范围内的唯一随机数。
我已经实际使用它了
随机播放每首歌曲的 MP3 播放器,但每个专辑/目录仅播放一次 像素级视频帧溶解效果(快速流畅) 为签名和标记(隐写术)在图像上创建一个秘密的“噪声”雾 用于通过数据库序列化大量 Java 对象的数据对象 ID 三重多数内存位保护 地址+值加密(每个字节不仅被加密,而且被移动到缓冲区中一个新的加密位置)。这真的让密码分析人员对我很生气:-) SMS、电子邮件等的纯文本到类似地穴的纯文本加密。 我的德州扑克计算器 (THC) 我的几款游戏用于模拟、“洗牌”、排名 更多它是开放的,免费的。试试看吧……
【讨论】:
该方法是否适用于十进制值,例如将 3 位十进制计数器打乱以始终得到 3 位十进制结果? 作为Xorshift算法的一个例子,它是一个LFSR,具有所有相关的扭结(例如,序列中超过k
的值永远不会同时出现)。【参考方案16】:
用值 0-1000 初始化一个包含 1001 个整数的数组,并将变量 max 设置为数组的当前最大索引(从 1000 开始)。选择一个介于 0 和 max 之间的随机数 r,将位置 r 处的数字与位置 max 处的数字交换,然后返回位置 max 处的数字。将 max 减 1 并继续。当 max 为 0 时,将 max 设置回数组的大小 - 1 并重新开始,无需重新初始化数组。
更新: 虽然我在回答这个问题时自己想出了这个方法,但经过一些研究,我意识到这是 Fisher-Yates 的修改版本,称为 Durstenfeld-Fisher-Yates 或 Knuth-Fisher-Yates。由于描述可能有点难以理解,我在下面提供了一个示例(使用 11 个元素而不是 1001 个):
数组从 11 个元素开始,初始化为 array[n] = n,最大值从 10 开始:
+--+--+--+--+--+--+--+--+--+--+--+
| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|
+--+--+--+--+--+--+--+--+--+--+--+
^
max
每次迭代,在0和max之间选择一个随机数r,array[r]和array[max]交换,返回新的array[max],max递减:
max = 10, r = 3
+--------------------+
v v
+--+--+--+--+--+--+--+--+--+--+--+
| 0| 1| 2|10| 4| 5| 6| 7| 8| 9| 3|
+--+--+--+--+--+--+--+--+--+--+--+
max = 9, r = 7
+-----+
v v
+--+--+--+--+--+--+--+--+--+--+--+
| 0| 1| 2|10| 4| 5| 6| 9| 8| 7: 3|
+--+--+--+--+--+--+--+--+--+--+--+
max = 8, r = 1
+--------------------+
v v
+--+--+--+--+--+--+--+--+--+--+--+
| 0| 8| 2|10| 4| 5| 6| 9| 1: 7| 3|
+--+--+--+--+--+--+--+--+--+--+--+
max = 7, r = 5
+-----+
v v
+--+--+--+--+--+--+--+--+--+--+--+
| 0| 8| 2|10| 4| 9| 6| 5: 1| 7| 3|
+--+--+--+--+--+--+--+--+--+--+--+
...
经过11次迭代,数组中的所有数字都被选中,max == 0,数组元素被打乱:
+--+--+--+--+--+--+--+--+--+--+--+
| 4|10| 8| 6| 2| 0| 9| 5| 1| 7| 3|
+--+--+--+--+--+--+--+--+--+--+--+
此时max可以重置为10,进程可以继续进行。
【讨论】:
杰夫关于洗牌的帖子表明这不会返回好的随机数。codinghorror.com/blog/archives/001015.html @Peter Rounce:我不这么认为;这在我看来就像 Fisher Yates 算法,也在 Jeff 的帖子中引用(作为好人)。 @robert:我只是想指出它不会像问题的名称那样产生“O(1) 中的唯一随机数”。 @mikera:同意,尽管从技术上讲,如果您使用固定大小的整数,则可以在 O(1) 中生成整个列表(具有较大的常数,即 2^32)。此外,出于实际目的,“随机”的定义很重要——如果你真的想使用系统的熵池,限制是随机位的计算而不是计算本身,在这种情况下 n log n 是相关的再次。但在你可能会使用(相当于)/dev/urandom 而不是 /dev/random 的情况下,你会回到“实际上”O(n)。 我有点困惑,你必须执行N
迭代(在本例中为11)才能获得所需的结果,这是否意味着它是O(n)
?因为您需要进行 N
迭代才能从相同的初始状态获得 N!
组合,否则您的输出将只是 N 个状态之一。【参考方案17】:
对于像 0...1000 这样的小数字,创建一个包含所有数字的列表并将其改组非常简单。但是,如果要从中提取的数字集非常大,还有另一种优雅的方法:您可以使用密钥和加密哈希函数构建伪随机排列。请参阅以下 C++-ish 示例伪代码:
unsigned randperm(string key, unsigned bits, unsigned index)
unsigned half1 = bits / 2;
unsigned half2 = (bits+1) / 2;
unsigned mask1 = (1 << half1) - 1;
unsigned mask2 = (1 << half2) - 1;
for (int round=0; round<5; ++round)
unsigned temp = (index >> half1);
temp = (temp << 4) + round;
index ^= hash( key + "/" + int2str(temp) ) & mask1;
index = ((index & mask2) << half1) | ((index >> half2) & mask1);
return index;
这里,hash
只是一些任意的伪随机函数,它将一个字符串映射到一个可能很大的无符号整数。函数 randperm
是 0...pow(2,bits)-1 内所有数字的排列,假设一个固定的键。这源于构造,因为更改变量index
的每一步都是可逆的。这是受到Feistel cipher 的启发。
【讨论】:
与***.com/a/16097246/648265 相同,序列的随机性失败。 @ivan_pozdeev:理论上,假设计算能力无限,是的。然而,假设上面代码中使用的hash()
是一个安全的伪随机函数,这个构造将可证明(Luby & Rackoff,1988)产生一个pseudorandom permutation,它不能与使用显着更少的真正随机洗牌区分开来比对整个密钥空间的详尽搜索要付出更多的努力,而整个密钥空间是密钥长度的指数。即使对于大小合理的密钥(例如 128 位),这也超出了地球上可用的总计算能力。
(顺便说一句,为了让这个论点更严格一点,我更愿意用HMAC 替换上面的临时hash( key + "/" + int2str(temp) )
构造,其安全性反过来可以证明可以降低到底层哈希压缩函数。此外,使用 HMAC 可能会降低某人错误地尝试将此构造与不安全的非加密哈希函数一起使用的可能性。)【参考方案18】:
您可以使用具有 10 位的良好 pseudo-random number generator,然后丢弃 1001 到 1023,留下 0 到 1000。
我们从here 获得了 10 位 PRNG 的设计..
10 位,反馈多项式 x^10 + x^7 + 1(周期 1023)
使用 Galois LFSR 获得快速代码
【讨论】:
@Phob 不,这不会发生,因为基于线性反馈移位寄存器的 10 位 PRNG 通常是由一个假设所有值(一个除外)的构造制成,然后返回第一个价值。换句话说,它只会在一个循环中准确地选择 1001 一次。 @Phob 这个问题的重点是每个数字只选择一次。然后你抱怨 1001 不会连续出现两次?具有最佳传播的 LFSR 将以伪随机方式遍历其空间中的所有数字,然后重新开始循环。换句话说,它不用作通常的随机函数。当用作随机数时,我们通常只使用比特的一个子集。稍微了解一下,很快就会明白。 唯一的问题是给定的 LFSR 只有一个序列,因此在选择的数字之间具有很强的相关性 - 特别是不会生成所有可能的组合。【参考方案19】:你甚至不需要一个数组来解决这个问题。
您需要一个位掩码和一个计数器。
将计数器初始化为零并在连续调用时将其递增。将计数器与位掩码(在启动时随机选择或固定)进行异或运算以生成伪随机数。如果不能有超过 1000 的数字,请不要使用超过 9 位的位掩码。 (换句话说,位掩码是一个不大于 511 的整数。)
确保当计数器超过 1000 时,将其重置为零。此时,您可以选择另一个随机位掩码(如果您愿意)以不同的顺序生成相同的一组数字。
【讨论】:
这比 LFSR 骗的人少。 "bitmask" 512...1023 以内也可以。如需更多虚假随机性,请参阅我的答案。 :-) 本质上等同于***.com/a/16097246/648265,但序列的随机性也失败了。【参考方案20】:使用Maximal Linear Feedback Shift Register。
它可以在几行 C 中实现,并且在运行时只做几个测试/分支、一点加法和位移。这不是随机的,但它欺骗了大多数人。
【讨论】:
“这不是随机的,但它会愚弄大多数人”。这适用于所有伪随机数生成器以及该问题的所有可行答案。但大多数人不会考虑。所以省略这个注释可能会导致更多的赞成...... @bobobobo:O(1) 内存就是原因。 Nit:它是 O(log N) 内存。 使用该方法,你如何生成数字,比如说 0 到 800000 之间?有些人可能会使用周期为 1048575 (2^20 - 1) 的 LFSR,如果数字超出范围,则获取下一个,但这不会有效。 作为 LFSR,这不会产生均匀分布的序列:将生成的整个序列由第一个元素定义。【参考方案21】:您可以使用 A Linear Congruential Generator。其中m
(模数)将是大于 1000 的最接近的素数。当您得到一个超出范围的数字时,只需获取下一个。该序列只会在所有元素都发生后重复,您不必使用表格。但请注意此生成器的缺点(包括缺乏随机性)。
【讨论】:
1009 是 1000 之后的第一个素数。 一个LCG在连续数字之间具有高度相关性,因此组合在很大程度上不会是完全随机的(例如,序列中距离大于k
的数字永远不会一起出现) .
m 应该是元素的数量 1001(1000 + 1 表示零),您可以使用 Next = (1002 * Current + 757) mod 1001;【参考方案22】:
另一种可能性:
您可以使用一组标志。当它已经被选中时,再拿下一个。
但是,请注意,调用 1000 次后,该功能将永远不会结束,因此您必须做好安全措施。
【讨论】:
这是 O(k^2),平均而言,附加步骤的数量与迄今为止选择的值的数量成正比。以上是关于O(1)中的唯一(非重复)随机数?的主要内容,如果未能解决你的问题,请参考以下文章