目录
前言:
1、算法效率
2、时间复杂度
1、大O的渐近表示法(不是一个准确的)
2、时间复杂度练习题(没有明确要求,计算的时间复杂度就是最坏情况下)
3、空间复杂度
前言:
如何衡量一个算法的好坏呢?这里就要提到时间复杂度和空间复杂度的概念。
1、算法效率
算法效率分为两种
第一种是时间效率:时间效率别成为时间复杂度。时间复杂度主要衡量一个算法的运行速度。
第二种是空间效率:空间效率被称为空间复杂度。空间复杂度主要衡量一个算法所需要的额外空间。
2、时间复杂度
时间复杂度的定义:在计算机科学中,算法的实践复杂度是一个数学函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法的时间复杂度与这个代码中的执行次数最多的语句有关。
1、大O的渐近表示法(不是一个准确的)
大O符号(Big O notation): 是用于描述函数渐进行为的数学符号。
推导大O阶方法
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶。
- 如果最高阶项系数不是1,则去除与这个项目相乘的常数,得到的结果就是大O阶。
算法的时间复杂度还存在最好、平均和最坏的情况:在计算一个算法的时间复杂度的时候,没有明确说明的时候,我们计算的时间复杂度,都是最坏情况下的时间复杂度
- 最坏情况:任意输入规模的最大运行次数(上界)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数(下界)
举例:
在一个长度为N的数组中查找一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况。
来看一个例子:请计算一下func方法基本操作执行了多上次?
public class Test {
void func(int N){
int count = 0;
for(int i = 0;i < N;i++){
for(int j = 0;j < N;j++){
count++;
}
}
for(int k = 0;k<2*N;k++ ){
count++;
}
int M = 10;
while((M--)>0){
count++;
}
}
}
我们在计算一个代码的时间复杂度的时候,会找这个代码当中执行次数最多的部分
分析:
上述代码中执行次数最多的是,三个循环中的代码
- 第一个for循环,由于是for循环中嵌套for循环,所以执行次数为N+N+N+...+N,化简一下为。
- 第二个for循环,执行次数为2*N次。
- 第三个while循环,执行次数为10次。
所以上述代码中的比较精确时间复杂度为 F(N) = N^2+2*N+10
用大O渐近表示法表示上述代码的时间复杂度为O( )
2、时间复杂度练习题(没有明确要求,计算的时间复杂度就是最坏情况下)
代码一:
void func2(int N){
int count = 0;
for(int k = 0;k <2*N;k++){
count++;
}
int M = 10;
while((M--)>0){
count++;
}
System.out.println(count);
}
👀分析:
比较精确的时间复杂度为:F(N) = 2*N + 10;
用大O渐近表示法来表示时间复杂度:O(N)
代码二:
void func3(int N,int M){
int count = 0;
for(int K = 0; K<M;K++){
count++;
}
for(int K = 0;K < N; K++){
count++;
}
System.out.println(count);
}
👀分析:
第一个for循环中的是M,第二个for循环中的是N,M和N并没有任何关系。
所以用大O渐近表示法来表示时间复杂度为:O(M+N)
代码三:
void func4(int N){
int count = 0;
for(int K = 0;K < 100; K++ ){
count++;
}
System.out.println(count);
}
👀分析:
比较精确的时间复杂度为:F(N) = 100;
用大O渐近表示法:因为大O渐近表示法的规则中表示,运行中所有加法常数用常数1取代
所以用大O渐近表示法,时间复杂度为:O(1).
代码四:
void bubbleSort(int[] array){
for(int end = array.length;end > 0;end--){
boolean sorted = true;
for(int i = 1;i < end;i++){
if(array[i - 1] > array[i]){
Swap(array,i-1,i);
sorted = false;
}
}
if(sorted == true){
break;
}
}
}
👀分析:
这里来说明一下N在时间复杂度中代表的是问题的规模,他是一个范围,并不是说这个算法变量为N,时间复杂度就一定用N表示。
- 这个算法是循环嵌套,第一个for循环循环n次,第二个for循环n-1次,第一个for循环每进一次,第二个for循环就要循环n-1次,所以它比较精确的时间复杂度为 F(N) = N(N-1)
- 用大O渐近表示法,表示时间复杂度为:O()(最坏的情况下)
- 这个代码的时间复杂度在最好的情况下为:要遍历的数组是有序的,只需要进入第一个for循环一次,就可以得到想要的元素,所以最好情况下为O(N)
代码五:
public static void bubbleSort(int[] array){
for(int i = 0;i < array.length-1;i++){
boolean flg = false;
for(int j = 0;j < array.length-1-i;j++){
if(array[j] >array[j+1]){
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flg = true;
}
}
if(flg == false){
return;
}
}
}
👀分析:
上述代码为冒泡排序:冒泡排序的思想是,相邻两个元素进行比较,交换位置
第一个for循环执行N-1次,表示的是趟数;
第二个for循环执行次数为:N-1+N-2+.....+1,N-1,N-2表示的是每趟比较的次数。这里是一个等差数列,使用等差数列的求和公式:Sn = (首相+尾项)*项数/2 ,用大O渐近表示法,则为O()
代码五:
int binarySearch(int[] array, int value){
int begin = 0;
int end = array.length-1;
while(begin <= end ){
int mid = begin+((end-begin)/2);
if(array[mid]<value)
begin = mid+1;
else if(array[mid]>value)
end = mid-1;
else
return mid;
}
return -1;
}
👀分析:
本题是二分查找的代码:计算此题的时间复杂度,不能只看代码,要结合二分查找的思想,来计算时间复杂度。
上图用公式表示为
计算过程为:
化简:
得:
所以用大O渐近表示法,时间复杂度为:O().
代码六:计算递归函数的时间复杂度(单路递归)
long factorial(int N){
return N < 2?N:factorial(N-1)*N;
}
👀分析:
递归的时间复杂度:(递归的次数) * (每次递归后,代码的执行次数)
上述代码中factorial方法中的代码只有一个三目运算符,所以这里的每次递归后代码的执行次数为1
所以该函数的时间复杂度为:F(N) = N*1.
用大O渐近表示法为: O(N).
代码七: 计算斐波那契递归的时间复杂度(多路递归)
int fibonacci(int N){
return N < 2?N:fibonacci(N-1)+fibonacci(N-2);
}
👀分析:
第一行为1,第二行为2,第三行为4,按照规律,将每行的个数相加,得到的是一个等比数列,根据等比数列求和公式
1+2+4+...+2^(n-1)从2^0开始到2^(n-1),共n项。
通过计算可得:2^n-1
所以该算法的时间复杂度用大O渐近法表示为:O()
3、空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的度量。一个算法在计算机 存储器 上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。空间复杂度计算的规则基本和时间复杂度类似,也使用大O渐近表示法。
代码一:
public static void bubbleSort(int[] array){
for(int i = 0;i < array.length-1;i++){
boolean flg = false;//标志位,用来判断两个相邻元素是否还能进行比较,位置交换
for(int j = 0;j < array.length-1-i;j++){
if(array[j] >array[j+1]){
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flg = true;
}
}
if(flg == false){
return;
}
}
}
👀分析:
这个算法的问题规模:对N个数据进行排序。
- 看在这个算法运行过程中有没有临时占用内存的空间,有的同学就会说形参int[ ] array是临时开辟的空间,这个不算,他是冒泡排序所必须的。
- 这里定义的flg变量是临时的,他和算法的规模没有关系,不管规模多大,他只有一个,他的作用就是作为标志位
- 上述代码使用了常数个额外空间,所以空间复杂度为O(1)
代码二:
long[] fibonacci(int n){
long[] fibArray = new long[n+1];
fibArray[0] = 0;
fibArray[1] = 1;
for(int i = 2;i <= n;i++){
fibArray[i] = fibArray[i-1]+fibArray[i-2];
}
return fibArray;
}
👀分析:
计算Fibonacci(斐波那契),当你有n个数据,那么在计算之前先要创建空间,用来存储计算后的结果,随着问题的规模(n)越大,那么创建的数组也就越来越大。该算法开辟了N个空间,所以空间复杂度为O(N).
代码三:计算阶乘递归factorial的空间复杂度
long factorial(int N){
return N < 2?N:factorial(N-1)*N;
}
👀分析:
递归,每次都要在栈帧上开辟内存,当在递的过程中,开辟空间,当在归的时候,释放空间,这就相当于开辟的临时空间。当我们以N为问题的规模的时候,那么该算法的空间复杂度为O(N).