存储数千个电话号码的最有效方法


94

这是一个Google面试问题:

大约有数千个电话号码要存储,每个电话号码有10位数字。您可以假设每个千位数字中的前5位数字相同。您必须执行以下操作:搜索是否存在给定号码。b。打印所有号码

什么是最有效的节省空间的方法?

我回答了哈希表,然后回答了霍夫曼编码,但我的面试官说我没有朝着正确的方向前进。请在这里帮助我。

使用后缀特里可以帮助吗?

理想情况下,每个数字存储1000个数字需要占用4个字节,因此总共要存储4000个字节才能存储1000个数字。从数量上讲,我希望将存储空间减少到<4000字节,这就是我的面试官向我解释的内容。


28
我会回答,使用普通数据库,您可以将它们存储为文本,甚至可以存储成千上万,并且查找操作仍然非常快。我建议不要做“聪明”的事情,因为如果他们将来希望支持国际号码,或者如果以“ 0”开头的电话号码开始出现,或者政府决定这样做,则必须重做整个系统。更改电话号码格式,依此类推。
Thomas Bonini 2011年

1
@AndreasBonini:我可能会给出答案,除非我在像Google或Facebook这样的公司进行面试,否则请不要使用现成的解决方案。尽管例如postgres也有尝试,但我不确定这些是否会减少google需要处理的数据吞吐量。
李考2011年

1
@LiKao:请记住,OP特别声明了“大约一千个数字”
Thomas Bonini,2011年

@AndreasBonini:的确,也许也曾经是一个测试,被访者知道正确地解释了这些限制,并据此选择了最佳解决方案。
2011年

4
确实需要定义此问题中的“有效”-哪种方式有效?空间,时间,两者?
马特b

Answers:


36

这是对aix答案的改进。考虑对数据结构使用三个“层”:第一个是前五个数字(17位)的常量;因此从这里开始,每个电话号码只剩下剩余的五位数。我们将剩下的五位数字视为17位二进制整数,并使用一种方法存储这些位的k,使用另一种方法存储17- k = m,最后确定k以最小化所需的空间。

我们首先对电话号码进行排序(所有电话号码均减少为5个小数位)。然后,我们看看有多少电话号码有针对由第一的二进制数位都是0,对于电话多少个号码第一位至多0 ... 01,进行电话多少个号码第一位最多为0 ... 10,以此类推,直到前m位为1 ... 11 的电话号码计数为止-最后一个计数为1000(十进制)。有2 ^ m个这样的计数,每个计数最多为1000。如果我们省略最后一个计数(因为无论如何我们都知道是1000),则可以将所有这些数字存储在(2 ^ m -1)的连续块中* 10位。(10位足以存储小于1024的数字。)

所有(减少的)电话号码的最后k位连续存储在内存中;因此,如果k为7,则该存储块的前7位(位0至6)对应于第一个(减少的)电话号码的后7位,位7至13对应于后7位第二个(减少的)电话号码的编号,等等。这需要1000 * k位,总共需要17 +(2 ^(17- k)-1)* 10 + 1000 * k,对于k = 10 ,其最小值为11287。因此,我们可以将所有电话号码存储在ceil( 11287/8)= 1411个字节。

观察到我们的数字都不能以例如1111111(二进制)开头,这可以节省更多空间,因为以该数字开头的最低数字是130048,而我们只有五个十进制数字。这使我们可以从第一块内存中删除一些条目:我们只需要ceil(99999/2 ^ k)而不是2 ^ m -1个计数。这意味着公式变为

17 + ceil(99999/2 ^ k)* 10 + 1000 * k

对于k = 9和k = 10或ceil(10997/8)= 1375字节,这足以达到其最小值10997 。

如果我们想知道某个电话号码是否在我们的电话机中,请首先检查前五个二进制数字是否与我们存储的五个数字匹配。然后,我们将剩余的五位数字分成高位的m = 7位(即m位数字M)和低位的k = 10位(数字K)。我们现在发现的数量减少电话号码,第一[M-1] 的数字是最多中号 - 1,数 [M]减少的电话号码,其中第一个的数字是最多中号,都来自第一个位块。现在我们之间签一个[M-1]和第一个 [M]的第序列ķ在存储器的第二块的比特,以查看是否发现ķ ; 在最坏的情况下,有1000个这样的序列,因此,如果我们使用二进制搜索,则可以完成O(log 1000)个操作。

