【问题标题】:Finding maximum in O(logn) time?在 O(logn) 时间内找到最大值?
【发布时间】:2013-09-12 02:27:34
【问题描述】:

我一直认为迭代搜索是在未排序列表中查找最大值的首选方法。

我的想法相当随意,但简而言之:我相信我可以在 O(logn) 时间内完成任务,其中 n 是输入数组的大小。

采用归并排序的方法:分而治之。

第一步:将 findMax() 任务分成两个子任务findMax(leftHalf)findMax(rightHalf)。这个划分应该在O(logn)时间完成。

第 2 步:合并两个最大的候选项。 此步骤中的每一层都应花费恒定时间O(1),并且根据上一步,O(logn) 这样的层。所以它也应该在O(1) * O(logn) = O(logn)时间完成(请原谅符号的滥用)。这是错误的。每次比较都是在恒定时间内完成的,但是有2^j/2 这样的比较需要完成(第 j 级的 2^j 对候选人)。

因此,整个任务应该在O(logn)时间完成。O(n)时间。

但是,当我尝试对其计时时,我得到的结果清楚地反映了线性 O(n) 运行时间。

大小 = 100000000 最大值 = 0 时间 = 556

大小 = 200000000 最大值 = 0 时间 = 1087

大小 = 300000000 最大值 = 0 时间 = 1648

大小 = 400000000 最大值 = 0 时间 = 1990

大小 = 500000000 最大值 = 0 时间 = 2190

大小 = 600000000 最大值 = 0 时间 = 2788

大小 = 700000000 最大值 = 0 时间 = 3586

怎么会?

这是代码(我未初始化数组以节省预处理时间,据我测试,该方法准确识别未排序数组中的最大值):

public static short findMax(short[] list) {
    return findMax(list, 0, list.length);
}

public static short findMax(short[] list, int start, int end) {
    if(end - start == 1) {
        return list[start];
    }
    else {
        short leftMax = findMax(list, start, start+(end-start)/2);
        short rightMax = findMax(list, start+(end-start)/2, end);
        return (leftMax <= rightMax) ? (rightMax) : (leftMax);
    }
}

public static void main(String[] args) {
    for(int j=1; j < 10; j++) { 
        int size = j*100000000; // 100mil to 900mil
        short[] x = new short[size];
        long start = System.currentTimeMillis();
        int max = findMax(x);
        long end = System.currentTimeMillis();
        System.out.println("size = " + size + "\t\t\tmax = " + max + "\t\t\t time = " + (end - start));
        System.out.println();
    }
}

【问题讨论】:

    标签: java max divide-and-conquer


    【解决方案1】:

    您应该计算实际发生的比较次数:

    在最后一步中,在找到前 n/2 个数字和后 n/2 个数字中的最大值后,您需要再进行 1 次比较才能找到整个数字集的最大值。

    在上一步中,您必须找到第一组和第二组 n/4 个数字的最大值以及第三组和第四组 n/4 个数字的最大值,因此您有 2 次比较。

    最后,在递归结束时,你有 n/2 组,每组 2 个数字,你必须比较每一对,所以你有 n/2 个比较。

    当你把它们全部加起来时,你会得到:

    1 + 2 + 4 + ... + n/2 = n-1 = O(n)

    【讨论】:

    • 真可惜,我只是假设每一层都是在恒定时间内完成的,因为比较很快(我认为比在 merge_sort 中合并两个数组要快),我忘记了有 2^j/每层要进行 2 次比较。
    【解决方案2】:

    您确实创建了log(n) 层。

    但归根结底,您仍然会遍历每个已创建存储桶的每个元素。因此,您会遍历每个元素。所以总的来说你还是O(n)

    【讨论】:

      【解决方案3】:

      有了 Eran 的回答,你已经知道你的推理出了什么问题。

      但无论如何,有一个定理叫做主定理,它有助于递归函数的运行时间分析。

      它符合以下等式:

      T(n) = a*T(n/b) + O(n^d)
      

      其中 T(n) 是大小为 n 的问题的运行时间。

      在您的情况下,递归方程将是T(n) = 2*T(n/2) + O(1) 所以a=2b=2d=0。之所以如此,是因为对于您的问题的每个 n 大小的实例,您将其分解为大小为 n / 2 (b) 的 2 (a) 个子问题,并将它们组合成 O(1) = O(n^0) .

      主定理简单地陈述了三种情况:

      如果a = b^d,则总运行时间为O(n^d*log n)

      如果a &lt; b^d,则总运行时间为O(n^d)

      如果a &gt; b^d,则总运行时间为O(n^(log a / log b))

      您的案例与第三个匹配,因此总运行时间为 O(n^(log 2 / log 2)) = O(n)

      尝试了解这三种情况背后的原因是一个很好的练习。它们只是以下情况:

      1st) 我们为每个递归级别做相同数量的总工作(这是合并排序的情况),所以我们只需将合并时间 O(n^d) 乘以级别数 log n。

      2nd) 我们为第二个递归级别做的工作比第一个递归级别少,以此类推。因此,总工作量基本上是最后一个合并步骤(第一个递归级别)的工作量,O(n^d)。

      3rd)我们为更深层次(你的情况)做了更多的工作,所以运行时间是 O(递归树中的叶子数)。在你的情况下,你有 n 叶子用于更深的递归级别,所以 O(n)。

      有一些关于斯坦福 cousera 课程的短视频非常好地解释了主方法,可在https://www.coursera.org/course/algo 获取。我相信即使没有注册,您也可以随时预览课程。

      【讨论】:

      • 花式你应该提到主方法。如果我比我更怀疑自己的直觉,我会应用它。我从主方法中得到的是对你总结的三种情况的直觉,就像我上面评论的那样,我完全忘记了子任务的扩散,只记住了比较任务的简单性。
      猜你喜欢
      • 1970-01-01
      • 2012-06-05
      • 1970-01-01
      • 1970-01-01
      • 2020-06-08
      • 1970-01-01
      • 2012-08-28
      • 2014-03-24
      • 1970-01-01
      相关资源
      最近更新 更多