大学里的基础课程例如操作系统,计算机网络,通信原理,硬件基础和数据结构等作为技术人员的内功,是每个从事IT行业的优质程序员必备的基础知识。只有拥有扎实的基础知识,才能在这个瞬息万变的年代中,以不变应万变,掌握主动性。今天是2018年12月15日周六,庆幸自己意识到的还不至于太晚,接下来这段时间,我会将自己算法的学习笔记整理在博客上,每周至少更新三篇左右,请各位监督。
广义来说,数据结构就是一组数据的存储结构,算法就是一组数据的操作方法。两者相辅相成,数据结构是为算法服务,算法是作用于某个特定的数据结构,脱离了数据结构的算法好似空中楼阁,脱离算法孤立存在的数据结构没有存在的意义。
数据结构和算法解决的是如何更省、更快的存储和处理数据的问题。因此,为了能更直观的考量效率和资源消耗,提出了一个复杂度分析方法,复杂度分析方法是算法的精髓。
数组,链表,栈,队列,散列表,二叉树,堆,跳表,图,Trie树;
递归,排序,二分查找,搜索,哈希算法,贪心算法,分治算法,回溯算法,动态规划,字符串匹配算法
一个不需要具体的测试数据来测试,就可以粗略估计算法执行效率的方法。算法执行效率粗略的讲就是算法代码执行的时间,但是如何在不运行代码的情况下用“肉眼”得到一段代码的运行时间呢?这是一段从1到n累加和的代码:
int test(int n){
int sum=0;
for(int i=1;i<=n;i++){
sum=sum+i;
}
return sum;
}
从cpu角度来说,假设每一行代码的操作读数据-写数据-运算执行时间相等(实际这三种操作执行时间并不相同)。但我们估计来说,假设他们每执行一次操作需要一个time的时间,从上面的代码来说,总共执行了2次写操作,2n次运算,即执行的时间为:
( 2 n + 2 ) ∗ t i m e (2n+2)*time (2n+2)∗time
所以在这个代码中,T(n)=O(2n+2)。把这个规律总结成一个公式,即:
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
其中,T(n)是代码执行的时间;n表示数据规模的大小;f(n)表示所有代码执行的次数总和。公式中的O表示T(n)和f(n)成正比。这就是大O时间复杂度表示法。当n的取值非常大时,公式中的低阶常量系数并不能对增长趋势造成很大影响,所以忽略后,我们只需要记录一个最大量级就可以了。所以,第一个代码的时间复杂度变成了我们最熟悉的一个式子:
T ( n ) = O ( n ) T(n)=O(n) T(n)=O(n)
大O复杂度表示法中说了要忽略掉公式中的常量系数低阶等,所以时间复杂度分析只关注循环执行次数最多的代码。
总的时间复杂度等于量级最大的代码的时间复杂度。即:
T 1 ( n ) = O ( f ( n ) ) ; T 2 ( n ) = O ( g ( n ) ) ; T1(n)=O(f(n));T2(n)=O(g(n)); T1(n)=O(f(n));T2(n)=O(g(n));
那么:
T ( n ) = T 1 ( n ) + T 2 ( n ) = m a x ( ( O f ( n ) , O ( g ( n ) ) ) = O ( m a x ( f ( n ) , g ( n ) ) ) T(n)=T1(n)+T2(n)=max((Of(n),O(g(n)))=O(max(f(n),g(n))) T(n)=T1(n)+T2(n)=max((Of(n),O(g(n)))=O(max(f(n),g(n)))
嵌套代码的复杂度等于嵌套内外代码复杂度的乘积。即:
T 1 ( n ) = O ( f ( n ) ) ; T 2 ( n ) = O ( g ( n ) ) ; T1(n)=O(f(n));T2(n)=O(g(n)); T1(n)=O(f(n));T2(n)=O(g(n));
那么:
T ( n ) = T 1 ( n ) ∗ T 2 ( n ) = O ( f ( n ) ) ∗ O ( g ( n ) ) = O ( f ( n ) ∗ g ( n ) ) T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n)) T(n)=T1(n)∗T2(n)=O(f(n))∗O(g(n))=O(f(n)∗g(n))
O(1):表示常量级时间复杂度,代码执行时间不随n的增大而增大;一般代码中不含有循环或者递归,即使再多的代码时间复杂度也是O(1)。
O(logn)、O(nlogn):
i=1;
while (i<=n){
i=i*3
}
显然,这段代码的时间复杂度就是O(log3n),在对数时间复杂度的表示方法中,我们忽略对数的底,统一表示为O(logn),具体方法为对数换底公式和低阶省略;将时间复杂度为O(logn)的代码循环执行n遍,时间复杂度便为O(nlogn);归并排序、快速排序时间复杂度都是O(nlogn)。
O(m+n)、O(m*n):对于加法法则,需要考虑m和n的量级,而乘法法则不需要。
时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。所以空间复杂度全称就是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。常见的空间复杂度是O(1),O(n)O(n2);
O(1):
int i=1;
O(n):
int[]a=new int [n];
同一段代码在不同的输入量级下时间复杂度会出现很大的变化。为了适应不同的输入量级对算法的影响,更全面更准确的描述算法的时间复杂度,又提出了最坏情况时间复杂度、最好情况时间复杂度、平均时间复杂度和均摊时间复杂度。
int find(int[] array,int n,int x){
int pos=-1;
for(int i=0;i<=n;i++){
if(x==array[i])
pos=i;
}
}
return pos;
在最理想的情况下,执行这段代码的时间复杂度。例如,数组查找时第一个元素就是我们要找的元素,时间复杂度为O(1),这个时候就是最好情况时间复杂度分析。
在最不理想的情况下,执行这段代码的时间复杂度。例如,在数组查找元素时,一直遍历到最后一个元素或者查找完毕没有发现该元素,是最坏的情况,时间复杂度为O(n),对应的就是最坏情况时间复杂度。
int find(int[] array,int n,int x){
int pos=-1;
for(int i=0;i<=n;i++){
if(x==array[i]){
pos=i;
break;
}
}
return pos;
}
这个情况下,代码的时间复杂度发生了变换。最好情况和最坏情况时间复杂度都是在极端条件下的时间复杂度,用它来描述算法的时间复杂度显然不具有普适性。为了更好的表示一般情况下的时间复杂度,提出了平均情况时间复杂度。
查找变量x在数组中的位置,有n+1种可能,n种在数组中的任一位置,一种不在数组中。如果把每种情况下查找需要遍历的元素个数累加起来,再除以(n+1),就得到了找到这个元素需要遍历的平均元素个数。即:
( 1 + 2 + 3 + . . . + n + n ) / ( n + 1 ) = ( n ( n + 3 ) ) / ( 2 ( n + 1 ) ) (1+2+3+...+n+n)/(n+1)=(n(n+3))/(2(n+1)) (1+2+3+...+n+n)/(n+1)=(n(n+3))/(2(n+1))
根据大O表示法,低阶常量可忽略即:该算法平均时间复杂度为O(n)。
不过由于变量x在数组中和不在数组中的概率为1/2,所以x在数组任意位置出现的概率为1/2n,所以上面的计算方法是有问题的,没有把各种情况发生的概率考虑进去。考虑之后,计算公式为:
1 ∗ 1 / 2 n + 2 ∗ 1 / 2 n + 3 ∗ 1 / 2 n + . . . + n ∗ 1 / 2 n + n ∗ 1 / 2 = ( 3 n + 1 ) / 4 1*1/2n+2*1/2n+3*1/2n+...+n*1/2n+n*1/2=(3n+1)/4 1∗1/2n+2∗1/2n+3∗1/2n+...+n∗1/2n+n∗1/2=(3n+1)/4
这个公式是概率论中的加权平均值,也叫做期望值,所以平均时间复杂度也叫加权平均时间复杂度或者期望时间复杂度。
可以看到,这个公式得出的平均时间复杂度经过低阶省略后也是O(n)。
是一种特殊的平均时间复杂度