下面是用于打印所有1000个数字的伪代码,在这里我 [K]的形式访问第一个存储块的第Kk位条目,以b [M]形式访问第二个存储块的第Mm位条目。(这两者都需要进行一些乏味的写操作)。前五位数字为c

i := 0;
for K from 0 to ceil(99999 / 2^k) do
  while i < a[K] do
    print(c * 10^5 + K * 2^k + b[i]);
    i := i + 1;
  end do;
end do;

对于K = ceil(99999/2 ^ k)的边界情况,也许出了点问题,但这很容易解决。

最后,从熵的角度来看,不可能在少于ceil(log [2](binomial(10 ^ 5,10 ^ 3))的情况下存储全部小于10 ^ 5的10 ^ 3个正整数的子集)=8073。包括前5位数字所需的17,仍然存在10997-8090 = 2907位的间隔。看看是否有更好的解决方案仍然可以相对有效地访问数字,这是一个有趣的挑战!


4
您在此处描述的数据结构实际上只是trie的一个非常有效的版本,它只使用索引所需的很少的内容,而仅使用两个级别。在实践中,很高兴看到它是否可以击败更多级别的人,但是我认为这很大程度上取决于数字的分布(在实际的直播电话中,数字不是完全随机的,而是几乎是随机的)。
2011年

嗨,埃里克(Erik),您可能已经看过其他替代产品,所以请查看我的解决方案。它解决了8,580位的问题,仅比理论最小值低490位。查找单个数字效率不高,但存储空间非常紧凑。
Briguy37

1
我想一个理智的面试官更喜欢答案“特里”而不是“一个复杂的定制数据库”。如果您想展示自己的133t黑客技巧,则可以添加-“如果需要,可以为这种特殊情况制定特定的树算法”。
2011年

嗨,您能解释一下5位数字如何占用17位吗?
Tushar Banne'1

@tushar五位数字编码一个介于00000和99999(含)之间的数字。用二进制表示该数字。2 ^ 17 = 131072,所以17位就足够了,但16位就不够。
Erik P.

43

接下来,我将数字视为整数变量(而不是字符串):

  1. 对数字进行排序。
  2. 将每个数字分为前五位和后五位。
  3. 前五个数字在数字上是相同的,因此只存储一次。这将需要17位存储空间。
  4. 分别存储每个数字的最后五位数字。每个数字需要17位。

回顾一下:前17位是公共前缀,后1000组的17位是每个数字的后五位,以升序存储。

总的来说,我们正在寻找1000个数字的2128个字节,即每10位电话号码的17.017位。

搜索为O(log n)(二进制搜索),完整枚举为O(n)


嗯,空间在哪里?
aioobe 2011年

用于构建特里树的O(n k)相比,用于构建的时间太长(O(log(n)* n k)(k是长度))。空间也不是最优的,因为较长的公共前缀是单独存储的。搜索时间也不是最佳的。对于这样的字符串数据,很容易忘记数字的长度,而数字的长度在搜索中占主导地位。即二进制搜索为O(log(n)* k),而trie只需要O(k)。当k为常数时,可以减少这些表达式,但这将在推理存储字符串的数据结构时显示一个普遍的问题。
2011年

@LiKao:谁说过关于弦的事?我只处理整数变量,所以k无关紧要。
NPE

1
好,那我看错了答案。尽管如此,公共部分仍未存储在一起,因此保留了有关空间效率的观点。对于1000个5位数的数字,将有相当数量的通用前缀,因此减少这些通用前缀将大有帮助。同样在数字的情况下,对于字符串,我们有O(log(n))对O(k),这仍然更快。
2011年

1
@极客:1001组的17位是17017位或2128字节(有一些变化)。
NPE 2015年

22

http://en.wikipedia.org/wiki/Acyclic_deterministic_finite_automaton

我曾经接受采访,他们询问数据结构。我忘记了“数组”。


1
+1绝对是要走的路。在我还是学生的时候,我用另一个名字,图书馆树或词法搜索树或其他东西学到了这个名字(如果有人记得那个旧名字,请告诉我)。
瓦蒙德2011年

