草庐IT

一篇文章吃透算法时间复杂度

Albert Edison 2023-04-18 原文

文章目录


前言

我们经常听到数据结构这个词,它指的是数据之间的结构组织。数据组织的结构不同,数据的存取效率就会产生巨大的差异。而这,也就是我们对数据进行组织的原因。

数据结构的三要素分别是逻辑结构、存储结构、数据的运算。我们可以将数据结构理解为一组数据在计算机中的存储结构,或者是相互之间存在一种或多种特定关系的数据集合。

再说算法。它是操作数据、解决特定问题的求解步骤和方法,在计算机中,表现为指令的有限序列,并且每条指令表示一个或者多个操作。通常来说,对于给定的问题,可以有多种解决问题的算法 。当然,这些算法有好有坏。

从数据结构与算法的关系来看,数据结构服务于算法,算法也要作用于特定的数据结构之上,两者相辅相成,不能孤立。

1. 什么是好的算法

算法通常具备这样五个特性。

  • 输入:传递给算法的参数或数据。算法可以有零个或者多个输入。
  • 输出:算法处理的结果。算法必须有输出,否则算法就没有存在的意义,所以算法至少有一个或者多个输出,输出结果可以显示在屏幕上或从算法中返回结果值。
  • 有穷性:在有限的步骤内执行完,不会出现无限循环,每个步骤也能在可接受的时间内完成。
  • 确定性:相同的输入只能产生唯一的输出结果。换句话说,算法的每个步骤都具有确定的含义,不会出现二义性。
  • 可行性:可以用已有的基本操作来实现算法,算法的每一步都能够通过执行有限次数来完成。

不过,具备以上特性的算法就可以了么?我们知道,解决同一个问题的算法可能有多个,它们有好有坏。那么,一个好的算法在设计上通常需要满足什么要求呢?一般有四个。

  • 正确性:算法能够正确反映问题的需求,能够正确解决问题。
  • 可读性:对算法的描述及实现代码要具备良好的可读性,以便于阅读、理解和交流。
  • 健壮性:输入数据不合法时,算法能做适当处理,而不是产生异常或无法预知的结果。
  • 满足高时间效率和低存储量需求:高时间效率指算法运行的速度快,节省时间,低存储量指算法运行时所需的内存空间少。

所以我们就把重点放在 “高效率” 上。要想知道自己的代码是不是能运行得更快,更节省存储空间,那么下一步,就要度量算法的执行效率了。

2. 算法的效率度量

度量的方法通常有两种,事后统计方法,以及事前分析估算方法。

事后统计方法 是通过设计好的测试程序和数据,通过计时,比较不同算法编写的程序的时间,确定算法效率的高低。但这种方法缺陷比较大,并不准确。为什么这么说呢?

一方面,测试结果受测试环境影响很大,比如不同的机器硬件性能、不同的操作系统、编程语言、编译器等都会影响测试结果。另一方面,测试结果受数据规模影响很大,比如效率高的算法在少量测试数据时无法体现。

事前分析估算方法 是在程序编写前依据一些统计方法对算法进行粗略估算的方法,它会抛开计算机硬件、软件等因素,单纯以算法本身作为估算的核心。通常,我们将它分为算法的时间复杂度分析与空间复杂度分析这两类。这两个复杂度分析方法非常重要,我们一起来把它学通、学透。

3. 时间复杂度

算法的时间复杂度用于度量算法的执行时间。在进行算法的时间复杂度分析之前,我们首先了解一下它的表示方法。

4. 大 O 时间复杂度表示法

先来看一个算法例子的代码:

void calc(int n)
{int sum = 0;                   //执行1次  for(int i = 1; i <= n; ++i)    //执行(判断)n+1次{
	④    sum = sum + i;            //执行n次}
	⑥cout << sum << endl;            //执行1次
}

上面的 calc 函数用于求从 1 到 n 的和值,这就是一个算法。

