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

Calculating algorithm complexity by counting steps

本文关键字:计算 算法 复杂度      更新时间:2023-10-16

试图通过计算步骤来计算函数的大0。我想这些就是如何按照例子中的方法计算每一步,但不确定如何计算总数。

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

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

简而言之 (非常简单的)一个循环必须考虑如下的渐近复杂度:

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;
}

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

  • 首先,我们需要选择输入相对于来计算我们的复杂度函数。这种情况非常简单:变量是数组的大小。这是因为我们想知道当输入数组的大小增加时,程序是如何反应的。

  • 循环将重复SIZE_ARRAY次。因此,执行主体的次数是SIZE_ARRAY次(注意:该值是可变的,不是恒定值)。

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

总的来说,我们重复SIZE_ARRAY次,这条指令占用恒定数量的CPU时钟(假设k是该值,是恒定的)。

因此,从数学上讲,为这个简单的程序执行的CPU时钟的数量将是SIZE_ARRAY * k

用O大符号可以描述极限行为。这是当自变量趋于无穷时函数的行为

我们可以这样写:

O(SIZE_ARRAY * k) = O(SIZE_ARRAY)

这是因为k是一个常数值,并且根据大0符号的定义该常数不会在无穷远处增长(永远是常数)。

如果我们将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)

免责声明:这不是一个数学解释。这是一个简化的版本,我认为它可以帮助那些刚接触复杂世界的人,就像我第一次接触这个概念时一样毫无头绪。我也不会给你答案。试着帮你到达那里。


这个故事的寓意是:不要数台阶。复杂度不是指执行了多少条指令(我将用这个来代替"步骤")。这本身(几乎)完全无关紧要。用外行的话来说(时间),复杂性是关于执行时间如何随着输入的增长而增长——这就是我最终对复杂性的理解。

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

常数复杂度:0 (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) -恒定的复杂性。

线性复杂度:0 (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)

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

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

Read:当你从…对. .执行时间可以增加…*

  • 从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次。i循环有n次迭代。对于每个i迭代,有n j迭代。所以我们总共有 n * n = n^2j次迭代。因此,O(n^2)

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

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

保持简单:

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
}

语句要完成,我们需要"wait"或"cover"或"step" (n + 1) * n * n * (n + 1) => O(~ n ^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个步骤=> 0 (~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)步=> 0 (~ n ^2)

在这些简单的情况下,您可以通过查找最常执行的指令来确定时间复杂度,然后找出该数字如何依赖于n

例1中,count++执行n^4次=> 0 (n^4)

例2中,sum += i;执行n次=> 0 (n)

在例3中,sum ++;执行n^2次=> 0 (n^2)

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