这是一个谷歌面试问题:
每个电话号码大约有1000个,每个都有10个数字。您可以假设每个数字的前5位数字相同。您必须执行以下操作: 一个。搜索是否存在给定数字。 湾打印所有数字
最有效的节省空间的方法是什么?
我回答哈希表和后来的霍夫曼编码,但我的采访者说我没有朝着正确的方向前进。请帮帮我。
可以使用后缀trie帮助吗?
理想情况下,1000个数字存储每个数字需要4个字节,因此总共需要4000个字节来存储1000个数字。在数量上,我希望将存储减少到< 4000字节,这是我的采访者向我解释的。
答案 0 :(得分:43)
在下文中,我将数字视为整数变量(而不是字符串):
总结一下:前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)
答案 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's
由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位。
因此我们假设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字节。因此,我们的理想存储如下:
总存储量:
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),但实际上是不可用的。
在一次采访中,我只会给出这个答案,如果我确定的话,该公司正在寻找一个能够开箱即用的人。否则,这可能会让你看起来像一个没有现实世界关注的理论家。
编辑:好的,这是一个“实施”。
构建实施的步骤:
这不是程序,而是一种元程序,它将构建一个现在可以在程序中使用的巨大LUT。计算空间效率时通常不计算程序的常量,因此在进行最终计算时我们不关心这个数组。
以下是如何使用此LUT:
这意味着存储我们只需要8091位,我们在此证明这是最佳编码。然而,找到正确的块需要O(100 000 *(100 000选择1000)),根据数学规则是O(1),但实际上总是需要比宇宙时间更长的时间。
查找很简单:
打印所有数字也很简单(实际上需要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。