从程序执行的角度来看,程序中的每一行执行的操作都类似 —— 读、写、运算。虽然每行代码执行的时间都不太一样,但作为时间复杂度分析,只需要进行粗略估计,所以我们简单的认为每行代码执行的时间相同即可。

当我们运行代码 calc(1000) 时,调用 calc 函数后,因为传递进去的实参是 1000,这表示问题规模 n=1000。参考代码中圆圈数字表示的行号,就可以知道具体的执行次数了。

这意味着 calc 函数中有效代码行一共执行了 1+1001+1000+1=2003 次。如果把 1000 用 n 替换,那么一共执行的次数就是 2n+3 次。

好,现在我们引入一个新的算法 —— calc2 函数,在 calc 函数基础之上,再增加一个双重 for 循环。

void calc2(int n)
{int sum = 0;                       //执行1次  for(int i = 1; i <= n; ++i)       //执行(判断)n+1次{
    ④    sum = sum + i;               //执行n次}for(int j = 1; j <= n; ++j)       //执行n + 1次{for(int k = 1; k <= n; ++k)   //执行n*(n + 1)次{
        ⑩    sum = sum + k;           //执行n*n次}}
    ⑬cout << sum << endl;               //执行1次
}

上面的 calc2 函数中,第一个 for 循环用于求从 1 到 n 的和值。接着,双重 for 循环用于求 n 次从 1 到 n 的和值。

那么,当我们运行代码 calc2(1000) 时,调用 calc2 函数后,又引入了一个双重循环,这个时候,calc2 函数中的代码行执行次数应该怎么算呢?

这意味着 calc2 函数中有效代码行一共执行了 1+1001+1000+1001+1001000+1000000+1=2004004 次。如果把 1000 用 n 替换,那么一共执行的次数就是 2 n 2 + 4 n + 4 2n^2+4n+4 2n2+4n+4。现在,大 O 时间复杂度表示法就要登场了!

这其中:

  • n,表示问题规模的大小。
  • T(n),表示算法的执行时间(T 指的是 Time),也就是算法的时间复杂度。既然 T(n) 这对圆括号中包含了一个 n,则表示算法的执行时间必然与问题规模有紧密的关系。
  • f(n),表示代码的执行次数总和。
  • O,表示代码的执行时间 T(n) 与 f(n) 的函数关系(正比关系)。

