使用1 MB RAM对1百万个8位十进制数字进行排序


726

我有一台具有1 MB RAM且没有其他本地存储的计算机。我必须使用它来通过TCP连接接受一百万个8位十进制数字,对它们进行排序,然后通过另一个TCP连接将排序后的列表发送出去。

数字列表可能包含重复项,我不能丢弃。该代码将放置在ROM中,因此我不必从1 MB中减去代码的大小。我已经有了驱动以太网端口和处理TCP / IP连接的代码,它的状态数据需要2 KB,包括一个1 KB的缓冲区,代码将通过该缓冲区读写数据。有解决这个问题的方法吗?

问题和答案的来源:

slashdot.org

cleaton.net


45
嗯,一百万乘以8位十进制数字(最少27位整数)> 1MB ram
Mr47 2012年

15
1M的RAM意味着2 ^ 20字节?在这个架构上一个字节有多少位?“ 100万个8位数十进制数字”中的“百万”是否为SI百万(10 ^ 6)?什么是8位十进制数字,自然数<10 ^ 8,有理数字(其小数表示形式需要8位数字,但不包括小数点)或其他内容?

13
1百万个8位十进制数字还是1百万个8位数字?
Patrick White

13
它使我想起《 Dobb博士日记》(1998-2001年之间的某个地方)上的一篇文章,其中作者在阅读电话号码时使用插入排序对电话号码进行了排序:那是我第一次意识到有时速度会变慢算法可能会更快...
Adrien Plisson

103
还没有人提到过另一种解决方案:购买具有2MB RAM的硬件。它不应该昂贵得多,它会使问题变得非常容易解决。
丹尼尔·瓦格纳

Answers:


716

到目前为止,这里还没有提到一个相当狡猾的把戏。我们假设您没有额外的存储数据的方法,但是严格来说并非如此。

解决问题的一种方法是执行以下可怕的事情,任何人在任何情况下都不应尝试以下事情:使用网络流量存储数据。不,我不是说NAS。

您可以通过以下方式仅用几个字节的RAM对数字进行排序:

  • 首先获取2个变量:COUNTERVALUE
  • 首先将所有寄存器设置为0;
  • 每次您收到一个整数I,递增COUNTER并设置VALUEmax(VALUE, I);
  • 然后将带有数据集的ICMP回显请求数据包发送I到路由器。擦除I并重复。
  • 每次收到返回的ICMP数据包时,您只需提取整数并在另一个回显请求中再次将其发送出去。这会产生大量的ICMP请求,其中包含整数的ICMP请求正反转换。

一旦COUNTER到达1000000,您就将所有值存储在不断的ICMP请求流中,并且VALUE现在包含最大整数。选择一些threshold T >> 1000000。设置COUNTER为零。每次收到ICMP数据包时,都将递增COUNTERI在另一个回显请求中将包含的整数发送出去,除非I=VALUE,在这种情况下,将其传输到已排序整数的目的地。一旦COUNTER=T,递减VALUE1,复位COUNTER至零和重复。一旦VALUE达到零,您应该按从最大到最小的顺序将所有整数传输到目的地,并且仅将约47位RAM用于两个持久变量(以及临时值所需的任何数量)。

我知道这很可怕,而且我知道可能存在各种各样的实际问题,但是我认为这可能会让你们中的一些人大笑或至少使您恐惧。


27
因此,您基本上是在利用网络延迟并将路由器变成某种形式的路由器?
Eric R.

335
这种解决方案不仅在盒子外面。它似乎已经忘记了在家中的盒子:D
弗拉迪斯拉夫·佐罗夫(Fladislav Zorov),2012年

28
很好的答案...我喜欢这些答案,因为它们确实揭示了解决方案对问题的多样性
StackOverflowed 2012年

33
ICMP不可靠。
sleeplessnerd 2012年

13
@MDMarra:您会在最上面注意到我说:“解决您的问题的一种方法是做以下可怕的事情,任何人在任何情况下都不应尝试”。我这么说是有原因的。
Joe Fitzsimons 2012年

423

这是一些可以解决该问题的C ++代码

满足内存约束的证明:

编辑器:在本文或他的博客中,没有提供作者提供的最大内存要求的证据。由于对一个值进行编码所需的位数取决于先前编码的值,因此这种证明可能并非无关紧要。作者注意到,根据经验1011732,他可能偶然发现的最大编码大小为,并1013000任意选择了缓冲区大小。

typedef unsigned int u32;

namespace WorkArea
{
    static const u32 circularSize = 253250;
    u32 circular[circularSize] = { 0 };         // consumes 1013000 bytes

    static const u32 stageSize = 8000;
    u32 stage[stageSize];                       // consumes 32000 bytes

    ...

这两个阵列一起占用1045000字节的存储空间。剩下的1048576-1045000-2×1024 = 1528个字节可用于剩余变量和堆栈空间。

它在我的至强W3520上运行约23秒。假设程序名称为,您可以使用以下Python脚本验证程序是否正常运行sort1mb.exe

from subprocess import *
import random

sequence = [random.randint(0, 99999999) for i in xrange(1000000)]

sorter = Popen('sort1mb.exe', stdin=PIPE, stdout=PIPE)
for value in sequence:
    sorter.stdin.write('%08d\n' % value)
sorter.stdin.close()

result = [int(line) for line in sorter.stdout]
print('OK!' if result == sorted(sequence) else 'Error!')

有关该算法的详细说明,请参见以下系列文章:


8
@preshing是,我们非常希望对此进行详细说明。
T Suds 2012年

25
我认为关键的观察结果是,一个8位数字具有大约26.6位的信息,而100万个数字是19.9位。如果您增量压缩列表(存储相邻值的差异),则差异范围为0(0位)到99999999(26.6位),但对之间不能有最大增量。最坏的情况实际上应该是一百万个均匀分布的值,要求增量(26.6-19.9)或每个增量约6.7位。存储100万个6.7位的值很容易就可以达到1M。增量压缩需要连续合并排序,因此您几乎可以免费获得它。
本杰克逊

4
甜蜜的解决方案。你们应该访问他的博客的解释 preshing.com/20121025/...
davec

9
@BenJackson:您的数学中有错误。有2.265 x 10 ^ 2436455唯一可能的输出(10 ^ 6个8位整数的有序集)需要8.094 x 10 ^ 6位来存储(即,兆字节以下的头发)。没有任何巧妙的方案可以压缩超出此信息理论极限而不会丢失。您的解释意味着您需要的空间要少得多,因此是错误的。确实,上述解决方案中的“圆形”大小足以容纳所需的信息,因此预敲似乎已将其考虑在内,但您却错过了它。
2012年

5
@JoeFitzsimons:我没有计算出递归(从0..m是n的唯一排序的n个数字集(n+m)!/(n!m!)),所以你一定是对的。大概是我的估计,b位的增量需要b位来存储-显然0的增量就不需要0位来存储。
本杰克逊

371

请参阅第一个正确答案以后的算术编码答案您可能会在下面找到一些有趣的东西,但并不是100%的防弹解决方案。

这是一个非常有趣的任务,这是另一个解决方案。我希望有人会觉得结果有用(或至少很有趣)。

阶段1:初始数据结构,粗略压缩方法,基本结果

让我们做一些简单的数学运算:我们最初有1M(1048576字节)RAM用于存储10 ^ 6 8位十进制数字。[0; 99999999]。因此,要存储一个数字,需要27位(假设将使用无符号数字)。因此,要存储原始流,需要约3.5M的RAM。有人已经说过这似乎不可行,但是我要说的是,只要输入“足够好”就可以解决任务。基本上,该想法是使用0.29或更高的压缩因子压缩输入数据,并以适当的方式进行排序。

让我们首先解决压缩问题。已经有一些相关的测试:

http://www.theeggeadventure.com/wikimedia/index.php/Java_Data_Compression

“我进行了一项测试,使用各种压缩形式压缩一百万个连续整数。结果如下:”

None     4000027
Deflate  2006803
Filtered 1391833
BZip2    427067
Lzma     255040

看起来LZMA(Lempel-Ziv-Markov链算法)是继续的好选择。我已经准备了一个简单的PoC,但仍有一些细节需要强调:

  1. 内存有限,因此其想法是对数字进行预排序并使用压缩的存储桶(动态大小)作为临时存储
  2. 通过预排序的数据更容易获得更好的压缩系数,因此每个存储区都有一个静态缓冲区(缓冲区中的数字要在LZMA之前进行排序)
  3. 每个存储桶都具有特定范围,因此可以分别对每个存储桶进行最终分类
  4. 可以正确设置存储桶的大小,因此将有足够的内存来解压缩存储的数据并分别对每个存储桶进行最终排序

内存中排序

请注意,附加代码是POC,不能用作最终解决方案,它只是演示了使用几个较小的缓冲区以某种最佳方式(可能是压缩的)存储预排序数字的想法。不建议将LZMA作为最终解决方案。它用作向此PoC引入压缩的最快方法。

请参阅下面的PoC代码(请注意,这只是一个演示,需要使用LZMA-Java进行编译):

public class MemorySortDemo {

static final int NUM_COUNT = 1000000;
static final int NUM_MAX   = 100000000;

static final int BUCKETS      = 5;
static final int DICT_SIZE    = 16 * 1024; // LZMA dictionary size
static final int BUCKET_SIZE  = 1024;
static final int BUFFER_SIZE  = 10 * 1024;
static final int BUCKET_RANGE = NUM_MAX / BUCKETS;

static class Producer {
    private Random random = new Random();
    public int produce() { return random.nextInt(NUM_MAX); }
}

static class Bucket {
    public int size, pointer;
    public int[] buffer = new int[BUFFER_SIZE];

    public ByteArrayOutputStream tempOut = new ByteArrayOutputStream();
    public DataOutputStream tempDataOut = new DataOutputStream(tempOut);
    public ByteArrayOutputStream compressedOut = new ByteArrayOutputStream();

    public void submitBuffer() throws IOException {
        Arrays.sort(buffer, 0, pointer);

        for (int j = 0; j < pointer; j++) {
            tempDataOut.writeInt(buffer[j]);
            size++;
        }            
        pointer = 0;
    }

    public void write(int value) throws IOException {
        if (isBufferFull()) {
            submitBuffer();
        }
        buffer[pointer++] = value;
    }

    public boolean isBufferFull() {
        return pointer == BUFFER_SIZE;
    }

    public byte[] compressData() throws IOException {
        tempDataOut.close();
        return compress(tempOut.toByteArray());
    }        

    private byte[] compress(byte[] input) throws IOException {
        final BufferedInputStream in = new BufferedInputStream(new ByteArrayInputStream(input));
        final DataOutputStream out = new DataOutputStream(new BufferedOutputStream(compressedOut));

        final Encoder encoder = new Encoder();
        encoder.setEndMarkerMode(true);
        encoder.setNumFastBytes(0x20);
        encoder.setDictionarySize(DICT_SIZE);
        encoder.setMatchFinder(Encoder.EMatchFinderTypeBT4);

        ByteArrayOutputStream encoderPrperties = new ByteArrayOutputStream();
        encoder.writeCoderProperties(encoderPrperties);
        encoderPrperties.flush();
        encoderPrperties.close();

        encoder.code(in, out, -1, -1, null);
        out.flush();
        out.close();
        in.close();

        return encoderPrperties.toByteArray();
    }

    public int[] decompress(byte[] properties) throws IOException {
        InputStream in = new ByteArrayInputStream(compressedOut.toByteArray());
        ByteArrayOutputStream data = new ByteArrayOutputStream(10 * 1024);
        BufferedOutputStream out = new BufferedOutputStream(data);

        Decoder decoder = new Decoder();
        decoder.setDecoderProperties(properties);
        decoder.code(in, out, 4 * size);

        out.flush();
        out.close();
        in.close();

        DataInputStream input = new DataInputStream(new ByteArrayInputStream(data.toByteArray()));
        int[] array = new int[size];
        for (int k = 0; k < size; k++) {
            array[k] = input.readInt();
        }

        return array;
    }
}

static class Sorter {
    private Bucket[] bucket = new Bucket[BUCKETS];

    public void doSort(Producer p, Consumer c) throws IOException {

        for (int i = 0; i < bucket.length; i++) {  // allocate buckets
            bucket[i] = new Bucket();
        }

        for(int i=0; i< NUM_COUNT; i++) {         // produce some data
            int value = p.produce();
            int bucketId = value/BUCKET_RANGE;
            bucket[bucketId].write(value);
            c.register(value);
        }

        for (int i = 0; i < bucket.length; i++) { // submit non-empty buffers
            bucket[i].submitBuffer();
        }

        byte[] compressProperties = null;
        for (int i = 0; i < bucket.length; i++) { // compress the data
            compressProperties = bucket[i].compressData();
        }

        printStatistics();

        for (int i = 0; i < bucket.length; i++) { // decode & sort buckets one by one
            int[] array = bucket[i].decompress(compressProperties);
            Arrays.sort(array);

            for(int v : array) {
                c.consume(v);
            }
        }
        c.finalCheck();
    }

    public void printStatistics() {
        int size = 0;
        int sizeCompressed = 0;

        for (int i = 0; i < BUCKETS; i++) {
            int bucketSize = 4*bucket[i].size;
            size += bucketSize;
            sizeCompressed += bucket[i].compressedOut.size();

            System.out.println("  bucket[" + i
                    + "] contains: " + bucket[i].size
                    + " numbers, compressed size: " + bucket[i].compressedOut.size()
                    + String.format(" compression factor: %.2f", ((double)bucket[i].compressedOut.size())/bucketSize));
        }

        System.out.println(String.format("Data size: %.2fM",(double)size/(1014*1024))
                + String.format(" compressed %.2fM",(double)sizeCompressed/(1014*1024))
                + String.format(" compression factor %.2f",(double)sizeCompressed/size));
    }
}

static class Consumer {
    private Set<Integer> values = new HashSet<>();

    int v = -1;
    public void consume(int value) {
        if(v < 0) v = value;

        if(v > value) {
            throw new IllegalArgumentException("Current value is greater than previous: " + v + " > " + value);
        }else{
            v = value;
            values.remove(value);
        }
    }

    public void register(int value) {
        values.add(value);
    }

    public void finalCheck() {
        System.out.println(values.size() > 0 ? "NOT OK: " + values.size() : "OK!");
    }
}

public static void main(String[] args) throws IOException {
    Producer p = new Producer();
    Consumer c = new Consumer();
    Sorter sorter = new Sorter();

    sorter.doSort(p, c);
}
}

使用随机数会产生以下结果:

bucket[0] contains: 200357 numbers, compressed size: 353679 compression factor: 0.44
bucket[1] contains: 199465 numbers, compressed size: 352127 compression factor: 0.44
bucket[2] contains: 199682 numbers, compressed size: 352464 compression factor: 0.44
bucket[3] contains: 199949 numbers, compressed size: 352947 compression factor: 0.44
bucket[4] contains: 200547 numbers, compressed size: 353914 compression factor: 0.44
Data size: 3.85M compressed 1.70M compression factor 0.44

对于一个简单的升序(使用一个存储桶),它将产生:

bucket[0] contains: 1000000 numbers, compressed size: 256700 compression factor: 0.06
Data size: 3.85M compressed 0.25M compression factor 0.06

编辑

结论:

  1. 不要试图愚弄大自然
  2. 使用更简单的压缩方式,减少内存占用
  3. 确实需要一些其他线索。通用的防弹解决方案似乎并不可行。

第二阶段:增强压缩效果,最终结论

如前一节所述,可以使用任何合适的压缩技术。因此,让我们摆脱LZMA的束缚,转而采用更简单,更好(如果可能)的方法。有很多好的解决方案,包括算术编码基数树等。

无论如何,简单但有用的编码方案将比另一个外部库更具说明性,并提供了一些漂亮的算法。实际的解决方案非常简单:由于存在带有部分排序数据的存储桶,因此可以使用增量代替数字。

编码方案

随机输入测试显示出更好的结果:

bucket[0] contains: 10103 numbers, compressed size: 13683 compression factor: 0.34
bucket[1] contains: 9885 numbers, compressed size: 13479 compression factor: 0.34
...
bucket[98] contains: 10026 numbers, compressed size: 13612 compression factor: 0.34
bucket[99] contains: 10058 numbers, compressed size: 13701 compression factor: 0.34
Data size: 3.85M compressed 1.31M compression factor 0.34

样例代码

  public static void encode(int[] buffer, int length, BinaryOut output) {
    short size = (short)(length & 0x7FFF);

    output.write(size);
    output.write(buffer[0]);

    for(int i=1; i< size; i++) {
        int next = buffer[i] - buffer[i-1];
        int bits = getBinarySize(next);

        int len = bits;

        if(bits > 24) {
          output.write(3, 2);
          len = bits - 24;
        }else if(bits > 16) {
          output.write(2, 2);
          len = bits-16;
        }else if(bits > 8) {
          output.write(1, 2);
          len = bits - 8;
        }else{
          output.write(0, 2);
        }

        if (len > 0) {
            if ((len % 2) > 0) {
                len = len / 2;
                output.write(len, 2);
                output.write(false);
            } else {
                len = len / 2 - 1;
                output.write(len, 2);
            }

            output.write(next, bits);
        }
    }
}

public static short decode(BinaryIn input, int[] buffer, int offset) {
    short length = input.readShort();
    int value = input.readInt();
    buffer[offset] = value;

    for (int i = 1; i < length; i++) {
        int flag = input.readInt(2);

        int bits;
        int next = 0;
        switch (flag) {
            case 0:
                bits = 2 * input.readInt(2) + 2;
                next = input.readInt(bits);
                break;
            case 1:
                bits = 8 + 2 * input.readInt(2) +2;
                next = input.readInt(bits);
                break;
            case 2:
                bits = 16 + 2 * input.readInt(2) +2;
                next = input.readInt(bits);
                break;
            case 3:
                bits = 24 + 2 * input.readInt(2) +2;
                next = input.readInt(bits);
                break;
        }

        buffer[offset + i] = buffer[offset + i - 1] + next;
    }

   return length;
}

请注意,这种方法:

  1. 不消耗大量内存
  2. 与流一起使用
  3. 提供了不错的结果

完整的代码可以在这里找到,BinaryInput和BinaryOutput实现可以在这里找到

定论

没有最终结论:)有时候,将自己升级到一个级别并从元级别的角度审查任务确实是个好主意。

花一些时间来完成这项任务很有趣。顺便说一句,下面有很多有趣的答案。感谢您的关注和满意。


17
我使用了Inkscape。顺便说一句很棒的工具。您可以使用此图作为示例。
Renat Gilmanov

21
在这种情况下,LZMA肯定需要太多内存才能使用吗?作为一种算法,它意味着将必须存储或传输的数据量减到最少,而不是高效地存储。
Mjiig 2​​012年

67
这是胡说八道...获取1百万个随机的27位整数,对其进行排序,并使用7zip,xz压缩,无论您想要什么LZMA。结果超过1MB。最重要的前提是压缩序号。用0bit的Delta编码就是数字,例如1000000(例如4字节)。对于连续和重复(无间隙),数字1000000和1000000位= 128KB,其中0表示重复编号,1表示下一个标记。当您的间隙很小甚至很小时,LZMA就是荒谬的。它不是为此设计的。
alecco 2012年

30
这实际上是行不通的。我进行了模拟,虽然压缩数据超过1MB(约1.5MB),但它仍使用100MB以上的RAM来压缩数据。因此,即使压缩整数也无法解决问题,更不用说运行时RAM的使用了。授予您赏金是我在stackoverflow上遇到的最大错误。
最喜欢的Onwuemene 2012年

10
这个答案之所以如此,是因为很多程序员喜欢闪亮的想法,而不是经过验证的代码。如果这个想法行得通,那么您会看到选择并证明了一种实际的压缩算法,而不是仅仅断言肯定有一个可以做到的压缩算法...当很可能没有一个可以做到这一点时。
奥拉西(Olathe)

185

仅由于1兆字节和1百万字节之间的差异,才可能找到解决方案。8093729.5的幂大约有2种,可以选择100万个允许重复且顺序不重要的8位数字,因此,只有100万字节RAM的计算机没有足够的状态来表示所有可能性。但是1M(对于TCP / IP,小于2k)是1022 * 1024 * 8 = 8372224位,因此可以解决。

第1部分,初始解决方案

这种方法所需的资源略高于1M,我将对其进行改进以使其在以后适应1M。

我将数字的紧凑排序列表存储在0到99999999之间,作为7位数字子列表的序列。第一个子列表保存从0到127的数字,第二个子列表保存从128到255的数字,依此类推。100000000/128恰好是781250,因此需要781250这样的子列表。

每个子列表由一个2位子列表标头和一个子列表正文组成。子列表主体每个子列表条目占用7位。子列表全部串联在一起,并且格式可以告诉一个子列表在哪里结束,下一个开始。完全填充的列表所需的总存储量为2 * 781250 + 7 * 1000000 = 8562500位,大约为1.021 M字节。

4个可能的子列表标头值为:

00空的子列表,什么也没有。

01 Singleton,子列表中只有一个条目,接下来的7位保存该条目。

10子列表至少包含2个不同的数字。条目以非降序存储,但最后一个条目小于或等于第一个条目。这样可以确定子列表的末尾。例如,数字2,4,6将存储为(4,6,2)。数字2,2,3,4,4将存储为(2,3,4,4,2)。

11子列表包含一个数字的2个或更多重复。接下来的7位给出数字。然后是零个或多个值为1的7位条目,然后是值为0的7位条目。子列表主体的长度决定了重复次数。例如,数字12,12将存储为(12,0),数字12,12,12将存储为(12,1,0),12,12,12,12将存储为(12,1 ,1,0)等。

我从一个空列表开始,读入一堆数字并将其存储为32位整数,将新数字排序到位(可能使用堆排序),然后将它们合并为新的紧凑排序列表。重复直到没有更多的数字可读取,然后再次遍历压缩列表以生成输出。

下面的行表示列表合并操作开始之前的内存。“ O”是保存排序的32位整数的区域。“ X”是保存旧压缩列表的区域。“ =”符号是紧凑列表的扩展空间,“ O”中的每个整数7位。“ Z”是其他随机开销。

ZZZOOOOOOOOOOOOOOOOOOOOOOOOOO==========XXXXXXXXXXXXXXXXXXXXXXXXXX

合并例程从最左边的“ O”和最左边的“ X”开始读取,并从最左边的“ =”开始写入。在合并所有新的整数之前,写入指针不会捕获紧凑列表读取指针,因为两个指针对于旧子压缩列表中的每个子列表前进2位,对于每个条目前进7位,并且有足够的额外空间来容纳压缩列表读取指针。新数字的7位输入。

第2部分,塞入1M

要将上述解决方案压缩为1M,我需要使压缩列表格式更紧凑。我将摆脱一种子列表类型,以便只有3种可能的子列表标头值。然后,我可以使用“ 00”,“ 01”和“ 1”作为子列表标题值并保存一些位。子列表类型为:

一个空的子列表,什么也没有。

B Singleton,子列表中只有一个条目,接下来的7位保存该条目。

C子列表至少包含2个不同的数字。条目以非降序存储,但最后一个条目小于或等于第一个条目。这样可以确定子列表的末尾。例如,数字2,4,6将存储为(4,6,2)。数字2,2,3,4,4将存储为(2,3,4,4,2)。

D子列表由2个或多个重复的单个数字组成。

我的3个子列表标题值为“ A”,“ B”和“ C”,因此我需要一种表示D型子列表的方法。

假设我有C型子列表标头,后跟3个条目,例如“ C [17] [101] [58]”。如上所述,这不能成为有效C类型子列表的一部分,因为第三个条目小于第二个条目,但大于第一个条目。我可以使用这种类型的构造来表示D型子列表。用位表示,在任何我有“ C {00 ?????} {1 ??????} {01 ?????}”的地方,都是不可能的C类型子列表。我将使用它来表示一个由3个或更多重复的单个数字组成的子列表。前两个7位字对数字进行编码(下面的“ N”位),后跟零个或多个{0100001}字,后跟一个{0100000}字。

For example, 3 repetitions: "C{00NNNNN}{1NN0000}{0100000}", 4 repetitions: "C{00NNNNN}{1NN0000}{0100001}{0100000}", and so on.

剩下的列表只包含一个数字的2个重复。我将用另一个不可能的C型子列表模式来代表它们:“ C {0 ??????} {11 ??????} {10 ?????}”。前两个字中的数字的7位有足够的空间,但是此模式比它表示的子列表更长,这使事情变得更加复杂。可以将结尾处的五个问号视为模式的一部分,因此我将“ C {0NNNNNN} {11N ????} 10”作为我的模式,并将要重复的数字存储在“ N”中的。那太长了2位。

