简体   繁体   English

什么会导致算法具有 O(log log n) 复杂度?

[英]What would cause an algorithm to have O(log log n) complexity?

This earlier question addresses some of the factors that might cause an algorithm to have O(log n) complexity. 这个较早的问题解决了一些可能导致算法具有 O(log n) 复杂度的因素。

What would cause an algorithm to have time complexity O(log log n)?什么会导致算法的时间复杂度为 O(log log n)?

O(log log n) terms can show up in a variety of different places, but there are typically two main routes that will arrive at this runtime. O(log log n) 项可以出现在各种不同的地方,但通常有两条主要路线会到达此运行时。

Shrinking by a Square Root按平方根缩小

As mentioned in the answer to the linked question, a common way for an algorithm to have time complexity O(log n) is for that algorithm to work by repeatedly cut the size of the input down by some constant factor on each iteration.正如在对链接问题的回答中提到的,算法具有时间复杂度 O(log n) 的一种常见方法是让该算法通过在每次迭代中将输入的大小反复减少某个常数因子来工作。 If this is the case, the algorithm must terminate after O(log n) iterations, because after doing O(log n) divisions by a constant, the algorithm must shrink the problem size down to 0 or 1. This is why, for example, binary search has complexity O(log n).如果是这种情况,算法必须在 O(log n) 次迭代后终止,因为在除以常数 O(log n) 之后,算法必须将问题大小缩小到 0 或 1。这就是为什么,例如,二分查找的复杂度为 O(log n)。

Interestingly, there is a similar way of shrinking down the size of a problem that yields runtimes of the form O(log log n).有趣的是,有一种类似的方法可以缩小问题的规模,从而产生 O(log log n) 形式的运行时。 Instead of dividing the input in half at each layer, what happens if we take the square root of the size at each layer?如果我们每层大小的平方根,而不是在每一层将输入分成两半,会发生什么?

For example, let's take the number 65,536.例如,让我们以数字 65,536 为例。 How many times do we have to divide this by 2 until we get down to 1?在我们得到 1 之前,我们必须将其除以 2 多少次? If we do this, we get如果我们这样做,我们会得到

  • 65,536 / 2 = 32,768 65,536 / 2 = 32,768
  • 32,768 / 2 = 16,384 32,768 / 2 = 16,384
  • 16,384 / 2 = 8,192 16,384 / 2 = 8,192
  • 8,192 / 2 = 4,096 8,192 / 2 = 4,096
  • 4,096 / 2 = 2,048 4,096 / 2 = 2,048
  • 2,048 / 2 = 1,024 2,048 / 2 = 1,024
  • 1,024 / 2 = 512 1,024 / 2 = 512
  • 512 / 2 = 256 512 / 2 = 256
  • 256 / 2 = 128 256 / 2 = 128
  • 128 / 2 = 64 128 / 2 = 64
  • 64 / 2 = 32 64 / 2 = 32
  • 32 / 2 = 16 32 / 2 = 16
  • 16 / 2 = 8 16 / 2 = 8
  • 8 / 2 = 4 8 / 2 = 4
  • 4 / 2 = 2 4 / 2 = 2
  • 2 / 2 = 1 2 / 2 = 1

This process takes 16 steps, and it's also the case that 65,536 = 2 16 .这个过程需要16步,65,536 = 2 16也是如此。

But, if we take the square root at each level, we get但是,如果我们在每个级别上取平方根,我们会得到

  • √65,536 = 256 √65,536 = 256
  • √256 = 16 √256 = 16
  • √16 = 4 √16 = 4
  • √4 = 2 √4 = 2

Notice that it only takes four steps to get all the way down to 2. Why is this?请注意,只需要四个步骤就可以一直降到 2。这是为什么呢?

First, an intuitive explanation.首先,直观的解释。 How many digits are there in the numbers n and √n?数字n和√n有多少位数字? There are approximately log n digits in the number n, and approximately log (√n) = log (n 1/2 ) = (1/2) log n digits in √n.数 n 中大约有 log n 位,而 √n 中大约有 log (√n) = log (n 1/2 ) = (1/2) log n 位。 This means that, each time you take a square root, you're roughly halving the number of digits in the number.这意味着,每次取平方根时,数字中的位数大致减半。 Because you can only halve a quantity k O(log k) times before it drops down to a constant (say, 2), this means you can only take square roots O(log log n) times before you've reduced the number down to some constant (say, 2).因为您只能将数量 k O(log k) 次减半,然后才能下降到一个常数(例如,2),这意味着您只能在将数量减少之前取平方根 O(log log n) 次到一些常数(例如,2)。