所以,calc 函数中 T ( n ) = O ( 2 n + 3 ) T(n)=O(2n+3) T(n)=O(2n+3)。同样的,calc2 函数的时间复杂度可以怎么表示呢?是的, T ( n ) = O ( 2 n 2 + 4 n + 4 T(n)=O(2n^2+4n+4 T(n)=O(2n2+4n+4)。

需要注意的是,算法的时间复杂度表示的不是代码真正的执行时间,而是代码执行时间随问题规模增长的变化趋势,或者说代码的执行时间与问题规模之间的增长关系。 所以,我们也把它叫作算法的渐进时间复杂度,简称时间复杂度。

在大 O 时间复杂度表示法中,当算法的问题规模 n 足够大的时候,f(n) 中的系数、常量、低阶都变得无关紧要,可以忽略掉,不会影响代码执行时间随问题规模增长的变化趋势。

我们还是从 calc 函数和 calc2 函数这两个例子去说。

(1)calc 函数中 T ( n ) = O ( 2 n + 3 ) T(n)=O(2n+3) T(n)=O(2n+3) 可以直接记为 T ( n ) = O ( n ) T(n)=O(n) T(n)=O(n)。对于 2n+3 而言:

(2)calc2 函数中 T ( n ) = O ( 2 n 2 + 4 n + 4 ) T(n)=O(2n^2+4n+4) T(n)=O(2n2+4n+4) 可以直接记为 T ( n ) = O ( n 2 ) T(n)=O(n^2) T(n)=O(n2)。对于 2 n 2 + 4 n + 4 2n^2+4n+4 2n2+4n+4 而言:

5. 算法时间复杂度计算规则

不过,当一个算法中的代码比较复杂,比如有多条循环语句,甚至循环语句中还嵌套着循环语句等情形发生时,要按照什么样的规则来求得算法的时间复杂度呢?这就要引出算法时间复杂度计算规则了。

🍑 规则 1:只关注循环中的代码段

在分析一个算法的时间复杂度时,我们只需要关注循环中的代码段,该代码段的执行次数对应的问题规模 n 的阶数(数量级)就是整个算法的时间复杂度。

举个例子,在 calc 函数中,第 1、6 行代码与问题规模 n 无关,计算时间复杂度时忽略,而第 2、4 行循环语句的执行次数与问题规模 n 直接相关,这两行代码一共执行了 2n+1 次。又因为系数、常量可以忽略,所以算法的总时间复杂度就是 O(n)。

🍑 规则 2:加法规则

什么是加法规则呢?举个例子,在 calc2 函数中,第 2 行到第 5 行是一个单重循环,而第 6 行到第 12 行是一个双重循环。我们可以分别分析这个单重循环和双重循环的时间复杂度,根据加法规则的公式,取其中阶数最高(数量级最大)的作为整个算法的时间复杂度。

回到例子中,单重循环的时间复杂度为 O(n),双重循环的时间复杂度为 O ( n 2 ) O(n^2) O(n2),所以整个算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。这说明,加法规则的算法时间复杂度取决于阶数最高的代码段的复杂度。

用公式表达,即为:

🍑 规则 3:乘法规则

最后,我们来说乘法规则。什么意思呢?先看下面两个函数的代码,其中在 calc3 函数中调用了 testfunc 函数。

int testfunc(int n)
{
	int sum = 0;
	for (int i = 1; i <= n; ++i)
	{
		sum = sum + i;
	}
	return sum;
}

void calc3(int n)
{
	int sum = 0;
	for (int i = 1; i <= n; ++i)
	{
		sum = sum + testfunc(i);
	}
}

显然,testfunc 函数的时间复杂度是 O(n),而如果 calc3 在 for 循环中没有调用 testfunc 函数,而只是一个普通运算,那么 calc3 函数的时间复杂度也应该为 O(n),但因为在 calc3 函数中调用了 testfunc 函数,这相当于一个双重循环,所以 calc3 函数的时间复杂度应该是 O ( n ∗ n ) = O ( n 2 ) O(n*n) = O(n2) O(nn)=O(n2)

这说明,乘法规则的算法时间复杂度取决于内外循环(将对 testfunc 函数的调用看成是内循环)代码段时间复杂度的乘积。

这说明,乘法规则的算法时间复杂度取决于内外循环(将对 testfunc 函数的调用看成是内循环)代码段时间复杂度的乘积。

用公式表达,即为:

6. 常见算法时间复杂度分析

清楚了这些算法时间复杂度计算规则,我们再来说 6 个常见的算法时间复杂度,让我们之后对算法时间复杂度的计算更加得心应手。

🍑 O ( 1 ) O(1) O(1)

O(1) 是常数阶时间复杂度表示法。不过,它并不意味着只执行了一行代码。

我们看 calc4 函数的代码。

void calc4(int n)
{
	int i = 100;
	int j = 15;
	int sum = i + j + n;
	cout << sum << endl;
}

上面即便有 4 行代码,它的时间复杂度仍旧是 O(1) 而不是 O(4)。换句话说,只要算法的执行时间不随问题规模 n 的增加而增大(执行时间恒定),那么算法的时间复杂度就都是 O(1)。即便算法中包含成百上千行代码也是如此,O 后的圆括号中只能是 1,不能是其他数字。

🍑 O ( l o g 2 n ​ ) O(log_2^{n}​) O(log2n) O ( n l o g 2 n ​ ) O(nlog_2^{n}​) O(nlog2n)

先说 O ( l o g 2 n ​ ) O(log_2^{n}​) O(log2n)。这是是对数阶时间复杂度表示法,这种时间复杂度很常见却不好分析。不过不用着急,先来看看 calc5 函数代码。

void calc5(int n)
{
	int i = 1;
	while (i <= n)
	{
		i = i * 2;
	}
}

根据计算代码时间复杂度的规则,“只关注循环中的代码段”,上面的代码中,i 的初始值从 1 开始,每循环一次则乘以 2,当 i 值大于 n 时,整个循环执行结束。也就是说,i 乘以一次 2,距离 n 就更近了一些。

我们假设 i 乘以了 x 次 2 后值大于 n,此时就会退出循环。也就是说,x 就是 i = i ∗ 2 i = i*2 i=i2;这行代码的执行次数,当 2 x > n 2^x>n 2x>n,也就是循环次数 x = l o g 2 n ​ + 1 x=log_2^{n}​+1 x=log2n+1 时,i 的值将大于 n,此时会跳出 while 循环,所以,这段代码的时间复杂度就是 O ( l o g 2 n ​ ) O(log_2^{n}​) O(log2n)

可能你会问,上述代码的循环体中,如果代码不是 i = i ∗ 2 i=i*2 i=i2,而是 i = i ∗ 3 i=i*3 i=i3,那么代码的时间复杂度又是多少呢?

显然,代码的时间复杂度应该是 O ( l o g 3 n ​ ) O(log_3^{n}​) O(log3n)

但是,根据对数换底公式 l o g b n = l o g b a ∗ l o g a n log_b^{n}=log_b^{a}*log_a^{n} logbn=logbalogan 可知,对数之间可以相互转换,所以 l o g 3 n = l o g 3 2 ∗ l o g 2 n log_3^{n}=log_3^{2}*log_2^{n} log3n=log32log2n,因此 l o g 3 n = O ( l o g 3 2 l o g 2 n ) log_3^{n}=O(log_3^{2}log_2^{n}) log3n=O(log32log2n),而其中的 l o g 3 2 log_3^{2} log32​ 是一个常数,作为系数可以忽略。

最后得出, O ( l o g 3 n ​ ) = O ( l o g 2 n ​ ) O(log_3^{n}​)=O(log_2^{n}​) O(log3n)=O(log2n)

你看,在讨论对数阶时间复杂度时,不管是以 2 为底,3 为底,还是以其他数字为底,我们统一认为是以 2 为底就可以了,而且在书写时间复杂度时,通常这个底数 2 也忽略不写,统一表示为 O ( l o g n ) O(logn) O(logn)

再说 O ( n l o g 2 n ​ ) O(nlog_2^{n}​) O(nlog2n)。它是线性对数阶时间复杂度表示法,同理,这里的底数 2 也可以忽略不写,所以线性对数阶时间复杂度表示为 O ( n l o g n ) O(nlogn) O(nlogn)

上述 calc5 中 while 循环代码段的时间复杂度是 O ( l o g n ) O(logn) O(logn),那么 O ( n l o g n ) O(nlogn) O(nlogn) 的代码是什么样子的呢?

只需要把这段 while 循环代码段循环执行 n 次,根据时间复杂度的乘法规则,得到的时间复杂度就是 O ( n l o g n ) O(nlogn) O(nlogn),你可以参考下面 calc6 函数代码段。

void calc6(int n)
{
    int i = 1;
    for (int count = 0; count < n; ++count)
    {
        while (i <= n)
        {
            i = i * 2;
        }
    }
}

🍑 O ( n ) O(n) O(n)

O ( n ) O(n) O(n) 是线性阶时间复杂度表示法。我们先来看看 calc7 函数的代码。

void calc7(int n)
{
	int sum = 0;
	for (int i = 1; i <= n; ++i)
	{
		sum += i;
	}
}

上面的代码中,因为循环体中的代码需要执行 n 次,因此代码的时间复杂度为 O ( n ) O(n) O(n)

🍑 O ( n 2 ) O(n^2) O(n2) O ( m ∗ n ) O(m*n) O(mn)

先说 O ( n 2 ) O(n^2) O(n2)。它是平方阶时间复杂度表示法。我们看看 calc8 函数的代码。

void calc8(int n)
{
    int sum = 0;
    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= n; ++j)
        {
            sum++;
        }
    }
}

上面的代码是个循环嵌套,它的内循环代码的时间复杂度刚刚分析过,是 O ( n ) O(n) O(n),而外循环又循环了 n 次,因此整个代码段的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。不难想象,如果在外循环外面再套一个 n 次循环,则整个代码段的时间复杂度为 O ( n 3 ) O(n^3) O(n3)

那如果改造一下 calc8 函数,将内循环的 j 初值从 1 修改为 i,修改后的函数为 calc81,你能再尝试分析一下它的时间复杂度吗?

void calc81(int n)
{
    int sum = 0;
    for (int i = 1; i <= n; ++i)
    {
        for (int j = i; j <= n; ++j)
        {
            sum++;
        }
    }
}

上面的代码中,当 i=1 时,内循环执行了 n 次,当 i=2 时,内循环执行了 n-1 次,当 i=3 时,内循环执行了 n-2 次……以此类推,当 i=n 时,内循环就只执行了 1 次。所以内循环的总执行次数是 n + ( n − 1 ) + ( n − 2 ) + … + 1 n+(n-1)+(n-2)+…+1 n+(n1)+(n2)++1,反过来也就是 1 + 2 + 3 + … + n 1+2+3+…+n 1+2+3++n

根据等差数列求和公式, 1 + 2 + 3 + … + n = n ( n + 1 ) / 2 ​ = n 2 / 2 ​ + n / 2 ​ 1+2+3+…+n=n(n+1)/2​=n^2/2​+n/2​ 1+2+3++n=n(n+1)/2​=n2/2​+n/2​,因为大 O 时间复杂度表示法中的系数、低阶都可以忽略掉,所以这段代码的时间复杂度为 O ( n 2 ) O(n^2) O(n2)

