数据结构、算法第一节:复杂度分析

大 O 复杂度表示法

算法的执行效率,粗略地讲,就是算法代码执行的时间。

下方这段代码很简单,意为计算1 + 2 + 3 +…+ n的和

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

假设每行代码执行的时间都一样,为 x
第二行定义变量,执行一次,定义for循环和循环内部各执行n次,就是2n,那么这段代码的总执行时间就是:(2n + 1) * x

尽管我们不知道x的具体时间,但是通过代码执行时间的推导过程,可以得到一个非常重要的规律,那就是,所有代码的执行时间 T(n) 与每行代码的执行次数 f(n) 成正比。

总结为公式就是:

T(n) = O(f(n))

  • T(n) 表示代码执行的时间;
  • n 表示数据规模的大小;
  • f(n) 表示每行代码执行的次数总和。

大 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。也叫做大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度,简称时间复杂度。

当 n 很大时,你可以把它想象成 10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。只需要记录一个最大量级就可以了。

时间复杂度分析

1. 只关注循环执行次数最多的一段代码

因为在大O时间复杂度表示法中,低阶、常量、系数都可以忽略不计,所以在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了。这段核心代码执行次数的 n 的量级,就是整段要分析代码的时间复杂度。

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

还是最开始的一段代码,其中第二行定义变量,与n的大小无关,所以对于复杂度并没有影响。循环执行次数最多的是第 3、4 行代码,所以这块代码要重点分析。这两行代码被执行了 n 次,所以总的时间复杂度就是 O(n)。

2. 加法法则

总复杂度等于量级最大的那段代码的复杂度

public int cal(int n) {
	int num = 0;
	for (int i = 1;i <= 100;i++) {
		num = num + i;
	}

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

	return num + sum;
}

这段代码分为两个部分,分别是求num和sum,返回最终相加的结果,分开剖析两段代码的时间复杂度,然后把它们放到一块儿,再取一个量级最大的作为整段代码的复杂度。

第一段代码共执行100次,所以是一个常量的执行时间,跟 n 的规模无关。即便这段代码循环 10000 次、100000 次,只要是一个已知的数,跟 n 无关,照样也是常量级的执行时间。当 n 无限大的时候,就可以忽略。尽管对代码的执行时间会有很大影响,但是回到时间复杂度的概念来说,它表示的是一个算法执行效率与数据规模增长的变化趋势,所以不管常量的执行时间多大,我们都可以忽略掉。因为它本身对增长趋势并没有影响。

第二段代码共执行n次,时间复杂度为O(n)。

我们取其中最大的量级。所以,整段代码的时间复杂度就为O(n)。也就是说:总的时间复杂度就等于量级最大的那段代码的时间复杂度。

3. 乘法法则

嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

public void cal(int n) {
	int num = 0;
	for (int i = 0;i < n;i++) {
		num = num + calTest(i);
	}
}

private int calTest(int n) {
	int sum = 0;
	for (int i = 0;i < n;i++) {
		sum = sum + i;
	}
	return sum;
}

单独看 cal() 函数。假设 calTest() 只是一个普通的操作,那这段代码的时间复杂度就是O(n),但 calTest() 函数本身不是一个简单的操作,它的时间复杂度是O(n),所以,整个 cal() 函数的时间复杂度就是,O(n*n) = O(n²)。

常见的时间复杂度

  • 常量阶O(1)
  • 对数阶O(logn)
  • 线性阶O(n)
  • 线性对折阶O(nlogn)
  • 平方阶O(n²)
  • 立方阶O(n³)
  • k次方阶O(n的k次方)
  • 指数阶O(2的n次方)
  • 阶乘阶O(n!)

1. O(1)

O(1) 只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。比如这段代码,即便有 3 行,它的时间复杂度也是 O(1),而不是 O(3)。

int i = 0;
int j = 0;
int k = 0;

总结:只要代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度我们都记作 O(1)。或者说,一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。

2.O(logn)、O(nlogn)

public void cal() {
	i=1; 
	while (i <= n) { 
		i = i * 2; 
	}
}

根据前面的复杂度分析方法,第三行代码是循环执行次数最多的。所以,只要能计算出这行代码被执行了多少次,就能知道整段代码的时间复杂度。

从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。实际上,变量 i 的取值就是一个等比数列。一个一个列出来,就应该是这个样子的:

2的零次方,2的一次方,2的2次方,2的三次方…2的x次方 = n

通过 2的x次方 = n 求解。x=log2n,所以,这段代码的时间复杂度就是 O(log2n)。

同理将代码中的 i * 2 改为-> i * 3,时间复杂度为 O(log3n)。

基于前面的一个理论:在采用大 O 标记复杂度的时候,可以忽略系数,因此,在对数阶时间复杂度的表示方法里,忽略对数的“底”,统一表示为 O(logn)。

O(nlogn) 就很容易理解了。根据上述的乘法法则,如果一段代码的时间复杂度是 O(logn),循环执行 n 遍,时间复杂度就是 O(nlogn) 了。而且,O(nlogn) 也是一种非常常见的算法时间复杂度。比如,归并排序、快速排序的时间复杂度都是 O(nlogn)。

3.O(m+n)、O(m*n)

public int cal(int m, int n) { 
	int sum_1 = 0; 
	for (int i = 1;i < m;i++) {
		sum_1 = sum_1 + i; 
	}
	
	int sum_2 = 0; 
	for (int j = 1;j < n;j++) { 
		sum_2 = sum_2 + j;
	} 
	return sum_1 + sum_2;
}

从代码中可以看出,m 和 n 是表示两个数据规模。无法事先评估 m 和 n 谁的量级大,所以在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复杂度就是 O(m+n)。

乘法法则依然有效:T1(m) * T2(n) = O(f(m) * f(n))。

空间复杂度分析

时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度全称就是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系

public void print(int n) {
    int i = 0;
    int[] a = new int[n];
    for (i;i < n;i++) {
        a[i] = i * i;
    }

    for (i = n-1; i >= 0; i--) {
  	    System.out.println(a[i]);
    }
}

跟时间复杂度分析一样,可以看到,第 2 行代码中,申请了一个空间存储变量 i,但是它是常量阶的,跟数据规模 n 没有关系,所以可以忽略。第 3 行申请了一个大小为 n 的 int 类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是 O(n)。

小节

复杂度也叫渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率与数据规模之间的增长关系,可以粗略地表示,越高阶复杂度的算法,执行效率越低。常见的复杂度并不多,从低阶到高阶有:O(1)、O(logn)、O(n)、O(nlogn)、O(n²)。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘水水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值