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


106

我对big-O的知识是有限的,当对数项出现在等式中时,甚至会使我失望得多。

有人可以简单地向我解释什么是O(log n)算法吗?对数从何而来?

当我尝试解决这个期中练习问题时,特别是这样:

令X(1..n)和Y(1..n)包含两个整数列表,每个列表以非降序排列。给出O(log n)时间算法以查找所有2n个组合元素的中值(或第n个最小整数)。例如,X =(4、5、7、8、9),Y =(3、5、8、9、10),则7是合并列表的中位数(3、4、5、5、7 ,8、8、9、9、10)。[提示:使用二进制搜索的概念]


29
O(log n)可以看作:如果问题大小加倍n,则算法仅需要更多的固定步数即可。
phimuemue'2

3
这个网站帮助我了解了大O符号:recursive-design.com/blog/2010/12/07/…–
Brad

1
我想知道为什么7是上面示例的中位数,但为什么也可能是8。不是很好的例子吗?
stryba 2012年

13
考虑O(log(n))算法的一个好方法是在每一步中,它们将问题的大小减小一半。以二进制搜索示例为例-在每一步中,您都要检查搜索范围中间的值,将范围分为两半;之后,您从搜索范围中消除了一半,另一半成为下一步的搜索范围。因此,在每一步中,搜索范围的大小都会减半,从而使算法的O(log(n))复杂度降低。(减少不必精确地减少一半,也可以减少任何恒定百分比的三分之一,减少25%;最常见的减少一半)
Krzysztof Kozielczyk

谢谢大家,解决以前的问题,很快就会解决,非常感谢您的回答!稍后会再进行研究
user1189352'2

Answers:


290

我必须同意,当您第一次看到O(log n)算法时,这很奇怪……对数从何而来?但是,事实证明,有多种不同的方法可以使日志项以big-O表示法显示。这里有一些:

反复除以常数

取任意数字n;例如,16.在得到小于或等于1的数字之前,可以将n除以2多少次?对于16,我们有

16 / 2 = 8
 8 / 2 = 4
 4 / 2 = 2
 2 / 2 = 1

请注意,这最终需要完成四个步骤。有趣的是,我们还有2 2 = 4的日志。嗯... 128呢?

128 / 2 = 64
 64 / 2 = 32
 32 / 2 = 16
 16 / 2 = 8
  8 / 2 = 4
  4 / 2 = 2
  2 / 2 = 1

这花费了七个步骤,并且log 2 128 =7。这是巧合吗?不!这是有充分的理由的。假设我们将数字n除以2 i次。然后我们得到数字n / 2 i。如果我们要求解最大为1的i的值,则得到

N / 2 ≤1

N≤2

对数2 n≤i

换句话说,如果我们选择一个整数i,使得i≥log 2 n,那么在将n除以i的一半之后,我们将得到一个最大为1的值。可以保证的最小i大约为log 2 n,因此,如果我们有一个除以2的算法,直到数字变得足够小,那么可以说它以O(log n)步长终止。

一个重要的细节是,您将n除以哪个常数(只要它大于1)就无关紧要;如果用常数k除,将需要log k n个步才能达到1。因此,任何将输入大小重复除以某个分数的算法都将需要O(log n)迭代来终止。这些迭代可能会花费很多时间,因此净运行时不必为O(log n),但是步数将是对数的。

那么,这在哪里出现呢?一个经典的例子是二进制搜索,它是一种用于在排序数组中搜索值的快速​​算法。该算法的工作原理如下:

  • 如果数组为空,则返回该元素不存在于数组中。
  • 除此以外:
    • 查看数组的中间元素。
    • 如果它等于我们要寻找的元素,则返回成功。
    • 如果它大于我们要查找的元素:
      • 扔掉阵列的后半部分。
      • 重复
    • 如果小于我们要查找的元素:
      • 扔掉阵列的前半部分。
      • 重复

例如,在数组中搜索5

1   3   5   7   9   11   13

我们首先来看一下中间元素:

1   3   5   7   9   11   13
            ^

