时间复杂度的种类(按照复杂度的增长趋势来进行排列)
- O(1)
- O(logn)
- O(n)
- O(nlogn)
- O(n2)
- O(n3)
- O(2n)
- O(n!)
时间复杂度增长图
时间复杂度为O(1)的操作
- if语句中的判断 例如:if(i === 1)。
- 赋值/加减乘除运算。例如:a=1,result = 3+4,result = n*2,result = 10000 * 10000。
- 数组的push与pop操作。例如:array.push(‘a’),array.pop()。
- map操作。map.set(1,1),map.get(1,1)。
- 在计算的时候O(1)一般会被忽略,因为时间复杂度是取层级最高的那个。
时间复杂度为O(n)的操作
- for循环,while循环(不使用二分搜索)
- 例如:for(let i=0;i<n;i++); let i=0;while(i<n)。
例题:
let a = 0
// O(n)操作
for(let i=0;i<n;i++){
// O(1)操作
a+=i;
}
时间复杂度为O(n2)的操作
- 嵌套for循环,嵌套while循环
例如:
// O(n)的平方
for(let i=0;i<n;i++){
for(let j=0;j<n;j++){
// O(1)操作
a+=j
}
}
会被误以为是O(n^2)但实际是O(n)的例子1:
// O(n)的操作
for(let i=0;i<n;i++){
...
}
// O(n)的操作
for(let j=0;j<n;j++){
...
}
会被误以为是O(n^2)但实际是O(n)的例子2:
// O(n)的操作
while(i<n){
i++,j=1;
// 当符合这一层的条件时,则进入这一层循环,使得i++
while(j<n){
// 在这里i++,j++,当不满足这一while循环的条件时则跳出循环。
// 但是因为在此操作中进行了i++,所以外层也是会从已经加过的i开始
// 例如进入到while循环时的i是4,出去的时候是8,则外层循环仍然是从8开始遍历
// 符合O(n)的策略
i++;
j++;
}
}
时间复杂度为logn的操作
- 二分查找
let left = 0,
right = arr.length - 1
while (left <= right) {
let middle = Math.floor((left + right) / 2)
if (arr[middle] > target) {
right = middle - 1
} else if (arr[middle] < target) {
left = middle + 1
} else if (arr[middle] === target) {
return middle
}
}
return -1
时间复杂度为nlogn的操作
- 编程语言中arr.sort
时间复杂度为n3
- 对标leetcode四数之和
时间复杂度为2n
- 对于2n在树结构和递归结构中比较常见,比如说n步,每步则有两个选择则变成2 * 2 * 2 * 2…,则会有2n的时间复杂度
可优化的结构(从低一级的复杂度寻找灵感)
- 如果此时的的时间复杂度为O(n)则可优化的程度为O(logn),比如说查找已排序的数组中的元素,我们可以从for循环->二分查找去降低复杂度。
- 如果此时的的时间复杂度为O(nlogn)则可优化的程度为O(n),比如说我们需要将其排序,去解除此题,我们可以考虑使用map,set去将时间复杂度降低为O(n)
- 如果此时的的时间复杂度为O(n2)则可优化的程度为O(nlogn),比如说我们遇到嵌套循环,这个时候我们可以考虑将其排序在使用一个for循环去降低时间复杂度。
时间复杂度的计算
对于复杂度的计算,我们是取复杂度最高的一项作为总体复杂度,前面的常数忽略。
- n3+n2+1的时间复杂度为n3
- 2n2+3n+6的时间复杂度为n^2
空间复杂度
空间复杂度为O(1)的例子
创建了一个为a的变量,其值为1
- const a = 1
- int a = 2
对应反转链表题目:
var reverseList = function(head) {
// 我们在这个过程中只是创建了几个指针,对于所接收到的链表进行反转
// 而在这个过程中我们并没有自己去创建一个长度为n的链表,所以空间复杂度为O(1)
let prev = null
let curr = head
while(curr){
const next = curr.next
curr.next = prev
// 之前的节点为当前的节点
prev = curr
// 当前的节点为下一个节点
curr = next
}
// 此时prev就是为头结点
return prev;
};
空间复杂度为O(n)的例子
- 定义一个长度为n的数组
- 定义一个长度为n的set和map
- 用for循环生成一个长度为n的链表
对应两数相加题目:
// 在计算过程中会生成一个长度为n的链表并进行返回,所以空间复杂度为O(n)
var addTwoNumbers = function(l1, l2) {
let listnode = new ListNode()
let curr = listnode
let carry = 0
while(l1 !== null || l2 !== null){
let sum = 0
if(l1 !== null){
sum += l1.val
l1 = l1.next
}
if(l2 !== null){
sum += l2.val
l2 = l2.next
}
sum += carry
// 在这个地方每次去生成一个新的节点,并向下续加
curr.next = new ListNode(sum%10)
carry = Math.floor(sum/10)
curr = curr.next
}
if(carry > 0){
curr.next = new ListNode(carry)
}
return listnode.next
};
空间复杂度为O(n2)的例子
- 二维数组
- 一个一维数组存放一个长度为n的set或者map或者链表
时间复杂度与空间复杂度的取舍
分析题目的时间复杂度与空间复杂度:
function twoSum(nums, target) {
// 定义一个变量,空间复杂度为O(1)
let n = num.length
// 因为为双层for循环,时间复杂度为为O(n^2^)
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
if (nums[i] + nums[j] === target) {
// 返回一个长度为2的数组,则空间复杂度为O(1)
return [nums[i], nums[j]]
}
}
}
return []
}
所以此题目的时间复杂度为O(n2),空间复杂度为O(1)。
var twoSum = function (nums, target) {
// 创建一个变量,空间复杂度为O(1)
let len = nums.length
// 我们创建了map,并且会将nums中的元素放入到这个map中,所以此时空间复杂度为O(n)
let map = new Map()
// 一个for循环所以此时的时间复杂度为O(n)
for (let i = 0; i < len; i++) {
let currNum = target - nums[i]
// map.has操作时间复杂度为O(1)
if (map.has(currNum)) {
// map.get操作时间复杂度为O(1)
// 返回一个长度固定为2的数组,所以此时的空间复杂度也为O(1)
return [map.get(currNum), i]
} else {
map.set(nums[i], i)
}
}
};
所以此题目的时间复杂度为O(n),空间复杂度为O(n)。
对于常见算法来讲,我们需要尽量取舍时间复杂度比较低的那个,除非题目显式的要求(原地算法),如果时间复杂度一样,则看空间复杂度。