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

时间:2011-10-07 09:55:09

标签: algorithm data-structures

这是一个谷歌面试问题:

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

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

我回答哈希表和后来的霍夫曼编码,但我的采访者说我没有朝着正确的方向前进。请帮帮我。

可以使用后缀trie帮助吗?

理想情况下,1000个数字存储每个数字需要4个字节,因此总共需要4000个字节来存储1000个数字。在数量上,我希望将存储减少到< 4000字节,这是我的采访者向我解释的。

13 个答案:

答案 0 :(得分:43)

在下文中,我将数字视为整数变量(而不是字符串):

  1. 对数字进行排序。
  2. 将每个数字拆分为前五位数和后五位数。
  3. 前五位数字的数字相同,因此只存储一次。这将需要17位存储空间。
  4. 分别存储每个号码的最后五位数字。这将需要每个数字17位。
  5. 总结一下:前17位是公共前缀,后续1000组17位是按升序存储的每个数字的最后5位数。

    总的来说,我们看到的是1000个数字的2128个字节,或者每个10位数电话号码的17.017个字节。

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

答案 1 :(得分:36)

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

我们首先对电话号码进行排序(全部减少到5位小数)。然后我们计算有多少电话号码,其中包含第一个 m 位的二进制数都是0,第一个 m 位最多有多少个电话号码0 ... 01,对于多少个电话号码,第一个 m 位最多为0 ... 10,等等,最多为第一个 m 的电话号码数em>位是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,我们只有五位小数。这允许我们从第一块内存中删除一些条目:而不是2 ^ m - 1个计数,我们只需要ceil(99999/2 ^ k )。这意味着公式变为

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

令人惊讶的是, k = 9且 k = 10或ceil(10997/8)= 1375字节时,其最小值达到10997。

如果我们想知道某个电话号码是否在我们的集合中,我们首先检查前五个二进制数字是否与我们存储的五位数字相匹配。然后我们将剩余的五位数分成顶部的 m = 7位(也就是说, m - 位数 M ) lower k = 10位(数字 K )。我们现在找到减少电话号码的 a [M-1],其中第一个 m 数字最多 M - 1,并且第一个 m 数字最多 M 的电话号码减少的电话号码 a [M],均来自第一个比特块。我们现在检查第二块内存中的 a [M-1]和 a [M]序列的 k 位到看看我们是否找到 K ;在最坏的情况下,有1000个这样的序列,所以如果我们使用二进制搜索,我们可以在O(log 1000)操作中完成。

用于打印所有1000个数字的伪代码,我在其中访问第一个内存块的 K ' k 位条目 a [K]和第二个内存块的 M ' m -bit条目 b [M](这两个都会需要一些繁琐的操作才能写出来。前五位数字为 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 )的边界情况可能出现问题,但这很容易解决。

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

答案 2 :(得分:22)

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

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

答案 3 :(得分:16)

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

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

搜索将在恒定时间内执行,并且将以线性时间执行打印。

答案 4 :(得分:15)

我之前听说过这个问题(但没有前5位数是相同的假设),最简单的方法是Rice Coding

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

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

编辑:对基数为128的Rice编码进行估算(不是因为它会产生最佳结果,而是因为它更容易计算):

我们将按原样保存第一个值(32位) 剩余的999个值是差异(我们预计它们很小,平均为100)将包含:

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

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

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

答案 5 :(得分:7)

这是Bentley编程珍珠的一个众所周知的问题。

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

要打印所有数字,只需打印设置该位的所有数字 与前缀相连。要搜索给定的数字,请执行必要的操作 用于检查数字的按位表示的算术。

你可以在O(1)中搜索一个数字,由于比特表示,空间效率最大。

HTH Chris。

答案 6 :(得分:5)

为1,000个号码固定存储1073个字节:

此存储方法的基本格式是存储前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's0表示计数。例如,如果我们在一个组中有5个数字,则计数将由111110表示。使用这种方法,如果有1,000个数字,我们最终得到1000个1和782个0,总计 1000 + 782 = 1,782位计数

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

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

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,得到8,580位= 1,073字节。因此,我们的理想存储如下:

  • 团体规模:2 ^ 6 = 64
  • 团体人数:1,562
  • 总存储量:

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

答案 7 :(得分:1)

将此作为一个纯粹的理论问题并留下实现的帮助,最有效的方法是在巨大的索引表中索引所有可能的10000个最后数字集。假设您有超过1000个数字,则需要略多于8000位来唯一标识当前集合。没有更大的压缩可能,因为那样你将有两个被识别为相同状态的集合。

