学会“大O分析法”再谈算法吧

去BAT这类大公司面试,“求算法的时间复杂度”是一个必备的技能。通过计算算法的时间复杂度可以很好的反应出你对这个算法的理解程度。“时间复杂度”(time complexity)怎么求?就要用到“大O分析法”(Big-O Analysis)了。

毫不夸张的说,如果你还不懂“大O分析法”,最好别去面试,珍惜面试机会!相反,如果你对“大O分析法”有很好的理解,会给面试官留下一个好印象。

本文会通过一些简单的例子帮助你理解“大O分析法”。你甚至可以把本文看成一个傻瓜式教程,因为它真————的很容易理解。

“大O分析法”在计算机科学中的意义

在解决一个计算机科学领域的问题时,通常会有多个解决方案,或者说有多种算法。我们肯定需要知道这些算法中,哪个最高效?

“大O分析法”就是用来解决这个问题的,它为我们衡量一个算法的效率提供了基准。这里有个对于“大O分析法”更详尽的定义:“大O分析法”基于某个算法运行一个特定输入规模(Input Size)的函数所消耗的时间来衡量一个算法的效率高低。”句子有点长,我们拆分一下理解。首先,去掉“时间”的定语就是,“大O分析法”基于时间来衡量一个算法的效率高低。什么时间呢?算法运行一个特定输入规模的函数所消耗的时间。其中,函数的输入可以是数组或链表等,而输入规模则是数组的大小或链表的长度。

听起来有点枯燥吧?

下面通过真实的例子解释一下。

对“大O表示法”的练习

即使你已经知道了“大O表示法”(Big-O Notation)是什么,你仍然可以在不看答案的前提下计算出下面每个算法的“大O表示法”,以增强自己对“大O表示法”的理解。

案例说明

编写一个函数,找出一个数组中的最小值。

为了更好的解释“大O分析法”的工作原理,我们使用两种不同的算法实现上面的函数。

第一种算法,只是简单的遍历数组的每一个元素,然后用变量curMin保持当前的最小值。

假设向函数传入一个大小为10的整数数组,数组中的值都是随机生产的。

CompareSmallestNumber 函数

int CompareSmallestNumber(int array[]) {

  int curMin;

  //把数组的第一个元素赋值给当前最小值

  curMin = array[0];

  /*

   * 遍历数组找出最小值

   */

  for (int x = 1; x < 10; x++) {

    if (array[x] < curMin) {

      curMin = array[x];

    }

  }

  // 返回最小值

  return curMin;

​}

而在第二种算法中,会让数组中的每一个元素和数组中的所有元素对比,如果某个元素小于或等于数组中所有的元素,那么这么元素就是数组中的最小元素。

CompareToAllNumbers 函数

int CompareToAllNumbers(int array[]) {

  boolean isMin;

  int  min =0 ;

  /*

   * 遍历数组中的每一个元素

   */

  for (int i = 0; i < 10; i++) {

    isMin = true;

    for (int j = 0; j < 10; j++) {

          /*

       * array[i]和数组的其他元素比较

       * 如果array[i]大于数组中的任何一个元素

       * 就说明array[i]不是最小值 

       */

      if (array[i] > array[j]){

        isMin = false;

      }

    }

    //如果是最小元素,保存下该元素并结束外层循环

    if (isMin){

      min =array[i];

      break;

    }

  }

  return min;

}

现在,我们用两个不同的算法解决了同一问题。那么,哪一种算法更高效?下面我们用“大O分析法”来分析一下。

算法的“大O分析法”    

为了代码的清晰,我们假设输入数组大小是10。但是,在讨论“大O分析法”的时候,对于函数的输入规模(Input Size),我们不想使用一个确切的数值,这里用n表示输入规模,即函数的输入规模是n。

再次强调一下:“大O分析法”基于某个算法运行一个特定输入规模的函数所消耗的时间来衡量一个算法的效率高低。

