引入
在学习数据结构与算法之前,我们首先要掌握算法效率 的衡量标准。一个算法的好坏,主要从两个维度去评判:时间效率 和空间效率。
- 时间复杂度 :衡量算法运行快慢
- 空间复杂度 :衡量算法运行过程中额外占用的内存空间大小
我们写代码不仅要实现功能,更要追求运行更快、占用内存更少。
一、时间复杂度
1.1 概念
时间复杂度用来定量描述:随着数据规模 N 增大,算法中基本语句的执行次数增长趋势。我们不精确计算代码运行的毫秒数,只看执行次数的变化趋势。
1.2 大 O 渐进表示法
算法分析统一使用 大 O 符号 O() 来表示时间复杂度,它描述的是算法执行次数的量级,忽略常数、低次项。
例题:计算 func1 基本操作执行次数
void func1(int N){
int count = 0;
// 第一层循环:N次
for (int i = 0; i < N ; i++) {
// 第二层循环:N次
for (int j = 0; j < N ; j++) {
count++;
}
}
// 循环:2*N 次
for (int k = 0; k < 2 * N ; k++) {
count++;
}
int M = 10;
// 固定循环10次
while ((M--) > 0) {
count++;
}
System.out.println(count);
}
总执行次数:N2+2N+10
1.3 大 O 阶推导三步走(必背)
- 所有常数全部替换为 1
- 只保留式子中最高阶项
- 去掉最高阶项前面的系数
补充:算法分析三种情况
- 最好情况:输入规模下,最小执行次数(下界)
- 平均情况:期望执行次数
- 最坏情况 :输入规模下,最大执行次数(笔试面试统一看最坏情况)
举例:数组中查找元素 x
- 最好:1 次找到
- 最坏:N 次找到
- 平均:2N 次找到
1.4 常见时间复杂度例题详解
例题 1
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);
}
执行次数:2N+10时间复杂度:O(N)
例题 3
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);
}
执行次数:M+N**时间复杂度:O(M+N)**两个未知数都无法忽略。
例题 4
void func4(int N) {
int count = 0;
// 固定循环100次,和N无关
for (int k = 0; k < 100; k++) {
count++;
}
System.out.println(count);
}
执行次数固定为 100**时间复杂度:O(1)**只要是固定常数次循环,一律记为 O(1)。
例题 5 冒泡排序 bubbleSort
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 次
- 最坏情况:完全逆序,执行 2N(N−1) 次面试只看最坏情况时间复杂度:O(N2)
例题 6 二分查找 binarySearch
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;
}
每查找一次,数据规模砍掉一半。折纸思想:N→2N→4N⋯→1执行次数:log2N时间复杂度:O(logN)
例题 7 递归阶乘 factorial
long factorial(int N) {
return N < 2 ? N : factorial(N-1) * N;
}
递归调用 N 次时间复杂度:O(N)
例题 8 递归斐波那契 fibonacci
int fibonacci(int N) {
return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}
递归展开是一棵二叉树,节点总数约 2N时间复杂度:O(2N)
二、空间复杂度
2.1 概念
空间复杂度衡量:算法运行过程中,额外开辟的临时内存空间大小 。不计算原本传入的数据、形参空间,只统计++额外新开的变量、数组、递归栈帧++。
2.2 常见空间复杂度例题
例题 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 无关空间复杂度:O(1)
例题 2 迭代斐波那契数组版
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;
}
动态开辟了长度为 n 的数组空间复杂度:O(N)
例题 3 递归阶乘
long factorial(int N) {
return N < 2 ? N : factorial(N-1)*N;
}
递归调用 N 次,产生 N 个栈帧,每个栈帧空间固定空间复杂度:O(N)
三、核心总结
- 时间复杂度 看代码执行次数增长趋势,用大 O 表示法;
- 大 O 推导三原则:常数变 1、只留最高次项、去掉系数;
- 算法分析默认看最坏情况;
- 常见时间复杂度排序:O(1)<O(logN)<O(N)<O(NlogN)<O(N2)<O(2N)
- 空间复杂度统计额外开辟的空间:固定变量为 O(1),开数组 / 递归层数为 O(N);
- 递归复杂度口诀:
- 递归调用几次,时间 / 空间就是 O(N)
- 二分递归时间为 O(logN),二叉树型递归多为指数级。