通过计算步骤计算算法复杂度

时间:2016-09-28 15:42:35

标签: c++ algorithm data-structures big-o

尝试通过计算步数来计算函数的大数。我认为这些是如何按照他们在示例中的方式来计算每一步,但不知道如何计算总数。

int function (int n){
   int count = 0;                              // 1 step
   for (int i = 0; i <= n; i++)                // 1 + 1 + n * (2 steps)
      for (int j = 0; j < n; j++)              // 1 + 1 + n * (2 steps)
         for (int k = 0; k < n; k++)           // 1 + 1 + n * (2 steps)
            for (int m = 0; m <= n; m++)       // 1 + 1 + n * (2 steps)
               count++;                        // 1 step
  return count;                                // 1 step
}

我想说这个函数是O(n ^ 2),但我不明白它是如何计算的。

我一直在看的例子

int func1 (int n){
       int sum = 0;                                // 1 step
       for (int i = 0; i <= n; i++)                // 1 + 1 + n * (2 steps)
          sum += i;                                // 1 step
       return sum;                                 // 1 step
}                                                  //total steps: 4 + 3n

int func2 (int n){
           int sum = 0;                                // 1 step
           for (int i = 0; i <= n; i++)                // 1 + 1 + n * (2 steps)
              for (int j = 0; j <= n; j++)             // 1 + 1 + n * (2 steps)
                  sum ++;                              // 1 step
           for (int k = 0; k <= n; k++)                // 1 + 1 + n * (2 steps)
               sum--;                                  // 1 step
           return sum;                                 // 1 step
    }      
                                                       //total steps: 3n^2 + 7n + 6

4 个答案:

答案 0 :(得分:2)

你刚才提出的是非常简单的例子。 在我看来,你只需要理解循环中的复杂性如何工作,以便理解你的例子。

简而言之非常简短)必须在asymptotic complexity中考虑一个周期,如下所示:

loop (condition) :
  // loop body
end loop
  • 循环的condition应该告诉您循环执行的次数输入的大小 相比。
  • 身体的复杂性(您可以将身体视为子功能并计算复杂性)必须乘以循环的复杂性。

原因非常直观:身体中的内容会被重复执行,直到条件得到验证,即循环(以及身体)执行的次数。

只是一些例子:

// Array linear assignment
std::vector<int> array(SIZE_ARRAY);

for (int i = 0; i < SIZE_ARRAY; ++i) {
  array[i] = i;
}

让我们分析一下这个简单的循环:

  • 首先,我们需要选择输入 relative 来计算我们的复杂度函数。这种情况非常简单:变量是数组的大小。那是因为我们想知道我们的程序如何尊重输入数组的大小增长。

  • 循环将重复SIZE_ARRAY次。所以正文执行的次数是SIZE_ARRAY次(注意:这些值是可变的,不是常数值)。

  • 现在考虑循环体。指令array[i] = i不依赖于数组的大小。它需要未知数量的CPU周期,但该数字始终相同,即常量

总结一下,我们重复SIZE_ARRAY次指令,该指令占用恒定数量的CPU时钟(假设k是该值,是常量)。

因此,从数学上来说,该简单程序将执行的CPU时钟数为SIZE_ARRAY * k

使用O Big notation,我们可以描述限制行为。这是当自变量变为无穷大时函数将采取的行为。

我们可以写:

O(SIZE_ARRAY * k) = O(SIZE_ARRAY)

那是因为k是一个常数值,根据 Big O Notation 的定义,常数不会在无穷大处增长(永远是常数)。

如果我们将SIZE_ARRAY称为N(输入的大小),我们可以说我们的函数的时间复杂度为O(N)

最后一个(“更复杂”)的例子:

for (int i = 0; i < SIZE_ARRAY; ++i) {
  for (int j = 0; j < SIZE_ARRAY; ++j) {
    array[j] += j; 
  }
}

与之前一样,我们将问题规模与SIZE_ARRAY进行比较。

不久:

  • 第一个周期将执行SIZE_ARRAY次,即O(SIZE_ARRAY)
  • 第二个周期将执行SIZE_ARRAY次。
  • 第二个循环的主体是一个指令,它将占用一定数量的CPU周期,假设该数字为k

我们考虑第一个循环的执行次数,然后乘以它的主体复杂度。

O(SIZE_ARRAY) * [first_loop_body_complexity].

但第一个循环的主体是:

for (int j = 0; j < SIZE_ARRAY; ++j) {
    array[j] += j; 
}

这是前一个例子中的单个循环,我们刚刚计算出的是复杂性。这是一个O(SIZE_ARRAY)。所以我们可以看到:

[first_loop_body_complexity] = O(SIZE_ARRAY)

最后,我们的整个复杂性是:

O(SIZE_ARRAY) * O(SIZE_ARRAY) = O(SIZE_ARRAY * SIZE_ARRAY)

那是

O(SIZE_ARRAY^2)

使用N代替SIZE_ARRAY

O(N^2)

答案 1 :(得分:1)

免责声明:这不是数学解释。这是一个愚蠢的版本,我认为可以帮助那些被介绍到复杂世界的人,并且像我第一次遇到这个概念时一样无能为力。我也不给你答案。试着帮助你到达那里。

故事的道德:不计算步骤。复杂性不是关于执行多少指令(我将使用它而不是“步骤”)。这本身(几乎)完全无关紧要。通俗地说(时间)复杂性是指执行时间如何根据输入的增长而增长 - 这就是我最终理解复杂性的方式。

让我们一步一步地了解一些最常见的复杂性:

常数复杂度:O(1)

这表示一种算法,其执行时间不依赖于输入。当输入增长时,执行时间不会增长。

例如:

auto foo_o1(int n) {
   instr 1;
   instr 2;
   instr 3;
   if (n > 20) {
      instr 4;
      instr 5;
      instr 6;
   }
   instr 7;
   instr 8;
};

此功能的执行时间不依赖于n的值。请注意,即使根据n的值执行某些指令,我也可以说。数学上这是因为O(constant) == O(1)。直观地说,这是因为指令数量的增长与n不成比例。在同一个想法中,如果函数有10个instr或1k指令则无关紧要。它仍然是O(1) - 不变的复杂性。

线性复杂度:O(n)

这表示一种算法,其执行时间与输入成比例。当给出小输入时,它需要一定量。增加输入时,执行时间按比例增长:

auto foo1_on(int n)
{
   for (i = 0; i < n; ++i)
      instr;
}

此功能为O(n)。这意味着当输入加倍时,执行时间会增加一个因子。任何输入都是如此。例如,当您将输入从10加倍到20并且将输入从1000加倍到2000时,在算法执行时间的增长中或多或少地存在相同因素。

与忽略对“最快”增长没有太多​​贡献相对的想法一致,所有下一个功能仍然具有O(n)复杂性。数学O复杂性是上限的。这会导致O(c1*n + c0) = O(n)

auto foo2_on(int n)
{
   for (i = 0; i < n / 2; ++i)
      instr;
}

此处:O(n / 2) = O(n)

auto foo3_on(int n)
{
   for (i = 0; i < n; ++i)
     instr 1;
   for (i = 0; i < n; ++i)
     instr 2;
}

此处O(n) + O(n) = O(2*n) = O(n)

多项式阶数2复杂度:O(n ^ 2)

这告诉你,当你增加输入时,执行时间会越来越大。例如,下一个是O(n^2)算法的有效行为:

读取:当您将输入从...加倍到...时,可以增加执行时间...次

  • 从100到200:1.5倍
  • 从200到400:1.8次
  • 从400到800:2.2次
  • 800至1600:6次
  • 从1600到3200:500次

试一试!。编写O(n^2)算法。并加倍输入。首先,您会看到计算时间略有增加。有一次它只是而你必须等待几分钟,而在之前的步骤中它只需要几秒钟。

查看n^2图表后,您可以轻松了解这一点。

auto foo_on2(int n)
{
   for (i = 0; i < n; ++i)
      for (j = 0; j < n; ++j)
         instr;
}

这个功能O(n)怎么样?简单:第一个循环执行n次。 (我不在乎它是n times plus 3还是4*n。然后,对于第一个循环的每一步,第二个循环执行n次。有n次迭代i循环。对于每次迭代,都有n次j迭代。总共我们有n * n = n^2次j迭代。因此O(n^2)

还有其他有趣的复杂性,如对数,指数等等。一旦你理解了数学背后的概念,就会变得非常有趣。例如,对数复杂度O(log(n))的执行时间随着输入的增长而增长更慢。当你查看日志图时,你可以清楚地看到它。

网上有很多关于复杂性的资源。搜索。读。不明白!再次搜索。读。拿纸和笔。了解!。重复。

答案 2 :(得分:0)

在这些简单的情况下,您可以通过查找最常执行的指令来确定时间复杂度,然后找出这个数字取决于count++的方式。

在示例1中,sum += i;执行n ^ 4次=&gt;为O(n ^ 4)

在示例2中,sum ++;执行n次=&gt;为O(n)

在示例3中,function enqueue_foundation() { wp_enqueue_style( 'foundation-css', get_template_directory_uri() . '/css/foundation.min.css'); wp_enqueue_script( 'foundation-js', get_template_directory_uri() . '/js/foundation.min.js'); wp_enqueue_script( 'foundation-jquery', get_template_directory_uri() . '/js/jquery.js'); } add_action('wp_enqueue_scripts', 'enqueue_foundation'); 执行n ^ 2次=&gt;为O(n ^ 2)

嗯,实际上这是不正确的,因为你的一些循环执行n + 1次,但这根本不重要。在示例1中,指令实际执行(n + 1)^ 2 * n ^ 2次,其与n ^ 4 + 2 n ^ 3 + n ^ 2相同。对于时间复杂度,只有最大的功率计数。

答案 3 :(得分:0)

为了简单起见:

O(N)表示小于或等于到N.因此,在一个代码段中,我们忽略所有并专注于采用最多步数的代码(最高功率)解决问题/完成执行。

按照你的例子:

int function (int n){
   int count = 0;                              // ignore

   for (int i = 0; i <= n; i++)                // This looks interesting
      for (int j = 0; j < n; j++)              // This looks interesting
         for (int k = 0; k < n; k++)           // This looks interesting
            for (int m = 0; m <= n; m++)       // This looks interesting
               count++;                        // This is what we are looking for. 

  return count;                                // ignore
}

要完成该陈述,我们需要“等待”或“覆盖”或“步骤”(n + 1)* n * n *(n + 1)=&gt; O(〜N R个4)。

第二个例子:

int func1 (int n){
       int sum = 0;                                // ignore

       for (int i = 0; i <= n; i++)                // This looks interesting
          sum += i;                                // This is what we are looking for. 

       return sum;                                 // ignore
} 

要完成它需要n + 1步= => O(〜N)。

第三个例子:

int func2 (int n){
           int sum = 0;                                // ignore

           for (int i = 0; i <= n; i++)                // This looks interesting
              for (int j = 0; j <= n; j++)             // This looks interesting
                  sum ++;                              // This is what we are looking for. 

           for (int k = 0; k <= n; k++)                // ignore
               sum--;                                  // ignore

           return sum;                                 // ignore
    }      

为此我们需要(n + 1)*(n + 1)步骤=&gt; O(〜N ^ 2)