一、什么是数据结构?
数据结构是计算机存储、组织数据的方式
![](https://img.haomeiwen.com/i2470124/ba546c7841d9a0ee.png)
二、什么是算法?
算法是用于解决特定问题的一系列的执行步骤。
使用不同算法,解决同一个问题,效率可能相差非常大。比如:
求第 n 个斐波那契数(fibonacci number)
三、如何评判一个算法的好坏?
一般从以下维度来评估算法的优劣:
- 正确性、可读性、健壮性(对不合理输入的反应能力和处理能力)
- 时间复杂度(time complexity):估算程序指令的执行次数(执行时间)
- 空间复杂度(space complexity):估算所需占用的存储空间
一般用大O表示法
来描述负责度,它表示的是数据规模n
对应的复杂度
大O表示法
仅仅是一种粗略的分析模型,是一种估算
,能帮助我们短时间内了解一个算法的执行效率。
四、时间复杂度
引出重要概念,所有代码的执行时间 T(n)
与每行代码的执行次数 n
成正
T(n) = O(f(n))
它表示一个算法的渐进时间复杂度。
其中 f(n) 表示代码执行次数之和
O表示正比例关系
// 来看一个例子:
for (int i = 1; i <= n; i++) {
x++;
}
我们知道这个for loop
有n
个循环,假设其中x++
计算的消耗是一个单位
第一次循环是1单位
第二次循环是2单位
所以整个循环语句就要消耗n个单位
可以发现,消耗的单位时间随着循环的次数而变化
循环次数为1,时间为1单位
循环次数为10,时间为10单位
循环次数为n,时间为n单位
所以这个算法的「时间复杂度」可以表示为:T (n) = O(n)
// 再看一个例子:
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
x++;
}
}
在外层循环中,i 总共需要n层循环,在每一次内层循环中,j 也会循环n次。那么两个循环语句的复杂度就是 O(n^2)
如果我们将这两个算法合并到一起:
for (int i = 1; i <= n; i++) {
x++;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
x++;
}
}
整个算法复杂度就变为 O(n + n2)
,在n无限大的情况下,可以简化为 O(n2)
斐波那契数列 - 递归复杂度分析
// O(2^n)
public static int fib1(int n) {
if (n <= 1) return n;
return fib1(n - 1) + fib1(n - 2);
}
![](https://img.haomeiwen.com/i2470124/924b7fd82e29fd0c.png)
忽略常数、系数、低阶:
9 >> O(1)
2n + 3 >> O(n)
n2 + 2n + 6 >> O(n2)
4n3 + 3n2 + 22n + 100 >> O(n3)
写法上,n3 等价于 n^3
常见的时间复杂度
![](https://img.haomeiwen.com/i2470124/30656489acc4a5c9.png)
对数阶的一些细节
先看一下对数定义:
如果
a
的x
次方等于N
(a>0,且a不等于1),那么数x
叫做以a为底
N的对数,记作x = log a^N
. 其中,a
叫做对数的 [底数],N
叫做[真数]
❤️ 在计算机信息学中,提到对数,一般就是以2为底
的
比如:
log2^8 = 3
log2^16 = 4
...
log3^9 = 2
log4^16 = 2
我们在衡量复杂度的时候,对于对数阶来说,一般都会忽略底数
log2^n, log3^n, log4^n ... 都统统成为logn
第一次听说 O(log n)
时间复杂度可能是在学二分搜索算法
的时候;
在最好情况下二分搜索的时间复杂度是 O(1),最坏情况(平均情况)下 O(log n), 直接来看O(log n)情况
已知有 16 个元素的有序数组,举个最坏情况的例子,比如我们要找的是数字 13
![](https://img.haomeiwen.com/i2470124/83832d04deb3e2de.png)
选中间的元素作为中心点(长度的一半)
![](https://img.haomeiwen.com/i2470124/707113fc938e7d67.png)
通过比较发现,13 小于中心点,所以不用考虑数组的后一半,只考虑前一半
![](https://img.haomeiwen.com/i2470124/ede57fc021db4cbc.png)
![](https://img.haomeiwen.com/i2470124/b649c3e4c8bffd93.png)
![](https://img.haomeiwen.com/i2470124/14a68163455c672d.png)
![](https://img.haomeiwen.com/i2470124/b3c96b7c63bf0224.png)
重复这个过程,每次都寻找子数组的中间元素,每次和中间元素比较都会使搜索范围减半
所以为了从 16 个元素中找到目标元素,我们需要把数组平均分割 4 次, 也就是说:
![](https://img.haomeiwen.com/i2470124/4b09b7ec861859d6.png)
如果有 n
个元素:
![](https://img.haomeiwen.com/i2470124/f2404372d7b36497.png)
归纳简化下公式:
![](https://img.haomeiwen.com/i2470124/ae37ac101ba4d23c.png)
等式两边同时乘以 2^k
![](https://img.haomeiwen.com/i2470124/9fcea50ac11c6b23.png)
![](https://img.haomeiwen.com/i2470124/0868562fd0425b65.png)
最终得到:
![](https://img.haomeiwen.com/i2470124/5735470b7ae9886d.png)
对数形式
![](https://img.haomeiwen.com/i2470124/10466d1e872bc27d.png)
最终得出二分搜索算法在最坏的情况下时间复杂度就是O(logn)
级别的
时间复杂度分析
1. 常数阶O(1)
只要没有循环或递归等复杂逻辑,无论代码执行多少行,代码复杂度都为O(1),如下:
int x = 0;
int y = 1;
int temp = x;
x = y;
y = temp;
上述代码在执行的时候,所消耗的时间不会随着特定变量的增长而增长,即使有几万行这样的代码,我们都可以用O(1)来表示它的时间复杂度。
2.线性阶O(n)
上述的例子中讲解过O(n)的算法:
for (int i = 1; i <= n; i++) {
x++;
}
在这段代码中,for循环会执行n遍,因此计算消耗的时间是随着n的变化而变化,因此这类代码都可以用O(n)来表示其时间复杂度
3.对数阶O(logN)
int i = 1;
while(i < n) {
i = i * 2;
}
在上面的循环中,每次i都会被乘以2,也意味着每次 i 都离 n 更进一步。那需要多少次循环 i 才能等于或大于 n 呢,也就是求解2的x次方等于n,答案x=log2^n。也就是说循环 log2^n次之后,i会大于等于n,这段代码就结束了。所以此代码的复杂度为:O(logN)
4.线性对数阶O(nlogN)
线性对数阶O(nlogN)很好理解,也就是将复杂度为O(logN)的代码循环n遍:
for(int i = 0; i <= n: i++) {
int x = 1;
while(x < n) {
x = x * 2;
}
}
因为每次循环的复杂度为O(logN),所以n * logN = O(nlogN)
5.平方阶O(n²)
在上面的例子也讲过,O(n²)就是将循环次数为n的代码再循环n遍:
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
x++;
}
}
O(n²)的本质就是n * n,如果我们将内层的循环次数改为m:
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
x++;
}
}
复杂度就变为 n * m = O(n * m)
关于一些更高的阶级比如O(n³)或者O(n^k),我们可以参考O(n²)来理解即可,O(n³)相当于三层循环,以此类推
空间复杂度分析
「空间复杂度」并不是用来计算程序具体占用的空间。
随着问题量级的变大,程序需要分配的内存空间也可能会变得更多,而「空间复杂度」反映的则是内存空间增长的趋势
。
常用的空间复杂度
比较常用的空间复杂度有:O(1)、O(n)、O(n²),我们一般用 S(n) 来定义「空间复杂度」
1.O(1)空间复杂度
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,此算法空间复杂度为一个常量,可表示为 O(1):
int x = 0;
int y = 0;
x++;
y++;
其中x, y所分配的空间不随着处理数据量变化,因此「空间复杂度」为 O(1)
2.O(n)空间复杂度
以下的代码给长度为n的数组赋值:
int[] newArray = new int[n];
for (int i = 0; i < n; i++) {
newArray[i] = i;
}
在这段代码中,我们创建了一个长度为 n 的数组,然后在循环中为其中的元素赋值。因此,这段代码的「空间复杂度」取决于 newArray 的长度,也就是 n,所以 S(n) = O(n)
「时间复杂度」和「空间复杂度」这两个复杂度反映的是:
随着问题量级的增大,时间和空间增长的趋势
。学会了复杂度的分析,我们就可以对比算法之间的优劣势啦~
网友评论