用于打印混洗列表的算法,就地并使用O(1)存储器

时间:2009-12-08 12:37:29

标签: algorithm theory shuffle

在阅读this question后,我开始怀疑:是否可以使用不会修改或复制原始列表的混洗算法?

说清楚:

想象一下,您将获得一个对象列表。列表大小可以是任意的,但假设它非常大(例如,10,000,000个项目)。您需要以随机顺序打印列表中的项目,并且需要尽快完成。但是,你不应该:

  • 复制原始列表,因为它非常大并且复制会浪费大量内存(可能达到可用RAM的限制);
  • 修改原始列表,因为它以某种方式排序,之后的其他部分依赖于它的排序。
  • 创建一个索引列表,因为同样,列表非常大,复制需要花费太多时间和内存。 (澄清:这是指任何其他列表,与原始列表具有相同数量的元素)。

这可能吗?

已添加:更多说明。

  1. 我希望列表以真正随机的方式进行洗牌,所有排列都同样可能(当然,假设我们有一个合适的Rand()函数可以开始)。
  2. 建议我列出指针列表,索引列表或任何其他列表与原始列表具有相同数量的元素,这些建议被原始问题明确认为是低效的。如果需要,您可以创建其他列表,但它们应该比原始列表小很多。
  3. 原始列表就像一个数组,您可以通过O(1)中的索引从中检索任何项目。 (所以没有双重链接列表的东西,你必须遍历列表才能找到你想要的项目。)
  4. 已添加2 :好的,让我们这样说吧:你有1TB硬盘驱动器填充数据项,每个512字节大(单个扇区)。您希望将所有这些数据复制到另一个1TB HDD,同时洗牌所有项目。您希望尽快完成此操作(单次传递数据等)。您有512MB的RAM可用,并且不依赖于交换。 (这是一个理论场景,我在实践中没有这样的东西。我只是想找到完美的算法。项目。)

11 个答案:

答案 0 :(得分:11)

嗯,这取决于你除了改组之外的什么样的随机性,即所有的改组都应该是可能的,或者分布是否可能是倾斜的。

有几种数学方法可以产生N个整数的“随机查找”排列,所以如果P是从0..N-1到0..N-1的这种排列,你可以将x从0迭代到N -1并输出列表项L(P(x))而不是L(x),你已经获得了一个改组。这样的排列可以例如获得。使用模块化算术。例如,如果N是素数,则P(x)=(x * k)mod N是任何0

应该注意的是,模幂运算是许多加密算法(例如RSA,Diffie-Hellman)的基础,并且被本领域的专家认为是强伪随机运算。

另一种简单的方法(不需要素数)首先要扩展域,这样你就可以考虑M而不是N,其中M是N中N的最小幂。所以,例如,如果N = 12,则设置M = 16。然后使用双射位操作,例如

P(x) = ((x ^ 0xf) ^ (x << 2) + 3) & 0xf

然后当你输出你的列表时,只有当P(x)实际上是<1时,你才将x从0迭代到M-1并输出L(P(x))。 Ñ

可以通过修复加密强块密码(例如AES)和随机密钥(k)然后迭代序列来构建“真正的,无偏的随机”解决方案

AES(k, 0), AES(k, 1), ...

并且从序列iff AES(k,i)&lt;中输出相应的项目。 N.这可以在恒定空间(密码所需的内部存储器)中完成,并且与随机排列(由于密码的加密属性)无法区分,但显然非常慢。在AES的情况下,您需要迭代直到i = 2 ^ 128。

答案 1 :(得分:4)

您不能复制,修改或跟踪您访问过哪些元素?我要说这不可能。除非我误解了你的第三个标准。

我认为这意味着你不能说,制作10,000,000个相应布尔值的数组,当你打印相应的元素时设置为true。而且你不能列出10,000,000个索引,洗牌,然后按顺序打印元素。

答案 2 :(得分:2)

这些10,000,000个项目只是实际项目的引用(或指针),因此您的列表不会那么大。对于所有引用,只有大约40MB的32位架构+该列表的内部变量的大小。如果您的商品小于参考尺寸,则只需复制整个清单。

答案 3 :(得分:2)

使用真正的随机数生成器无法执行此操作,因为您必须:

  • 记住已经选择了哪些数字并跳过它们(这需要一个O(n)布尔值列表,并且当你跳过越来越多的数字时,逐渐恶化的运行时间);或
  • 在每次选择后减少池(需要修改原始列表或单独的O(n)列表进行修改)。

这些都不是你问题的可能性,所以我不得不说“不,你不能这样做。”

在这种情况下,我倾向于使用的是使用值的掩码,但不是跳过,因为如上所述,随着使用值的累积,运行时间会变得更糟。

比特掩码将比39Gb的原始列表(1000万比特只有大约1.2M)好得多,比你要求的要少很多个数量级,即使它仍然是O(n)。