由于7> 5,并且由于对数组进行了排序,因此我们知道数字5不能在数组的后半部分,因此我们可以丢弃它。这离开

1   3   5

现在,我们来看一下中间元素:

1   3   5
    ^

由于3 <5,我们知道5不能出现在数组的前半部分,因此我们可以将前半部分数组丢掉

        5

再次,我们看一下该数组的中间部分:

        5
        ^

由于这正是我们要查找的数字,因此我们可以报告数组中确实存在5。

那么,效率如何?好吧,在每次迭代中,我们至少丢弃了剩余数组元素的一半。一旦数组为空或找到所需的值,该算法就会停止。在最坏的情况下,元素不存在,因此我们将数组的大小减半,直到用完元素。这需要多长时间?好吧,由于我们不断将数组切成两半,因此最多可以进行O(log n)次迭代,因为在运行之前,我们不能将数组切成O(log n)次以上的一半数组元素不足。

由于同样的原因,遵循分而治之的通用技术(将问题切成碎片,求解这些碎片,然后将问题重新组合在一起)的算法往往在其中包含对数项-您无法继续在其中分割对象是O(log n)倍的一半。您可能希望将合并排序作为一个很好的例子。

一次处理一位数字的值

以10为底的数字n中有几位数?好吧,如果数字中有k个数字,那么我们可以认为最大数字是10 k的倍数。最大的k位数字是k次的999 ... 9,等于10 k +1-1。因此,如果我们知道n中有k位数字,那么我们知道n的值是最多10 k +1-1。如果我们要用n来求解k,我们得到

n≤10 k + 1-1

n + 1≤10 k + 1

对数10(n + 1)≤k + 1

(log 10(n + 1))-1≤k

从中我们得出k大约是n的以10为底的对数。换句话说,n中的位数是O(log n)。

例如,让我们考虑将两个太大的数字相加的复杂性,这些数字太大而无法放入一个机器字中。假设我们有那些以10为底的数字,我们将其称为m和n。添加它们的一种方法是通过年级学校方法-一次将数字写出一位,然后从右到左进行操作。例如,要添加1337和2065,我们首先将数字写为

    1  3  3  7
+   2  0  6  5
==============

我们添加最后一位数字并携带1:

          1
    1  3  3  7
+   2  0  6  5
==============
             2

然后,我们添加倒数第二个(“倒数第二个”)数字并携带1:

       1  1
    1  3  3  7
+   2  0  6  5
==============
          0  2

接下来,我们添加倒数第二个(“倒数第二个”)数字:

       1  1
    1  3  3  7
+   2  0  6  5
==============
       4  0  2

最后,我们添加倒数第四位(“ preantepenultimate” ...我喜欢英语):

       1  1
    1  3  3  7
+   2  0  6  5
==============
    3  4  0  2

现在,我们做了多少工作?每个数字我们总共进行O(1)个工作(即工作量不变),并且总共需要处理O(max {log n,log m})个数字。因为我们需要访问两个数字中的每个数字,所以总共有O(max {log n,log m})复杂度。

许多算法都是从某个基数上一次工作一位获得O(log n)项。一个经典的例子是radix sort,它一次将整数排序一位。基数排序有多种形式,但它们通常在时间O(n log U)中运行,其中U是要排序的最大可能整数。这样做的原因是,每次通过排序都需要O(n)时间,并且总共需要O(log U)次迭代来处理要排序的最大数字的每个O(log U)数字。许多高级算法,例如Gabow的最短路径算法Ford-Fulkerson最大流算法的缩放版本,在复杂性上都有一个对数项,因为它们一次只能工作一位。


关于您如何解决该问题的第二个问题,您可能需要看一下相关的问题,以探索更高级的应用程序。鉴于此处描述的问题的一般结构,当您知道结果中有一个对数词时,您现在可以更好地思考问题,因此建议您在给出答案之前不要查看答案有些想法。

希望这可以帮助!


8

