衡量一个算法的复杂度,我们通常是通过比较这个算法的时间复杂度和空间复杂度来进行算法优劣的比较,而这种比较不需要将程序跑起来,通过得到结果的快慢进行比较,而是在没有运算之前就已经可以通过估算得到这个结果。这样做,可以使程序员可以很清楚的了解到这个算法的好坏,从而方便地进行更正和优化。
提起时间复杂度,我们第一时间想到的肯定是计算时间的,但是,时间复杂度其实和程序的运行时间关系不大,它说的其实是程序运行的次数多少,那么下面的这个程序,你们能看出来时间复杂度是多少吗?
void test(int n) { int iCount=0; for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { iCount++; } } for(int k=0;k<2*n;k++) { iCount++; } int count=10; while(count--) { iCount++; } }
语句执行的总次数是:f(n)=n^2+2*n+10
所以说,时间复杂度就是一个函数,该函数计算的就是执行基本操作的次数
这里需要说明一下,为什么不用运行时间来衡量算法的好坏呢,因为现在的计算机运行速度差不多都在每秒几千万次了,单凭运行时间来比较算法好坏的话,那是看不出来什么差距的,因为运行时间实在太短暂了,基本可以忽略掉,这也就是为什么用运行次数进行比较更好的原因了。
算法在进行时间复杂度分析的时候,通常会考虑到三种情况,这三种情况分别是
1.最好情况 :算法运行最好的次数就可以达到目标,通常也不会出现(下界)
2.最坏情况 :算法运行最多的次数(上界)
3.平均情况 :算法运行时期望的次数
我们一般在考虑时间复杂度的时候,只会考虑最坏的情况,因为这种情况通常最接近真实的运行环境。当然了,平均情况也是可以的,但是,我们研究时间复杂度的时候,通常n都会取一个很大的值,这时候,平均情况和最坏情况就会近似等价了
时间复杂度是如何表示的呢一般情况下,我们普遍用的是大O表示法,关于大O算法,我们必须知道这几个相关的规则
1.用常数取代运行时间中的所有加法常数
2.在修改后的运行次数函数中,只保留最高项
有了这两条规则,我们就可以将最后的结果进行优化,使它可以更快的进行不同算法的比较
那么,接下来,我们就来看看典型的两个递归算法的时间复杂度。
所以说,时间复杂度就是一个函数,该函数计算的就是执行基本操作的次数
这里需要说明一下,为什么不用运行时间来衡量算法的好坏呢,因为现在的计算机运行速度差不多都在每秒几千万次了,单凭运行时间来比较算法好坏的话,那是看不出来什么差距的,因为运行时间实在太短暂了,基本可以忽略掉,这也就是为什么用运行次数进行比较更好的原因了。
算法在进行时间复杂度分析的时候,通常会考虑到三种情况,这三种情况分别是
1.最好情况 :算法运行最好的次数就可以达到目标,通常也不会出现(下界)
2.最坏情况 :算法运行最多的次数(上界)
3.平均情况 :算法运行时期望的次数
我们一般在考虑时间复杂度的时候,只会考虑最坏的情况,因为这种情况通常最接近真实的运行环境。当然了,平均情况也是可以的,但是,我们研究时间复杂度的时候,通常n都会取一个很大的值,这时候,平均情况和最坏情况就会近似等价了
时间复杂度是如何表示的呢一般情况下,我们普遍用的是大O表示法,关于大O算法,我们必须知道这几个相关的规则
1.用常数取代运行时间中的所有加法常数
2.在修改后的运行次数函数中,只保留最高项
有了这两条规则,我们就可以将最后的结果进行优化,使它可以更快的进行不同算法的比较
那么,接下来,我们就来看看典型的两个递归算法的时间复杂度。
long long fib(int n) { if(n<3) return 1; return fib(n-1)+fib(n-2); }
由于递归算法本身就有的缺陷,导致了递归算法是一个理论上想起来很完美,但是实际应用的话就会出现很多弊端的算法,那么我就来分析一下吧
假设要求第十个菲波那切数,那么我们要先知道第九个和第八个数,然后第九个数需要知道第八个和第七个数字的值,第八个需要知道第七个和第六个数的值,依次类推,直到第一个数,然后再重新return回去,就可以得到第十个数字的值了,但是,在递归调用的时候,我们会把1到8的求值函数调用多次,这样就会造成时间复杂度的递增,并且空间复杂度也会很大,所以,递归算法的效率极低,现实情况下不建议用递归来解决问题,上述的时间复杂度是2^n,当n是10的时候,会进行1024次运算,但是当n为30的时候,函数需要进行30亿次运算,而处理这么多次数,普通的计算机已经无法得到结果了
二分法,比起冒泡排序法,是一个时间复杂度和空闲复杂度相对来说都更低的算法,为什么它会如此的高效呢,一个图或许可以让你了解一下,每一次的折半,都会让数据减少一半,所以即使是很大的数字,程序也只会运行很少的次数,并且申请的空间也会很少。二分法的时间复杂度只有log n,所以它是一种很高效的算法,代码如下
假设要求第十个菲波那切数,那么我们要先知道第九个和第八个数,然后第九个数需要知道第八个和第七个数字的值,第八个需要知道第七个和第六个数的值,依次类推,直到第一个数,然后再重新return回去,就可以得到第十个数字的值了,但是,在递归调用的时候,我们会把1到8的求值函数调用多次,这样就会造成时间复杂度的递增,并且空间复杂度也会很大,所以,递归算法的效率极低,现实情况下不建议用递归来解决问题,上述的时间复杂度是2^n,当n是10的时候,会进行1024次运算,但是当n为30的时候,函数需要进行30亿次运算,而处理这么多次数,普通的计算机已经无法得到结果了
二分法,比起冒泡排序法,是一个时间复杂度和空闲复杂度相对来说都更低的算法,为什么它会如此的高效呢,一个图或许可以让你了解一下,每一次的折半,都会让数据减少一半,所以即使是很大的数字,程序也只会运行很少的次数,并且申请的空间也会很少。二分法的时间复杂度只有log n,所以它是一种很高效的算法,代码如下
void quiksort(int a[],int low,int high) { int i=low; int j=high; int temp=a[i]; if(low<high) { while(i<j) { while(a[j]>=temp &&(i<j)) { j--; } a[j]=a[i]; } a[i]=temp; quiksort(a,low,i-1); quiksort(a,j+1,high); } else { return; } }
我们也可以简单的推算出,为什么结果就是 log n
假设我们要在一个线性数组里面找到一个数,那么每进行一次运算,我们都会让数组中的元素个数减少一半,再假设我们要找的这个数字是最后一次折半查找才会找到,那么我们总共找了多少次呢。假设有十个数字,那么我们只需要折半三次就可以得到想查找的数,其实就是求n的2的几次方的最接近的数
下图是我画出来的二分法的实现的线性模型,帮助你们认识