一、概念
-
时间复杂度:
全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。
-
空间复杂度:
全称就是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。
-
总结
算法的执行效率由执行时间、存储空间两个方面决定。复杂度分析就是用来分析算法执行效率与数据规模之间的关系,包括时间复杂度和空间复杂度
二、大O表示法
总结:
Θ等于的意思。 Ο表示上界,小于等于的意思。 ο表示上界,小于的意思。 Ω表示下界,大于等于的意思。 ω表示下界,大于的意思。
计算复杂度的小技巧
-
忽略常数项
如:T(n) = 2n+20 与 T(n) = 2n
随着n的增加,两个数值无限接近,常量值20可以忽略了
-
忽略低次项
如: T(n)=2n^2+3n+10 与 T(n)=2n^2
随着n的增大,两个数值无限接近,可以忽略低次项及常量项:3n+10
-
忽略系数
如: T(n)=3n^2+2n 与 T(n)=5n^2+7n
随着n值变大, 两个数值无限接近,说明这种情况下,系数5和3可以忽略
总结:
-
计算时间复杂度的时候忽略常数项、忽略低次项、忽略系数
-
T(n)不同,但时间复杂度可能相同, 如: 如:T(n)=n^2+7n+6与T(n)=3n^2+2n+2它们的T(n)不同,但时间复杂相同,都为O(n^2).
-
计算时间复杂度的方法
-
1、用常数1代替运行时间中的所有加法常数T(n)=n^2+7n+6 =>T(n)=n^2+7n+1
-
2、修改后的运行次数函数中,只保留最高阶项T(n)=n^2+7n+1 => T(n)=n^2
-
3、去除最高阶项的系数T(n)=n^2 =>T(n)=n^2 => O(n^2)
-
-
三 、常见的时间复杂度
- 常量阶 O(1)
- 对数阶 O(logn)
- 线性阶 O(n)
- 线性对数阶 O(nlogn)
- 平方阶 O(n^2)
- 立方阶 O(n^3)
- 指数阶 O(2^n)
- 阶乘阶 O(n!)
注:指数和阶乘随着n的增大,执行时间急剧增长,十分抵消,项目中尽量别用
1、常数阶O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:
var a = 1
var b = 2
var c = a + b
最常用的就是哈希表
[1. 两数之和]
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
var twoSum = function(nums, target) {
let map = new Map();
for(let i = 0, len = nums.length; i < len; i++){
if(map.has(target - nums[i])){
return [map.get(target - nums[i]), i];
}else{
map.set(nums[i], i);
}
}
return [];
};
2、对数阶 O(logn)
当数据增大n倍时,耗时增大logn倍。
代表经典二分法
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
var search = function(nums, target) {
let low = 0, high = nums.length - 1;
while (low <= high) {
const mid = Math.floor((high - low) / 2) + low;
const num = nums[mid];
if (num === target) {
return mid;
} else if (num > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
};
3、线性阶 O(n)
数据量增大几倍,耗时也增大几倍,如:单层for循环
const n = 996;
for (let i = 0; i <= n; i++) {
console.log('加班第' + i +'天');
}
4、线性对数阶 O(nlogn)
将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)
常见的如:快速排序、归并排序
- 快速排序
var arr = [1,9,23,6,12,19] // => [1,6,9,12,19,23]
var quickSort = function(arr) {
if (arr.length <= 1) { return arr; }
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
};
console.log(quickSort(arr)) // [1,6,9,12,19,23]
- 归并排序
var arr = [1,9,23,6,12,19] // => [1,6,9,12,19,23]
function merge(leftArr, rightArr){
var result = [];
while (leftArr.length > 0 && rightArr.length > 0){
if (leftArr[0] < rightArr[0])
result.push(leftArr.shift()); //把最小的最先取出,放到结果集中
else
result.push(rightArr.shift());
}
return result.concat(leftArr).concat(rightArr); //剩下的就是合并,这样就排好序了
}
function mergeSort(array){
if (array.length == 1) return array;
var middle = Math.floor(array.length / 2); //求出中点
var left = array.slice(0, middle); //分割数组
var right = array.slice(middle);
return merge(mergeSort(left), mergeSort(right)); //递归合并与排序
}
var arr = mergeSort(arr);
console.log(arr); // [1,6,9,12,19,23]
5、平方阶 O(n^2)
平方阶就是把 O(n) 的代码再嵌套一层循环,它的时间复杂度就是 O(n^2), 简单理解就是双层for循环
常见的如:冒泡排序、插入排序、选择排序
- 冒泡排序
var arr = [1, 9, 23, 6, 12, 19];
function bubleSort(arr) {
// 遍历数组
for (var i = 0; i < arr.length - 1; i++) {
// 这里要根据外层for循环的 i ,逐渐减少内层 for 循环的次数
for (var j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
var num = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = num;
}
}
}
return arr
}
console.log(bubleSort(arr)) // [1,6,9,12,19,23]
- 插入排序
var arr = [1, 9, 23, 6, 12, 19];
function insertSort(arr) {
//默认第一个元素已经被排序
for (let i = 1, len = arr.length; i < len; i++) {
//从后往前依次和 当前元素比较 并交换位置
for (let j = i; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
var num = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = num;
} else {
break
}
}
}
return arr;
}
console.log(insertSort(arr)) // [1,6,9,12,19,23]
- 选择排序
var arr = [1, 9, 23, 6, 12, 19];
function selectSort(arr) {
for (let i = 0, len = arr.length; i < len; i++) {
let k = i;//用于存放当前循环中最小值得index 默认为循环初识值的index
for (let j = i; j < len; j++) {
if (arr[j] < arr[k]) {
k = j;
}
}
if (k !== i) {
var num = arr[i];
arr[i] = arr[k];
arr[k] = num;
}
}
return arr;
}
console.log(selectSort(arr)) // [1,6,9,12,19,23]
6、立方阶 O(n^3)
在 O(n^2) 的基础上再嵌套一层循环。(罗斯套娃)
例如: DOM树节点对比
react的diff 从O(n^3)到 O(n) ,请问 O(n^3) 和O(n) 是怎么算出来?
四、平均时间复杂度、最坏时间复杂度
-
平均时间复杂度:
是指所有可能的输入实例均以概率出现的情况下,该算法的运行时间
-
最坏时间复杂度:
是指在最坏情况下的时间复杂度称为最坏时间复杂度。一般讨论时间复杂度均是最坏情况下的时间复杂度。
五、空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势
空间复杂度比较常用的有:O(1)、O(n)、O(n²)
-
O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
const a = 1;
let b = 2;
-
O(n)
arr 所占用的内存由 n 来决定,会随着 n 的增大而增大,所以它的空间复杂度就是 O(n)
let arr = []; const n = 996; for (let i = 0; i < n; i++) { arr.push(i) } -
O(n²)
如果初始化一个二维数组
n*n,那么它的空间复杂度就是 O(n^2)
const arr = Object.entries({ foo: 'bar', baz: 42 })
console.log(arr) // [ ["foo", "bar"], ["baz", 42] ]