当我们谈论big-Oh的描述时,通常是在谈论解决给定大小的问题所需的时间。通常,对于简单的问题,大小仅由输入元素的数量来表征,通常称为n或N。(显然,这并不总是正确的-图的问题通常以顶点数量,V和边数E;但现在,我们将讨论对象列表,列表中有N个对象。)

我们说,当且仅当一个问题“是(N的某些函数)的大哦” :

对于所有N>任意N_0,都有一个常数c,因此算法的运行时间少于该常数c倍(N的某些函数)。

换句话说,不要考虑小问题,而设置问题的“固定开销”很重要,而要考虑大问题。和思考大问题,大哦(N的某些功能)表示运行时间时比一些常数时光总是小于功能。总是。

简而言之,该函数是一个上限,上限为一个常数。

因此,“ log(n)的大哦”的意思与我上面所说的相同,只是“ N的某些功能”被替换为“ log(n)”。

因此,您的问题告诉您考虑二进制搜索,让我们考虑一下。假设您有一个N元素的列表,这些列表按升序排序。您想找出该列表中是否存在某些给定的数字。一种不是二分查找的方法是只扫描列表中的每个元素,看看它是否是您的目标编号。您可能会很幸运,并且在第一次尝试时就找到了它。但在最坏的情况下,您将检查N次不同的时间。这不是二进制搜索,也不是log(N)的大麻烦,因为没有办法将其强加到我们上面概述的条件中。

您可以将任意常数选择为c = 10,如果列表中有N = 32个元素,则可以:10 * log(32)= 50,大于32的运行时。但是如果N = 64 ,10 * log(64)= 60,小于64的运行时。您可以选择c = 100或1000或gazillion,仍然可以找到一些违反该要求的N。换句话说,没有N_0。

但是,如果执行二进制搜索,则选择中间元素,然后进行比较。然后我们扔掉一半的数字,然后一次又一次地重复,依此类推。如果您的N = 32,则只能执行5次,即log(32)。如果您的N = 64,则只能执行大约6次,依此类推。现在,您可以选择该任意常数c,从而始终满足大N值的要求。

在所有这些背景下,O(log(N))通常意味着您有某种方法可以做一件简单的事情,从而将问题的大小减少一半。就像上面的二进制搜索一样。将问题减半后,可以一次又一次地减半。但是,至关重要的是,您不能做的是一些预处理步骤,该步骤将花费比O(log(N))时间更长的时间。因此,例如,您不能将两个列表混为一个大列表,除非您也找到一种在O(log(N))时间内完成此操作的方法。

(注意:Log(N)几乎总是表示以日志为底的第二个,这就是我上面假设的。)


4

在以下解决方案中,所有具有递归调用的行都在X和Y子数组的给定大小的一半上完成。其他行在固定时间内完成。递归函数为T(2n)= T(2n / 2)+ c = T(n)+ c = O(lg(2n))= O(lgn)。

您从MEDIAN(X,1,n,Y,1,n)开始。

MEDIAN(X, p, r, Y, i, k) 
if X[r]<Y[i]
    return X[r]
if Y[k]<X[p]
    return Y[k]
q=floor((p+r)/2)
j=floor((i+k)/2)
if r-p+1 is even
    if X[q+1]>Y[j] and Y[j+1]>X[q]
        if X[q]>Y[j]
            return X[q]
        else
            return Y[j]
    if X[q+1]<Y[j-1]
        return MEDIAN(X, q+1, r, Y, i, j)
    else
        return MEDIAN(X, p, q, Y, j+1, k)
else
    if X[q]>Y[j] and Y[j+1]>X[q-1]
        return Y[j]
    if Y[j]>X[q] and X[q+1]>Y[j-1]
        return X[q]
    if X[q+1]<Y[j-1]
        return MEDIAN(X, q, r, Y, i, j)
    else
        return MEDIAN(X, p, q, Y, j, k)

3

Log项在算法复杂度分析中经常弹出。以下是一些说明:

1.您如何表示数字?

令数字X =245436。这种“ 245436”符号中包含隐式信息。使信息明确:

X = 2 * 10 ^ 5 + 4 * 10 ^ 4 + 5 * 10 ^ 3 + 4 * 10 ^ 2 + 3 * 10 ^ 1 + 6 * 10 ^ 0

