【408数据结构】一篇文章吃透算法时间复杂度

news2024/10/7 14:30:23

文章目录


前言

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

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

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

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

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! 阶乘级变化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/403521.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

两道有关链表的练习

目录 一、分割链表 二、奇偶链表 一、分割链表 给你一个链表的头节点 head 和一个特定值 x &#xff0c;请你对链表进行分隔&#xff0c;使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你不需要 保留 每个分区中各节点的初始相对位置。 示例 1&#xff1a; 输…

国科大论文latex模板中可能的注意事项

背景 国科大2022年9月发布了毕业论文的LaTeX模板&#xff0c;它是在ucasthesis上修改而来的&#xff0c;但近日使用国科大发布版本时发现有几点不同以及需要注意的地方。本人只会简单使用latex&#xff0c;但并不熟悉latex样式编辑&#xff0c;因此以下介绍与方法仅供参考。仅…

基于 Flink CDC 的实时同步系统

摘要&#xff1a;本文整理自科杰科技大数据架构师张军&#xff0c;在 FFA 2022 数据集成专场的分享。本篇内容主要分为四个部分&#xff1a;功能概述架构设计技术挑战生产实践Tips&#xff1a;点击「阅读原文」查看原文视频&演讲 ppt科杰科技是专门做大数据服务的供应商&am…

Regmap API 实验

目录 一、Regmap regmap 结构体 regmap_config 结构体 regmap_config 掩码设置 二、Regmap 操作函数 1、Regmap 申请与初始化 2、 regmap 设备访问 API 函数 3、regmap_update_bits 函数 4、regmap_bulk_read函数 5、regmap_bulk_write 三、修改SPI实验 1、添加regmap…

Kubenates中的日志收集方案ELK(下)

1、rpm安装Logstash wget https://artifacts.elastic.co/downloads/logstash/logstash-6.8.7.rpm yum install -y logstash-6.8.7.rpm2、创建syslog配置 input {beats{port> 5044 } }output {elasticsearch {hosts > ["http://localhost:9200"]index …

【博客632】k8s service ession affinity原理与iptables recent模块

k8s service ession affinity原理与iptables recent模块 1、iptables recent模块 作用&#xff1a; iptables的recent模块用于限制一段时间内的连接数, 是谨防大量请求攻击的必杀绝技! 善加利用该模块可充分保证服务器安全。 recent常用参数&#xff1a; –name 设定列表名称…

Git使用教程:最详细、最傻瓜、最浅显、真正手把手教

GITGIT版本控制版本控制的意义分布式图形化客户端环境搭建仓库的操作分支使用场景命令远程仓库操作生成公钥命令冲突忽略列表的配置时机配置方式版本回退练习&#xff1a;GIT 版本控制 把文件系统中的文件&#xff0c;按照修改的版本进行记录&#xff0c;进行管理的操作。 版…

Spring Cloud ( Consul注册、发现 )

操作步骤&#xff1a; 安装Consul服务端在服务内添加客户端依赖修改配置类&#xff0c;添加注解编写yml文件一、安装Consul服务端 链接&#xff1a;https://www.consul.io/downloads.html&#xff0c;解压 开启cmd&#xff0c;进入你的Consul解压路径&#xff0c;我是在E盘 …

扩展欧几里得算法及其应用

前言 由于数论的板子真的很抽象&#xff0c;也很难背&#xff0c;所以特此记录扩展欧几里得算法的板子和它的用途 本篇文章只涉及应用&#xff0c;不涉及证明&#xff0c;如需理解证明还请各位移步其他优秀的讲解&#xff01; 扩展欧几里得算法 先粘一下板子的代码 typedef lo…

JAVA面向对象特征之——封装

4.封装 private关键字 是一个权限修饰符 可以修饰成员(成员变量和成员方法) 作用是保护成员不被别的类使用&#xff0c;被private修饰的成员只在本类中才能访问 针对private修饰的成员变量&#xff0c;如果需要被其他类使用&#xff0c;提供相应的操作 提供 “get变量名()…

面向数据安全共享的联邦学习研究综述

