在.NET

时间:2016-01-30 12:59:01

标签: c# .net algorithm random int

在.NET中是否有一种方法可以按随机顺序生成一个全部 32位整数(Int32)的序列,而不会重复,并且以内存效率的方式生成?内存效率意味着最多只能使用几百兆字节的主内存。

理想情况下,序列应该类似于IEnumerable<int>,并且只有在请求时才会延迟返回下一个数字。

我做了一些快速研究,我发现了一些部分解决方案:

还有另一种方法来看待这个问题 - 也许利用固定的价值范围 - 这将提供满足内存要求的解决方案吗?也许.NET类库带有一些有用的东西?

更新1

感谢大家对解决方案的深刻见解和创意建议。我将尝试尽快实施和测试(正确性和内存效率)这里提出的2或3个最有希望的解决方案,发布结果然后选择一个&#34;赢家&#34;。

更新2

我尝试在comment below中实施hvd的建议。我尝试使用.NET中的BitArray和我的自定义实现,因为.NET只限于int.MaxValue条目,因此不足以覆盖整个整数范围。

我喜欢这个想法的简单性,我愿意&#34;牺牲&#34;那些512 MB的内存,如果它工作正常。不幸的是,运行时间非常慢,花费数十秒来生成我的机器上的下一个随机数,该机器具有3.5 GHz Core i7 CPU。所以不幸的是,如果要求生成许多随机数,这是不可接受的。我猜它是可预测的,如果我没有弄错,它是一个O(M x N)算法,其中N是2 ^ 32而M是请求的整数的数量,所以那些迭代承担费用。

理想情况下,我想在O(1)时间内生成下一个随机数,同时仍满足内存要求,这里建议的下一个算法可能适用于此。我会尽快给他们试一试。

更新3

我刚刚测试了Linear Congruential Generator,我可以说我对结果非常满意。对于这个主题中的赢家来说,它看起来是一个强有力的竞争者。

正确性:所有整数只生成一次(我使用了一个位向量来检查)。

随机性:相当不错。

内存使用:非常好,只需几个字节。

运行时间:非常快速地生成下一个随机整数,正如您可以从O(1)算法中获得的那样。生成每个整数总共花费大约。我机器上11秒钟。

总而言之,如果你不是在寻找高度随机化的序列,我认为这是一种非常合适的技术。

更新4

下面描述的模块化multiplicative inverse technique与LCG技术的行为非常相似 - 这并不奇怪,因为两者都是基于模运算 - 虽然我发现它实现起来不那么简单,以便产生令人满意的随机序列。

我发现一个有趣的区别是这种技术似乎比LCG更快:生成整个序列需要大约8秒,而LCG则需要11秒。除此之外,关于内存效率,正确性和随机性的所有其他评论都是相同的。

更新5

看起来用户TomTom在没有通知的情况下删除了他们的答案,我在评论中指出我发现它比所需的更快地生成重复的数字。所以我想这完全排除了Mersenne Twister。

更新6

我测试了另一种看似有希望的建议技术Skip32,虽然我真的很喜欢随机数的质量,但算法不适合在可接受的时间内生成整个整数范围。不幸的是,与能够完成该过程的其他技术相比,它不足。顺便提一句,我使用了here中的C#实现 - 我更改了代码以将轮数减少到1,但它仍然无法及时完成。

毕竟,根据上述结果判断,我个人对解决方案的选择是 modular multiplicative inverses 技术,紧随其后的是linear congruential generator。有些人可能会说这在某些方面比其他技术要低,但鉴于我原来的限制,我认为它最适合他们。

8 个答案:

答案 0 :(得分:12)

如果您不需要随机数加密,则可以使用Linear Congruential Generator

LCG是X_n + 1 = X_n * a + c(mod m)形式的公式,它需要每个生成数字的常量记忆和恒定时间。
如果选择了适当的LCG值,它将具有一个完整的周期长度,这意味着它将输出介于0和您选择的模数之间的每个数字。

