学习来源:极客时间王争老师的《数据结构与算法之美》
记录一下笔记及自己的感想
关于数据结构与算法,简单来说,数据结构是存储数据的方式,而算法是操作数据的方法,二者相辅相成,互相依赖。
比如 二分法是基于数组的随机访问的特性才能实现的,对于链表来说是没办法用二分查找法的。
复杂度分析,是数据结构和算法学习的精髓。因为数据结构和算法解决的是如何更省、更快地存储和处理数据问题,所以复杂度分析就是一个考量效率和资源消耗的方法。
复杂度分析
大O复杂度表示法
int cal(int n){
int sum = 0;
int i = 1;
for(int i = 0 ; i<n; ++i){
sum = sum + i;
}
return sum;
}
从CPU角度看,这段代码执行操作类似:读数据-运算-写数据。
假设每段代码执行时间都一样,为unit_time。
2,3行代码分别需要1个unit_time执行时间,4,5行代码都运行了n遍,所以是2n*unit_time
总共执行时间为2unit_time + 2n*unit_time = (2n+2)unit_time
可以看出代码的执行时间T(n)与每段代码的执行次数成正比
int cal(int n){
int sum = 0;
int i = 1;
int j = 1;
for(int i = 0 ; i<n; ++i){
j = 1;
for(int j = 0 ; j< n ; j++){
sum = sum + i * j;
}
}
return sum;
}
2,3,4行代码需要1个unit_time执行时间,5,6行需要n个unit_time执行时间,7,8代码循环执行了n²,所以总共需要的执行时间为
3unit_time + 2n×unit_time + unit_time×2n² = (3+2n+2n²)*unit_time
根据上述例子,可以得到一个重要规律:所有代码的执行时间T(n)与每行代码的执行次数f(n)成正比
T(n):代码执行时间
n:数据规模大小
f(n):表示每行代码执行的次数总和
O:标识代码执行时间T(n)与f(n)表达式成正比
上述例子套用该公式
T(n) = O(2n+2)
T(n) = O(2n²+2n+3)
大O时间复杂度表示法,实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫做渐进时间复杂度,简称时间复杂度。
当n很大时(代码数),公式中的低阶、常量、系数三部分并不左右增长趋势,多以可以忽略,只需要记录一个最大量级即可。(也就是极限思想,当一个数趋于无穷大的时候,计算极限值时只需要抓大头即可)
T(n) = O(n)
T(n) = O(n²)
时间复杂度分析
1.只关注循环执行次数最多的一段代码
在分析一个算法、一段代码的时间复杂度的时候,只关注循环执行次数最多的那一段代码。即核心代码执行次数的n的量级,就是整段要分析代码的时间复杂度。
2.加法法则:总复杂度等于量级最大的那段代码的复杂度
3.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积。
几种常见时间复杂度实例分析
常量阶:O(1)
对数阶:O(logn)
线性阶:O(n)
线性对数阶:O(nlogn)
平凡阶:O(n²)
指数阶:O(2^n)
阶乘阶:O(n!)
对于复杂度量级,可以分为多项式量级和非多项式量级。
把时间复杂度为非多项式量级的算法问题叫做NP(Non-Deterministic Polynomial,非确定多项式)问题。
1.O(1)
int i = 8;
int j = 6;
int sum = i + j;
只要算法中不存在循环语句、递归语句、即使有成千上万行的代码,其时间复杂度也是O(1)。
2.O(logn)、O(nlogn)
i=1;
while(i <= n){
i = i * 2;
}
2 * 2^2 * 2^3 ... 2 ^x = n
即 只要知道x的值,就可以知道该行代码执行的次数。
x=log2^n
实际上不管以几为底,都可以记为logn。
因为换底公式,对数之间可以互相转换,又因为在采用大O标记复杂度的时候可以忽略系数,即O(Cf(n)) = O(f(n))。所以忽略对数的底数,统一为O(logn)
如果该代码循环执行n遍,则时间复杂度为O(nlogn)。
O(nlogn)是一种非常常见的算法时间复杂度。比如递归排序、快速排序的时间复杂度都是O(nlogn)
3.O(m+n)、O(m*n)
int cal(int m,int n){
int sum_1 = 0;
int i = 1;
for(int i = 0 ; i < m ; i++){
sum_1 = sum_1 + i;
}
int sum_2 = 0;
int j = 1;
for(; j < n ; j++){
sum_2 = sum2 + j;
}
return sum_1 + sum_2;
}
该时间复杂度由两个数据的规模来决定。
由上述代码看出 m和n表示两个数据规模,但由于无法事先评估m和n谁的量极大,所以在表示时间复杂度时,不能无法抓大头,所以时间复杂度为O(m+n)
空间复杂度分析
void print(int n){
int i = 0 ;
int[] a = new int[n]; //占用n个大小int类型
for(i;i<n;i++){
a[i] = i * i;
}
for(i = n-1;i>=0;--i){
print out a[i];
}
}
//整段代码的空间复杂度为O(n)
时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。
空间复杂度表示算法的存储空间与数据规模之间的增长关系。
最好、最坏情况时间复杂度
int find(int[] array,int n,int x){
int i = 0;
int pos = -1;
for(; i < n ; ++i){
if(array[i] == x){
pos = i;
}
}
return pos;
}
//时间复杂度为:O(n)
//优化
int find(int[] array,int n,int x){
int i = 0;
int pos = -1;
for(; i < n ; ++i){
if(array[i] == x){
pos = i;
break;
}
}
return pos;
}
//时间复杂度根据x的位置是不同的
最好情况时间复杂度:
在最理想的情况下,执行这段代码的时间复杂度。即x是数组的第一个元素/
最坏情况时间复杂度:
在最糟糕的情况下,执行这段代码的时间复杂度。即x不在数组里。
平均情况时间复杂度
要查找的x变量x在数组中的位置,有n+1种情况:在数组的0~n-1位置中和不在数组中。
将每种情况,需要遍历的元素个数累加起来,在除以n+1,就可以得到需要遍历的元素个数的平均值。
1+2+3+…+n+n / n+1
简化后为O(n)
考虑到x出现在各个位置的概率,即在数组中和不再数组中的概率假设为1/2,在0~n-1的概率为1/n,根据概率乘法法则,x在0~n-1的概率就是)(1/2n)
将概率考虑进上面公式
1*1/2n + 2 * 1/2n + 3 * 1/2n … + n * 1/2n + n * 1/2 = 3n+1 / 4
上面的值为概率论中的加权平均值,也叫期望值。即平均时间复杂度的全称应该叫加权平均时间复杂度或者期望时间复杂度
简化后为O(n)
只有同一块代码在不同情况下,时间复杂度有量级的差距,我们才会使用这三种复杂度表示法来区分。
————————————————————————————
还好最近在学高数,极限的计算刚好可以用得上,瞬间感觉数学真的是任何事任何时间都息息相关