在进行“大O”分析时,“输入”(input)的具体内容根据要解决的问题的不同而变化。我们的例子中,“输入”就是一个数组,也可以看成是链表中的元素,二叉树中的节点,或者其他任何数据结构。

那么,“大O分析法”是怎么衡量算法效率的?“大O”会设法表达出n个输入项被“使用”了多少次。“使用”一词在不同的算法中会有不同的意思,在一个算法中表示“输入项”和一个常量了多少次,而在另外一个算法中可能表示“输入项”被往数据结构中添加了多少次,等等。

但在我们的 CompareSmallestNumber 和 CompareToAllNumbers函数中,“使用”意思是数组中的元素和其他元素比较了一次。

“大O表示法”和时间复杂度

在CompareSmallestNumber函数中,n(例子中是10)个输入项,每一个输入项仅在和最小值比较的时候被使用了一次。在“大O表示法”(Big-O Notation)中,它被写作O(n),也就是我们熟知的线性时间(linear time)。线性时间意味着,算法运行所需的时间和输入规模成正比。例如,80的输入规模要比79(或则任何小于79)的输入规模的运行时间长一些。另外一种表达方式说,这个算法有“n数量级的时间复杂度”( order of n time complexity)

你可能也看到了,在CompareSmallestNumber函数中,我们把变量curMin初始化为数组的第一个值,也就是说,“输入项”被使用了一次。所以“大O表示法”应该是O(n+1)才对。实际上,“大O表示法”关心的是当输入规模n趋于无穷大时算法的运行时间。当‘n’趋于无穷大时,常量1就变得微不足道,可以忽略了。

所以,函数CompareSmallestNumber的“大O表示法”是O(n)而不是O(n+1)。

同理,对于 n³+n,当n趋于无穷大时,“+n”就变得很微小了,所以我们扔掉“+n”,最终用O(n³)替代O(n³+n),或者说该算法有“n³数量级的时间复杂度”。由此,我们可以看到:“大O表示法”会扔掉表达式中的常量项和较低数量级的子表达式

大O表示法的最坏情况

接下来,我们讨论一下CompareToAllNumbers函数。对于这个问题(求数组中的最小值),这可能是最坏的一种解决方案了,意味着,它的运行时间也可能是最长的。

现在,我们用“大O分析法”分析一下CompareToAllNumbers函数。这个算法最坏的情况是怎样的?数组中的最小值是数组的最后一个元素,就是最坏的情况,因为为了找到最小值,它不得不把数组从头到尾遍历一遍。数组中的每一个元素,都要和其他元素(包括自己)比较一次,一共做了100次比较,因为我们的输入规模是10,10x10=100=10²。当输入规模是n时,输入项会被使用n²次。也就是说函数CompareToAllNumbers使用了一个O(n²)的算法。

大O分析法衡量算法的效率

我们比较一下这两个函数:CompareToAllNumbers是O(n²),CompareSmallestNumber是O(n)。

当输入规模为10000时,CompareSmallestNumber函数中输入项被“使用”了10000次,而CompareToAllNumbers使用了10000的平方次,100,000,000次,一亿次!这个差距是很大的,你可以想象到CompareSmallestNumber一定比CompareToAllNumbers快很多。特别的,当输入规模更大呢?

由此可见,效率可以造成巨大的差异。更重要的是,它提醒我们怎样设计出更高效的算法。

在面试中,可能会让你求出一个算法的“大O表示法”。即使没有被直接问到,你也应该谈论一些关于这个算法的时间复杂度问题,这表明你有要想出一个更高效的算法的意识。

常见的“大O符号”

对于一些常见的大O符号,如 O(n²),O(log n),O(n log n)以及典型的面试题会在之后的文章中有更详细的举例说明。

 

 

posted @ 2016-05-30 00:00  忙碌的键盘  阅读(142)  评论(0编辑  收藏  举报