这是数字的十进制扩展。因此,我们需要表示此数字的最小信息量6位数字。这不是巧合,因为可以用d位数字表示小于10 ^ d的任何数字。

那么,代表X需要多少位数?那等于X加1中10的最大指数。

==> 10 ^ d> X
==> log(10 ^ d)> log(X)
==> d * log(10)> log(X)
==> d> log(X)//和出现log再次...
==> d = floor(log(x))+ 1

另请注意,这是表示此范围内数字的最简洁方法。任何减少都会导致信息丢失,因为丢失的数字可以映射到其他10个数字。例如:12 *可以映射到120、121、122,…,129。

2.如何搜索(0,N-1)中的数字?

取N = 10 ^ d,我们使用最重要的观察结果:

唯一标识一个值的最小信息量,范围为0到N-1 = log(N)个数字。

这意味着,当要求在整数行上搜索从0到N-1的数字时,我们至少 需要log(N)尝试找到它。为什么?任何搜索算法在搜索数字时都需要一个接一个地选择数字。

它需要选择的最小位数为log(N)。因此,在大小为N的空间中搜索数字所需的最小操作数为log(N)。

您能猜出二元搜索,三元搜索或十进制搜索的顺序复杂性吗?
它的O(log(N))!

3.如何对一组数字进行排序?

当被要求将一组数字A排序到数组B中时,这是它的样子->

置换元素

原始数组中的每个元素都必须映射到已排序数组中的相应索引。因此,对于第一个元素,我们有n个位置。为了正确地找到从0到n-1范围内的相应索引,我们需要... log(n)操作。

下一个元素需要log(n-1)操作,下一个log(n-2),依此类推。总计为:

==> log(n)+ log(n-1)+ log(n-2)+…+ log(1)

使用log(a)+ log(b)= log(a * b),

==> log (n!)

这可以近似为nlog(n)-n。
这是O(n * log(n))!

因此,我们得出结论,没有排序算法能比O(n * log(n))做得更好。一些具有这种复杂性的算法是流行的合并排序和堆排序!

这就是为什么我们在算法的复杂性分析中经常看到log(n)的原因。同样可以扩展到二进制数。我在这里制作了一个视频。
为什么在算法复杂性分析期间log(n)出现如此频繁?

干杯!


2

当解决方案基于n之上的迭代时,我们将时间复杂度称为O(log n),其中每次迭代完成的工作是前一次迭代的一小部分,因为算法朝着解决方案努力。


1

无法发表评论...坏死了!Avi Cohen的答案不正确,请尝试:

X = 1 3 4 5 8
Y = 2 5 6 7 9

所有条件都不成立,因此MEDIAN(X,p,q,Y,j,k)会同时减少这五个。这些是不递减的序列,并非所有值都是不同的。

还可以尝试使用具有不同值的此均匀长度示例:

X = 1 3 4 7
Y = 2 5 6 8

现在MEDIAN(X,p,q,Y,j + 1,k)将剪切这四个。

相反,我提供了此算法,并使用MEDIAN(1,n,1,n)进行调用:

MEDIAN(startx, endx, starty, endy){
  if (startx == endx)
    return min(X[startx], y[starty])
  odd = (startx + endx) % 2     //0 if even, 1 if odd
  m = (startx+endx - odd)/2
  n = (starty+endy - odd)/2
  x = X[m]
  y = Y[n]
  if x == y
    //then there are n-2{+1} total elements smaller than or equal to both x and y
    //so this value is the nth smallest
    //we have found the median.
    return x
  if (x < y)
    //if we remove some numbers smaller then the median,
    //and remove the same amount of numbers bigger than the median,
    //the median will not change
    //we know the elements before x are smaller than the median,
    //and the elements after y are bigger than the median,
    //so we discard these and continue the search:
    return MEDIAN(m, endx, starty, n + 1 - odd)
  else  (x > y)
    return MEDIAN(startx, m + 1 - odd, n, endy)
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.