6
这不符合4000字节的要求。仅对于指针存储,最坏的情况是您需要1个指针用于1-4级叶子到下一级,10个指针用于5级,100个用于第六级,1000个用于第七,8和9级,这使我们的指针总数达到3114。这至少为指针指向提供了3114个不同的存储位置,这意味着每个指针至少需要12位。12 * 3114 = 37368位= 4671字节> 4000字节,这甚至还没有说明如何表示每个叶子的值!
Briguy37 2011年

16

我可能会考虑使用Trie的某些压缩版本(可能是@Misha建议的DAWG)。

这将自动利用它们都具有共同前缀的事实。

搜索将在固定时间内执行,而打印将在线性时间内执行。


问题是关于最节省空间的数据存储方式。您介意为这种方法提供1000个电话号码需要多少空间的估计吗?谢谢。
NPE

特里的空间最大为O(n * k),其中n是字符串的数量,k是每个字符串的长度。考虑到您不需要8位字符来表示数字,我建议您存储4个十六进制索引十六进制,其余的存储一个。这样,每个数字最多需要17位。因为在任何情况下,使用此编码在各个级别上都有冲突,所以您实际上可以达到此水平。预期我们可以存储1000个数字,因此我们已经可以为第一级冲突总共保存250位。最好在示例数据上测试正确的编码。
2011年

@LiKao,是的,并且要注意,例如1000个数字的后两位数不能超过100个,则trie可能会在最后一级严重折叠。
aioobe 2011年

@aioobe:叶子可能在最后一层塌陷,因为没有孩子。但是,倒数第二个级别的叶子需要2 ^ 10 = 1024状态(每个最后一位可以打开或关闭),因此在这种情况下它是不可约的,因为只有1000个数字。这意味着最坏情况的指针数保持在3114(请参阅我对Misha答案的评论),而所需的叶子数为5 + 10 + 100 + 1000 + 1000 + 10 = 2125,这不会更改每个指针所需的12个字节指针。因此,这仅在仅考虑指针的情况下仍将Trie解决方案设置为4671字节。
Briguy37,2011年

@ Briguy37,不确定我是否收到您的“ 每个最后一位数字可以打开或关闭 ”的参数。所有数字都是10位数字,对不对?
aioobe 2011年

15

我以前曾听说过此问题(但没有相同的前5位数字的假设),最简单的方法是Rice编码

1)由于顺序无关紧要,我们可以对它们进行排序,并只保存连续值之间的差异。在我们的例子中,平均差异为100.000 / 1000 = 100

2)使用莱斯代码(基数128或64)甚至Golomb码(基数100)对差异进行编码。

编辑:以128为底的Rice编码的估计(不是因为它会提供最佳结果,而是因为它更易于计算):

我们将按原样保存第一个值(32位)。
其余999个值是差异(我们希望它们很小,平均100个)将包含:

一元值value / 128(可变位数+ 1位作为终止符)
二进制值value % 128(7位)

我们必须以某种方式估算VBL变量位数的限制(下称它):
下限:考虑到我们是幸运的,并且没有任何差异大于基数(在这种情况下为128)。这将意味着再增加0位。
上限:由于所有小于基数的差异都将以数字的二进制部分进行编码,因此我们需要以一进制编码的最大数字为100000/128 = 781.25(甚至更少,因为我们不希望大多数差异为零)。

因此,结果是32 + 999 *(1 + 7)+变量(0..782)位= 1003 +变量(0..98)字节。


您能否提供有关编码方式和最终尺寸计算的更多详细信息。1101字节或8808位似乎非常接近8091位的理论极限,因此令我感到非常惊讶的是,实际上可以实现类似的功能。
2011年

会不会是32 + 999 * (1 + 7 + variable(0..782))位?999个数字中的每个数字都需要用表示value / 128
柯克·布罗德赫斯特

1
@Kirk:不,如果它们都在5位数范围内。这是因为我们希望所有这些差异的总和(记住,我们对连续值之间的差异进行编码,而不是第一个和第N个值之间的差异)将低于100000(即使在最坏的情况下)
ruslik 2011年

您需要34位而不是32位来表示第一个值(9,999,999,999> 2 ^ 32 = 4,294,967,296)。另外,由于数字是唯一的,因此最大差异将为00000到99001,这将为基数128添加774 1而不是782。因此,基数128的1,000个数字的存储范围是8026-8800位或1004-1100字节。64位基数可提供更好的存储,范围从879-1072字节。
Briguy37 2011年