开放隐私计算 摘 要&#xff1a;跨部门、跨地域、跨系统间的数据共享是充分发挥分布式数据价值的有效途径&#xff0c;但是现阶段日益严峻的数据安全威胁和严格的法律法规对数据共享造成了诸多挑战。联邦学习可以联合多个用户在不传输本地数据的情况下协同训练机器学习模型&am…

【SpringCloud】SpringCloud详解之Feign远程调用

目录前言SpringCloud Feign远程服务调用一.需求二.两个服务的yml配置和访问路径三.使用RestTemplate远程调用(order服务内编写)四.构建Feign(order服务内配置)五.自定义Feign配置(order服务内配置)六.Feign配置日志(oder服务内配置)七.Feign调优(order服务内配置)八.抽离Feign前…

SNS (Simple Notification Service)简介

SNS (Simple Notification Service) 是一种完全托管的发布/订阅消息收发和移动通知服务&#xff0c;用于协调向订阅终端节点和客户端的消息分发。 和SQS (Simple Queue Service)一样&#xff0c;SNS也可以轻松分离和扩展微服务&#xff0c;分布式系统和无服务应用程序&#xf…

九龙证券|直逼1.5万亿!A股融资余额创年内新高,青睐这些行业和个股

2023年以来&#xff0c;A股商场震动重复&#xff0c;商场走势整体先扬后抑&#xff0c;各路资金看法纷歧&#xff0c;但数据显现&#xff0c;融资客在此期间整体持续净买入&#xff0c;未受到商场动摇的明显冲击&#xff0c;融资余额日前已迫临1.5万亿元&#xff0c;创出年内新…

磨金石教育摄影技能干货分享|烟花三月下扬州,是时候安排了!

人间三月最柔情&#xff0c;杨柳依依水波横。三月的风将要吹来&#xff0c;春天的门正式打开。对中国人来说&#xff0c;古往今来&#xff0c;赏春最好的地方是江南。人人都说江南好&#xff0c;可是江南哪里好呢&#xff1f;古人在这方面早就给出了答案&#xff1a;故人西辞黄…

使用高精度秒表StopWatch测试DateTime.Now的精度

StopWatch使用的命名空间&#xff1a;using System.Diagnostics;StopWatch的使用方法&#xff1a;创建Stopwatch对象&#xff1a;stopwatch&#xff1b;stopwatch计时表开启&#xff1a;stopwatch.Start();stopwatch计时表关闭&#xff1a;stopwatch.Stop();计算stopwatch.Stop…

【剧前爆米花--爪哇岛寻宝】进程的调度以及并发和并行,以及PCB中属性的详解。

作者&#xff1a;困了电视剧 专栏&#xff1a;《JavaEE初阶》 文章分布&#xff1a;这是关于进程调度、并发并行以及相关属性详解的文章&#xff0c;我会在之后文章中更新有关线程的相关知识&#xff0c;并将其与进程进行对比&#xff0c;希望对你有所帮助。 目录 什么是进程/…

redis布隆过滤器与四个缓存问题

目录布隆过滤器定义特性使用场景解决缓存穿透的问题黑白名单校验底层原理哈希冲突案例添加key查询key总结四个缓存问题缓存雪崩定义解决方案缓存穿透定义解决方案方案一方案二(guava实现)代码案例源码分析方案三(RedisSon实现)代码实现方案四(直接安装redis插件,应用层解决方案…

港科夜闻|香港科大与中国联通成立联合实验室,推动智慧社会研究发展

关注并星标每周阅读港科夜闻建立新视野 开启新思维1、香港科大与中国联通成立联合实验室&#xff0c;推动智慧社会研究发展。香港科大与中国联通于3月9日签署两份协议以加强战略合作&#xff0c;并成立「香港科技大学 - 中国联通智慧社会联合实验室」&#xff0c;就香港科大建构…

基于支持向量机SVM的风电场NWP数据预测,SVM的详细原理

目录 支持向量机SVM的详细原理 SVM的定义 SVM理论 Libsvm工具箱详解 简介 参数说明 易错及常见问题 SVM应用实例,基于SVM的风电场NWP预测 结果分析 展望 支持向量机SVM的详细原理 SVM的定义 支持向量机(support vector machines, SVM)是一种二分类模型,它的基本模型是定…