为了解决运行时问题,每次只生成一个随机数,如果相关的“已使用”位已经设置,则向前扫描位掩码,直到找到不是< / em>设置。

这意味着你不会闲逛,迫切希望随机数生成器为你提供一个尚未使用的数字。运行时间只会比扫描1.2M数据所花费的时间差。

当然这意味着任何时候选择的具体数字都会根据已经选择的数字而有所不同,但是,由于这些数字是随机的,因此倾斜是随机的(如果数字不是真正随意开始,然后倾斜无关紧要。)

如果你想要更多种类,你甚至可以改变搜索方向(向上或向下扫描)。

结论:我不相信你所要求的是可行的,但要记住我以前错了,因为我的妻子会很快和频繁地证明:-)但是,就像所有事情一样,那里有通常是解决这些问题的方法。

答案 4 :(得分:2)

这是一个非常简单的证明,没有PRNG方案可以工作:

  

PRNG的想法有两个阶段:第一,选择PRNG及其初始状态;第二,使用PRNG来改变输出。好吧,有 N!可能的排列,所以你需要至少 N!不同的可能开始状态,进入第2阶段。这意味着在第2阶段的开始你必须至少有 log 2 N!状态位,这是不允许的。

然而,这并不排除算法在环境中从环境接收新的随机位的方案。例如,可能有一个PRNG读取其初始状态 lazily ,但保证不会重复。我们可以证明没有吗?

假设我们有一个完美的改组算法。想象一下,我们开始运行它,当它完成一半时,我们让计算机进入睡眠状态。现在,程序的完整状态已保存在某个地方。让 S 成为程序在此中间标记处可能处于的所有可能状态的集合。

由于算法是正确的并且保证终止,因此有一个函数 f ,在给定保存的程序状态加上任何足够长的位串的情况下,产生一个有效的磁盘读写序列序列洗牌。计算机本身实现了这个功能。但请将其视为一种数学函数:

f (S×位)→读写顺序

然后,通常,存在一个函数 g ,只有 保存的程序状态,才会产生一组尚未读写的磁盘位置。 (只需将一些任意字符串传递给 f ,然后查看结果。)

g S 要读写的位置集

证明的剩余部分是为了表明 g 的域至少包含 N C N / 2 < / em>不同的集合,无论选择何种算法。如果这是真的,那么 S 必须至少包含那么多元素,因此程序的状态必须至少包含 log 2 N < / sub> C N / 2 位于中间位置,违反了要求。

我不知道如何证明最后一点,因为要么要设置的位置要读取这些位置集根据算法,写入可以是低熵。我怀疑信息理论有一些明显的原则可以解决问题。标记这个社区维基,希望有人会提供它。

答案 5 :(得分:1)

听起来不可能。

但10,000,000个64位指针只有大约76MB。

答案 6 :(得分:1)

线性反馈移位寄存器可以完成您想要的任务 - 生成一个数字列表,但是以(合理的)随机顺序生成。它产生的模式在统计上类似于你对try随机性的期望,但是它甚至不接近加密安全性。 Berlekamp-Massey算法允许您根据输出序列对等效LFSR进行逆向工程。

鉴于您需要大约10,000,000个项目的列表,您需要24位最大长度LFSR,并且只需丢弃大于列表大小的输出。

对于它的价值,与同期的典型线性同余PRNG相比,LFSR通常非常快。在硬件中,LFSR 非常简单,由N位寄存器和M 2输入XOR组成(其中M是抽头数 - 有时只有一对,很少超过半打左右。)

答案 7 :(得分:0)

如果有足够的空间,您可以将节点的指针存储在数组中,创建位图并获得指向下一个所选项的随机整数。如果已经选择了(你将它存储在你的位图中),那么得到最接近的一个(左或右,你可以随机化),直到没有剩下的项目。

如果没有足够的空间,那么你可以在不存储节点指针的情况下做同样的事情,但时间会受到影响(这就是时空权衡☺)。

答案 8 :(得分:0)

您可以使用分组密码创建伪随机“安全”排列 - 请参阅here。他们的关键见解是,给定n位长度的分组密码,您可以使用“折叠”将其缩短为m <1。 n比特,然后已经提到的技巧antti.huima从它产生一个较小的排列而不花费大量的时间来丢弃超出范围的值。

答案 9 :(得分:0)

基本上你需要的是一个随机数生成器,每个生成数字0..n-1一次。

这是一个半生不熟的想法:你可以通过选择略大于n的素数p,然后在1和p-1之间选择一个随机x,其乘法组mod p中的阶数为p-1(选择随机xs和测试哪些满足x ^ i!= 1对于i&lt; p-1,你只需要在找到之前测试一些)。由于x然后生成该组,只需计算x ^ i mod p为1&lt; = i&lt; = p-2,这将给出p-2不同的随机(ish)数字,介于2和p-1之间。减去2并抛出那些&gt; = n,这将为您提供一系列要打印的索引。