1
@raisercostin:这是柯克问的。在您的示例中,通过对前两个值之间的20k差进行一次编码,将来最多只能出现80k的最大范围。这将
用掉

7

这是Bentley的《 Programming Pearls》中众所周知的问题。

解决方案:从数字中删除前五个数字,因为每个数字都相同。然后使用按位运算来表示剩余的9999个可能值。您只需要2 ^ 17位即可代表数字。每个位代表一个数字。如果该位置1,则该号码在电话簿中。

要打印所有数字,只需打印设置了该位和前缀的所有数字。要搜索给定的数字,请执行必要的位算法以检查数字的按位表示。

您可以在O(1)中搜索一个数字,并且由于位表示的原因,空间效率最大。

克里斯·HTH


3
对于一组密集的数字,这将是一个好方法。不幸的是,这里的集合非常稀疏:可能的100,000个中只有1,000个数字。因此,这种方法平均每个数字需要100位。请参阅我的答案,以了解仅需要〜17位的替代方案。
NPE

1
打印所有数字所花费的时间是否正比于100,000,而不是1,000?
aioobe,2011年

结合这两个想法,您基本上可以立即获得成功。使用具有100,000个条目的位向量是一种方法,占用大量空间。但是,O(log(n))查找通常太慢(取决于此处的查询数量)。因此,使用分层的位集合进行索引,每个数字最多可以存储17位,同时仍然可以进行O(1)查找。这就是Trie的工作方式。同样,打印时间在Trie的O(n)中,它是从已排序的大小写继承的。
2011年

这不是“最有效的节省空间的方法”。
杰克·伯杰

5

固定存储1073字节的1,000个数字:

此存储方法的基本格式是存储前5位数字,每个组的计数以及每个组中每个数字的偏移量。

前缀:
我们的5位数字前缀占用前17位

分组:
接下来,我们需要找出数字的适当大小分组。让我们尝试每组大约1个号码。由于我们知道要存储大约1000个数字,因此我们将99,999个数字分为1000个部分。如果将组大小选择为100,则会浪费比特,因此让我们尝试使用128位的组大小,该大小可以用7位表示。这使我们有782个小组可以合作。

计数:
接下来,对于782个组中的每个组,我们需要存储每个组中的条目计数。每个组的7位计数将产生7*782=5,474 bits,这非常低效,因为由于我们选择组的方式,所代表的平均数约为1。

因此,取而代之的是,我们对组中的每个数字使用可变大小的计数,前导数字为1,后跟0。因此,如果我们x在组中具有数字,则x 1's后跟一个a 0代表计数。例如,如果我们在一个组中有5个数字,则计数将由表示111110。使用这种方法,如果有1,000个数字,我们最终将得到1000 1和782 0 ,计数总数为1000 + 782 = 1,782位

偏移量:
最后,每个数字的格式将只是每个组的7位偏移量。例如,如果00000和00001是0-127组中的唯一数字,则该组的位将为110 0000000 0000001。假设有1,000个数字,则偏移量将为7,000位

因此,假设1000个数字,我们的最终计数如下:

17 (prefix) + 1,782 (counts) + 7,000 (offsets) = 8,799 bits = 1100 bytes

现在,让我们检查通过四舍五入至128位的分组大小选择是否是分组大小的最佳选择。选择x代表每个组的位数,大小的公式为:

Size in bits = 17 (prefix) + 1,000 + 99,999/2^x + x * 1,000

最小化该方程为的整数值x给出x=6,其产生8580个比特= 1073个字节。因此,我们的理想存储如下:

  • 小组人数:2 ^ 6 = 64
  • 团体人数:1,562
  • 总存储空间:

    1017 (prefix plus 1's) + 1563 (0's in count) + 6*1000 (offsets) = 8,580 bits = 1,073 bytes


1

将其作为纯粹的理论问题并留待实现,唯一的最有效方法是仅对巨大的索引表中的10000个最后一位数字的所有可能集合进行索引。假设您正好有1000个数字,那么您将需要超过8000位才能唯一地标识当前集合。不可能进行更大的压缩,因为这样您将拥有两个标识为相同状态的集合。

问题是,您必须将程序中的2 ^ 8000个集合中的每个都表示为lut,甚至google都无法做到这一点。

查找为O(1),打印所有数字O(n)。插入将是O(2 ^ 8000),理论上是O(1),但实际上是不可用的。

