如何衡量一个算法的好坏?
看这个算法的时间复杂度和空间复杂度, 这两者是度量算法好坏的指标;
一般来说,时间复杂度和空间复杂度越低则说明这个算法越好。
一.时间复杂度
1.概念
计算机科学中, 算法的时间复杂度是一个数学函数, 它定量描述了该算法的运行时间。
一个算法所花费的时间与其中语句的执行次数成正比例, 算法中的基本操作的执行次数,为算法的时间复杂度。
2.大O的渐进表示法
在大O符号表示法中,时间复杂度的公式是: T(n) = O( f(n) ),其中f(n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。
思考以下代码的基本操作执行多少次?
public class Demo1 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int count = 0;
int n = scanner.nextInt();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
count++;
}
}
System.out.println(count);
}
}
解释: 外层for循环执行了 n+1次[加的一次为最后的判断], 内层循环执行了n次, 其余的语句均只执行了一次。
注: 在实际中计算时间复杂度的时候, 我们不需要精确语句的执行次数, 只需要大概执行次数.
3.推导大O阶方法
- 用常数1取代运行时间中的所有加法常数
- 修改后的运行次数函数值, 只保留最高阶项
- 如果最高阶项存在且不是1, 则去除最高阶项的常数
注: 在实际情况中, 我们一般关注的是算法的最坏运行情况
注: 时间复杂度的计算, 不能只看代码, 还要结合思想进行计算
4.常见时间复杂度分析
- 常数阶: O(1)
常数阶表示一个代码中不管有多少行代码,只要某个变量不去循环增长,那么该代码的时间复杂度还是O(1)
public void calculate() {
int count = 0;
for (int i = 0; i < 100; i++) {
count++;
}
}
注: 不要因为这个地方是循环就一定以为是O(n), 这里循环的次数是常数
- 对数阶: O(logN)
对数阶表示代码中的值不断的取一半,用于下一次的计算。
如下述的二分查找,不断的对 mid 取一半。
// 二分查找
public int binarySearch(int array[], int target) {
int left = 0;
int right = array.length;
while (left <= right) {
// 取中值
int mid = left + (right - left) / 2;
if (array[mid] > target) {
left = mid + 1;
} else if (array[mid] < target) {
right = mid - 1;
} else {
return mid;
}
}
// 没找到
return -1;
}
- 线性阶: O(N)
线性阶表示一个代码中某个变量循环了n次,n代表不可预期执行多少次。
// 和为s的两个数字
public int[] twoSum(int[] nums, int target) {
// 定义左右指针
int left = 0;
int right = nums.length - 1;
// 左右指针相遇后跳出循环
while(left < right) {
// 左右指针值之和刚好为目标值
if(nums[left] + nums[right] == target) {
return new int[] {nums[left], nums[right]};
} else if(nums[left] + nums[right] > target) {
// 左右指针值之和大于目标值
right--;
} else {
// 左右指针值之和小于目标值
left++;
}
}
// 若循环跳出后则说明没有和为s的两个数组元素
return new int[]{-1,-1};
}
// 阶乘计算
public long factorial(int n) {
return n < 2 ? n : factorial(n - 1) * n;
}
- 线性对数阶: O(NlogN)
线性对数阶表示一个代码中某个变量循环执行了n次,但是在这个循环过程中,另外一个变量在该循环中又不断的用一半值去进行下一次的判断。
// 快速排序优化
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
/**
* 快速排序
*/
private static void quick(int[] array, int left, int right) {
// left和right相遇或大于right: 只有一个节点或一个节点也没有
if (left >= right) return;
// 优化为插入排序
if (right - left + 1 <= 7) {
// 插入排序
insertSortRange(array, left, right);
return;
}
// 三数取中
int index = midOfThree(array, left, right);
// 交换基准值
swap(array, index, left);
// 获取基准值
int pivot = partition(array, left, right);
// 继续递归划分左右区间
quick(array, left, pivot - 1);
quick(array, pivot + 1, right);
}
/**
* 插入排序
*/
private static void insertSortRange(int[] array, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int tmp = array[i];
int j = i - 1;
for (; j >= left; j--) {
if (array[j] > tmp) {
array[j + 1] = array[j];
} else {
break;
}
}
array[j + 1] = tmp;
}
}
/**
* 三数取中
*/
private static int midOfThree(int[] array, int left, int right) {
// 中间值
int mid = (left + right) / 2;
if (array[left] < array[right]) {
if (array[mid] < array[left]) {
return left;
} else if (array[mid] > array[right]) {
return right;
} else {
return mid;
}
} else {
if (array[mid] < array[right]) {
return right;
} else if (array[mid] > array[left]) {
return left;
} else {
return mid;
}
}
}
/**
* Hoare法
*/
private static int partition1(int[] array, int left, int right) {
int key = array[left];
int i = left;
while (left < right) {
while (left < right && array[right] >= key) {
right--;
}
// 找到了比key小的值
while (left < right && array[left] <= key) {
left++;
}
// 找到了比key大的
// 交换
swap(array, left, right);
}
// 交换基准
swap(array, i, left);
// 返回基准值
return left;
}
/**
* 挖空法
*/
private static int partition(int[] array, int left, int right) {
int key = array[left];
while (left < right) {
while (left < right && array[right] >= key) {
right--;
}
// 找到了比key小的值
// 填空
array[left] = array[right];
while (left < right && array[left] <= key) {
left++;
}
// 找到比key大的值
// 填空
array[right] = array[left];
}
array[left] = key;
return left;
}
/**
* 左右指针法
*/
private static int partition2(int[] array, int left, int right) {
int prev = left;
int cur = left + 1;
while (cur <= right) {
// 遇到比key小的值且prev自增后的值和cur相等,则交换值
// 遇到array[cur] > array[left]说明,cur++, prev停留
if (array[cur] < array[left] && array[++prev] != array[cur]) {
swap(array, cur, prev);
}
cur++;
}
return prev;
}
private static void swap(int[] array, int left, int right) {
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}
- 平方阶: O(N^2)
平方阶表示代码中某个变量把O(N)的代码再循环了一遍。
// 三数之和
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
// 排序
Arrays.sort(nums);
int len = nums.length;
// 每次均需要固定最小的值
for(int i = 0; i < len; i++) {
// 左右指针
int left = i + 1;
int right = len - 1;
// 目标值
int target = -nums[i];
while(left < right) {
// 获取left和right下标值的和
int sum = nums[left] + nums[right];
if(sum > target) { // sum的值大于目标值
right--;
} else if(sum < target) { // sum的值小于目标值
left++;
} else {
// 添加到新的list中去
list.add(new ArrayList<Integer>(Arrays.asList(nums[i],nums[left],nums[right])));
//List<Integer> tmp = new ArrayList<>();
//tmp.add(nums[i]);
//tmp.add(nums[left]);
//tmp.add(nums[right]);
//list.add(tmp);
// 找到一组值后,需要缩小左右区间的范围
left++;
right--;
// 去重和left下标相同的值
while(left < right && nums[left] == nums[left-1]) {
left++;
}
// 去重和right下标相同的值
while(left < right && nums[right] == nums[right+1]) {
right--;
}
}
}
// 去重和当前下标i相同的值
while(i < len - 1 && nums[i] == nums[i+1]) {
i++;
}
}
return list;
}
时间复杂度的大小排序:
O(1) < O(logN) < O(N) < O(NlogN) < O(N^2) < O(N^3) < ....
注:一般到O(N^3)后基本上就涉及不到这种代码了
二.空间复杂度
1.概念
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的度量.
2.常见空间复杂度分析
- 空间复杂度O(1): 只开辟了常数个的额外空间
// 冒泡排序
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean flag = false;
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
flag = true;
int tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
}
}
if (!flag) {
break;
}
}
}
- 空间复杂度O(n): 开辟n个额外空间
// 斐波那契数列
public int[] fibonacci(int n) {
long[] fibArray = new long[n + 1];
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i < fibArray.length; i++) {
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
常见的空间复杂度: O(1) < O(logN) < O(N) < O(N^2)...
注: O(logN) -> 在二叉树中常见到[单支树为O(N)]