题外话:
正如 cmets 中的大佬所说,算法确实是O(n log n)。这恰好与将内循环的复杂度乘以外循环的结果相同,即O(log i) x O(n)。
这可能会让你相信我们可以简单地添加另一个内部循环的迭代来获得(log n)2 部分:
for (int i = 1; i < n; i++) {
int k = i;
while (k >= 1)
k /= 2;
int j = i;
while (j >= 1)
j /= 2;
}
}
但是让我们看看原始复杂度是如何得出的:
(使用 Sterling 近似值)
因此,建议的修改将给出:
这不是我们想要的。
我可以从最近的个人项目中想到的一个例子是半朴素的 KD-tree 构造。伪代码如下:
def kd_recursive_cons (list_points):
if length(list_points) < predefined_threshold:
return new leaf(list_points)
A <- random axis (X, Y, Z)
sort list_points by their A-coordinate
mid <- find middle element in list_points
list_L, list_R <- split list_points at mid
node_L <- kd_recursive_cons(list_L)
node_R <- kd_recursive_cons(list_R)
return new node (node_L, node_R)
end
因此,时间复杂度函数由下式给出:
n log n 部分来自排序。我们显然可以忽略Dn 线性部分,以及常数C。因此:
这是我们想要的。
现在以相同的时间复杂度编写一段更简单的代码。我们可以利用我们在上面推导中得到的总和......
注意到传递给log函数的参数在每个循环中都被除以二,因此我们可以编写代码:
for (int i = 1; i < n; i++) {
for (int k = n; k >= 1; k /= 2) {
int j = k;
while (j >= 1)
j /= 2;
}
}
这看起来像开头提到的“幼稚”但不正确的解决方案,不同之处在于那里的嵌套循环有不同的界限(j 不依赖于k,但k 依赖于@987654346 @ 而不是直接在n)。
编辑:一些数值测试以确认复杂性符合预期:
测试功能代码:
int T(int n) {
int o = 0;
for (int i = 1; i < n; i++)
for (int j = n; j >= 1; j /= 2)
for (int k = j; k >= 1; k /= 2, o++);
return o;
}
数值结果:
n T(n)
-------------------
2 3
4 18
8 70
16 225
32 651
64 1764
128 4572
256 11475
512 28105
1024 67518
2048 159666
4096 372645
8192 860055
16384 1965960
32768 4456312
65536 10026855
131072 22413141
262144 49807170
524288 110100270
然后我绘制了sqrt(T(n) / n) 和n。如果复杂度是正确的,这应该给出一个log(n) 图表,或者如果用对数刻度水平轴绘制,则给出一个直线。
这确实是我们得到的: