时间复杂度
执行时间固定的操作都是常数时间的操作。
执行时间不固定的操作,都不是常熟时间操作。
首先是需要确定算法流程的总操作数量与样本数量之间的表达式关系,
只看表达式最高阶项的部分。
这里以选择排序为例
首先从0-N-1的位置找到最小值
(
N个数看一遍,找最小值(依次比较)
将找到的最小值放0位置(O(1))
)
再从 1-N-1的位置找到最小值
(
N-1个数看一遍,找最小值(依次比较)
将找到的最小值放0位置(O(1))
)
2-N-1
(
N-2个数看一遍,找最小值(依次比较)
将找到的最小值放0位置(O(1))
)
如上看加比较的次数是个等差数列
n+n-1+n-2…
再加上n次交换次数
得到 an^2 + bn,只看最高阶 ->复杂度为O(n^2)
选择排序代码如下:
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0 ~ N-1 找到最小值,在哪,放到0位置上
// 1 ~ n-1 找到最小值,在哪,放到1 位置上
// 2 ~ n-1 找到最小值,在哪,放到2 位置上
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) { // i ~ N-1 上找最小值的下标
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
再来看冒泡排序:
首先是在N个数的范围上两两交换,把最大的放在最右边(交换的复杂度为O(1))
接着是在N-1个数的范围上两两交换,把最大的放在第N-1个位置
然后再依次推下去
最后得到的时间复杂度为O(n^2)
代码实现如下:
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0 ~ N-1
// 0 ~ N-2
// 0 ~ N-3
for (int e = arr.length - 1; e > 0; e--) { // 0 ~ e
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
// 交换arr的i和j位置上的值
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
再来推插入排序(相较于冒泡排序来说,冒泡排序无论是否已经排好序了都是要走O(n^2)的时间复杂度
,而插入排序如果对于已经排好部分序的数组时间复杂度会大大降低):
应以最差的情况下来估算
先是 0-1范围上的有序,如果是倒序的数组的话,那么需要交换一次
再是0-2范围上的有序,如果是倒序的数组的话,那么需要交换二次
再是0-3范围上的有序,如果是倒序的数组的话,那么需要交换三次
再是0-n范围上的有序,如果是倒序的数组的话,那么需要交换n-1次
这是一个等差数列那么时间复杂度为O(n^2)
代码如下:
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 不止1个数
for (int i = 1; i < arr.length; i++) { // 0 ~ i 做到有序
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
// i和j是一个位置的话,会出错
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
注意:
1、算法的过程,和具体的语言是无关的
2、想分析一个算法流程的时间复杂度的前提,是对该流程非常熟悉
3、一定要确保在拆分算法流程时,拆分出来的所有行为都是常数时间的操作,这
意味着写算法时,对自己的通过的每一个系统API,都非常熟悉,否则将会影响你对时间复杂度的估算。
时间复杂度的意义:
当我们要处理的样本量很大很大时,我们会发现低阶项是什么不是最重要的;每一项的系数是什 么,不是最重要的。真正重要的就是最高阶项是什么。
这就是时间复杂度的意义,它是衡量算法流程的复杂程度的一种指标,该指标只与数据量有关,与过程之外的优化无关。
额外空间复杂度
你要实现一个算法流程,在实现算法流程的过程中,你需要开辟一些空间来支持你的算法流程。作为输入参数的空间,不算额外空间。作为输出结果的空间,也不算额外空间。因为这些都是必要的、和现实目标有关的。所以都不算。但除此之外,你的流程如果还需要开辟空间才能让你的流程继续下去。这部分空间就是额外空间。如果你的流程只需要开辟有限几个变量,额外空间复杂度就是O(1)。
算法流程的常数项
我们会发现,时间复杂度这个指标,是忽略低阶项和所有常数系数的。难道同样时间复杂度的流程,在实际运行时候就一样的好吗?当然不是。时间复杂度只是一个很重要的指标而已。如果两个时间复杂度一样的算法,你还要去在时间上拼优劣,就进入到拼常数时间的阶段,简称拼常数项。
常数项比拼方式:
放弃理论分析,生成随机数据直接测。为什么不去理论分析?不是不能纯分析,而是没必要。因为不同常数时间的操作,虽然都是固定时间,但还是有快慢之分的。比如,位运算的常数时间原小于算术运算的常数时间,这两个运算的常数时间又远小于数组寻址的时间。所以如果纯理论分析,往往会需要非常多的分析过程。都已经到了具体细节的程度,莫不如交给实验数据好了。
算法的最优解
一般情况下,认为解决一个问题的算法流程,在时间复杂度的指标上,一定要尽可能的低,先满足了时间复杂度最低这个指标之后,使用最少的空间的算法流程,叫这个问题的最优解。一般说起最优解都是忽略掉常数项这个因素的,因为这个因素只决定了实现层次的优化和考虑,而和怎么解决整个问题的思想无关。
常见时间复杂度:
排名从好到差: O(1) O(logN) O(N) O(N*logN) O(N^2) O(N^3) … O(N^K) O(2^N) O(3^N) … O(K^N) O(N!)