在一次采访中,如果我确定,我只会给出这个答案,即该公司正在寻找能够开箱即用地思考的人。否则,这可能会使您看起来像是没有现实世界顾虑的理论家。

编辑:好的,这是一个“实现”。

构造实现的步骤:

  1. 取大小为100000 *(1000选择100000)位的常量数组。是的,我知道以下事实:该阵列比宇宙中的原子需要几个数量级的空间。
  2. 将这个大数组分成10万个大块。
  3. 在每个块中,存储最后五个数字的一​​个特定组合的位数组。

这不是程序,而是一种元程序,它将构造一个巨大的LUT,现在可以在程序中使用它。在计算空间效率时,通常不计入程序的常量内容,因此在进行最终计算时,我们并不关心此数组。

这是使用此LUT的方法:

  1. 当有人给您1000个数字时,您将前五个数字分开存储。
  2. 找出数组中的哪个块与此集合匹配。
  3. 将组号存储在单个8074位号中(称为c)。

这意味着对于存储,我们只需要8091位,在这里我们证明这是最佳的编码。然而,找到正确的块需要O(100000 *(100000选择1000)),根据数学规则,该时间为O(1),但实际上,其花费的时间总是比宇宙的时间长。

查找很简单:

  1. 前五个数字带(其余数字称为n')。
  2. 测试它们是否匹配
  3. 计算i = c * 100000 + n'
  4. 检查LUT中i处的位是否设置为1

打印所有数字也很简单(实际上需要O(100000)= O(1),因为您始终必须检查当前块的所有位,因此我在上面对此计算有误)。

我不会将其称为“实现”,因为公然无视局限性(宇宙的大小和宇宙的生存时间或地球将会存在)。但是从理论上讲,这是最佳解决方案。对于较小的问题,实际上可以这样做,有时也可以做到。例如,排序网络就是这种编码方式的一个示例,可以用作递归排序算法的最后一步,以大大提高速度。


1
什么是最有效的节省空间的方法?
Sven

1
在计算运行时空间时,可以轻松证明这是最有效的节省空间的方法,因为您只用一个数字即可枚举系统的任何可能状态。没有任何较小的编码可以解决此问题。这个答案的诀窍是,在进行计算时几乎不会考虑程序的大小(尝试找到将这个考虑在内的任何答案,您就会明白我的意思了)。因此,对于任何有大小限制的问题,您都可以始终枚举所有状态,以获得最节省空间的处理方式。
2011年

1

这等效于存储1000个非负整数,每个整数均小于100,000。我们可以使用类似算术编码的方法来执行此操作。

最终,数字将存储在排序列表中。我注意到列表中相邻数字之间的预期差异为100,000 / 1000 = 100,可以用7位表示。在许多情况下,需要超过7位。表示这些不常见情况的一种简单方法是采用utf-8方案,其中一个字节代表7位整数,除非设置了第一位,在这种情况下,读取下一个字节以生成14位整数,除非它的第一位被置位,在这种情况下,下一个字节被读取以表示一个21位整数。

因此,连续整数之间至少有一半的差异可以用一个字节表示,而几乎所有其余的都需要两个字节。几个数字(相差大于16,384)将需要三个字节,但其中不能超过61个字节。这样,平均存储量将约为每个数字12位,或者更少,或者最多1500个字节。

这种方法的缺点是现在检查数字的存在为O(n)。但是,未指定时间复杂度要求。

写完之后,我注意到ruslik已经建议了上面的区别方法,唯一的区别是编码方案。矿山可能更简单但效率更低。


1

只是为了快速询问任何我们不希望将数字更改为基数36的原因。它可能不会节省那么多的空间,但是肯定会节省搜索时间,因为u的查找量要少于10位数。或者我会根据每个组将它们拆分为文件。所以我会命名一个文件(111)-222.txt,然后我只在其中存储适合该组的数字,然后以数字顺序将它们按数字顺序编排,这样我就可以随时查看文件是否退出。在我进行更大的搜索之前。或正确的说,我将对文件进行二进制搜索以查看其是否退出。以及另一个对文件内容的二进制搜索


0

为什么不保持简单?使用结构数组。

因此,我们可以将前5位数字保存为常量,因此暂时不要再输入。

65535是可以存储的最多16位数字,我们可以拥有的最大数字为99999,与第17位数字相匹配,最大为131071。

使用32位数据类型是一种浪费,因为我们只需要多余的16位中的1位...因此,我们可以定义一个具有布尔值(或字符)和16位数字的结构。

假设C / C ++

typedef struct _number {

    uint16_t number;
    bool overflow;
}Number;

该结构仅占用3个字节,我们需要一个1000个数组,因此总共需要3000个字节。我们将总空间减少了25%!

至于存储数字,我们可以做简单的按位数学

overflow = (number5digits & 0x10000) >> 4;
number = number5digits & 0x1111;

反之

//Something like this should work
number5digits = number | (overflow << 4);

要打印所有这些,我们可以在数组上使用一个简单的循环。由于它是一个数组,因此检索特定数字的时间当然是固定的。

for(int i=0;i<1000;i++) cout << const5digits << number5digits << endl;

要搜索数字,我们需要一个排序数组。因此,当数字被保存后,对数组进行排序(我个人选择一个合并排序,O(nlogn))。现在进行搜索,我将采用合并排序方法。拆分数组,看看我们的数字介于哪个数组之间。然后仅在该数组上调用该函数。递归执行此操作,直到找到匹配项并返回索引为止;否则,该索引不存在并显示错误代码。该搜索将非常快,并且最坏的情况还是比O(nlogn)好,因为它绝对比合并排序要花费更少的时间执行(每次仅递归拆分的一侧,而不是两侧:)),这是O(nlogn)。


