2.4.2 对数级算法的性能
酒保和顾客打了一个10 000 美元的赌。酒保说:“我会从1~1 000 000中挑选一个秘密数字,你有20次的机会来猜这个数字。每次猜完,我会告诉你结果是高了、低了,还是猜中了。如果你能在20个问题之内猜到我的秘密数字,我给你10 000美元,否则你给我10 000美元。”你会打这个赌吗?回答当然是应该打,因为你能够稳赢。表2-1给出了范围为1~8的示例场景。表中展示了如何通过一系列的询问,每次将候选数据缩减一半。
表2-1:在1~8之间猜数字的示例行为
酒保的每次回答都可以将数字范围缩减一半,直到剩下最后一个可能的数字。最后的情况会在1 + log2 (n)次询问之后出现,其中log2(x)是计算以2为底数的x的对数。向下取整函数x将数字x向下取整到小于等于x的最大整数。例如,如果酒保选择的数字在1~10,你需要猜测的次数为1 + log2 (10)= 1 + 3.32,即4次。如果需要进一步证实上述公式,可以假设酒保在两个数字中选择一个,那么你需要两次才能保证猜到该数字,即1 + log2 (2) = 1 + 1 = 2。需要注意的是,根据酒保的规则,你必须要说出你猜的数字。
这种方法在1 000 000个数字的时候也同样可行。事实上,例2-1所示的猜数算法能够对于任意范围[low, high]有效,并且在1 +log2 (high-low+1)次内猜测到隐藏的数字n。如果有1 000 000个数字,那么这个算法将在最多1 +log2 (1 000 000)= 1 + 19.93亖最多猜20次(最坏情况)就可以知道是哪个数字。
例2-1:在范围[low, high]之间猜数字的Java代码
// 当n确认在范围[low, high]时,计算需要猜测的次数
public static int turns (int n, int low, int high) {
int turns = 0;
// 如果还有潜在的数字需要猜测,则继续
while (high >= low) {
turns++;
int mid = (low + high)/2;
if (mid == n) {
return turns;
} else if (mid < n) {
low = mid + 1;
} else {
high = mid -1;
}
AI 代码解读
}
return turns;
}
对数级算法是非常高效的,因为它们能够快速收敛得到解。这种算法的成功之处在于可以每次将问题的规模缩减一半。以上的猜数算法最多经过k = 1 + log2 (n)次迭代就可以得到解,在第i次(0在书中接下去的部分中,log(n)均指代以2为底的对数,因此我们会舍弃log2 (n)中的下标。
另外一个展现高效对数级算法的例子是使用二分(bisection)算法求一元方程的根,即求出满足连续型函数f(x) = 0的x。已知有两个初始值a和b,其中f(a)和f(b)正负符号相反,即一个是正数,另一个是负数。在每一步中,算法二分范围[a, b]并计算它的中间点c,以此来决定根所在的半区间。因此,每一轮都会使得c更加近似根值,并将误差值削减一半。
为了求f(x) = xsin(x)-5x -cos(x)的根,已知a = -1,b = 1。算法会逐渐收敛至f(x) = 0,x=-0.189302759即为方程的根(见表2-2)。
表2-2:二分法