当且仅在以下情况下,LCG有一个完整的期间:

  • 模量和增量是相对素数,即GCD(m, c) = 1
  • a - 1可被m
  • 的所有素数因子整除
  • 如果m可被4整除,则a - 1必须可被4整除。

我们的模数为2 ^ 32,意味着a必须是4k + 1形式的数字,其中k是任意整数,c不能被2整除。 / p>

虽然这是一个C#问题,但我编写了一个小型C ++程序来测试这个解决方案的速度,因为我对这种语言感觉更舒服:

#include <iostream>
#include <stdlib.h>

class lcg {
private:
    unsigned a, c, val;
public:
    lcg(unsigned seed=0) : lcg(seed, rand() * 4 + 1, rand() * 2 + 1) {}
    lcg(unsigned seed, unsigned a, unsigned c) {
        val = seed;
        this->a = a;
        this->c = c;
        std::cout << "Initiated LCG with seed " << seed << "; a = " << a << "; c = " << c << std::endl;
    }

    unsigned next() {
        this->val = a * this->val + c;
        return this->val;
    }
};

int main() {
    srand(time(NULL));
    unsigned seed = rand();
    int dummy = 0;
    lcg gen(seed);
    time_t t = time(NULL);
    for (uint64_t i = 0; i < 0x100000000ULL; i++) {
        if (gen.next() < 1000) dummy++; // Avoid optimizing this out with -O2
    }
    std::cout << "Finished cycling through. Took " << (time(NULL) - t) << " seconds." << std::endl;
    if (dummy > 0) return 0;
    return 1;
}

你可能会注意到我没有在lcg类的任何地方使用模数运算,那是因为我们使用32位整数溢出来进行模运算。
这将生成[0, 4294967295]范围内的所有值 我还必须为编译器添加一个虚拟变量,以便不优化所有内容 在没有优化的情况下,此解决方案在大约15秒内完成,而使用-O2时,在5秒内完成适度优化。

如果“真实”随机性不是问题,这是一个非常快速的解决方案。

答案 1 :(得分:8)

  

.NET中有没有办法

实际上,这可以用大多数语言来完成

  

生成所有32位整数(Int32)的序列

  

按随机顺序,

这里我们需要就术语达成一致,因为&#34;随机&#34;不是大多数人认为的那样。稍等一下。

  

不重复,

  

并以记忆效率的方式?

  

内存效率意味着最多只能使用几百兆字节的主内存。

好的,几乎没有记忆可以接受吗? ; - )

在得到建议之前,我们需要澄清&#34;随机性&#34;的问题。真正随机的东西没有明显的模式。因此,连续数百万次运行算法可能理论上在所有迭代中返回相同的值。如果你抛出&#34的概念;必须与先前的迭代&#34;不同,那么它就不再是随机的。然而,综合考虑所有要求,似乎所有真正被要求的是整数分布的不同模式&#34;。这是可行的。

那么如何有效地做到这一点?利用Modular multiplicative inverses。我用它来回答以下问题,该问题在某些范围内生成非重复的伪随机样本数据的要求类似:

Generate different random time in the given interval

我首先在这里了解了这个概念(generate seemingly random unique numeric ID in SQL Server),您可以使用以下任一在线计算器来确定您的&#34;整数&#34;和&#34;模块化乘法逆(MMI)&#34;值:

在此处应用该概念,您将使用Int32.MaxSize作为Modulo值。

这会给出随机分布的明确外观,不会发生冲突,也不需要内存来存储已使用的值。

唯一的初始问题是,在相同的情况下,分配模式总是相同的&#34;整数&#34;和&#34; MMI&#34;值。所以,你可以通过添加&#34;随机&#34;来提出不同的模式。生成Int到起始值(我相信我在关于在SQL Server中生成示例数据的答案中做了),或者您可以预先生成&#34; Integer&#34;的几个组合。和相应的&#34; MMI&#34;值,将它们存储在配置文件/字典中,并使用.NET随机函数在每次运行开始时选择一个。即使您存储了100种组合,也几乎没有内存使用(假设它不在配置文件中)。实际上,如果同时存储Int和字典使用Int作为索引,那么1000个值大约是12k?

