【问题标题】:Would this algorithm run in O(n)?这个算法会在 O(n) 中运行吗?
【发布时间】:2015-08-11 20:36:09
【问题描述】:

注意:这是Cracking the Coding Interview 5th Edition中的问题4.3

问题:给定一个排序(升序)数组,编写一个算法来创建一个高度最小的二叉搜索树

这是我的算法,用Java编写来解决这个问题

  public static IntTreeNode createBST(int[] array) {
         return createBST(array, 0, array.length-1);
   }
   private static IntTreeNode createBST(int[] array, int left, int right) {
        if(right >= left) {
            int middle = array[(left + right)/2;
            IntTreeNode root = new IntTreeNode(middle);
            root.left = createBST(array, left, middle - 1);
           root.right = createBST(array, middle + 1, right);
            return root;
         } else {
             return null;
         }
    }

我根据作者的代码检查了这段代码,它几乎是相同的。
但是我很难分析这个算法的时间复杂度。
我知道这不会在 O( logn) 喜欢Binary Search,因为您在每个递归级别上所做的工作量不同。 E.G在第一级,1个工作单元,2级-2个工作单元,3级-4个工作单元,一直到log2(n)级-n个工作单元。

因此,基于此,此算法所采取的步骤数将受此数学表达式的限制

看了Infinite geometric series之后,我评价为

或 2n 将在 O(n)

你们是否同意我在这里的工作以及这个算法会在 O(n) 中运行,或者我错过了什么或者它实际上在 O(nlogn) 中运行还是其他一些函数类?

【问题讨论】:

  • 是的,它是 O(n)。如果我更清楚地知道什么是复杂性的良好证明,我会给出答案。
  • @Beta 没有证据你怎么能说出来?
  • 我看到算法调用自身两次,每次在 n/2 个元素上,额外的工作量为 O(1),并且删除了一个元素。我不认为这是一个严格的证明(如“让f(n)g(n) 成为这样的函数......”),但足以让我在脑海中看到它,就像一根绳子一样分成几块,没有重叠。
  • @Beta 为了确定,这里的空间复杂度会是 O(log n),因为最深递归调用的高度是 log n?
  • 我理解的“空间复杂度”这个词是指存储空间而不是堆栈深度。调用堆栈的最大深度是 log(n),但空间复杂度是 O(n),因为这是结果树的大小(以及 O(log(n)) 开销 adds至此,它不会相乘)。

标签: algorithm big-o time-complexity complexity-theory asymptotic-complexity


【解决方案1】:

有时您可以通过计算结果中每个项目的时间量来简化计算,而不是求解递归关系。这个技巧在这里适用。首先将代码更改为这种明显等效的形式:

private static IntTreeNode createBST(int[] array, int left, int right) {
    int middle = array[(left + right)/2;
    IntTreeNode root = new IntTreeNode(middle);
    if (middle - 1 >= left) {
        root.left = createBST(array, left, middle - 1);
    }
    if (right >= middle + 1) {
        root.right = createBST(array, middle + 1, right);
    }
    return root;
}

现在每次调用createBST 都会直接创建一个节点。由于最终树中有 n 个节点,因此对 createBST 的总调用次数必须是 n,并且由于每次调用都直接执行恒定量的工作,因此总时间复杂度为 O(n)。

【讨论】:

  • 打败我。我只是坐下来写同样的论点。
  • 感谢这部分论点是有道理的——“由于最终树中有 n 个节点,因此必须总共调用 n 次 createBST”。但是,更改该代码如何显示每个调用执行的工作量是恒定的?一个呼叫仍然可以进行两个呼叫,它们的大小相同,但比原始呼叫小。那些相同大小的调用每次都在变小,所以每次调用都不会因为调用大小属性的减少而做固定的工作量?
  • @committedandroider 我更改了代码,以便对 createBST 的调用和结果的节点是一对一的。在原始代码中,一些对 createBST 的调用返回 null。该参数可以适应原始代码,但有点混乱:例如,您可以争辩说最多有 N 次调用 createBST 来创建节点,最多有 2N 次调用返回 null。
  • @PaulHankin 谢谢你能澄清一下“每个调用直接执行恒定量的工作”部分吗?像 createBST(array, left, middle - 1) 这样的调用如何在 left 和 middle 对每个调用都不同时成为常量工作的一部分?
  • “直接”是指“排除在递归调用中完成的工作”。如果有 N 个调用(即:从顶层调用或递归调用)并且每个调用的直接工作量为 K,那么总工作量为 NK。这就是避免重复关系的技巧的核心思想。
【解决方案2】:

如果您对递归感到困惑,请将递归调用(当然是心理上的)替换为循环。例如,在您的上述函数中,您可以想象递归调用位于“while 循环”中。因为现在是一个while循环,直到遍历所有n个节点为止,复杂度是O(n)。

【讨论】:

  • 但这不适用于快速排序。您访问所有元素,但运行时间将是 O(n log n),而不是 O(n)。对于这种情况,您将如何改写您的方法?我喜欢你在 while 循环中所做的。
  • 是的,我承认很难想象像快速排序这样的算法会出现对数增长的循环。然而,这个想法非常适用于所有指数增加的函数,如 O(n^2)、O(n^3) 等。我们只需要使用嵌套循环。
猜你喜欢
  • 2016-01-27
  • 2020-01-23
  • 2017-02-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多