在这种模式下,我将不得不借用2个比特并从4个未使用的比特中偿还它们。读取时,遇到“ C {0NNNNNN} {11N00AB} 10”时,输出2个以“ N”表示的数字实例,在末尾用位A和B覆盖“ 10”,然后将读取指针后退2位。此算法可以进行破坏性读取,因为每个紧凑列表仅被遍历一次。

当写一个由2个重复组成的子列表时,写“ C {0NNNNNN} 11N00”并将借位计数器设置为2。在每次写时借位计数器为非零值时,写的每一位都会递减,当计数器为零时,将写入“ 10”。因此,接下来写入的2位将进入插槽A和B,然后将“ 10”放到末尾。

使用“ 00”,“ 01”和“ 1”表示的3个子列表标题值,我可以将“ 1”分配给最受欢迎的子列表类型。我需要一个小表将子列表标题值映射到子列表类型,并且每个子列表类型都需要一个出现计数器,以便知道最佳的子列表标题映射是什么。

当所有子列表类型都同样受欢迎时,最坏的情况就是最小化完全填充的紧凑列表的表示。在那种情况下,我为每3个子列表标题保存1位,因此列表大小为2 * 781250 + 7 * 1000000-781250/3 = 8302083.3位。向上舍入到32位字边界,即8302112位或1037764字节。

1M减去TCP / IP状态的2k,缓冲区为1022 * 1024 = 1046528字节,剩下8764字节可用来玩。

但是,更改子列表标头映射的过程呢?在下面的内存映射中,“ Z”是随机开销,“ =”是可用空间,“ X”是压缩列表。

ZZZ=====XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

从最左边的“ X”开始阅读,并从最左边的“ =”开始书写,然后向右工作。完成后,压缩列表将短一些,并且将位于错误的内存末尾:

ZZZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=======

因此,我需要将其右移:

ZZZ=======XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

