这个早期的问题讨论了一些可能导致算法具有 O(log n) 复杂度的因素。
什么会导致算法具有时间复杂度 O(log log n)?
O(log log n)项可能出现在不同的地方,但通常有两条主要路线会到达这个运行时间。
如链接问题的回答中所提到的,算法具有O(log n)的时间复杂度的一种常见方式是让算法在每次迭代中将输入大小按某个常数因子重复缩小。 如果是这种情况,算法必须在O(log n)次迭代后终止,因为经过O(log n)次常量除法之后,算法必须将问题大小缩小到0或1。 这就是为什么二分查找具有O(log n)复杂度的原因。
有趣的是,还有一种缩小问题大小的类似方式可以得到形式为O(log log n)的运行时间。 在每一层取输入的平方根会发生什么?
例如,让我们取数字65,536。 我们需要将其除以2多少次才能将其减小到1? 如果这样做,我们得到
这个过程需要16步,也就是说65,536 = 216。
但是,如果我们在每一层上取平方根,我们会得到
注意,只需四个步骤就可以将其降至2。为什么会这样呢?
首先,直观的解释。数字n和√n中有多少位数字?数字n中大约有log n位数字,而√n中大约有log (√n) = log (n1/2) = (1/2) log n位数字。这意味着,每次取平方根,你大致上是将数字位数减半。因为你只能将一个数量k减半O(log k)次,然后它就会降至某个常数(比如2),这意味着在将数字降至某个常数之前,你只能取平方根O(log log n)次。
现在,让我们进行一些数学运算,使其更加严谨。让我们用二的幂来重新表达上面的序列:
这里有一个现实世界中的例子,van Emde Boas 树(vEB树)数据结构。vEB树是一种专门用于存储0至N-1范围内整数的数据结构。它的工作方式如下:树的根节点有√N个指针,在0至N-1范围内分割成大约√N个桶,每个桶包含大约√N个整数的范围。然后,每个桶都被内部细分为大约√(√N)个桶,每个桶包含大约√(√N)个元素。要遍历这棵树,你需要从根节点开始,确定自己属于哪个桶,然后在适当的子树中进行递归操作。由于vEB树的结构,您可以在O(1)时间内确定要进入的子树,因此经过O(log log N)步骤后,您将到达树的底部。相应地,在vEB树中查找只需要O(log log N)的时间。
另一个例子是Hopcroft-Fortune closest pair of points algorithm,该算法试图在一组2D点中找到两个最接近的点。它通过创建一个桶的网格并将点分配到这些桶中来工作。如果在算法的任何时刻发现一个桶中有超过√N个点,则递归处理该桶。递归的最大深度因此为O(log log n),并且使用递归树的分析可以证明树中的每一层都需要做O(n)的工作。因此,该算法的总运行时间为O(n log log n)。还有一些算法可以通过在大小为O(log n)的对象上使用类似于二分查找的算法来实现O(log log n)的运行时间。例如,x-fast trie数据结构在高度为O(log U)的树层上执行二分查找,因此其某些操作的运行时间为O(log log U)。相关的y-fast trie通过维护每个包含O(log U)个节点的平衡BST来获得它的O(log log U)运行时间,允许在这些树中进行搜索时间为O(log log U)。tango tree和相关的multisplay tree数据结构由于维护包含O(log n)项的树而在其分析中出现了一个 O(log log n) 项。
其他算法以其他方式实现了O(log log n)的运行时间。插值搜索可望在排序数组中找到一个数字的期望运行时间为O(log log n),但分析相当复杂。最终,分析的结果表明迭代次数等于n2-k ≤ 2的数量k,其中log log n是正确的解决方案。一些算法,如Cheriton-Tarjan MST算法,通过解决复杂的约束优化问题来获得涉及O(log log n)的运行时间。
在时间复杂度中看到O(log log n)因子的一种方法是通过类似于其他答案中解释的除法,但是还有另一种方法可以看到这个因子,当我们想要在算法的时间和空间/时间和逼近/时间和难度等方面进行权衡,并且我们的算法上有一些人为的迭代时。
例如,单源最短路径(SSSP)在平面图上具有O(n)算法,但在那个复杂的算法之前,有一个更简单的算法(但仍然相当困难),其运行时间为O(n log log n),算法的基础如下(只是非常粗略的描述,我建议跳过理解这部分并阅读答案的其他部分):
但我的观点是,这里我们选择的分割大小为O(log n/(log log n))。如果我们选择其他分割,如O(log n/(log log n)^2),可能会更快地运行并带来另一个结果。我的意思是,在许多情况下(例如近似算法、随机化算法或像上面的SSSP算法),当我们迭代某些东西(子问题、可能的解决方案等)时,我们选择与我们拥有的交易相对应的迭代次数(时间/空间/算法复杂度/算法的常数因子等)。因此,在真正的工作算法中,我们可能会看到比“log log n”更复杂的东西。