0

我的解决方案:最佳情况7.025位/数字,最坏情况14.193位/数字,粗略平均值8.551位/数字。流编码,无随机访问。

即使在阅读ruslik的答案之前,我也立即想到对每个数字之间的差异进行编码,因为它很小并且应该相对一致,但是解决方案还必须能够适应最坏的情况。我们有一个100000个数字的空间,其中仅包含1000个数字。在完全统一的电话簿中,每个数字都比前一个数字大100:

55555-12 3 45
55555-12 4 45
55555-12 5 45

如果真是这样,由于它是一个已知的常数,因此需要零存储来对数字之间的差进行编码。不幸的是,数字可能与理想步长100不同。我将对与理想增量100的差进行编码,因此,如果两个相邻的数字相差103,则我将对数字3进行编码;如果两个相邻的数字相差92,则I将编码为-8。我称理想增量为100的增量为“ 方差 ”。

变化范围可以从-99(即两个连续的数字)到99000(整个电话簿由数字00000…00999和最远的数字99999组成),范围为99100个可能值。

我的目标是,如果我遇到更大的差异(像来分配最小的存储进行编码的最常见的差异,扩大存储的Protobufvarint)。我将使用七个位的块,其中六个用于存储,最后使用一个额外的标志位来指示此方差与当前块之后的另一个块一起存储,最多三个块(最多提供三个) 3 * 6 = 18位存储空间,即262144个可能值,比可能的变化数(99100)多。在加高标志之后的每个附加块均具有较高的重要性位,因此第一个块始终具有位0-在图5中,可选的第二块具有位6-11,并且可选的第三块具有位12-17。

单个块提供六位存储空间,可以容纳64个值。我想映射64个最小方差以适合该单个块(即-32至+31的方差),因此我将使用ProtoBuf ZigZag编码,最多使用-99至+98的方差(因为不需要(超出-99的负方差),这时我将切换为常规编码,偏移98:  

差异| 编码值
----------- + ----------------
    0 | 0
   -1 | 1个
    1 | 2
   -2 | 3
    2 | 4
   -3 | 5
    3 | 6
   ... | ...
  -31 | 61
   31 | 62
  -32 | 63
----------- | ---------------- 6位
   32 | 64
  -33 | 65
   33 | 66
   ... | ...
  -98 | 195
   98 | 196
  -99 | 197
----------- | ---------------之字形结尾
   100 | 198
   101 | 199
   ... | ...
  3996 | 4094
  3997 | 4095
----------- | --------------- 12位
  3998 | 4096
  3999 | 4097
   ... | ...
 262045 | 262143
----------- | ---------------- 18位

关于如何将方差编码为位的一些示例,包括指示额外块的标志:

差异| 编码位
----------- + ----------------
     0 | 000000 0
     5 | 001010 0
    -8 | 001111 0
   -32 | 111111 0
    32 | 000000 1 000001 0
   -99 | 000101 1 000011 0
   177 | 010011 1 000100 0
 14444 | 001110 1 100011 1 000011 0

因此,样本电话簿的前三个数字将被编码为如下的位流:

BIN 000101001011001000100110010000011001 000110 1 010110 1 00001 0
PH#55555-12345 55555-12448 55555-12491
POS 1 2 3

最好的情况是,电话簿在某种程度上是均匀分布的,并且没有两个电话号码的方差大于32,因此它将使用每个号码7位加上32位作为起始号码,总计32 + 7 * 999 = 7025位
混合方案,其中800个电话号码的方差适合一个块(800 * 7 = 5600),180个电话号码适合两个块(180 * 2 * 7 = 2520),而19个电话号码适合三个块(20 * 3) * 7 = 399),加上开头的 32位,总计8551位
最坏的情况是,25个数字适合三个块(25 * 3 * 7 = 525位),其余974个数字适合两个块(974 * 2 * 7 = 13636位),外加第一个数字的32位,用于盛大总数是14193位

   编码数量|
 1块| 2块| 3块| 总位数
--------- + ---------- + ---------- + ------------
   999 | 0 | 0 | 7025
   800 | 180 | 19 | 8551
    0 | 974 | 25 | 14193

我可以看到可以执行四个其他优化来进一步减少所需的空间:

  1. 第三个块不需要完整的七个位,它可以仅仅是五个位而没有标志位。
  2. 可以通过数字的初始传递来计算每个块的最佳大小。也许对于某些电话簿,最好让第一个块具有5 + 1位,第二个7 + 1和第三个5 + 1。这将进一步将大小减小到最小6 * 999 + 32 = 6026位,再加上两组三位来存储块1和2的大小(块3的大小是所需的16位的剩余部分),总计6032位!
  3. 相同的初始遍历可以计算出比默认值100更好的预期增量。也许有一本电话簿从55555-50000开始,所以它的号码范围是一半,因此预期增量应该是50。或者也许是非线性的分布(也许是标准偏差)和其他一些最佳预期增量。这将减少典型的方差,并可能允许使用更小的第一块。
  4. 可以在第一遍中进行进一步的分析,以对电话簿进行分区,每个分区都有自己的预期增量和组块大小优化。对于电话簿的某些高度均匀的部分,这将允许较小的第一块大小(减少消耗的位数),对于不均匀的部分将允许较大的块大小(减少在连续标志上浪费的位数)。

0

真正的问题是存储五位数的电话号码之一。

诀窍是您需要17位来存储0..99,999范围内的数字。但是在传统的8字节字边界上存储17位是很麻烦的。这就是为什么他们问您是否可以通过不使用32位整数来在不到4k的时间内完成操作。

问题:所有数字组合都可能吗?

由于电话系统的性质,可能少于65k的可能组合。我以为是,因为我们正在谈论电话号码中的后五个位置,而不是区号或交换前缀。

问题:此列表是静态列表还是需要支持更新?

如果它是static,那么当需要填充数据库时,请对<50,000的位数和> = 50,000的位数进行计数。分配两个阵列uint16适当长度的:一个用于低于50,000的整数,一个用于该组高。将整数存储在较高的数组中时,减去50,000,然后从该数组中读取整数,则加50,000。现在,您已将1,000个整数存储在2,000个8字节字中。

构建电话簿将需要两次输入遍历,但是平均而言,查找应该比使用单个数组进行查找的时间少一半。如果查找时间非常重要,则可以在较小的范围内使用更多的阵列,但我认为在这些大小下,性能限制将是从内存中拉出阵列,并且如果未在使用这些阵列的任何东西上注册空间,则2k可能会藏入CPU缓存中天。

如果它是动态的,则分配一个数组(约1000个)uint16,然后按排序顺序添加数字。将第一个字节设置为50,001,然后将第二个字节设置为适当的空值,例如NULL或65,000。存储数字时,请按排序顺序存储它们。如果数字低于50001,则将其存储 50001标记之前。如果数字是50,001或更大,则将其存储 50,001标记之后,但从存储的值中减去50,000。

您的数组将类似于:

00001 = 00001
12345 = 12345
50001 = reserved
00001 = 50001
12345 = 62345
65000 = end-of-list

因此,当您在电话簿中查找数字时,将遍历数组,如果达到50001的值,则会开始向数组值添加50,000。

这使插入变得非常昂贵,但是查找很容易,而且您在存储上的花费不会超过2k。

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.