<强>更新

注意:

  • 结果中有一个模式,但是除非你在任何特定时刻有足够的模式来查看结果,否则它是不可辨别的。对于大多数用例,这是可以接受的,因为没有值的接收者会有大量的集合,或者知道它们是按顺序分配的,没有任何间隙(并且需要知识才能确定是否存在模式)
  • 两个变量值中只有一个 - &#34;整数&#34;和&#34;模块化乘法逆(MMI)&#34; - 特定运行的公式中需要。因此:
    • 每对给出两个不同的序列
    • 如果在内存中维护一个集合,只需要一个简单的数组,并假设数组索引只是内存中与数组基址的偏移量,那么所需的内存应该只有4个字节*容量(即1024个选项只有4k,对吗?)

这是一些测试代码。它是用Microsoft SQL Server的T-SQL编写的,因为这是我主要工作的地方,它还具有使其真正易于测试唯一性,最小值和最大值等的优点,而无需编译任何东西。语法适用于SQL Server 2008或更高版本。对于SQL Server 2005,尚未引入变量初始化,因此包含DECLARE的每个=只需要自己分隔为DECLARESET @Variable = ...但是,该变量正在初始化。 SET @Index += 1;需要成为SET @Index = @Index + 1;

如果提供产生任何重复项的值,则测试代码将出错。最后的查询表明是否存在任何差距,因为可以推断如果表变量填充没有错误(因此没有重复),值的总数是预期的数量,那么如果实际MIN和MAX值中的任何一个或两个都超出预期值,则只能是间隙(即缺失值)。

请注意,此测试代码并不暗示任何值是预先生成的或需要存储的。代码仅存储值以测试唯一性和最小/最大值。在实践中,所需要的只是简单的公式,而传递给它的所有内容都是:

  • 容量(虽然在这种情况下也可以硬编码)
  • MMI /整数值
  • 当前&#34;索引&#34;

所以你只需要维持2到3个简单值。

DECLARE @TotalCapacity INT = 30; -- Modulo; -5 to +4 = 10 OR Int32.MinValue
                                 -- to Int32.MaxValue = (UInt32.MaxValue + 1)
DECLARE @MMI INT = 7; -- Modular Multiplicative Inverse (MMI) or
                      -- Integer (derived from @TotalCapacity)

DECLARE @Offset INT = 0; -- needs to stay at 0 if min and max values are hard-set
-----------
DECLARE @Index INT = (1 + @Offset); -- start

DECLARE @EnsureUnique TABLE ([OrderNum] INT NOT NULL IDENTITY(1, 1),
                             [Value] INT NOT NULL UNIQUE);
SET NOCOUNT ON;

BEGIN TRY
    WHILE (@Index < (@TotalCapacity + 1 + @Offset)) -- range + 1
    BEGIN
        INSERT INTO @EnsureUnique ([Value]) VALUES (
                 ((@Index * @MMI) % @TotalCapacity) - (@TotalCapacity / 2) + @Offset
                                                   );
        SET @Index += 1;
    END;
END TRY
BEGIN CATCH
    DECLARE @Error NVARCHAR(4000) = ERROR_MESSAGE();
    RAISERROR(@Error, 16, 1);
    RETURN;
END CATCH;

SELECT * FROM @EnsureUnique ORDER BY [OrderNum] ASC;
SELECT COUNT(*) AS [TotalValues],
       @TotalCapacity AS [ExpectedCapacity],
       MIN([Value]) AS [MinValue],
       (@TotalCapacity / -2) AS [ExpectedMinValue],
       MAX([Value]) AS [MaxValue],
       (@TotalCapacity / 2) - 1 AS [ExpectedMaxValue]
FROM   @EnsureUnique;

答案 2 :(得分:3)

点击率模式下的32位PRP似乎是我唯一可行的方法(您的第四种变体)。

