从1向上列出结果,
rounds(1) = 1
rounds(2) = 1 + rounds(2/2) = 1 + 1 = 2
接下来,当ceil(n/2) 为 2 时,rounds(n) 将为 3。这是 n = 3 和 n = 4。
rounds(3) = rounds(4) = 3
那么,当ceil(n/2) 为3 或4 时,结果将为4。3 <= ceil(n/2) <= 4 发生当且仅当2*3-1 <= n <= 2*4,所以
round(5) = ... = rounds(8) = 4
继续,你可以看到
rounds(n) = k+2 if 2^k < n <= 2^(k+1)
通过归纳。
你可以把它改写成
rounds(n) = 2 + floor(log_2(n-1)) if n > 1 [and rounds(1) = 1]
在数学上,您还可以通过将n = 1 重写为来统一对待它
rounds(n) = 1 + floor(log_2(2*n-1))
如果您使用的是固定宽度类型,最后一个公式可能会溢出。
所以问题是
- 你能以多快的速度将一个数字与 1 进行比较,
- 一个数字减 1 的速度有多快,
- 您能以多快的速度计算正整数的以 2 为底的对数(下限)?
对于固定宽度类型,即有界范围,所有这些当然都是 O(1) 操作,但是您可能仍然对使其尽可能高效感兴趣,即使计算复杂度没有进入游戏。
对于本机机器类型 - int 和 long 通常是 - 比较和减去整数是非常快速的机器指令,因此唯一可能有问题的是以 2 为底的对数。
许多处理器都有一个机器指令来计算机器类型值的前导 0 位,如果编译器可以访问它,您将获得以 2 为底的对数的非常快速的实现。如果没有,您可以获得比使用one of the classic bit-hacks 递归更快的版本。
例如,足够新的 gcc 和 clang 版本有一个 __builtin_clz(对应于 64 位类型的 __builtin_clzl)映射到处理器上存在的 bsr* 指令,并且可能是一个很好的如果处理器不提供,则使用一些位旋转来实现。
版本
unsigned rounds(unsigned long n) {
if (n <= 1) return n;
return sizeof n * CHAR_BIT + 1 - __builtin_clzl(n-1);
}
使用bsrq 指令需要(在我的盒子上)0.165 秒来计算 rounds 1 到 100,000,000,bit-hack
unsigned rounds(unsigned n) {
if (n <= 1) return n;
--n;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
n -= (n >> 1) & 0x55555555;
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
n = (n & 0x0F0F0F0F) + ((n >> 4) & 0x0F0F0F0F);
return ((n * 0x01010101) >> 24)+1;
}
耗时 0.626 秒,简单循环
unsigned rounds(unsigned n) {
unsigned r = 1;
while(n > 1) {
++r;
n = (n+1)/2;
}
return r;
}
需要 1.865 秒。
如果你不使用固定宽度的类型,而是使用任意精度的整数,情况会有所改变。天真的循环(或递归)仍然使用Θ(log n) 步骤,但这些步骤平均花费Θ(log n) 时间(或更糟),所以总的来说你有一个Θ(log² n) 算法(或更糟)。那么使用上面的公式不仅可以提供一种具有较低常数因子的实现,而且可以提供一种算法复杂度较低的实现。
- 比较 1 可以在恒定时间内完成合适的表示,
O(log n) 是合理表示的最坏情况。
- 从一个正整数中减去 1 会得到
O(log n) 的合理表示。
- 对于某些表示,可以在恒定时间内计算以 2 为底的对数,对于其他合理的表示,可以在
O(log n) 中完成 [如果它们使用 2 的幂,所有任意精度库我对做半熟悉;如果他们使用 10 次方基数,那就不一样了]。