好,再看 calc9 函数的代码。

void calc9(int m, int n)
{
    int sum = 0;
    for (int i = 1; i <= m; ++i)
    {
        for (int j = 1; j <= n; ++j)
        {
            sum++;
        }
    }
}

上面的代码中,不一样的地方是 m 和 n 都表示问题规模,而且并不知道 m 和 n 谁的数量级大,在嵌套循环中,外循环循环了 m 次,内循环循环了 n 次,因此代码的复杂度会根据这两个问题的规模决定。

那么根据乘法规则,整个代码段的时间复杂度就是 O ( m ∗ n ) O(m*n) O(mn)

🍑 O(m+n)

这仍是一个代码的复杂度会根据两个问题规模来决定的情形。我们来看 calc10 函数的代码。

void calc10(int m, int n)
{
	int sum1 = 0;
	for (int i = 1; i <= m; ++i)
	{
		sum1 += i;
	}

	int sum2 = 0;
	for (int j = 1; j <= n; ++j)
	{
		sum2 += j;
	}
}

上面的代码中,同样是 m 和 n 两个问题规模,不知道 m 和 n 谁的数量级大,而且和上面不同,不是嵌套的循环。所以,评估整个代码复杂度的时候,就不能用加法规则来取这两个 for 循环中数量级最大的循环的时间复杂度,而是相加。整个代码段的时间复杂度,就是 O ( m + n ) O(m+n) O(m+n)

7. 最好、最坏、平均情况时间复杂度

相信你经常听到这几个词,最好、最坏、平均情况时间复杂度,这些说的是什么情况呢?我们还是从实际的代码示例中找答案。

代码示例:

void calc11(int array[], int n, int x)
{
    int pos = -1;
    for (int i = 0; i < n; ++i)
    {
        if (array[i] == x)
        {
            pos = i;
            break;
        }
    }
    if (pos == -1)
    {
        cout << "没找到值为" << x << "的元素" << endl;
    }
    else
    {
        cout << "找到了值为" << x << "的元素,其在数组中的位置下标为:" << pos << endl;
    }
}

上面的 calc11 函数是在一个数组中查找某个元素 x,问题规模 n 代表的是数组的大小。可以用下面的代码对 calc11 函数进行调用。