Now, let's do some math to make this rigorous.现在,让我们做一些数学计算,使这个严格。 Le'ts rewrite the above sequence in terms of powers of two:让我们用二的幂重写上面的序列:

  • √65,536 = √2 16 = (2 16 ) 1/2 = 2 8 = 256 √65,536 = √2 16 = (2 16 ) 1/2 = 2 8 = 256
  • √256 = √2 8 = (2 8 ) 1/2 = 2 4 = 16 √256 = √2 8 = (2 8 ) 1/2 = 2 4 = 16
  • √16 = √2 4 = (2 4 ) 1/2 = 2 2 = 4 √16 = √2 4 = (2 4 ) 1/2 = 2 2 = 4
  • √4 = √2 2 = (2 2 ) 1/2 = 2 1 = 2 √4 = √2 2 = (2 2 ) 1/2 = 2 1 = 2

Notice that we followed the sequence 2 16 → 2 8 → 2 4 → 2 2 → 2 1 .请注意,我们遵循序列 2 16 → 2 8 → 2 4 → 2 2 → 2 1 On each iteration, we cut the exponent of the power of two in half.在每次迭代中,我们将 2 的幂的指数减半。 That's interesting, because this connects back to what we already know - you can only divide the number k in half O(log k) times before it drops to zero.这很有趣,因为这与我们已经知道的有关 - 在它降为零之前,您只能将数字 k 除以 O(log k) 次的一半。

So take any number n and write it as n = 2 k .因此,取任何数字 n 并将其写为 n = 2 k Each time you take the square root of n, you halve the exponent in this equation.每次取 n 的平方根,就将这个方程中的指数减半。 Therefore, there can be only O(log k) square roots applied before k drops to 1 or lower (in which case n drops to 2 or lower).因此,在 k 降至 1 或更低(在这种情况下 n 降至 2 或更低)之前,只能应用 O(log k) 平方根。 Since n = 2 k , this means that k = log 2 n, and therefore the number of square roots taken is O(log k) = O(log log n).由于 n = 2 k ,这意味着 k = log 2 n,因此平方根的数量是 O(log k) = O(log log n)。 Therefore, if there is algorithm that works by repeatedly reducing the problem to a subproblem of size that is the square root of the original problem size, that algorithm will terminate after O(log log n) steps.因此,如果有算法通过将问题重复地减少到大小为原始问题大小的平方根的子问题来工作,则该算法将在 O(log log n) 步后终止。

One real-world example of this is the van Emde Boas tree (vEB-tree) data structure.一个真实的例子是van Emde Boas 树(vEB-tree) 数据结构。 A vEB-tree is a specialized data structure for storing integers in the range 0 ... N - 1. It works as follows: the root node of the tree has √N pointers in it, splitting the range 0 ... N - 1 into √N buckets each holding a range of roughly √N integers. vEB-tree 是一种特殊的数据结构,用于存储 0 ... N - 1 范围内的整数。它的工作原理如下:树的根节点有 √N 个指针,将范围 0 ... N - 1 到 √N 个桶中,每个桶包含大约 √N 个整数的范围。 These buckets are then each internally subdivided into √(√ N) buckets, each of which holds roughly √(√ N) elements.然后这些桶在内部被细分为 √(√ N) 个桶,每个桶包含大约 √(√ N) 个元素。 To traverse the tree, you start at the root, determine which bucket you belong to, then recursively continue in the appropriate subtree.要遍历树,您从根开始,确定您属于哪个桶,然后在适当的子树中递归继续。 Due to the way the vEB-tree is structured, you can determine in O(1) time which subtree to descend into, and so after O(log log N) steps you will reach the bottom of the tree.由于 vEB 树的结构方式,您可以在 O(1) 时间内确定下降到哪个子树,因此在 O(log log N) 步之后您将到达树的底部。 Accordingly, lookups in a vEB-tree take time only O(log log N).因此,在 vEB 树中查找只需要 O(log log N) 的时间。

Another example is the Hopcroft-Fortune closest pair of points algorithm .另一个例子是Hopcroft-Fortune 最近点对算法 This algorithm attempts to find the two closest points in a collection of 2D points.该算法尝试在二维点集合中找到两个最近的点。 It works by creating a grid of buckets and distributing the points into those buckets.它的工作原理是创建一个桶网格并将点分布到这些桶中。 If at any point in the algorithm a bucket is found that has more than √N points in it, the algorithm recursively processes that bucket.如果在算法中的任何一点发现桶中的点数超过 √N,则算法会递归处理该桶。 The maximum depth of the recursion is therefore O(log log n), and using an analysis of the recursion tree it can be shown that each layer in the tree does O(n) work.因此递归的最大深度是 O(log log n),并且使用对递归树的分析可以证明树中的每一层都执行 O(n) 的工作。 Therefore, the total runtime of the algorithm is O(n log log n).因此,该算法的总运行时间为 O(n log log n)。

O(log n) Algorithms on Small Inputs O(log n) 小输入算法