在头映射更改过程中,多达1/3的子列表头将从1位更改为2位。在最坏的情况下,这些都将排在列表的首位,因此在开始之前,我至少需要781250/3位可用存储空间,这使我回到了精简版以前版本的内存要求: (

为了解决这个问题,我将781250个子列表分为10个子列表,每个子列表包含78125个子列表。每个组都有自己的独立子列表标头映射。将字母A到J用于组:

ZZZ=====AAAAAABBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ

在子列表标头映射更改期间,每个子列表组都会缩小或保持不变:

ZZZ=====AAAAAABBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAA=====BBCCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABB=====CCCCDDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCC======DDDDDEEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDD======EEEFFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEE======FFFGGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFF======GGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGG=======HHIJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHH=======IJJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHHI=======JJJJJJJJJJJJJJJJJJJJ
ZZZAAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ=======
ZZZ=======AAAAAABBCCCDDDDDEEEFFFGGGGGGGGGGHHIJJJJJJJJJJJJJJJJJJJJ

在映射更改期间,子列表组的临时扩展的最坏情况是78125/3 = 26042位,小于4k。如果我允许4k加上1037764字节来填充一个完全填充的压缩列表,那么内存映射中的“ Z”将剩下8764-4096 = 4668字节。

对于10个子列表标头映射表,30个子列表标头出现计数以及我需要的其他一些计数器,指针和小缓冲区,以及我不注意使用的空间(例如,函数调用返回地址的堆栈空间和局部变量。

第三部分,运行需要多长时间?

如果列表为空,则1位列表头将用于空子列表,列表的起始大小为781250位。在最坏的情况下,列表中每个增加的数字都会增长8位,因此,要将32位数字中的每个数字都放在列表缓冲区的顶部,然后对其进行排序和合并,则需要32 + 8 = 40位可用空间。在最坏的情况下,更改子列表标头映射会导致2 * 781250 + 7 *条目781250/3位的空间使用。

如果在列表中至少有80万个数字之后,每五次合并后更改子列表标头映射的策略,最坏的情况将涉及总共约30M的紧凑列表读写活动。

资源:

http://nick.cleaton.net/ramsortsol.html


15
我认为没有更好的解决方案(如果我们需要使用任何不可压缩的值)。但这可能会有所改善。不必在1位和2位表示之间更改子列表标头。取而代之的是,您可以使用算术编码,从而简化算法,并将每个标头的最坏情况下的位数从1.67减少到1.58。而且您不需要在内存中移动紧凑列表;而是使用循环缓冲区并仅更改指针。
Evgeny Kluev 2012年

5
那么,最后,这是面试问题吗?
mlvljr 2012年

2
其他可能的改进是使用100个元素的子列表,而不是128个元素的子列表(因为当子列表的数量等于数据集中的元素的数量时,我们得到了最紧凑的表示形式)。要用算术编码编码的子列表的每个值(每个值的相等频率为1/100)。这可以节省大约10000位,比压缩子列表头的要少得多。
Evgeny Kluev 2012年

对于情况C,您说:“条目以非降序存储,但最后一个条目小于或等于第一个。” 那您将如何编码2,2,2,3,5?{2,2,3,5,2}看起来就像是2,2
Rollie

1
子列表标头编码的更简单解决方案是可能的,而每个子标头具有相同的压缩比1.67位,而无需复杂地切换映射。您可以将每3个连续的子标头组合在一起,因为可以将其轻松编码为5位3 * 3 * 3 = 27 < 32。你把它们结合起来combined_subheader = subheader1 + 3 * subheader2 + 9 * subheader3
hynekcer 2012年

57

吉尔马诺夫的答案在其假设中是非常错误的。它开始基于一百万个连续整数的无意义度量进行推测。这意味着没有差距。那些随机的间隙,无论多么小,确实使它成为一个糟糕的主意。

自己尝试。获得1百万个随机的27位整数,对其进行排序,并使用7-Zip,xz 压缩,无论您要使用哪种LZMA。结果超过1.5 MB。最重要的前提是压缩序号。即使是增量编码,也超过1.1 MB。没关系,这正在使用超过100 MB的RAM进行压缩。因此,即使压缩的整数也无法解决问题,并且不必担心运行时RAM的使用情况

让我感到难过的是,人们如何赞成漂亮的图形和合理化。

#include <stdint.h>
#include <stdlib.h>
#include <time.h>

int32_t ints[1000000]; // Random 27-bit integers

int cmpi32(const void *a, const void *b) {
    return ( *(int32_t *)a - *(int32_t *)b );
}

int main() {
    int32_t *pi = ints; // Pointer to input ints (REPLACE W/ read from net)

    // Fill pseudo-random integers of 27 bits
    srand(time(NULL));
    for (int i = 0; i < 1000000; i++)
        ints[i] = rand() & ((1<<27) - 1); // Random 32 bits masked to 27 bits

    qsort(ints, 1000000, sizeof (ints[0]), cmpi32); // Sort 1000000 int32s

    // Now delta encode, optional, store differences to previous int
    for (int i = 1, prev = ints[0]; i < 1000000; i++) {
        ints[i] -= prev;
        prev    += ints[i];
    }

    FILE *f = fopen("ints.bin", "w");
    fwrite(ints, 4, 1000000, f);
    fclose(f);
    exit(0);

}

现在用LZMA压缩ints.bin ...

$ xz -f --keep ints.bin       # 100 MB RAM
$ 7z a ints.bin.7z ints.bin   # 130 MB RAM
$ ls -lh ints.bin*
    3.8M ints.bin
    1.1M ints.bin.7z
    1.2M ints.bin.xz

7
任何涉及基于字典的压缩的算法都无法逾越,我已经编写了一些自定义代码,并且它们都花了相当多的内存才能放置自己的哈希表(并且Java中没有HashMap,因为它特别占用资源)。最接近的解决方案是使用可变比特长度的delta编码,然后弹回您不喜欢的TCP数据包。伙伴将重新传输,但充其量仍会变态。
bestsss 2012年

@bestsss是的!看看我最后的答案。我想这也许是可能的。
alecco 2012年

3
抱歉,实际上,这似乎也无法回答问题
n611x007 2013年

@naxa是的,它的答案是:它不能在原始问题的参数范围内完成。仅当数字的分布具有非常低的熵时才可以这样做。
alecco 2015年

1
所有这些答案表明,标准压缩例程很难将数据压缩到1MB以下。可能存在或可能没有一种可以压缩数据以要求少于1MB的编码方案,但是此答案不能证明没有任何编码方案可以压缩这么多的数据。
Itsme2003年

41

我认为,从组合学的角度考虑这一问题的一种方法是:有多少种可能的排序数字顺序组合?如果我们给组合0,0,0,.....,0代号0,0,0,0,...,1代号1,而99999999、99999999,... 99999999代号N,什么是N?换句话说,结果空间有多大?

好吧,考虑这一点的一种方法是注意到这是在N x M网格中找到单调路径数的问题的一个双射,其中N = 1,000,000,M = 100,000,000。换句话说,如果您有一个1,000,000宽,100,000,000高的网格,那么从左下角到右上角的最短路径有多少条?当然,最短的路径只要求您向右或向上移动(如果要向下或向左移动,则将撤消先前已完成的进度)。若要查看这是我们的数字排序问题的双射,请注意以下几点:

您可以将路径中的任何水平支腿想象成订购中的数字,其中支腿的Y位置代表值。

在此处输入图片说明

因此,如果路径只是简单地一直向右移动到最后,然后一直跳到顶部,这相当于排序0,0,0,...,0。相反,如果它从一路跳到顶部开始,然后向右移动1,000,000次,相当于99999999,99999999,...,99999999。该路径向右移动一次,然后向上移动一次,然后向右移动一次,然后上升一次,以此类推,直到最后(然后必须一直跳到顶部),等于0、1、2、3,...,999999。

幸运的是,对于我们来说,这个问题已经解决了,这样的网格具有(N + M)个选择(M)个路径:

(1,000,000 + 100,000,000)选择(100,000,000)〜= 2.27 * 10 ^ 2436455

N因此等于2.27 * 10 ^ 2436455,因此代码0表示0,0,0,...,0,代码2.27 * 10 ^ 2436455,并且一些更改表示99999999,99999999,...,99999999。

为了存储从0到2.27 * 10 ^ 2436455的所有数字,您需要lg2(2.27 * 10 ^ 2436455)= 8.0937 * 10 ^ 6位。

1兆字节= 8388608位> 8093700位

因此,看来我们至少实际上有足够的空间来存储结果!当然,现在有趣的是,随着数字的流进来,正在进行排序。不确定是否有最佳的解决方案,我们还剩下294908位。我想象一种有趣的技术是在每个点上假设这是整个排序,找到该排序的代码,然后在您收到一个返回新数字并更新先前代码的代码时。手波手波。


这真的是很多挥手。一方面,从理论上讲这是解决方案,因为我们可以编写一个大型的但仍然有限的状态机。另一方面,该大状态机的指令指针的大小可能超过一个兆字节,这使其成为非启动器。要真正解决给定的问题,确实需要比这多得多的思考。我们不仅需要表示所有状态,而且还需要计算在任何给定的下一个输入数字上执行的操作所需的所有过渡状态。
丹尼尔·瓦格纳

4
我认为其他答案只是关于他们的挥手而已。既然我们现在知道结果空间的大小,我们就知道我们绝对需要多少空间。没有其他答案将能够以小于8093700位的任何形式存储每个可能的答案,因为那就是最终状态的数量。进行compress(final-state)有时最多只能减少空间,但是总会有一些答案需要完整的空间(没有压缩算法可以压缩每个输入)。
Francisco Ryan Tolmasky I

无论如何,其他几个答案已经提到了硬性下限(例如原始提问者的答案的第二句话),所以我不确定我是否知道这个答案正在为格式塔添加什么。
丹尼尔·瓦格纳

您是指3.5M存储原始流吗?(如果没有,我表示歉意,请忽略此回复)。如果是这样,那么那是一个完全不相关的下限。我的下限是结果将占用多少空间,下限是如果有必要存储输入,则输入将占用多少空间-假设问题被表述为来自TCP连接的流,尚不清楚您是否确实需要这样做,您可能一次读取一个数字并更新状态,因此不需要3.5M -无论哪种方式,都与该计算正交的是3.5。
Francisco Ryan Tolmasky I 2012年

“从8093729.5的幂中大约有2种选择100万个8位数的数字的方法,允许重复且顺序不重要” <-来自原始提问者的答案。不知道如何更清楚我在说什么。我在最近的评论中特别提到了这句话。
丹尼尔·瓦格纳

20

我的建议在很大程度上归功于Dan的解决方案

首先,我认为解决方案必须处理所有可能的输入列表。我认为,流行的答案并未做出这种假设(IMO是一个巨大的错误)。

众所周知,没有任何形式的无损压缩会减小所有输入的大小。

所有流行的答案都假定他们将能够有效地施加压缩以为其留出更多空间。实际上,有大量的额外空间,它们足以以未压缩的形式保存部分完成的列表的一部分,并允许他们执行排序操作。这只是一个错误的假设。

对于这样的解决方案,任何知道如何进行压缩的人都将能够设计出一些对于该方案无法很好压缩的输入数据,并且“解决方案”很可能会因为空间不足而中断。

相反,我采用数学方法。我们可能的输出是长度为LEN的所有列表,这些列表由范围为0..MAX的元素组成。此处LEN为1,000,000,而我们的MAX为100,000,000。

对于任意LEN和MAX,编码此状态所需的位数为:

Log2(MAX多选LEN)

因此,对于我们的数字,一旦完成接收和排序,我们将至少需要Log2(100,000,000 MC 1,000,000)位,以便可以唯一区分所有可能的输出的方式存储结果。

这是〜= 988kb。因此,我们实际上有足够的空间来保存结果。从这个角度来看,这是可能的。

[现在已经有了更好的例子,删除了毫无意义的闲逛...]

最佳答案在这里

另一个好的答案在这里,基本上使用插入排序作为将列表扩展一个元素的功能(缓冲几个元素和预排序,以允许一次插入多个,节省了一些时间)。也使用了很好的紧凑状态编码,七位增量的存储桶


第二天重新阅读您自己的答案总是很有趣...因此,尽管最重要的答案是错误的,但被接受的一个stackoverflow.com/a/12978097/1763801还是不错的。基本上使用插入排序作为函数来获取列表LEN-1并返回LEN。充分利用以下事实:如果对一小批商品进行预分类,则可以一次性将它们全部插入,以提高效率。状态表示非常紧凑(7位数字的存储桶),比我的手工建议要好,而且更直观。我的
同事

1
我认为您的算术有些差。我得到lg2(100999999!/(99999999!* 1000000!))= 1011718.55
NovaDenizen 2012年

是的,谢谢,它是988kb,而不是965。我在1024和1000方面比较草率。我们还有大约35kb的余地。我在答案中添加了指向数学计算的链接。
davec

18

假设此任务是可能的。在输出之前,将在内存中显示百万个排序的数字。有多少种不同的表示形式?由于可能存在重复的数字,因此我们不能使用nCr(选择),但是有一个称为multichoose的操作适用于多集

  • 2.2e2436455种方式可以选择0..99,999,999范围内的一百万个数字。
  • 这需要8,093,730位来表示每种可能的组合,即1,011,717字节。

因此,从理论上讲,如果您能提出一个合理(足够)的数字排序列表表示,则是有可能的。例如,疯狂的表示可能需要10MB的查询表或数千行代码。

但是,如果“ 1M RAM”表示一百万个字节,则显然没有足够的空间。从理论上说,增加5%的内存使之成为可能,这一事实向我表明,表示必须非常高效,并且可能并非理智。


选择一百万个数字的方法数量(2.2e2436455)接近(256 ^(1024 * 988)),即(2.0e2436445)。太好了,如果您从1M占用了大约32 KB的内存,则无法解决该问题。另外请记住,至少保留了3 KB的内存。
johnwbyrd

当然,这假设数据是完全随机的。据我们所知,是的,但我只是说:)
Thorarin 2012年

表示此数量可能状态的常规方法是采用对数为底2,并报告表示这些状态所需的位数。
NovaDenizen 2012年

@Thorarin,是的,我认为“解决方案”仅适用于某些输入,没有意义。
2012年

12

(我原来的答案是错误的,对不起,数学不好,请参阅下面的中断。)

这个怎么样?

前27位存储您所看到的最低数字,然后将差值存储到所看到的下一个数字,其编码如下:5位存储用于存储差异的位数,然后存储差异。使用00000表示您再次看到该数字。

之所以可行,是因为插入的数字越多,数字之间的平均差异就越小,因此当您添加更多的数字时,使用较少的位来存储差异。我相信这称为增量列表。

我能想到的最坏情况是所有数字均等分布(按100),例如,假设第一个数字为0:

000000000000000000000000000 00111 1100100
                            ^^^^^^^^^^^^^
                            a million times

27 + 1,000,000 * (5+7) bits = ~ 427k

Reddit进行救援!

如果您只需要对它们进行分类,那么此问题将很容易。存储您所看到的数字需要122k(100万比特)(如果看到0,则显示第0位;如果看到2300,则存储第2300位,依此类推。

您读取数字,将其存储在位字段中,然后在保持计数的同时将位移出。

但是,您必须记住您已经看过多少次。上面的子列表答案启发了我提出这个方案:

而不是使用一位,而是使用2或27位:

  • 00表示您没有看到该号码。
  • 01表示您曾经看过
  • 1表示您已看到它,接下来的26位是计数的次数。

我认为这行得通:如果没有重复项,则您的列表为244k。在最坏的情况下,您看到每个数字两次(如果一次看到三个数字,则会缩短列表的其余部分),这意味着您看到了50,000次以上,并且看到了950,000个项目0或1次。

50,000 * 27 + 950,000 * 2 = 396.7k。

如果使用以下编码,则可以进行进一步的改进:

0表示您没有看到数字10表示您曾经看到它11是保持计数的方式

平均而言,这将导致280.7k的存储量。

编辑:我星期天早上的数学是错误的。

最坏的情况是我们两次看到500,000个数字,因此数学公式变为:

500,000 * 27 + 500,000 * 2 = 1.77M

备用编码会导致平均存储

500,000 * 27 + 500,000 = 1.70M

:(


1
哦,不,因为第二个数字是500000
jfernand

也许添加一些中间值,例如11表示您看过该次数最多64次(使用接下来的6位),而11000000表示您使用另外32位存储该次数。
τεκ

10
您从哪里获得“ 100万比特”的数字?您说第2300位代表是否看到2300。(我认为您实际上是2301st。)哪一位代表是否看到过99,999,999(最大的8位数字)?大概是1亿位。
user94559

您得到了一百万,而您却得到一亿。一个值最多可能出现一百万次,并且只需要20位就可以表示一个值出现的次数。同样,您需要1亿个位字段(而不是1百万个),每个可能的值一个。
Tim R.

嗯,27 + 1000000 *(5 + 7)= 12000027位= 1.43M,而不是427K。
Daniel Wagner

10

对于所有可能的输入,都有一个解决此问题的方法。作弊。

  1. 通过TCP读取m个值,其中m接近可以在内存中排序的最大值,可能为n / 4。
  2. 排序250,000(或大约)数字并将其输出。
  3. 重复其他三个季度。
  4. 让接收者合并在处理它们时收到的4个号码列表。(这并不比使用单个列表慢很多。)

7

我会尝试一个树根。如果可以将数据存储在树中,则可以进行有序遍历以传输数据。

我不确定您是否可以将其装入1MB,但我认为值得尝试。


7

您正在使用哪种计算机?它可能没有任何其他“正常”本地存储,但是例如,它具有视频RAM吗?1兆像素x每个像素32位(例如)非常接近您所需的数据输入大小。

(我主要是在旧Acorn RISC PC的内存中询问,如果您选择低分辨率或低色深的屏幕模式,它可能会“借用” VRAM来扩展可用的系统RAM!)。这在只有几MB正常RAM的计算机上非常有用。


1
愿意发表评论,拒绝投票吗?-我只是想扩大问题的明显矛盾(即创造性地作弊;-)
DNA

可能根本没有计算机,因为有关黑客新闻的相关话题提到这曾经是Google的采访问题。
mlvljr 2012年

1
是的-在对问题进行编辑之前,我已经回答,表明这是一个面试问题!
DNA

6

由于基数树利用了“前缀压缩”,因此基数树表示将更接近处理此问题。但是很难想象一个基数树表示形式可以在一个字节中表示单个节点-两个大概是极限。

但是,无论数据如何表示,将其排序后都可以以前缀压缩的形式存储,其中数字10、11和12分别由001b,001b,001b表示,表示增量为1从前一个号码开始。也许10101b代表5的增量,1101001b代表9的增量,依此类推。


6

在10 ^ 8的范围内有10 ^ 6个值,因此平均每100个代码点有一个值。存储从第N个点到第(N + 1)个的距离。重复值的跳跃为0。这意味着该跳跃平均需要不到7位的存储空间,因此其中的一百万将很高兴地适合我们的800万存储位。

需要将这些跳过部分编码为比特流,例如通过霍夫曼编码。插入是通过遍历位流并在新值之后重写来进行的。通过迭代并写出隐含值来输出。出于实用性考虑,它可能想作为10 ^ 4个列表来完成,每个列表覆盖10 ^ 4个代码点(平均100个值)。

可以通过在跳过的长度上假设一个泊松分布(均值=方差= 100)来先验地建立好的随机数据霍夫曼树,但是真实的统计信息可以保留在输入上,并用于生成最佳树来处理病理病例。


5

我有一台具有1M RAM且没有其他本地存储的计算机

作弊的另一种方法:您可以改用非本地(网络)存储(您的问题并不排除这种情况),然后调用可以使用直接基于磁盘的合并排序(或仅足够的RAM来对内存进行排序)的联网服务。只需接受100万个数字),而无需已经给出的(非常精巧的)解决方案。

这可能是作弊,但尚不清楚您是在寻找解决实际问题的解决方案,还是在寻找难题以吸引规则的挑战……如果是后者,那么简单的作弊可能会比复杂的作弊更好。但是“正版”解决方案(正如其他人指出的那样,只能用于可压缩的输入)。


5

我认为解决方案是结合视频编码中的技术,即离散余弦变换。在数字视频中,不是将视频的亮度或颜色的变化记录为常规值(例如110112115116),而是从最后一个值中减去(类似于游程长度编码)。110 112 115 116变为110 2 31。值2 3 1比原始值需要更少的位。

因此,可以说我们在输入值到达套接字时创建了一个列表。我们在每个元素中存储的不是值,而是值在它之前的偏移量。我们按照进行排序,因此偏移量只会是正数。但是偏移量可以是8个十进制数字宽,适合3个字节。每个元素不能为3个字节,因此我们需要打包它们。我们可以将每个字节的高位用作“连续位”,指示下一个字节是数字的一部分,并且每个字节的低7位需要组合。零对重复项有效。

随着列表的填满,数字应该变得更接近,这意味着平均只有1个字节用于确定到下一个值的距离。7位值和1位偏移量(如果方便),但是可能存在一个最佳点,即“连续”值需要少于8位。

无论如何,我做了一些实验。我使用随机数生成器,可以将一百万个排序的8位十进制数字放入大约1279000字节中。每个数字之间的平均间隔始终为99 ...

public class Test {
    public static void main(String[] args) throws IOException {
        // 1 million values
        int[] values = new int[1000000];

        // create random values up to 8 digits lrong
        Random random = new Random();
        for (int x=0;x<values.length;x++) {
            values[x] = random.nextInt(100000000);
        }
        Arrays.sort(values);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        int av = 0;    
        writeCompact(baos, values[0]);     // first value
        for (int x=1;x<values.length;x++) {
            int v = values[x] - values[x-1];  // difference
            av += v;
            System.out.println(values[x] + " diff " + v);
            writeCompact(baos, v);
        }

        System.out.println("Average offset " + (av/values.length));
        System.out.println("Fits in " + baos.toByteArray().length);
    }

    public static void writeCompact(OutputStream os, long value) throws IOException {
        do {
            int b = (int) value & 0x7f;
            value = (value & 0x7fffffffffffffffl) >> 7;
            os.write(value == 0 ? b : (b | 0x80));
        } while (value != 0);
    }
}

4

在拥有所有数字之前,我们可以与网络堆栈一起按顺序发送数字。如果发送1M数据,则TCP / IP会将其分解为1500字节的数据包,然后将其流式传输到目标。每个数据包将被赋予一个序列号。

我们可以手工完成。就在我们填满RAM之前,我们可以对已有的东西进行排序,然后将列表发送到目标,但是在每个数字的序列中留下空缺。然后使用序列中的那些孔以相同的方式处理数字的21/2。

远端的网络堆栈将按顺序将结果数据流组合起来,然后再将其提交给应用程序。

它正在使用网络执行合并排序。这是一个完全的骇客,但我受到先前列出的其他网络黑客的启发。


4

Google的(不好的)方法,来自HN线程。存储RLE样式的计数。

您的初始数据结构为'99999999:0'(全零,没有看到任何数字),然后假设您看到数字3,866,344,因此您的数据结构将变为'3866343:0,1:1,96133654:0'可以看到数字总是在零位数和“ 1”位数之间交替,因此您可以假设奇数代表0位,偶数代表1位。这变成(3866343,1,96133654)

他们的问题似乎并未涵盖重复项,但假设他们对重复项使用“ 0:1”。

大问题#1:1M整数的插入会花费年龄

大问题2:像所有普通增量编码解决方案一样,某些发行版无法用这种方式覆盖。例如,距离为0:99的1m个整数(例如,每个+99)。现在想一想,但随机距离0:99范围内。(注意:99999999/1000000 = 99.99)

Google的方法既不值得(缓慢)又不正确。但是为了辩护,他们的问题可能有所不同。


3

为了表示排序后的数组,可以只存储第一个元素以及相邻元素之间的差。通过这种方式,我们可以对10 ^ 6个元素进行编码,这些元素最多可以累加10 ^ 8个。让我们把这种d。要编码D的元素,可以使用霍夫曼码。霍夫曼代码的字典可以随时创建,并且每次将新项插入已排序的数组(插入排序)时,都会更新该数组。请注意,当字典由于新项而更改时,应更新整个数组以匹配新的编码。

如果我们具有相等数量的每个唯一元素,则用于编码D的每个元素的平均位数将最大化。说元素D1D2,...,牛顿d各出现˚F倍。在那种情况下(在最坏的情况下,输入序列中既有0又有10 ^ 8)

sum(1 <= i <= N Fdi = 10 ^ 8

哪里

sum(1 <= i <= N F = 10 ^ 6或F = 10 ^ 6 / N,归一化频率为p = F / 10 ^ = 1 / N

平均位数为-log2(1 / P)= log2(N)。在这种情况下,我们应该找到使N最大化的情况。如果我们有从0开始的连续数字di,或者di = i -1,则会发生这种情况,因此

10 ^ 8 = sum(1 <= i <= N Fdi = sum(1 <= i <= N(10 ^ 6 / N)(i-1)=(10 ^ 6 / NNN -1)/ 2

N <=201。在这种情况下,平均位数为log2(201)= 7.6511,这意味着每个输入元素大约需要1个字节来保存排序后的数组。请注意,这并不意味着D通常不能包含201个以上的元素。它只是撒播如果D的元素均匀分布,则其唯一值不能超过201。


1
我认为您已经忘记了该号码可以重复。
bestsss 2012年

对于重复的数字,相邻数字之间的差将为零。不会造成任何问题。霍夫曼代码不需要非零值。
Mohsen Nosratinia,2012年

3

我将利用TCP的重传行为。

  1. 使TCP组件创建一个较大的接收窗口。
  2. 接收一定数量的数据包,而不发送ACK。
    • 处理过程以创建一些(前缀)压缩数据结构
    • 为不再需要的最后一个数据包发送重复的ack /等待重传超时
    • 转到2
  3. 所有数据包都被接受

这假定了铲斗或多次通过的某种好处。

可能是通过对批次/存储桶进行分类并合并。->基树

使用此技术来接受和排序前80%的内容,然后读取后20%的内容,并验证后20%所包含的数字不会落在最低数字的前20%中。然后发送最低20%的数字,从内存中删除,接受剩余20%的新数字并合并。**


3

是针对此类问题的通用解决方案:

一般程序

采取的方法如下。该算法在32位字的单个缓冲区上运行。它循环执行以下过程:

  • 我们从一个缓冲区填充了最后一次迭代的压缩数据开始。缓冲区看起来像这样

    |compressed sorted|empty|

  • 计算此缓冲区可以压缩和未压缩的最大数字量。将缓冲区分为两部分,从压缩数据的空间开始,到未压缩的数据结束。缓冲区看起来像

    |compressed sorted|empty|empty|

  • 用要排序的数字填充未压缩的部分。缓冲区看起来像

    |compressed sorted|empty|uncompressed unsorted|

  • 用就地排序对新数字进行排序。缓冲区看起来像

    |compressed sorted|empty|uncompressed sorted|

  • 在压缩部分将上次迭代中已压缩的数据右对齐。此时,缓冲区已分区

    |empty|compressed sorted|uncompressed sorted|

  • 在压缩部分执行流式解压缩-重新压缩,将未压缩部分中的排序数据合并。随着新压缩段的增长,旧的压缩段将被消耗。缓冲区看起来像

    |compressed sorted|empty|

执行此过程,直到所有数字都已排序。

压缩

当然,这种算法仅在有可能在实际知道实际要压缩的内容之前计算出新排序缓冲区的最终压缩大小时才起作用。紧接着,压缩算法必须足够好以解决实际问题。

所使用的方法分三个步骤。首先,该算法将始终存储排序的序列,因此我们可以纯粹存储连续条目之间的差异。每个差异在[0,99999999]范围内。

然后将这些差异编码为一元比特流。此流中的1表示“向累加器加1,A 0表示“将累加器作为条目输入并重置”。因此,差N将由N 1和一个0表示。

所有差异的总和将接近算法支持的最大值,所有差异的计数将接近算法中插入的值的数量。这意味着我们希望流最后包含最大值1和计数0。这使我们能够计算出流中0和1的预期概率。即,0 count/(count+maxval)的概率为,1的概率为maxval/(count+maxval)

我们使用这些概率在此比特流上定义算术编码模型。此算术代码将在最佳空间中精确编码此数量的1和0。我们可以计算出任何中间比特流13759这个模型的空间:bits = encoded * log2(1 + amount / maxval) + maxval * log2(1 + maxval / amount)。要计算算法所需的总空间,请将其设置encoded为数量。

为了不需要大量的迭代,可以将少量开销添加到缓冲区。这将确保算法至少在适合此开销的数量上运行,因为到目前为止,算法的最大时间成本是每个周期的算术编码压缩和解压缩。

紧接着,还需要一些开销来存储簿记数据并处理算术编码算法的定点近似中的细微误差,但总的来说,即使有一个额外的缓冲区可以容纳1MiB,该算法也可以容纳1MiB的空间。 8000个数字,总共1043916字节的空间。

最优性

除了减少算法的(较小)开销外,理论上不可能获得较小的结果。为了仅包含最终结果的熵,将需要1011717字节。如果减去为效率而增加的额外缓冲区,此算法将使用1011916字节来存储最终结果+开销。


2

如果输入流可以被接收几次,这将变得容易得多(没有关于此,思想和时间性能问题的信息)。

然后,我们可以计算十进制值。使用计数值,将很容易生成输出流。通过计算值进行压缩。这取决于输入流中的内容。


1

如果输入流可以被接收几次,这将容易得多(没有关于该信息,想法和时间性能问题的信息)。然后,我们可以计算十进制值。使用计数值,将很容易生成输出流。通过计算值进行压缩。这取决于输入流中的内容。


1

排序是这里的第二个问题。就像其他所说的那样,仅存储整数非常困难,并且不能在所有输入上使用,因为每个数字需要27位。

我对此的看法是:仅存储连续(排序)整数之间的差异,因为它们之间的差异很可能很小。然后使用一种压缩方案,例如每个输入数字具有2个附加位,以对该数字存储在多少位上进行编码。就像是:

00 -> 5 bits
01 -> 11 bits
10 -> 19 bits
11 -> 27 bits

在给定的内存限制内应该可以存储相当数量的可能的输入列表。如何选择压缩方案以使其在最大输入数量上工作的数学知识超出了我的范围。

我希望您也许能够利用您输入的特定领域知识来基于此找到足够好的整数压缩方案

哦,然后,当您接收数据时,您对该排序后的列表进行插入排序。


1

现在针对实际解决方案,仅使用1MB RAM即可覆盖8位范围内所有可能的输入情况。注意:正在进行中,明天将继续。使用排序的整数的增量的算术编码,最坏的情况是1M排序的整数每个条目的成本约为7位(因为99999999/1000000为99,而log2(99)几乎为7位)。

但是,您需要将1m个整数排序以获得7或8位!较短的序列将具有较大的增量,因此每个元素的位数更多。

我正在尝试尽可能多地压缩(几乎)就地压缩。接近25万个整数的第一批最多最多需要9位。因此结果大约需要275KB。重复几次剩余的可用内存。然后对这些压缩的块进行解压缩就地压缩。这是很难的,但是可能的。我认为。

合并的列表将越来越接近每个整数目标7位。但是我不知道合并循环需要进行多少次迭代。也许3。

但是算术编码实现的不精确性可能使其无法实现。如果根本不可能解决这个问题,那将是非常严格的。

有志愿者吗?


算术编码是可行的。可能需要注意,每个连续的增量都是从负二项式分布中得出的。
2012年

1

您只需要按顺序存储数字之间的差异,并使用编码来压缩这些序列号。我们有2 ^ 23位。我们将其分为6位块,并让最后一位指示该数字是否扩展到另外6位(5位加上扩展块)。

因此,000010是1,000100是2。000001100000是128。现在,我们考虑最差的强制转换,以表示最大为10,000,000的数字序列的差异。可以有大于2 ^ 5的10,000,000 / 2 ^ 5差异,大于2 ^ 10的10,000,000 / 2 ^ 10差异和大于2 ^ 15的10,000,000 / 2 ^ 15差异等。

因此,我们添加了代表序列所需的位数。我们有1,000,000 * 6 +汇总(10,000,000 / 2 ^ 5)* 6 +汇总(10,000,000 / 2 ^ 10)* 6 +汇总(10,000,000 / 2 ^ 15)* 6 +汇总(10,000,000 / 2 ^ 20)* 4 = 7935479。

2 ^ 24 =8388608。由于8388608> 7935479,我们应该轻松拥有足够的内存。插入新数字时,我们可能还需要一点内存来存储位置的总和。然后,我们遍历序列,找到在哪里插入我们的新数字,如有必要,减小下一个差异,然后将所有内容右移。


相信在这里的分析表明该方案不起作用(即使我们选择的大小不是五位也不能)。
丹尼尔·瓦格纳

@Daniel Wagner-您不必在每个块中使用统一数量的位数,甚至不必在每个块中使用整数位数。
2012年

@crowding如果您有具体的建议,我想听听。=)
丹尼尔·瓦格纳

@crowding对需要多少空间算术编码进行数学计算。哭了 然后再努力思考。
丹尼尔·瓦格纳

学到更多。右侧中间表示中符号的完整条件分布(Francisco具有最简单的中间表示,Strilanc也是如此)很容易计算。因此,编码模型在字面上可以是完美的,并且可以在熵限制的一比特之内。有限精度算术可能会增加一些位。
2012年

1

如果我们对这些数字一无所知,则受到以下约束的限制:

  • 我们需要先加载所有数字,然后才能对其进行排序,
  • 数字集不可压缩。

如果这些假设成立,那么您将无法执行任务,因为您将至少需要26,575,425位存储空间(3,321,929字节)。

您能告诉我们有关您数据的哪些信息?


1
您将它们读入并按照需要进行排序。理论上,它需要lg2(100999999!/(99999999!* 1000000!))位来将1M不可区分的项目存储在100M专有框中,这相当于1MB的96.4%。
NovaDenizen

1

技巧是将算法状态表示为整数多集,并将其表示为“增量计数器” =“ +”和“输出计数器” =“!”的压缩流。字符。例如,集合{0,3,3,4}将表示为“!+++ !! +!”,后跟任意数量的“ +”字符。要修改多集,您可以流出字符,一次仅保持恒定的解压缩量,然后在以压缩形式流回字符之前进行适当的更改。

细节

我们知道最后一组中恰好有10 ^ 6个数字,因此最多有10 ^ 6个“!” 字符。我们也知道我们的范围的大小为10 ^ 8,这意味着最多有10 ^ 8个“ +”字符。我们可以在10 ^ 8“ +”中排列10 ^ 6“!”的方式有(10^8 + 10^6) choose 10^6,因此指定某种特定的排列需要〜0.965 MiB`的数据。那会很合身。

我们可以将每个角色视为独立角色,而不会超出我们的配额。“ +”字符比“!”多100倍 字符,如果我们忘记它们是从属的,则简化为每个字符100:1的赔率是“ +”。100:101的奇数对应于每个字符约0.08位,总计约0.965 MiB(在这种情况下,忽略依赖项仅花费约12位!)。

存储具有已知先验概率的独立字符的最简单技术是霍夫曼编码。请注意,我们需要一棵不切实际的大树(用于10个字符的块的霍夫曼树每块的平均成本约为2.4位,总计〜2.9 Mib。用于20个字符的块的霍夫曼树每块的平均成本大约3位,总共约1.8 MiB。我们可能需要一个大约100个大小的块,这意味着树中的节点数超过了所有现有计算机设备可以存储的数量。 )。但是,根据问题,ROM在技术上是“免费的”,利用树中规则性的实际解决方案看起来基本相同。

伪码

  • 在ROM中存储足够大的霍夫曼树(或类似的逐块压缩数据)
  • 以10 ^ 8个“ +”字符的压缩字符串开头。
  • 要插入数字N,请输出压缩的字符串,直到N个“ +”字符过去,然后插入“!”。在您进行操作时,将重新压缩的字符串流式传输回上一个字符串,并保持一定数量的缓冲块,以避免过度/不足。
  • 重复一百万次:[输入,流解压缩>插入>压缩],然后解压缩以输出

1
到目前为止,这是我看到的唯一可以实际回答问题的答案!我认为算术编码比霍夫曼编码更简单,因为它省去了存储码本的需要,而不必担心符号边界。您也可以考虑依赖性。
2012年

输入的整数不排序。您需要先排序。
alecco 2012年

1
@alecco该算法对进程进行排序。它们永远不会未经分类存储。
Craig Gidney 2012年

1

我们有1 MB-3 KB RAM = 2 ^ 23-3 * 2 ^ 13位= 8388608-24576 = 8364032位可用。

我们在10 ^ 8的范围内给了10 ^ 6个数字。这给出了〜100 <2 ^ 7 = 128的平均差距

首先,让我们考虑一下所有间隙均小于128时,等间距数字的简单问题。这很容易。只需存储第一个数字和7位间隙:

(27位)+ 10 ^ 6 7位间隙数= 7000027位

请注意,重复数字的间隔为0。

但是,如果我们的差距大于127,该怎么办?

好的,假设直接表示小于127的间隙大小,但是间隙大小127之后是用于实际间隙长度的连续8位编码:

 10xxxxxx xxxxxxxx                       = 127 .. 16,383
 110xxxxx xxxxxxxx xxxxxxxx              = 16384 .. 2,097,151

等等

请注意,此数字表示形式描述了其自己的长度,因此我们知道下一个间隔数字何时开始。

如果间隙小于127,这仍然需要7000027位。

最多可以有(10 ^ 8)/(2 ^ 7)= 781250 23位间隙数,这需要额外的16 * 781,250 = 12,500,000位,这太多了。我们需要更加紧凑和缓慢增加的差距代表。

平均间隙大小为100,因此,如果将它们重新排序为[100,99,101,98,102,...,2,198,1,199,0,200,201,202,...]并为此索引使用密集的二进制斐波那契基本编码,没有成对的零(例如11011 = 8 + 5 + 2 + 1 = 16),其数字以'00'分隔,那么我认为我们可以使间隙表示足够短,但是需要更多分析。


0

在接收流时,请执行以下步骤。

第一设置一些合理的块大小

伪代码的想法:

  1. 第一步将是查找所有重复项,并将它们与计数一起粘贴在字典中,然后将其删除。
  2. 第三步是按其算法步骤的顺序放置存在的数字,并将它们与第一个数字及其步骤放在计数器专用字典中,例如n,n + 1 ...,n + 2、2n,2n + 1, 2n + 2 ...
  3. 开始压缩一些合理的数字范围,例如每1000或10000,剩下的似乎不经常重复的数字。
  4. 如果找到数字,则解压缩该范围,然后将其添加到该范围中,并长时间不压缩。
  5. 否则,只需将该数字添加到字节[chunkSize]

接收流时,请继续前4步。最后一步将是失败,如果超出内存,或者一旦开始收集范围的所有数据,则开始输出结果,方法是开始对范围进行排序并按顺序吐出结果,然后对需要解压缩的结果进行解压缩,然后对它们进行排序你去他们那里。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.