事实上,这个问题很有趣,因为它是一个信息论框架的重铸。
给定 20 个数字,您最终将得到 21 个 bin(包括 < 第一个和 > 最后一个)。
对于每个传入号码,您将映射到这 21 个垃圾箱之一。这种映射是通过比较完成的。每次比较都会为您提供 1 位信息(< 或 >= -- 两种状态)。
所以假设传入的数字需要5次比较才能确定它属于哪个bin,那么就相当于用5位来表示那个数字。
我们的目标是尽量减少比较次数!我们有 100 万个数字,每个数字属于 21 个有序的代码字。我们如何做到这一点?
这正是熵压缩问题。
让 a[1],.. a[20] 成为你的 20 个数字。
设 p(n) = pr { 传入号码 < n }。
构建决策树如下。
Step 1.
let i = argmin |p(a[i]) - 0.5|
define p0(n) = p(n) / (sum(p(j), j=0...a[i-1])), and p0(n)=0 for n >= a[i].
define p1(n) = p(n) / (sum(p(j), j=a[i]...a[20])), and p1(n)=0 for n < a[i].
Step 2.
let i0 = argmin |p0(a[i0]) - 0.5|
let i1 = argmin |p1(a[i1]) - 0.5|
等等...
当我们完成时,我们最终得到:
i, i0, i1, i00, i01, i10, i11, etc.
这些 i 中的每一个都为我们提供了比较位置。
所以现在我们的算法如下:
让 u = 输入数字。
if (u < a[i]) {
if (u < a[i0]) {
if (u < a[i00]) {
} else {
}
} else {
if (u < a[i01]) {
} else {
}
}
} else {
similarly...
}
所以我定义了一棵树,而 if 语句正在遍历树。我们也可以把它放到一个循环中,但是用一堆 if 来说明会更容易。
例如,如果您知道您的数据均匀分布在 0 到 2^63 之间,并且您的 20 数字是
0,1,2,3,...19
然后
i = 20 (notice that there is no i1)
i0 = 10
i00 = 5
i01 = 15
i000 = 3
i001 = 7
i010 = 13
i011 = 17
i0000 = 2
i0001 = 4
i0010 = 6
i0011 = 9
i00110 = 8
i0100 = 12
i01000 = 11
i0110 = 16
i0111 = 19
i01110 = 18
好的,基本上,比较如下:
if (u < a[20]) {
if (u < a[10]) {
if (u < a[5]) {
} else {
...
}
} else {
...
}
} else {
return 21
}
所以请注意,我不是在做二进制搜索!我首先检查终点。为什么?
它有 100*((2^63)-20)/(2^63)% 的机会大于 a[20]。这基本上是 99.999999999999999783159565502899% 的机会!
因此,对于具有上述属性的数据集,该算法的预期比较次数为 1!(这比日志日志更好:p)
请注意我在这里所做的是,我基本上使用较少的比较来查找更可能的数字,并使用更多的比较来查找不太可能的数字。例如,数字 18 需要 6 次比较(比二分查找多 1 次);但是,数字 20 到 2^63 只需要 1 次比较。同样的原理也用于无损(熵)数据压缩——使用更少的比特来编码经常出现的代码字。
构建树是一个一次性的过程,您可以在 100 万次之后使用该树。
问题是......这个决策树什么时候变成二分搜索?作业练习!:p 答案很简单。这类似于您无法再压缩文件时。
好的,所以我没有把这个从我的背后拉出来......基础在这里:
http://en.wikipedia.org/wiki/Arithmetic_coding