一、定义
数据结构:是计算机中存储、组织数据的方式。(来自维基百科)
算法:操作一组数据的方法(个人理解,实现某种目的的一种实现思路)。
二、复杂度
有了算法之后,那么如何去衡量一个算法的效率和资源的消耗。在计算机领域,通过“复杂度”来评判一个算法。衡量算法执行时间的叫做时间复杂度,衡量算法执行需要的存储空间的叫做空间复杂度。
如下代码,我们假设每一行代码执行时间为t, 用 T(n)来表示算法执行需要的时间,n来表示数据规模
1 int cadd(int n){
2 int sum;
3 for(int i=0; i< n; i++){
4 sum += i;
5 }
6 }上面的代码中 行2的执行时间为t,行3、行4循环了n次,所需要的执行时间 T(n) = (2n + 1) * t
大O复杂度表示法
表示随数据规模增长,算法执行时间的变化趋势。
在上面的式子中,用f(n) 表示2n+1,即f(n) = 2n+1,T(n)就可以表示为T(n) = f(n)*t。t是常数,所以T(n) 与f(n)是成正比。用大O来表示这种正比,可将T(n)表示如下T(n) = O(2n+1)。当n很大时,低阶、系数,常数对时间的影响就有限,于是T(n)又可以表示为T(n) = O(n),所以以上代码的时间复杂度用大O表示法表示为O(n)。
再看看下面的例子
int test(int n){
int sum = 1;
while(sum < n){
sum *= 2;
}
}假设while执行了m次,那么2^m >= n,执行的循环次数m= log(2)(n),由于对数低数是可以换成任意底的(高中知识,对数换地公式 log(a)(b) = log(c)(b)/log(c)(a)),所以时间复杂度就为 log2(n) 其实等于 log(c)(n) * 1/log(c)(2), c可以是任何数,1/log(c)(2)是常数,在大O表示法中可以忽略常数,c也是任意常数,所以我们可以将上面的代码的时间复杂度用大O表示法表示为O(logn)。
大O表示法表示的复杂度,我们只关注最高阶的复杂度,所以在分析复杂度时:1、只需要关注循环次数最多的那段代码2、嵌套代码的话,复杂度等于内外复杂度的乘积。
常见的复杂度模型有一下几种(图片来源于网络),可以看到随数据量(x轴)的增大,执行时间(y轴)增长的速度 O(n!) > O(2^n) > O(n^2) > O(nlogn) > O(n) > O(logn) > O(1)
最好,最坏,平均,均摊时间复杂度
有时候,由于数据规模,结构不同,会造成同一个算法可能执行的时间不一样,所以还可以具体分析算法的最好,最坏,平均时间复杂度,甚至还有均摊时间复杂度。
顾名思义,最好时间复杂度就是在最理想情况下,算法的时间复杂度。最坏时间复杂度就是最差的条件下执行的时间复杂度。平均时间复杂度就是平均每次执行的复杂度。
我们来看下插入排序,算法如下图,原理同打扑克牌时排序牌一样,一张张抽出牌,插入到手上有序的牌中(图片来源于算法导论)
下面的代码是插入排序
public static void insertOrder(int intList[]) {
int j = 1;
int i;
for(; j < intList.length; j++) {
i = j-1;
int key = intList[j];
while(i > -1 && intList[i] > key) {
intList[i+1] = intList[i];
i--;
}
intList[i+1] = key;
}
}我们可以看到算法里有两层循环,所以时间复杂度为O(n^2)。
最好的情况下,数组已经是有序的,即while里面的代码不需要执行,只执行外层循环,所以最好时间复杂度为O(n)。
最坏情况,数组为逆序,第j个元素每次都得插入到第一个位置,需移动j-1个元素,所以总时间T(n) = 1 + 2 + ..... n-1 = n(n-1)/2 (等比求和公式n(a1+an)/2 ,n为项数,a1为第一个元素,an为第n个元素),所以最坏时间复杂度为 O(n^2)。
平均时间复杂度,我们将第j个元素插入有序的 a[0] ~ a[j-1]中,平均来说,认为a[j]大于a[0] ~a[j]中的一半元素,所以插入a[j]需移动j/2个元素。所以总时间T(n) = 1/2 + 2/2+ ...... + n/2,即T(n) = 1/2(1+2+3+...+n)= n(n-1)/4,平均时间复杂度为O(n^2)。
插入排序的时间复杂度是O(n^2),最好时间复杂度是O(n) 平均时间复杂度O(n^2) 最坏时间复杂度为O(n^2)。
均摊时间复杂度是平均时间复杂度的特例,一般等于最好时间复杂度。
我们来看下ArrayList,我们知道数组是不允许扩展的,ArrayList允许我们扩展,其实就是在数组满的时候创建一个更大的数组,并将原来数组中的元素复制到新数组下。当数组没有满时,往数组中插入数据只需执行 tmp[m] = value,时间复杂度是O(1)。当我们执行了n次插入时数组满了,此时需要将旧数组复制到新数组下,需要遍历旧数组,时间复杂度为O(n)。
所以当我们往ArrayList中放入n+1个元素时,其实我们做了n次时间复杂度为O(1),还有一次时间复杂度为O(n)。我们将复杂度为O(n)的那次均摊到前面n次,那么前面n次的复杂度为O(2),还是常数阶时间复杂度,计为O(1),所以ArrayList插入元素的均摊时间复杂度是O(1)。