int main()
{
    int asz[5] = { 1,2,3,4,5 };
    calc11(asz, 5, 3); //5:数组中元素个数,3:要在数组中查找的值

	return 0;
}

calc11 函数的工作过程是从 array 数组的第一个数组元素开始往后查找,所以,如果要查找的元素 x 位于 array 数组中最前面的位置,那么整个 for 循环只需要执行一次就 break 出来了,此时算法的时间复杂度是 O ( 1 ) O(1) O(1)

而如果元素 x 位于数组 array 中最后的位置,那么整个循环必须要循环 n 次,查找到最后一个元素后才会执行 break 跳出 for 循环,当然,如果元素 x 并没有位于数组 array 中,那么整个循环也会执行 n 次后结束。

所以,对于要查找的元素位于数组的最后位置或者根本不在数组中的情况,算法时间复杂度就变成了 O ( n ) O(n) O(n)

你会发现,要查找的元素不同,这段代码的时间复杂度也不同。因此,这里我们就要引入三个概念了:最好情况时间复杂度、最坏情况时间复杂度以及平均情况时间复杂度。

  • 最好情况时间复杂度:就是在最理想情况下执行这段代码的时间复杂度。如果要查找的元素 x 正好位于数组中最前面的位置,那么此时的时间复杂度,就是最好情况时间复杂度 O ( 1 ) O(1) O(1)
  • 最坏情况时间复杂度:就是在最差情况下执行这段代码的时间复杂度。如果要查找的元素 x 正好位于数组中最后的位置或者不在数组中,就需要遍历一遍整个数组元素,此时的时间复杂度就是最坏情况时间复杂度 O ( n ) O(n) O(n)

最理想情况下和最差的情况通常很少发生,这个时候就需要引入平均情况时间复杂度来表示平均情况下的时间复杂度了。

那么,平均情况时间复杂度应该如何计算呢?

我们回到代码当中。因为数组 array 中有 n 个元素,因此可以假设元素 x 在任意一个下标位置出现的概率相同,都为 1 / n 1/n 1/n​。

不过,为了简化问题,“所查找的元素 x 不在数组中”的情形就不讨论了。因为所查找的元素 x 不在数组中的情形会使问题更加复杂,并且所求得的大 O 时间复杂度计算结果也只会多出一些可以忽略的系数和常量。

好了,说回到计算的方法。设想一下,如果元素 x 出现在数组中下标为 0 的位置,则只需要循环 1 次,如果元素 x 出现在数组中下标为 1 的位置则需要循环 2 次,如果元素 x 出现在数组中下标为 2 的位置则需要循环 3 次。以此类推,如果元素 x 出现在数组中下标为 n-1 的位置则需要循环 n 次。

那么,把每种情况下查找的次数累加起来,再除以 n,就得到了查找次数的平均值。

根据等差数列求和公式 1 + 2 + 3 + … + n 1+2+3+…+n 1+2+3++n 等于 n ( n + 1 ) / 2 n(n+1)/2 n(n+1)/2​,把该公式带入到查找次数平均值中去,就有查找次数平均值 ( n ( n + 1 ) / 2 ​ ) ∗ 1 / n ​ = ( n + 1 ) / 2 ​ (n(n+1)/2​)*1/n​=(n+1)/2​ (n(n+1)/2​)1/n=(n+1)/2​。而又因为大 O 时间复杂度表示法中,系数、常量都可以忽略掉,所以平均情况时间复杂度,兜兜转转,又回到了 O ( n ) O(n) O(n)

可能你会问了,那如果问需要给出一个明确的复杂度的时候,应该用哪个结果呢?

通常情况下,算法并不需要区分最好、最坏、平均情况时间复杂度,只有相同的代码段在不同情况下有数量级的差异时才会用到这三种情况的时间复杂度来区分。当谈论算法复杂度时,通常指最坏情况时间复杂度,平均情况时间复杂度也应该给予关注。平均情况时间复杂度分析起来一般比较复杂,而最好情况时间复杂度,基本也没有多少参考价值。

8. 总结