There are some other algorithms that achieve O(log log n) runtimes by using algorithms like binary search on objects of size O(log n).还有一些其他算法通过使用诸如对大小为 O(log n) 的对象进行二进制搜索之类的算法来实现 O(log log n) 运行时。 For example, the x-fast trie data structure performs a binary search over the layers of at tree of height O(log U), so the runtime for some of its operations are O(log log U).例如, x-fast trie数据结构在高度为 O(log U) 的树的层上执行二分查找,因此其某些操作的运行时间为 O(log log U)。 The related y-fast trie gets some of its O(log log U) runtimes by maintaining balanced BSTs of O(log U) nodes each, allowing searches in those trees to run in time O(log log U).相关的y-fast树通过维护每个 O(log U) 节点的平衡 BST 来获得一些 O(log log U) 运行时,允许在这些树中的搜索在 O(log log U) 时间内运行。 The tango tree and related multisplay tree data structures end up with an O(log log n) term in their analyses because they maintain trees that contain O(log n) items each.探戈树和相关的多重播放树数据结构在他们的分析中以 O(log log n) 项结束,因为它们维护的树每个都包含 O(log n) 项。

Other Examples其他例子

Other algorithms achieve runtime O(log log n) in other ways.其他算法以其他方式实现运行时间 O(log log n)。 Interpolation search has expected runtime O(log log n) to find a number in a sorted array, but the analysis is fairly involved.插值搜索预计运行时间为 O(log log n) 以在排序数组中找到一个数字,但分析相当复杂。 Ultimately, the analysis works by showing that the number of iterations is equal to the number k such that n 2 -k ≤ 2, for which log log n is the correct solution.最终,分析的工作原理是显示迭代次数等于 k ​​次数,使得 n 2 -k ≤ 2,其中 log log n 是正确解。 Some algorithms, like the Cheriton-Tarjan MST algorithm , arrive at a runtime involving O(log log n) by solving a complex constrained optimization problem.一些算法,如Cheriton-Tarjan MST 算法,通过解决复杂的约束优化问题达到涉及 O(log log n) 的运行时间。

Hope this helps!希望这可以帮助!

One way to see factor of O(log log n) in time complexity is by division like stuff explained in the other answer, but there is another way to see this factor, when we want to make a trade of between time and space/time and approximation/time and hardness/... of algorithms and we have some artificial iteration on our algorithm.在时间复杂度中查看 O(log log n) 因子的一种方法是除法,就像另一个答案中解释的东西一样,但是当我们想要在时间和空间/时间之间进行交易时,还有另一种方法可以查看这个因子和算法的近似/时间和硬度/...,我们对我们的算法进行了一些人工迭代。

For example SSSP(Single source shortest path) has an O(n) algorithm on planar graphs, but before that complicated algorithm there was a much more easier algorithm (but still rather hard) with running time O(n log log n), the base of algorithm is as follow (just very rough description, and I'd offer to skip understanding this part and read the other part of the answer):例如,SSSP(单源最短路径)在平面图上有一个 O(n) 算法,但在那个复杂的算法之前,有一个更简单的算法(但仍然相当困难),运行时间为 O(n log log n),算法的基础如下(只是非常粗略的描述,我建议跳过理解这部分并阅读答案的另一部分):

  1. divide graph into the parts of size O(log n/(log log n)) with some restriction.将图分成大小为 O(log n/(log log n)) 的部分,但有一些限制。
  2. Suppose each of mentioned part is node in the new graph G' then compute SSSP for G' in time O(|G'|*log |G'|) ==> here because |G'|假设每个提到的部分都是新图 G' 中的节点,然后在时间 O(|G'|*log |G'|) ==> 计算 G' 的 SSSP,因为 |G'| = O(|G|*log log n/log n) we can see the (log log n) factor. = O(|G|*log log n/log n) 我们可以看到 (log log n) 因子。
  3. Compute SSSP for each part: again because we have O(|G'|) part and we can compute SSSP for all parts in time |n/logn|计算每个部分的 SSSP:再次因为我们有 O(|G'|) 部分,我们可以及时计算所有部分的 SSSP |n/logn| * |log n/log logn * log (logn /log log n). * |log n/log logn * log (logn /log log n)。
  4. update weights, this part can be done in O(n).更新权重,这部分可以在 O(n) 中完成。 for more details this lecture notes are good.有关更多详细信息,本讲义很好。

But my point is, here we choose the division to be of size O(log n/(log log n)).但我的观点是,这里我们选择大小为 O(log n/(log log n)) 的除法。 If we choose other divisions like O(log n/ (log log n)^2) which may runs faster and brings another result.如果我们选择 O(log n/ (log log n)^2) 之类的其他划分,它可能会运行得更快并带来另一个结果。 I mean, in many cases (like in approximation algorithms or randomized algorithms, or algorithms like SSSP as above), when we iterate over something (subproblems, possible solutions, ...), we choose number of iteration corresponding to the trade of that we have (time/space/complexity of algorithm/ constant factor of the algorithm,...).我的意思是,在许多情况下(比如在近似算法或随机算法中,或者像上面的 SSSP 之类的算法),当我们迭代某些东西(子问题,可能的解决方案,......)时,我们选择与交易相对应的迭代次数我们有(时间/空间/算法的复杂性/算法的常数因子,...)。 So may be we see more complicated stuffs than "log log n" in real working algorithms.因此,在实际工作算法中,我们可能会看到比“log log n”更复杂的东西。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM