我有一台有1mb内存的电脑,没有其他本地存储。我必须使用它通过TCP连接接受100万个8位十进制数字,对它们进行排序,然后通过另一个TCP连接发送排序的列表。

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

问答来源:

slashdot.org

cleaton.net


当前回答

基数树表示可以接近于处理这个问题,因为基数树利用了“前缀压缩”的优势。但是很难想象一个基树表表法可以在一个字节中表示单个节点——两个可能是极限。

但是,不管数据是如何表示的,一旦它被排序,它就可以以前缀压缩的形式存储,其中数字10、11和12将由001b、001b、001b表示,表示从前一个数字增加1。那么,也许10101b表示增量5,1101001b表示增量9,以此类推。

其他回答

To represent the sorted array one can just store the first element and the difference between adjacent elements. In this way we are concerned with encoding 10^6 elements that can sum up to at most 10^8. Let's call this D. To encode the elements of D one can use a Huffman code. The dictionary for the Huffman code can be created on the go and the array updated every time a new item is inserted in the sorted array (insertion sort). Note that when the dictionary changes because of a new item the whole array should be updated to match the new encoding.

如果每个唯一元素的数量相等,则编码D中每个元素的平均比特数将最大化。比如元素d1 d2…, dN在D中各出现F次。在这种情况下(最坏的情况是输入序列中同时有0和10^8)我们有

sum(1<=i<=N) F. di = 10^8

在哪里

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

平均比特数为-log2(1/P) = log2(N)。在这种情况下,我们应该找到使n最大化的情况,这发生在di从0开始的连续数,或者di= i-1时

10 ^ 8 =(1 < =我< = N) f . di =(1 < =我< = N) (10 ^ 6 / N)(张)= (10 ^ 6 / N) N (N - 1) / 2

i.e.

N <= 201。在这种情况下,平均比特数是log2(201)=7.6511,这意味着我们将需要大约1字节的每个输入元素来保存排序的数组。注意,这并不意味着D一般不能有超过201个元素。它只是说明,如果D的元素是均匀分布的,那么D的唯一值不可能超过201个。

如果我们对这些数字一无所知,我们就会受到以下约束:

我们需要在排序之前加载所有的数字, 这组数字是不可压缩的。

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

你能跟我们说说你的数据吗?

现在的目标是一个实际的解决方案,覆盖所有可能的情况下,输入在8位数范围内,只有1MB的RAM。注:工作正在进行中,明天继续。使用对已排序整型的增量进行算术编码,对于1M个已排序整型,最坏的情况是每个条目花费大约7位(因为99999999/1000000是99,而log2(99)几乎是7位)。

但是你需要将1m个整数排序到7位或8位!级数越短,delta就越大,因此每个元素的比特数就越多。

我正在努力尽可能多地压缩(几乎)在原地。第一批接近250K的整数最多每个需要大约9位。因此结果大约需要275KB。重复使用剩余的空闲内存几次。然后解压缩-就地合并-压缩这些压缩块。这很难,但也是可能的。我认为。

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

但是算术编码实现的不精确性可能使它不可能实现。如果这个问题是可能的,它将是非常紧张的。

有志愿者吗?

谷歌的(坏)方法,从HN线程。存储rle风格的计数。

你的初始数据结构是“99999999:0”(都是零,没有看到任何数字),然后假设你看到了数字3,866,344,那么你的数据结构就变成了“3866343:0,1:1,96133654:0”,你可以看到数字总是在零位数和1位数之间交替,所以你可以假设奇数代表0位,偶数代表1位。这就变成了(3866343,1,96133654)

他们的问题似乎不包括副本,但让我们假设他们使用“0:1”来表示副本。

大问题#1:1M个整数的插入将花费很长时间。

大问题#2:像所有的普通增量编码解决方案一样,一些分布不能用这种方式覆盖。例如,1m整数,距离为0:99(例如,每个整数+99)。现在考虑相同的情况,但随机距离在0:99的范围内。(注:99999999/1000000 = 99.99)

谷歌的方法既不值得(缓慢),也不正确。但要为他们辩护,他们的问题可能略有不同。

解决方案可能只是因为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