这篇文章探讨了数据结构与算法的基本概念、算法的特性以及好算法的设计要求等等。其中,一个最重要的话题是讲解了算法时间复杂度。同时引入了算法时间复杂度计算规则,对常见的算法时间复杂度举例进行了分析。

算法时间复杂度主要用于评估一个算法写成的程序在执行时耗费时间的数量级(阶数),让我们对算法的执行时间做到心中有数,甚至更进一步,帮助我们优化算法的编写。

其实 O ( m + n ) O(m+n) O(m+n) O ( m ∗ n ) O(m*n) O(mn) 等等这些计算就是简单的加减乘除运算,辅助一些文中所举的代码例子,基本都可以清晰的读懂、明了,而且后续随着学习深入,这些知识的运用也会自然呈现,让你对它们的认识越来越深。当前,我们主要以理解为主。

思考下面的一些问题

(1)算法时间复杂度的加法规则和乘法规则分别是什么?

算法中,循环 A 中嵌套循环 B,它的时间复杂度是A的循环次数乘以B的循环次数。
 
如果循环 A 和循环B分开,则时间复杂度是 A的循环次数加B的循环次数

(2)常见算法时间复杂度有哪些?尝试做个归纳,写一写这些算法时间复杂度的关系式(大小关系)?

1)常量级 O ( 1 ) O(1) O(1) 随着数据规模n增大,对应算法的时间复杂度不变
 
2)对数级 O ( l o g n ) O(logn) O(logn) 随着数据规模n增大,对应算法的时间复杂度成对数曲线 l o g n logn logn 变化
 
3)线性级 O(n) 随着数据规模n增大,对应算法的时间复杂度成线性曲线 n n n 变化
 
4)线性对数级 O ( n l o g n ) O(nlogn) O(nlogn) 随着数据规模 n 增大,对应算法的时间复杂度成线性对数曲线 n l o g n nlogn nlogn 变化
 
5)平方级 O ( n 2 ) O(n^2) O(n2) 随着数据规模 n 增大,对应算法的时间复杂度成平方级 n 2 n^2 n2 变化
 
6)立方级 O ( n 3 ) O(n^3) O(n3) 随着数据规模 n 增大,对应算法的时间复杂度成立方级 n 3 n^3 n3 变化
 
7)K 次方级 O ( n k ) O(n^k) O(nk) 随着数据规模 n 增大,对应算法的时间复杂度成 n k n^k nk 次方级变化
 
8)指数级 O ( 2 n ) O(2^n) O(2n) 随着数据规模 n 增大,对应算法的时间复杂度成 2 n 2^n 2n 次方级变化
 
9)阶乘级 O ( n ! ) O(n!) O(n!) 随着数据规模 n 增大,对应算法的时间复杂度成 n ! n! n! 阶乘级变化