这不是非常随机,但你可以多次使用相同的技术,取上面的指数(+1)并使用它们作为另一个生成器的指数x2模数另一个素数p2(你需要n&lt; p2&lt; p),等等。十几次重复应该让事情变得随机。

答案 10 :(得分:0)

我的解决方案取决于一些巧妙计算的数字的数学性质

range = array size
prime = closestPrimeAfter(range)
root = closestPrimitiveRootTo(range/2)
state = root

通过这种设置,我们可以重复计算以下内容,它将以看似随机的顺序对数组的所有元素进行一次迭代,之后它将再次以相同的确切顺序循环遍历数组。

state = (state * root) % prime

我用 Java 实现并测试了这个,所以我决定将我的代码粘贴在这里以供将来参考。

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;

public class PseudoRandomSequence {

    private long            state;
    private final long  range;
    private final long  root;
    private final long  prime;
    //Debugging counter
    private int             dropped = 0;

    public PseudoRandomSequence(int r) {
        range = r;
        prime = closestPrimeAfter(range);
        root = modPow(generator(prime), closestPrimeTo(prime / 2), prime);
        reset();
        System.out.println("-- r:" + range);
        System.out.println("   p:" + prime);
        System.out.println("   k:" + root);
        System.out.println("   s:" + state);
    }

    // https://en.wikipedia.org/wiki/Primitive_root_modulo_n
    private static long modPow(long base, long exp, long mod) {
        return BigInteger.valueOf(base).modPow(BigInteger.valueOf(exp), BigInteger.valueOf(mod)).intValue();
    }

    //http://e-maxx-eng.github.io/algebra/primitive-root.html
    private static long generator(long p) {
        ArrayList<Long> fact = new ArrayList<Long>();
        long phi = p - 1, n = phi;
        for (long i = 2; i * i <= n; ++i) {
            if (n % i == 0) {
                fact.add(i);
                while (n % i == 0) {
                    n /= i;
                }
            }
        }
        if (n > 1) fact.add(n);
        for (long res = 2; res <= p; ++res) {
            boolean ok = true;
            for (long i = 0; i < fact.size() && ok; ++i) {
                ok &= modPow(res, phi / fact.get((int) i), p) != 1;
            }
            if (ok) {
                return res;
            }
        }
        return -1;
    }

    public long get() {
        return state - 1;
    }

    public void advance() {
        //This loop simply skips all results that overshoot the range, which should never happen if range is a prime number.
        dropped--;
        do {
            state = (state * root) % prime;
            dropped++;
        } while (state > range);
    }

    public void reset() {
        state = root;
        dropped = 0;
    }

    private static boolean isPrime(long num) {
        if (num == 2) return true;
        if (num % 2 == 0) return false;
        for (int i = 3; i * i <= num; i += 2) {
            if (num % i == 0) return false;
        }
        return true;
    }

    private static long closestPrimeAfter(long n) {
        long up;
        for (up = n + 1; !isPrime(up); ++up)
            ;
        return up;
    }

    private static long closestPrimeBefore(long n) {
        long dn;
        for (dn = n - 1; !isPrime(dn); --dn)
            ;
        return dn;
    }

    private static long closestPrimeTo(long n) {
        final long dn = closestPrimeBefore(n);
        final long up = closestPrimeAfter(n);
        return (n - dn) > (up - n) ? up : dn;
    }

    private static boolean test(int r, int loops) {
        final int array[] = new int[r];
        Arrays.fill(array, 0);
        System.out.println("TESTING: array size: " + r + ", loops: " + loops + "\n");
        PseudoRandomSequence prs = new PseudoRandomSequence(r);
        final long ct = loops * r;
        //Iterate the array 'loops' times, incrementing the value for each cell for every visit. 
        for (int i = 0; i < ct; ++i) {
            prs.advance();
            final long index = prs.get();
            array[(int) index]++;
        }
        //Verify that each cell was visited exactly 'loops' times, confirming the validity of the sequence
        for (int i = 0; i < r; ++i) {
            final int c = array[i];
            if (loops != c) {
                System.err.println("ERROR: array element @" + i + " was " + c + " instead of " + loops + " as expected\n");
                return false;
            }
        }
        //TODO: Verify the "randomness" of the sequence
        System.out.println("OK:  Sequence checked out with " + prs.dropped + " drops (" + prs.dropped / loops + " per loop vs. diff " + (prs.prime - r) + ") \n");
        return true;
    }

    //Run lots of random tests
    public static void main(String[] args) {
        Random r = new Random();
        r.setSeed(1337);
        for (int i = 0; i < 100; ++i) {
            PseudoRandomSequence.test(r.nextInt(1000000) + 1, r.nextInt(9) + 1);
        }
    }

}

这是受 Graphics Gems vol. 2 中描述的 2D 图形“溶解”效果的小型 C 实现的启发。 1,这反过来又是对 2D 的一种适应,对称为“LFSR”的机制进行了一些优化(维基百科文章 here,原始的 dissolve.c 源代码 here)。