问题在于,您必须将程序中的每个2 ^ 8000集合表示为lut,而google甚至都不具备此功能。

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

在一次采访中,我只会给出这个答案,如果我确定的话,该公司正在寻找一个能够开箱即用的人。否则,这可能会让你看起来像一个没有现实世界关注的理论家。

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

构建实施的步骤:

  1. 取一个大小为100 000 *(1000选择100 000)位的常数数组。是的,我知道这个数组需要的空间比宇宙中的原子多几个。
  2. 将这个大阵列分成每个10万块。
  3. 在每个块存储中,为最后五位数的一个特定组合提供位数组。
  4. 这不是程序,而是一种元程序,它将构建一个现在可以在程序中使用的巨大LUT。计算空间效率时通常不计算程序的常量,因此在进行最终计算时我们不关心这个数组。

    以下是如何使用此LUT:

    1. 当有人给你1000个号码时,你会分别存储前五个数字。
    2. 找出您的数组中哪个块与此集匹配。
    3. 将该组的编号存储在一个8074位编号中(请调用此c)。
    4. 这意味着存储我们只需要8091位,我们在此证明这是最佳编码。然而,找到正确的块需要O(100 000 *(100 000选择1000)),根据数学规则是O(1),但实际上总是需要比宇宙时间更长的时间。

      查找很简单:

      1. 前五位数的条带(剩余数字将被称为n')。
      2. 测试是否匹配
      3. 计算i = c * 100000 + n'
      4. 检查LUT中i的位是否设置为1
      5. 打印所有数字也很简单(实际上需要O(100000)= O(1),因为你总是要检查当前块的所有位,所以我上面错误估算了这一点。)

        我不会称之为“实施”,因为公然无视限制(宇宙的大小和宇宙所存在的时间或地球将存在)。但理论上这是最佳解决方案。对于较小的问题,这实际上可以完成,有时也会完成。例如sorting networks是这种编码方式的一个例子,可以作为递归排序算法的最后一步,以获得更大的加速。

答案 8 :(得分:1)

这相当于存储每个小于100,000的一千个非负整数。我们可以使用像算术编码这样的东西来做到这一点。

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

因此,连续整数之间的差异中的至少一半可以用一个字节表示,并且几乎所有其余的都需要两个字节。一些数字除以16,384之外的差异,需要三个字节,但不能超过61个。那么平均存储量将是每个数字大约12位,或者更少,或者最多1500个字节。

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

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

答案 9 :(得分:1)

只是快速询问我们不想将数字更改为基数36的原因。它可能不会节省太多空间但是肯定会节省搜索时间,因为你会看到很少的话10digts。或者我会根据每个组将它们分成文件。所以我会命名一个文件(111)-222.txt,然后我只会存储适合该组的数字,然后让它们以数字顺序可见,这样我总是可以查看该文件是否退出。在我进行biger搜索之前。或者是正确的我将运行二进制搜索一个文件,看看它是否退出。以及对文件内容的另一个博客搜索

答案 10 :(得分:0)

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

所以我们可以将前5个数字保存为常量,所以暂时不要忘记它们。

65535是可以存储在16位数字中的最多,我们可以拥有的最大数字是99999,这符合第17位数字,最大值为131071。

使用32位数据类型是一种浪费,因为我们只需要1位额外的16位...因此,我们可以定义一个具有布尔(或字符)和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)。

答案 11 :(得分:0)

答案 12 :(得分:0)

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

诀窍是你需要17位才能存储0..99,999的数字范围。但是,在传统的8字节字边界上存储17位是一件麻烦事。这就是为什么他们在不使用32位整数的情况下询问你是否可以在不到4k的时间内完成。

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

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

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

如果它是静态,那么当需要填充数据库时,计算位数&lt; 50,000和位数> = 50,000。分配适当长度的uint16两个数组:一个用于低于50,000的整数,一个用于高一组。在较高数组中存储整数时,减去50,000,当从该数组中读取整数时,添加50,000。现在,您已将2,000个整数存储为2,000个8字节的单词。

构建电话簿将需要两次输入遍历,但查找的平均时间应该是单个阵列的一半。如果查找时间非常重要,你可以使用更多的数组用于更小的范围,但我认为在这些大小的情况下,你的性能限制将从内存中拉出数组,如果没有在你使用这些数据的任何内容上注册空间,2k可能会隐藏到CPU缓存中天。

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

您的数组看起来像:

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

因此,当您在电话簿中查找某个数字时,您将遍历该数组,如果您已达到50,001的值,则开始为数组值添加50,000。

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