有关一篇文章吃透算法时间复杂度的更多相关文章

  1. ruby-on-rails - Ruby 检查日期时间是否为 iso8601 并保存 - 2

    我需要检查DateTime是否采用有效的ISO8601格式。喜欢:#iso8601?我检查了ruby​​是否有特定方法,但没有找到。目前我正在使用date.iso8601==date来检查这个。有什么好的方法吗?编辑解释我的环境,并改变问题的范围。因此,我的项目将使用jsapiFullCalendar,这就是我需要iso8601字符串格式的原因。我想知道更好或正确的方法是什么,以正确的格式将日期保存在数据库中,或者让ActiveRecord完成它们的工作并在我需要时间信息时对其进行操作。 最佳答案 我不太明白你的问题。我假设您想检查

  2. ruby-on-rails - 将 Ruby 中的日期/时间格式化为 YYYY-MM-DD HH :MM:SS - 2

    这个问题在这里已经有了答案:Railsformattingdate(4个答案)关闭4年前。我想格式化Time.Now函数以显示YYYY-MM-DDHH:MM:SS而不是:“2018-03-0909:47:19+0000”该函数需要放在时间中.现在功能。require‘roo’require‘roo-xls’require‘byebug’file_name=ARGV.first||“Template.xlsx”excel_file=Roo::Spreadsheet.open(“./#{file_name}“,extension::xlsx)xml=Nokogiri::XML::Build

  3. ruby - 查找字符串中的内容类型(数字、日期、时间、字符串等) - 2

    我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s

  4. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

  5. sql - 查询忽略时间戳日期的时间范围 - 2

    我正在尝试查询我的Rails数据库(Postgres)中的购买表,我想查询时间范围。例如,我想知道在所有日期的下午2点到3点之间进行了多少次购买。此表中有一个created_at列,但我不知道如何在不搜索特定日期的情况下完成此操作。我试过:Purchases.where("created_atBETWEEN?and?",Time.now-1.hour,Time.now)但这最终只会搜索今天与那些时间的日期。 最佳答案 您需要使用PostgreSQL'sdate_part/extractfunction从created_at中提取小时

  6. ruby - 在没有基准或时间的情况下用 Ruby 测量用户时间或系统时间 - 2

    因为我现在正在做一些时间测量,我想知道是否可以在不使用Benchmark类或命令行实用程序time的情况下测量用户时间或系统时间。使用Time类只显示挂钟时间,而不显示系统和用户时间,但是我正在寻找具有相同灵active的解决方案,例如time=TimeUtility.now#somecodeuser,system,real=TimeUtility.now-time原因是我有点不喜欢Benchmark,因为它不能只返回数字(编辑:我错了-它可以。请参阅下面的答案。)。当然,我可以解析输出,但感觉不对。*NIX系统的time实用程序也应该可以解决我的问题,但我想知道是否已经在Ruby中实

  7. ruby - 以毫秒为单位获取当前系统时间 - 2

    在Ruby中,以毫秒为单位获取自纪元(1970)以来的当前系统时间的正确方法是什么?我试过了Time.now.to_i,好像不是我想要的结果。我需要结果显示毫秒并且使用long类型,而不是float或double。 最佳答案 (Time.now.to_f*1000).to_iTime.now.to_f显示包含十进制数字的时间。要获得毫秒数,只需将时间乘以1000。 关于ruby-以毫秒为单位获取当前系统时间,我们在StackOverflow上找到一个类似的问题:

  8. ruby-on-rails - Ruby on Rails - 需要在每周的特定时间将消息发送到电子邮件 - 2

    我想知道我应该如何着手这个项目。我需要每周向人们发送一次电子邮件。但是,这必须在每周的特定时间自动生成并发送。编码有多难?我需要知道是否有任何书籍可以提供帮助,或者你们中的任何人是否可以指导我。它必须使用ruby​​onrails进行编程。因此有一个网络服务和数据库集成。干杯 最佳答案 为什么这么复杂?您只需安排工作。您可以使用Delayed::Job例如。Delayed::Job让您可以使用run_at符号在特定时间安排作业,如下所示:Delayed::Job.enqueue(SendEmailJob.new(...),:run_

  9. ruby - rspec 显示负时间 - 2

    我在ruby​​1.9.3p0上运行rails3.2.1和rspec2.8.1,在运行我的测试时它显示负时间值。这很烦人,因为我正在尝试优化我的测试。Running:spec/models/transaction_spec.rb................................................Finishedin-7603162.49414seconds我已经尝试将rspec更新到2.9.0,但这没有帮助。 最佳答案 你在使用timecopgem吗?确保在卡住后Timecop.return。或者你在某处

  10. ruby - 将 Logstash 中的时间戳时区转换为输出索引名称 - 2

    在我的场景中,Logstash收到的系统日志行的“时间戳”是UTC,我们在Elasticsearch输出中使用事件“时间戳”:output{elasticsearch{embedded=>falsehost=>localhostport=>9200protocol=>httpcluster=>'elasticsearch'index=>"syslog-%{+YYYY.MM.dd}"}}我的问题是,在UTC午夜,Logstash在外时区(GMT-4=>America/Montreal)结束前将日志发送到不同的索引,并且索引在20小时(晚上8点)之后没有日志,因为“时间戳”是UTC。我们已

随机推荐