你可以

  • 使用专用的32位分组密码。

    Skip32,Skipjack的32位变体是一个受欢迎的选择。

    作为质量/安全性与性能之间的权衡,您可以根据需要调整轮数。更多轮更慢但更安全。

  • 长度保留加密(格式保留加密的特例)

    FFX模式是典型的建议。但是在其典型的实例化中(例如,使用AES作为底层密码),它将比专用的32位分组密码慢很多

请注意,其中许多结构都有一个重大缺陷:它们甚至是排列。这意味着一旦你看到2 ^ 32-2输出,你就能够确定地预测倒数第二个输出,而不是只有50%。我认为Rogaways AEZ论文提到了解决这个缺陷的方法。

答案 3 :(得分:2)

我要在这个答案前言,说我意识到其他一些答案会更加优雅,并且可能比这个更适合你的需求。对于这个问题,这肯定是一种蛮力的方法。

如果获得真正随机的*(或伪随机*足以用于加密目的)很重要,您可以提前生成所有整数的列表,并将它们全部以随机顺序存储在磁盘上。在程序运行时,您可以从磁盘中读取这些数字。

以下是我建议生成这些数字的算法的基本概要。所有32位整数都可以存储在~16 GiB的磁盘空间中(32位= 4字节,4字节/整数* 2 ^ 32整数= 2 ^ 34字节= 16 GiB,加上OS /文件系统需要的任何开销),而且我已经采取了几百兆&#34;表示您希望一次读取不超过256 MiB的文件。

  1. 生成16 GiB / 256 MiB = 64个ASCII文本文件,256 MiB&#34; null&#34;每个字符(所有位设置为0)。命名每个文本文件&#34; 0.txt&#34;通过&#34; 64.txt&#34;
  2. 从Int32.MinValue到Int32.MaxValue顺序循环,跳过0.这是您当前正在存储的整数的值。
  3. 在每次迭代中,从您选择的随机性源(硬件真随机生成器,伪随机算法,等等)生成从0到UInt32.MaxValue的随机整数。这是您当前正在存储的值的索引。
  4. 将索引拆分为两个整数:6个最高有效位,其余为26.使用高位加载相应的文本文件。
  5. 将低26位乘以4并将其用作打开文件中的索引。如果该索引后面的四个字节仍然是&#34; null&#34;字符,将当前值编码为四个ASCII字符,并将这些字符存储在该位置。如果他们不是全部&#34; null&#34;性格,回到第3步。
  6. 重复直到所有整数都已存储。
  7. 这将确保数字来自已知的随机来源但仍然是唯一的,而不是具有其他一些提议的解决方案的限制。这需要很长时间来编译&#34; (特别是使用上面相对天真的算法),但它符合运行时效率要求。

    在运行时,您现在可以生成随机起始索引,然后按顺序读取文件中的字节以获得唯一的,随机*,非重复的整数序列。假设您一次使用相对较少的整数,您甚至可以随机索引到文件中,存储您使用过的索引并确保数字不会以这种方式重复。

    (*我理解通过强加&#34;唯一性&#34;约束来减少任何来源的随机性,但这种方法应该产生与原始来源相对接近的数字)

    TL; DR - 提前对整数进行随机播放,将所有这些整数存储在磁盘上的许多较小的文件中,然后在运行时根据需要从文件中读取。

答案 4 :(得分:1)

由于您的定义中的数字应该是随机,因此根据定义,除了存储所有数据之外没有其他方式,因为数字彼此之间没有内在关系。 因此,这意味着您必须存储您使用的所有值,以防止再次使用它们。

然而,在计算中没有真正的随机性。通常,系统通过执行具有巨大预定值和定时器值的乘法运算来计算随机数,使得它们超出存储器限制并因此随机选择。所以要么你使用你的第三个选项,要么你必须考虑生成这些伪随机数,你可以重现生成的每个数字的序列,并检查是否有重新复制的东西。这显然在计算上非常昂贵,但是你要求内存效率。

因此,您可以存储随机生成器所使用的数字以及您生成的元素数。每次需要一个新数字时,重新设置生成器并迭代生成的元素数量+ 1.这是您的新数字。现在再次重新设定并重复遍历序列以检查它是否发生过。

这样的事情:

int seed = 123;
Int64 counter = 0;
Random rnd = new Random(seed);

int GetUniqueRandom()
{
    int newNumber = rnd.Next();
    Random rndCheck = new Random(seed);

    counter++;

    for (int j = 0; j < counter; j++)
    {
        int checkNumber = rndCheck.Next();

        if (checkNumber == newNumber)
            return GetUniqueRandom();
    }

    return newNumber;        
}

编辑:有人指出counter会达到一个巨大的价值,并且在你获得所有40亿个价值之前,它是否会溢出无法确定。

考虑到这一点,递归调用也不适用于此,因为它几乎肯定会导致堆栈溢出(并且不必要地占用大量内存) - 但我只是想给你一般的想法。

答案 5 :(得分:1)

很好的谜题。我想到了一些事情:

  • 我们需要存储已使用的项目。如果大约足够好,您可能需要使用布隆过滤器。但是,由于您明确声明您想要所有数字,因此只有一个数据结构:位向量。
  • 您可能希望使用长周期的伪随机生成器算法。
  • 解决方案可能涉及使用多种算法。

我的第一次尝试是弄清楚伪随机数生成如何与简单的位向量一起工作。我接受碰撞(因此减速),但绝对没有太多的碰撞。这个简单的算法将在有限的时间内为您生成大约一半的数字。

static ulong xorshift64star(ulong x)
{
    x ^= x >> 12; // a
    x ^= x << 25; // b
    x ^= x >> 27; // c

    return x * 2685821657736338717ul;
}

static void Main(string[] args)
{
    byte[] buf = new byte[512 * 1024 * 1024];
    Random rnd = new Random();

    ulong value = (uint)rnd.Next(int.MinValue, int.MaxValue);
    long collisions = 0;

    Stopwatch sw = Stopwatch.StartNew();

    for (long i = 0; i < uint.MaxValue; ++i)
    {
        if ((i % 1000000) == 0)
        {
            Console.WriteLine("{0} random in {1:0.00}s (c={2})", i, sw.Elapsed.TotalSeconds, collisions - 1000000);
            collisions = 0;
        }

        uint randomValue; // result will be stored here
        bool collision;

        do
        {
            value = xorshift64star(value);
            randomValue = (uint)value;

            collision = (buf[randomValue >> 4] & (1 << (int)(randomValue & 7))) != 0;
            ++collisions;
        }
        while (collision);

        buf[randomValue >> 4] |= (byte)(1 << (int)(randomValue & 7));
    }

    Console.ReadLine();
}

在大约19亿随机数后,算法将开始停止运转。

1953000000随机发生在283.74s(c = 10005932) [...] 430.6000000随机在430.66s(c = 52837678)

所以,为了论证,让我们说你将把这个算法用于前+/- 20亿个数字。

接下来,你需要一个解决方案,这基本上是OP描述的问题。为此,我将随机数采样到缓冲区中,并将缓冲区与Knuth shuffle算法结合起来。如果您愿意,也可以从一开始就使用此功能。

这就是我想出来的(可能还有马车,所以要测试......):

static void Main(string[] args)
{
    Random rnd = new Random();

    byte[] bloom = new byte[512 * 1024 * 1024];
    uint[] randomBuffer = new uint[1024 * 1024];

    ulong value = (uint)rnd.Next(int.MinValue, int.MaxValue);
    long collisions = 0;

    Stopwatch sw = Stopwatch.StartNew();
    int n = 0;

    for (long i = 0; i < uint.MaxValue; i += n)
    {
        // Rebuild the buffer. We know that we have uint.MaxValue-i entries left and that we have a 
        // buffer of 1M size. Let's calculate the chance that you want any available number in your 
        // buffer, which is now:

        double total = uint.MaxValue - i;
        double prob = ((double)randomBuffer.Length) / total;

        if (i >= uint.MaxValue - randomBuffer.Length)
        {
            prob = 1; // always a match.
        }

        uint threshold = (uint)(prob * uint.MaxValue);
        n = 0;

        for (long j = 0; j < uint.MaxValue && n < randomBuffer.Length; ++j)
        {
            // is it available? Let's shift so we get '0' (unavailable) or '1' (available)
            int available = 1 ^ ((bloom[j >> 4] >> (int)(j & 7)) & 1);

            // use the xorshift algorithm to generate a random value:
            value = xorshift64star(value);

            // roll a die for this number. If we match the probability check, add it.
            if (((uint)value) <= threshold * available)
            {
                // Store this in the buffer
                randomBuffer[n++] = (uint)j;

                // Ensure we don't encounter this thing again in the future
                bloom[j >> 4] |= (byte)(1 << (int)(j & 7));
            }
        }

        // Our buffer now has N random values, ready to be emitted. However, it's 
        // still sorted, which is something we don't want. 
        for (int j = 0; j < n; ++j)
        {
            // Grab index to swap. We can do this with Xorshift, but I didn't bother.
            int index = rnd.Next(j, n);

            // Swap
            var tmp = randomBuffer[j];
            randomBuffer[j] = randomBuffer[index];
            randomBuffer[index] = tmp;
        }

        for (int j = 0; j < n; ++j)
        {
            uint randomNumber = randomBuffer[j];
            // Do something with random number buffer[i]
        }

        Console.WriteLine("{0} random in {1:0.00}s", i, sw.Elapsed.TotalSeconds);
    }

    Console.ReadLine();
}

回到要求:

  

在.NET中是否有办法以随机顺序生成所有32位整数(Int32)的序列,而不是重复,并且以内存效率的方式生成?内存效率意味着最多只能使用几百兆字节的主内存。

费用:512 MB + 4 MB。 重复:无。

它非常快。它根本就不是“统一的”。快速。每100万个数字,你必须重新计算缓冲区。

什么也很好:两种算法都可以一起工作,所以你可以非常快地生成第一个--20亿个数字,然后使用第二个算法。

答案 6 :(得分:1)

最简单的解决方案之一是在计数器中使用像AES这样的块加密算法。你需要一个与AES中的密钥相等的种子。接下来,您需要一个计数器,该计数器针对每个新的随机值递增。随机值是用密钥加密计数器的结果。由于明文(计数器)和随机数(密文)是双向的,并且由于鸽子孔原理,随机数是唯一的(对于块大小)。

内存效率:您只需要存储种子和计数器。

唯一的限制是AES具有128位块大小而不是32位。因此,您可能需要增加到128位或找到32位块大小的块密码。

对于你的IEnumerable,你可以编写一个包装器。该指数是柜台。

免责声明:您要求非重复/唯一:这取消了随机资格,因为通常您应该看到随机数的冲突。因此,您不应该长时间使用它。另请参阅https://crypto.stackexchange.com/questions/25759/how-can-a-block-cipher-in-counter-mode-be-a-reasonable-prng-when-its-a-prp

答案 7 :(得分:0)

你可以试试这个自制的块密码:

public static uint Random(uint[] seed, uint m)
{   
    for(int i = 0; i < seed.Length; i++)
    {
        m *= 0x6a09e667;
        m ^= seed[i];
        m += m << 16;
        m ^= m >> 16;
    }
    return m;
}

测试代码:

const int seedSize = 3; // larger values result in higher quality but are slower
var seed = new uint[seedSize];
var seedBytes = new byte[4 * seed.Length];
new RNGCryptoServiceProvider().GetBytes(seedBytes);
Buffer.BlockCopy(seedBytes, 0, seed, 0, seedBytes.Length);  

for(uint i = 0; i < uint.MaxValue; i++)
{
    Random(seed, i);
}

我尚未检查其输出的质量。在我的计算机上以seedSize = 3的速度在19秒内运行。