时间复杂度
前言
正式开始算法啦!!加油
一丶衡量算法的效率
我们学习和做事都会讲究效率,同样的,计算机程序在执行的过程中也会讲究效率。但是我们要如何知道一个程序的效率是好还是不好呢?
那么我们就需要衡量的标准。
算法的效率分为两种:
时
间
复
杂
度
和
空
间
复
杂
度
\color{red}{时间复杂度和空间复杂度}
时间复杂度和空间复杂度
时间效率就是时间复杂度,衡量的是一个算法的运行效率。
空间效率就是空间复杂度,衡量的是一个算法所需要的额外空间。
二丶时间复杂度
实际上,所谓的时间复杂度就是一个数学函数,它描绘了运行时间。在我们的算法中,它花费的时间与其中语句的执行次数成正比。
也就是说:
算
法
中
基
本
操
作
执
行
次
数
就
是
时
间
复
杂
度
\color{red}{算法中基本操作执行次数就是时间复杂度}
算法中基本操作执行次数就是时间复杂度
关于大O阶方法
我们在计算时间复杂度的时候,是不需要特别精确的,只需要大致即可。那么这里我们用大O阶方法,来进行计算。具体如下:
1.用常数1取代运行时间中所有的加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。即就是如果最高项存在且不是1,那么前面的常数可以省略。
关于大O阶,我们一般关注的是:
算
法
的
最
坏
运
行
情
况
\color{red}{算法的最坏运行情况}
算法的最坏运行情况
那么这里的话就用几道例题来大致讲解一下。
实例一 – 两种循环
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);
}
首先,可以看到,第一个for循环执行了2*N次,而while循环执行了10次,所以时间复杂度精确来说应该是: 2 N + 10 \color{blue}{2N+10} 2N+10,但是根据大O阶方法,只保留最高项,然后把前面系数变为1,那么就是: O ( N ) \color{blue}{O(N)} 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 + N \color{blue}{M+N} M+N次,但是根据大O法:这里有两个未知数,所以时间复杂度是: O ( M + N ) \color{blue}{O(M+N)} O(M+N)
实例三 – 常数循环
void func4(int N) {
int count = 0;
for (int k = 0; k < 100; k++) {
count++;
}
System.out.println(count);
}
可以看到,这里总共执行了100次,常数项最后全部用1表示,所以时间复杂度是: O ( 1 ) \color{blue}{O(1)} 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;
}
}
}
可以看到,这里两个for循环嵌套,如果按照最坏的情况来说,那就是
(arrary.lenth -1) + (arrary.lenth -2) + (arrary.lenth -3)+…+1
这很明显就是一个等差数列相加,那么按照等差数列相加总和公式:
(N*(N+1))/2
根据大O法,去掉常数项,然后最高项系数为1,那么时间复杂度就是:
O
(
N
2
)
\color{blue}{O(N^2)}
O(N2)
实例五 – 二分查找
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;
二分查找是针对有序的数组来进行查找,具体图示如下:
这里要说的是对于mid:mid是这个数组的中间位置的下标,我们把要查找的数组内容设置为key,那么如果key大于mid,我们就取右半部分,然后把原来mid的位置设置为left,mid取右半部分中间位置的下标。如果key小于mid,那么就取左半部分,把mid改为right,然后mid取左半部分中间位置。
然后不断重复上述操作,直到找到想找的内容。
可以发现,这里的操作其实就是(默认最坏情况):
数组长度不断除二,除二,除二最后等于1的过程,那么其实就是:2^N = arr.lenth,那么这里的N就是要操作的次数,也就是说时间复杂度是:log以2为底,arr.lenth的对数。时间复杂度也就是 ㏒ ₂ N \color{blue}{㏒₂N} ㏒₂N
实例六 – 阶乘
// 计算阶乘递归factorial的时间复杂度?
long factorial(int N) {
return N < 2 ? N : factorial(N-1) * N;
}
这里是求由递归实现阶乘,然后求其时间复杂度,我们画一个图来进行解释。可以发现,这里实现多少次完全由N来决定,所以复杂度就是 O ( N ) \color{blue}{O(N)} O(N)
实例七 – 斐波那契数列
// 计算斐波那契递归fibonacci的时间复杂度?
int fibonacci(int N) {
return N < 3 ? N : fibonacci(N-1)+fibonacci(N-2);
}
这里可能有点难懂,那么就直接画图来搞,就假设,我们现在要求fibonacci(int N),这里的N我们给它传一个6。
那么情况如下:
我这里蓝色箭头指的是为了方便我们计算时间复杂度,我们可以把这个蓝色方框里的移到我的箭头处,这样的话,就可以看到:
斐波那契数列,其实就是一个等比数列的前N项和。那么等比数列公式是什么呢?
这里的话我们的q是2,所以我把两个减法操作的被减数和减数调换前后位置,然后就是去掉常数项,去掉最高项的系数,我们可以发现,最后的时间复杂度也就是:
O
(
2
N
)
\color{blue}{O(2^N)}
O(2N)
三丶空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,这里所说的额外的空间其实就是变量的个数,空间复杂度的计算规则跟实践复杂度类似,也是使用大O渐进表示法。
老规矩还是用几个例子来讲述一下:
实例一:冒泡排序
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个空间,但是都是常数,最后合并为1,所以空间复杂度是 O ( 1 ) \color{blue}{O(1)} O(1)
实例二:斐波那契数列
// 计算fibonacci的空间复杂度?
int[] 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;
}
这里的话空间复杂度就是 O ( N ) \color{blue}{O(N)} O(N),因为动态开辟了N个空间
实例三:阶乘
// 计算阶乘递归Factorial的时间复杂度?
long factorial(int N) {
return N < 2 ? N : factorial(N-1)*N;
}
动态开辟了N个空间,所以空间复杂度就是 O ( N ) \color{blue}{O(N)} O(N)