一、评估算法优劣的核心指标是什么?
1、时间复杂度(流程决定)
2、额外空间复杂度(流程决定)
3、常数项时间(实现细节决定)
二、常数操作
1、什么是常数时间的操作?
如果一个操作的执行不以具体样本量为转移,每次执行时间都是固定时间。称这样的操作为常数时间的操作。
比如:执行一个数组的寻址,执行的时间和数据量没关系,第1000个位置和第1000万个位置,时间是一样的,计算机用偏移量获得。
2、常见的常数时间的操作
(1)常见的算术运算(+、-、*、/、%等)
(2)常见的位运算(>>、>>>、<<、|、&、^等)
>>是带符号右移,>>>是不带符号右移
(3)赋值、比较、自增、自减操作等
(4)数组寻址操作
总之,执行时间固定的操作都是常数时间的操作。
反之,执行时间不固定的操作,跟样本量有关的,都不是常数时间的操作。
3、什么不是常数时间操作
LinkedList,底层是一个双向列表。
要在list里面get一个数据,它不会像数组寻址一样,因为它不是连续区间。它是从0位置往下数。
4、时间复杂度就是来衡量流程中发生了多少次常数操作这件事
举例:选择排序
(1)先在0到n-1中,找到最小值位置在哪儿,然后把最小值和0位置的数交换
(2)0位置的数搞定了
(3)在1到n-1中,找到最小值的位置在哪儿,然后把最小值和1位置的数交换
(4)然后重复2到n-1,3到n-1。。。一直到n-1到n-1
这个流程发生了多少次常数操作?
步骤:
(1)单次:看+比
(2)一次换
n * (看+比) + 1
(n-1) * (看+比) + 1
(n-2) * (看+比) + 1
...
看是1次,比也是1次
总的表达式变为:
n * (2) + 1
(n-1) * (2) + 1
(n-2) * (2) + 1
...
= 2 * (n + n-1 + n-2 + ... + 1) + n
等差数列可以化简为(n(1+n)/2把系数换成a、b加个常数c)
= 2an² + bn + n + c
a、b、c是常数
最终的时间复杂度是抹掉常数项、高阶项系数和低阶项,最终得到n²(n的平方)
三、如何确定算法流程的总操作数量与样本数量之间的表达式关系?
1、想象该算法流程所处理的数据状况,要按照最差情况来。
2、把整个流程彻底拆分为一个个基本动作,保证每个动作都是常数时间操作。
3、如果数据量为N,看基本动作的数量和N是什么关系。
四、如何确定算法流程的时间复杂度?
当完成了表达式的建立,只要把最高阶项留下即可。低阶项都去掉,高阶项的系数也去掉。
记为:O(忽略掉系数的高阶项)
当样本量大到足够大的时候,决定算法快慢的就是高阶项的东西。
五、时间复杂度的意义
1、抹掉了好多东西,只剩下了一个最高阶项,那这个东西有什么意义呢?
2、时间复杂度的意义在于:
当我们要处理的样本量很大很大时,我们会发现低阶项是什么不是最重要的;每一项的系数是什么,不是最重要的。真正重要的就是最高阶项是什么。
3、这就是时间复杂度的意义,它是衡量算法流程的复杂度的一种指标,该指标只与数据量有关,与过程之外的优化无关。
六、实践时间复杂度的估算
1、选择排序
分析:见上面
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0~n-1
// 1~n-1
// 2~n-1
for (int i=0; i<arr.length-1; i++) { // i~n-1
// 最小值在哪个位置上i~n-1
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;
}
2、冒泡排序
分析:
数组:5 4 6 3 2 1
(1)0到5位置,0位置和1位置比谁大,交换到后面,1位置和2位置比谁大,交换到后面。。。
(2)最大值一定是到最后了
(3)在0到4位置上重复这件事
也是等差数列,复杂度为O(n²)
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];
}
3、插入排序
分析:
数组:5 4 3 4 3 2
(1)先想0~0范围有序,只有一个数5
(2)0~1范围有序,把小的往前交换
4 5 3 4 3 2
(3)0~2范围有序,往前看交换
3 4 5 4 3 2
(4)重复这件事
这个算法和选择排序、冒泡排序是不一样的,数据的初始值对算法有影响
1 2 3 4 5 6 最好的例子:O(n)
6 5 4 3 2 1 最差的例子:O(n²)
一路交换就等于逆向冒泡
估计时间复杂度是用最差的例子,所以插入排序为O(n²)
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0~0 有序的
// 0~i 想有序
for (int i=1; i<arr.length; i++) { // 0~i 做到有序
// 如果j没有越界,j位置的数大于j+1位置的数,则交换
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,都非常的熟悉。否则会影响你对时间复杂度的估算。
八、额外空间复杂度
1、你要实现一个算法流程,在实现算法流程的过程中,你需要开辟一些空间来支持你的算法流程。
2、作为输入参数的空间,不算额外空间。作为输出结果的空间,也不算额外空间。因为这些都是必要的、和现实目标功能有关的。所以都不算。
3、但除此之外,你的流程如果还需要开辟空间才能让你的流程继续下去。这部分空间就是额外空间。
4、如果你的流程只需要开辟有限几个变量,额外空间复杂度就是O(1)。
九、算法流程的常数项
1、我们会发现,时间复杂度这个指标,是忽略低阶项和所有常数系数的。
2、难道同样时间复杂度的流程,在实际运行时候就一样的好吗?当然不是。
3、时间复杂度只是一个很重要的指标而已。如果两个时间复杂度一样的算法,你还要去在时间上拼优劣,就进入到拼常数时间的阶段,简称拼常数项。
4、拼常数项时间,申请大样本直接去跑
加减运算就是比乘除运算时间要短,加减运算一定不如位运算
十、算法流程的常数项的比拼方式
1、放弃理论分析,生成随机数据直接测。
2、为什么不去理论分析?
(1)不是不能纯分析,而是没必要。因为不同常数时间的操作,虽然都是固定时间,但还是有快慢之分的。
(2)比如,位运算的常数时间远小于算术运算的常数时间,这两个运算的常数时间又远小于数组寻址的时间。
(3)所以如果纯理论分析,往往会需要非常多的分析过程。都已经到了具体细节的程度,莫不如交给实验数据好了。
十一、什么是一个问题的最优解?
1、一般情况下,认为解决一个问题的算法流程,在时间复杂度的指标上,一定要尽可能的低,先满足了时间复杂度最低这个指标之后,使用最少的空间的算法流程,叫这个问题的最优解。
2、一般说起最优解都是忽略掉常数项这个因素的,因为这个因素只决定了实现层次的优化和考虑,而和怎么解决整个问题的思想无关。
十二、常见的时间复杂度
排名从好到差:
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!)
十三、算法和数据结构学习的大脉络
1、知道怎么算的算法
2、知道怎么试的算法
1291

被折叠的 条评论
为什么被折叠?



