我有一台有1mb内存的电脑,没有其他本地存储。我必须使用它通过TCP连接接受100万个8位十进制数字,对它们进行排序,然后通过另一个TCP连接发送排序的列表。
数字列表可能包含重复的,我不能丢弃。代码将放在ROM中,所以我不需要从1 MB中减去我的代码的大小。我已经有了驱动以太网端口和处理TCP/IP连接的代码,它需要2 KB的状态数据,包括1 KB的缓冲区,代码将通过它读取和写入数据。这个问题有解决办法吗?
问答来源:
slashdot.org
cleaton.net
我有一台有1mb内存的电脑,没有其他本地存储。我必须使用它通过TCP连接接受100万个8位十进制数字,对它们进行排序,然后通过另一个TCP连接发送排序的列表。
数字列表可能包含重复的,我不能丢弃。代码将放在ROM中,所以我不需要从1 MB中减去我的代码的大小。我已经有了驱动以太网端口和处理TCP/IP连接的代码,它需要2 KB的状态数据,包括1 KB的缓冲区,代码将通过它读取和写入数据。这个问题有解决办法吗?
问答来源:
slashdot.org
cleaton.net
当前回答
如果数字的范围是有限的(只能有2个8位数,或者只有10个不同的8位数),那么你可以编写一个优化的排序算法。但如果你想对所有可能的8位数进行排序,这在内存那么少的情况下是不可能的。
其他回答
诀窍是将算法状态表示为“增量计数器”=“+”和“输出计数器”=“!”字符的压缩流,这是一个整数多集。例如,集合{0,3,3,4}将被表示为“!+++!!+!”,后面跟着任意数量的“+”字符。要修改多集,您可以输出字符,每次只保持恒定的解压缩量,并在以压缩形式流回之前进行适当的更改。
细节
我们知道最终集合中恰好有10^6个数字,所以最多有10^6个“!”字符。我们还知道我们的范围大小为10^8,这意味着最多有10^8个“+”字符。10^6 "的排列方式!s在10^8 "+"s中的值是(10^8 + 10^6)选10^6,因此指定某种特定的排列需要大约0.965 MiB '的数据。那太紧了。
我们可以独立对待每个角色而不超出我们的配额。“+”字符正好是“!”字符的100倍,如果我们忘记了它们是相互依赖的,那么每个字符是“+”的概率就简化为100:1。100:101的几率对应于每个字符0.08位,对于几乎相同的~0.965 MiB(忽略依赖关系在这种情况下只有~12位的代价!)
The simplest technique for storing independent characters with known prior probability is Huffman coding. Note that we need an impractically large tree (A huffman tree for blocks of 10 characters has an average cost per block of about 2.4 bits, for a total of ~2.9 Mib. A huffman tree for blocks of 20 characters has an average cost per block of about 3 bits, which is a total of ~1.8 MiB. We're probably going to need a block of size on the order of a hundred, implying more nodes in our tree than all the computer equipment that has ever existed can store.). However, ROM is technically "free" according to the problem and practical solutions that take advantage of the regularity in the tree will look essentially the same.
伪代码
Have a sufficiently large huffman tree (or similar block-by-block compression data) stored in ROM Start with a compressed string of 10^8 "+" characters. To insert the number N, stream out the compressed string until N "+" characters have gone past then insert a "!". Stream the recompressed string back over the previous one as you go, keeping a constant amount of buffered blocks to avoid over/under-runs. Repeat one million times: [input, stream decompress>insert>compress], then decompress to output
我认为解决方案是结合视频编码的技术,即离散余弦变换。在数字视频中,不是将视频的亮度或颜色的变化记录为常规值,如110 112 115 116,而是从最后一个中减去每一个(类似于运行长度编码)。110 112 115 116变成110 2 3 1。这些值,2,3 1比原始值需要更少的比特。
So lets say we create a list of the input values as they arrive on the socket. We are storing in each element, not the value, but the offset of the one before it. We sort as we go, so the offsets are only going to be positive. But the offset could be 8 decimal digits wide which this fits in 3 bytes. Each element can't be 3 bytes, so we need to pack these. We could use the top bit of each byte as a "continue bit", indicating that the next byte is part of the number and the lower 7 bits of each byte need to be combined. zero is valid for duplicates.
当列表填满时,数字之间的距离应该越来越近,这意味着平均只有1个字节用于确定到下一个值的距离。7位值和1位偏移(如果方便的话),但可能存在一个“继续”值需要少于8位的最佳点。
总之,我做了一些实验。我使用随机数生成器,我可以将100万个排序过的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);
}
}
如果数字的范围是有限的(只能有2个8位数,或者只有10个不同的8位数),那么你可以编写一个优化的排序算法。但如果你想对所有可能的8位数进行排序,这在内存那么少的情况下是不可能的。
Gilmanov的答案在假设上是非常错误的。它开始基于毫无意义的一百万个连续整数进行推测。这意味着没有差距。这些随机的间隙,不管有多小,真的是一个糟糕的主意。
你自己试试。获得100万个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
解决方案可能只是因为1兆字节和100万字节之间的差异。大约有2的8093729.5次方种不同的方法来选择100万个允许重复的8位数,顺序不重要,所以一台只有100万字节RAM的机器没有足够的状态来表示所有的可能性。但是1M (TCP/IP少2k)是1022*1024*8 = 8372224位,所以解决方案是可能的。
第一部分,初始解
这个方法需要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单例,在子列表中只有一个条目,并且接下来的7位保存它。
子列表至少包含两个不同的数字。除了最后一个条目小于或等于第一个条目外,条目以非递减顺序存储。这允许识别子列表的结尾。例如,数字2,4,6将被存储为(4,6,2)。数字2,2,3,4,4将被存储为(2,3,4,2)。
子列表包含单个数字的2个或更多重复。接下来的7位给出数字。然后是0个或多个值为1的7位条目,后面是一个值为0的7位条目。子列表体的长度决定了重复的次数。例如,数字12,12将存储为(12,0),数字12,12,12将存储为(12,1,0),数字12,12,12,12将存储为(12,1,1,0),以此类推。
我从一个空列表开始,读入一堆数字并将它们存储为32位整数,对新数字进行排序(可能使用heapsort),然后将它们合并到一个新的紧凑排序列表中。重复该操作,直到不再需要读取数字为止,然后再次遍历紧凑列表以生成输出。
下面的行表示列表合并操作开始前的内存。“O”是存放已排序的32位整数的区域。“X”是存放旧紧凑列表的区域。“=”符号是紧凑列表的扩展空间,“O”中的每个整数对应7位。“Z”是其他随机的开销。
ZZZOOOOOOOOOOOOOOOOOOOOOOOOOO==========XXXXXXXXXXXXXXXXXXXXXXXXXX
合并例程从最左边的“O”和最左边的“X”开始读取,并从最左边的“=”开始写入。直到所有的新整数被合并,写指针才会捕获紧凑列表的读指针,因为这两个指针为每个子列表前进2位,为旧紧凑列表中的每个条目前进7位,并且有足够的额外空间容纳新数字的7位条目。
第二部分,把它塞进1M
为了将上面的解决方案压缩到1M,我需要使紧凑列表的格式更紧凑一点。我将去掉其中一个子列表类型,这样就只有3个不同的子列表头值。然后我可以使用“00”,“01”和“1”作为子列表头值,并节省一些比特。子列表类型为:
空子列表,后面什么都没有。
B单例,在子列表中只有一个条目,接下来的7位保存它。
子列表至少包含2个不同的数字。除了最后一个条目小于或等于第一个条目外,条目以非递减顺序存储。这允许识别子列表的结尾。例如,数字2,4,6将被存储为(4,6,2)。数字2,2,3,4,4将被存储为(2,3,4,2)。
子列表由单个数字的2个或2个以上的重复组成。
我的3个子列表头值将是“A”,“B”和“C”,所以我需要一种方法来表示d类型的子列表。
Suppose I have the C-type sublist header followed by 3 entries, such as "C[17][101][58]". This can't be part of a valid C-type sublist as described above, since the third entry is less than the second but more than the first. I can use this type of construct to represent a D-type sublist. In bit terms, anywhere I have "C{00?????}{1??????}{01?????}" is an impossible C-type sublist. I'll use this to represent a sublist consisting of 3 or more repetitions of a single number. The first two 7-bit words encode the number (the "N" bits below) and are followed by zero or more {0100001} words followed by a {0100000} word.
For example, 3 repetitions: "C{00NNNNN}{1NN0000}{0100000}", 4 repetitions: "C{00NNNNN}{1NN0000}{0100001}{0100000}", and so on.
That just leaves lists that hold exactly 2 repetitions of a single number. I'll represent those with another impossible C-type sublist pattern: "C{0??????}{11?????}{10?????}". There's plenty of room for the 7 bits of the number in the first 2 words, but this pattern is longer than the sublist that it represents, which makes things a bit more complex. The five question-marks at the end can be considered not part of the pattern, so I have: "C{0NNNNNN}{11N????}10" as my pattern, with the number to be repeated stored in the "N"s. That's 2 bits too long.
我将不得不借2位,然后从这个模式中4位未使用的位中还钱。读取时,遇到“C{0NNNNNN}{11N00AB}10”时,输出“N”中数字的2个实例,用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 + 8 = 40位的空闲空间需要将每个32位数字放在列表缓冲区的顶部,然后排序和合并。在最坏的情况下,更改子列表报头映射将导致占用2*781250 + 7*entries - 781250/3位的空间。
如果策略是在列表中至少有800000个数字的情况下,每5次合并后更改子列表头映射,那么最坏的情况下运行将涉及大约30M的紧凑列表读写活动。
来源:
http://nick.cleaton.net/ramsortsol.html