时间复杂度
1. 什么是时间复杂度
就是算法的执行效率。 即算法的执行时间与算法的输入值之间的关系
例
function test(num){
total = 0 ------>假设该行运行时间为t1
for (i = 0 ; i < num ; i++){
total += i ------>假设该行运行时间为t2
}
return total ------>假设该行运行时间为t3
}--------->那么总运行时间为 (t1 + num*t2 + t3)
t1 t3为固定的时间,t2*num是根据num动态变化的,所以整体的时间取决于num
比如num=10000时,就可以忽略t1 t3,故用大O表示法为O(N)
2. 大O表示法
一种粗略的评价计算机算法效率的方法.后面的内容会用到表示效率的方法.
名称来源:使用大写字母O 含义: order of (大约是)
大O表示法实质并不是对运行时间给出实际值,而是表达运行时间是如何受数据项个数所影响
形式为:O()—>括号里是一个数学函数表达式f(N),比如1、logN、N、N^2等,
指明了该算法的耗时/耗空间程度与数据增长量之间的管理,N代表输入数据的量
3. 常见的时间复杂度
O(1) O(logn) O(n) O(nlogn) O(n^2)
大小关系:
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
常见时间复杂度之间的关系(图表):https://www.cnblogs.com/byron0918/p/10544937.html

例子:(以下代码为伪代码,没有var变量,示意而已)
O(1) function O1 (num){
i = num --->t1
j = num * 2 --->t2
return i + j --->t3
} ---> (t1 + t2 + t3)
t 为常数,与num无关,为O(1)
O(logn) function Ologn (num){
i = 1 --->t1
while(i < num){
i = i * 2 --->t2
}
return i --->t3
} ---> (t1 + ?*t2 + t3)
因为while循环里面不是每次加一,所以循环总时间tx不再是num*t2
假设要循环x次能跳出循环,则2^x = num,所以x = log2(num)
每一次循环时间为t2,循环总时间 tx=t2 * log2(num)
常量去除后简写为 O(logn)
O(n) function On (num){
total = 0 --->t1
for (i = 0 ; i < num ; i++){
total += i --->num*t2
}
return total --->t3
} ---->(t1 + num*t2 + t3)
for循环,循环时间为 num*t2,不考虑常量t2,即num所以为O(n)
O(n+m) function On (num1,num2){
total = 0 --->t1
for (i = 0 ; i < num1 ; i++){
total += i --->num*t2
}
for (j = 0 ; j < num2 ; j++){
total += j --->num*t3
}
return total --->t4
} ---->(t1 + num*t2 + num*t3 + t4)
for循环两次,同时因为是并列的循环,所以时间应该是两者相加,故同上,所以为O(n+m)
O(nlogn) function Onlogn (num1,num2){
total = 0
j = 0 --->t1
for (i = 0 ; i < num1 ; i++){
while(i < num2){
total += i + j
j = j * 2 --->t2
}
}
return total --->t3
} ---> (t1 + ?*t2 + t3)
双重嵌套循环,里面一层是O(logn),外面一层是O(n),相乘即为O(nlogn)
O(n2) function On2 (num){
total = 0 --->t1
for (i = 0 ; i < num ; i++){
for (j = 0 ; j < num ; j++){
total += i + j --->num*t2
}
return total --->t3
} ---->(t1 + num*t2 + t3)
双重嵌套循环,里面一层是O(n),外面一层是O(n),相乘即为O(n2)
先看是否有循环,没有循环基本是就是O(1),再看循环是否嵌套,分情况考虑
4. 常用数据结构增删查时间复杂度
| 数据结构 | 根据关键字查找 | 根据索引查找 | 插入 | 删除 |
|---|---|---|---|---|
| 数组 | O(n) | O(1) | O(n) | O(n) |
| 有序数组 | O(logn) | O(1) | O(n) | O(n) |
| 链表 | O(n) | O(n) | O(1) | O(1) |
| 有序链表 | O(n) | O(1) | O(n) | O(n) |
| 双向链表 | O(n) | O(1) | O(n) | O(n) |
| 二叉树(一般情况) | O(logn) | - | O(logn) | O(logn) |
| 二叉树(一般情况)最坏情况 | O(n) | - | O(n) | O(n) |
| 平衡树 | O(logn) | O(logn) | O(logn) | O(logn) |
| 排序二叉树 | O(logn)~O(n) | O(logn)~O(n) | O(logn)~O(n) | O(logn)~O(n) |
| 哈希表 | O(1) | - | O(1) | O(1) |
空间复杂度
1. 什么是空间复杂度
算法的存储空间与算法的输入值之间的关系
空间复杂度也用大O表示法
代码中,占空间的是变量(比如var x = 0),语句不占空间(如if、for、while等)
例
function test1(num){
total = 0 --->假设total变量占据一个位置b
for (i = 0 ; i < num ; i++){
total += i --->不管total如何增加变化,变化的也只是它代表的数值,不变的是占据的位置依旧是b
}
return total
}--------->那么占据的总存储空间为 b ,b为假设的常量,所以为O(1)
function test2(num){
array = [] --->array数组占据的空间跟里面的元素成正比
for (i = 0 ; i < num ; i++){
array.push(i) --->由于for循环,array的空间与num的大小成正比
}
return array
}--------->那么占据的总存储空间为O(n)
2. 常见的空间复杂度
O(1) < O(n) < O(n^2)
- O(1) 一般是单个常量类型
- O(n) 一般是list,array,map等多个变量集合在一起
注意递归算法,就算没用常量等变量,因为递归的算法是栈,会每层每层的保存下去,所以递归一般也是O(n) - O(n^2) 一般都不常见,其余的更少
3. Note
- 时间和空间在优化上一般只能二选一,要么时间换空间,要么空间换时间;
- 面试的时候,把时间最优和空间最优都告诉面试官
- 工作时一般时间>空间,执行效率优先
栈
1. 栈的应用场景
需要后进先出的场景,如:十进制转二进制;判断字符串的括号是否有效;函数调用堆栈;浏览器的history等。
2. 如何创建一个栈
用数组Array[]可以模拟栈结构
栈的常用操作:push、pop、stack[stack.length-1]
push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度
pop() 方法用于删除数组的最后一个元素并返回删除的元素
3. 时间复杂度:
访问栈顶元素 O(1) --只能栈顶
搜索元素 O(n)
插入元素 O(1) --只能栈顶
删除元素 O(1) --只能栈顶
见 leetcode题目 20. 496.
--------------------------------------------------------------------------------------------- (2021.3.15)
队列
1. 队列的应用场景
需要先进先出的场景,如:食堂排队;js任务队列;计算最近请求次数。
2. 如何创建一个队列
用数组Array[]可以模拟队列结构
队列的常用操作:push、shift、queue[0]
push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度
shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
3. 见 leetcode题目 933. 239.
链表
1. 链表的应用场景
需要后进先出的场景,如:十进制转二进制;判断字符串的括号是否有效;函数调用堆栈;浏览器的history等。
链表相对于数组来说,链表可以不连续,增删只需要改变指针指向即可
2. 如何创建一个链表
js没有链表结构
用Object可以模链表结构
如:
const a = {val:'a'}
const b = {val:'b'}
const c = {val:'c'}
a.next = b
b.next = c
链表的常用操作:
遍历链表:用循环 O(n)
let p = a
while (p){
console.log(p.val)
p = p.next
}
插入:改变next指向 O(1)
在b、c之间插入d
const d = {val:'d'}
b.next = d
d.next = c
删除:改变next指向 O(1)
删除b
a.next = c
push、pop、stack[stack.length-1]
push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度
pop() 方法用于删除数组的最后一个元素并返回删除的元素
3. 见 leetcode题目 237. 203. 206. 2. 83. 141. 92.
小结做题思路:
-
如果是遇到需要删除结点的,包括了第一个结点,就需要三个指针;
-
因为做题的链表一般为单指向链表,结构里面只有next,所以需要另一个指针来表示前一个node,还有一个指针用来表示链表的起始位置,(当然如果选择不用head来滑动,就用head当起始位置,新建一个滑动的指针)
-
一个head,不动,用来return整个链表;
一个current,用来移动,判断条件等;
一个prev,用来表示前一位;
如:需要删除当前node时:
prev指针不动,prev.val不动,prev.next = current.next
current指针往下移,current = current.next
如果不需要删除,只是移动指针就直接
prev = current,
current = current.next
--------------------------------------------------------------------------------------------- (2021.3.17)
集合
1. 集合的应用场景
一种无序且唯一的数据结构
2. 如何创建一个集合
ES6中有集合,名为Set,直接实例化就可以 const set = new Set()
集合的常用操作:去重、判断某元素是否在集合中、求交集
去重:
const arr = [1, 1, 2, 2]
const arr2 = new Set(arr)
console.log(arr2);//set结构
arr3= [...arr2]
console.log(arr3);//array结构
检查是否有重复:
原数据,加入集合后,会自动去重,比较返回的新数据长度,可以知道是否重复
var containsDuplicate = function(nums) {//LeetCode 217
return new Set(nums).size === nums.length? false :true
};
是否在集合中:
const isHas = arr2.has(3)
返回true
数组的判断是includes或者indexOf
求交集:
const arr = [1, 1, 2, 2]
const arr4 = new Set([2,3])
//转化到数组上,数组的筛选方法
const set = new Set([...arr].filter(item => arr4.has(item)))
求差集:
const set = new Set([...arr].filter(item=> !arr4.has(item)))
增删改查:(key value都一样,不需要改,所以只有增删查)
实例化后使用 const set = new Set()
set.add(xxx)
set.delete(xxx)
set.has(xxx)
for of 迭代
keys和values和本身迭代都是一样的
for(let item of set) console.log(item)
for(let item of set.keys) console.log(item)
for(let item of set.values) console.log(item)
entries方法可以返回keys和values
for(let [keys, values] of set.entries) console.log(keys, values)
Set有一个size属性,可以看里面元素的个数,类似array的length
3. 见 leetcode题目 349.
Set转Array:
const myArr = [...mySet] 或 const myArr = Array.from(mySet)
Array转Set:
const mySet = new Set(myArr)
--------------------------------------------------------------------------------------------- (2021.3.19)
字典
1. 字典的应用场景
与集合类似,也是一种存储唯一值的数据结果,但是是以键值对的形式存储的
2. 如何创建一个字典
ES6中有字典,名为Map,直接实例化就可以 const m = new Map()
字典的常用操作:键值对的增删改查
const m = new Map()
增:
m.set('a','aa')//第一个值是键,第二个是值
删:
m.delete('a')//删除指定键值对
m.clear()//删除所有的
改:
m.set('a','ab')//再次set就行,会覆盖
查:
m.get('a')--->返回'a'的值
m.has('a')--->返回true,判断是否存在
遍历:for of forEach
for (let [key, value] of m)
m.forEach((value,key) forEach键值对反着来的
https://www.runoob.com/w3cnote/es6-map-set.html 菜鸟教程里讲es6map的
3. 见 leetcode题目 349. 20. 1. 76. 3.
集合Set和字典Map的区别(个人做题总结)
Set每个数据只能保存一个信息量,Map则可以保存键值对,键和值分别不同就是两个
相同点都是对于相同的数据,只保留一个,可以理解为保留新的,覆盖旧的,当然Map是根据键的是否相同来判断的
比如题中给出数组,如果只考虑数组的去重之类的,两个都可以使用,但是Set更方便,
如果需要考虑数组的索引因素,就不是一个信息量能解决的,这个时候Map更合适
--------------------------------------------------------------------------------------------- (2021.3.20)
树
1. 树的应用场景
一种分层数据的抽象模型。
前端工作中常见的树包括:DOM树、级联选择、树形控件
二叉树的常用种类:
-
满二叉树:都有左右子节点(除了底层)
形态上看是完整的三角形,各层节点数量形成公比为2的等比数列,节点总数为2^k-1,k为深度 -
完全二叉树:比满二叉树少几个叶节点,从左向右放子节点。
-
平衡二叉树:空树或者它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树也都是平衡树。
-
二叉搜索树:空树或者二叉树的所有节点比他的左子节点大,比他的右子节点小。
-
红黑树:不仅是具有二叉搜索树的属性,还具有平衡树的属性,有序且子树差不超过1,颜色规则:根节点和特殊节点(即叶节点下面两个虚无的节点和未填写的节点)是黑的,红节点的左右子节点是黑的,最重要的是对于每个节点,从该节点到子孙叶节点的所有路径包含相同数目的黑节点。
2. 如何创建一个树结构
JS中没有树,需要用Object和Array来模拟构建一个树结构
如下
{
value:'China',
children:[
{
value:'jiangsu',
children:[
{
value:'xuzhou',
children:[]
},
{
value:'nanjing',
children:[]
}
]
},
{
value:'chongqing'
children:[]
}
]
}
对于完全二叉树,也可以用数组Array来构建
左侧子节点的位置是 2 * index + 1
右侧子节点的位置是 2 * index + 2
父侧子节点的位置是 (index - 1) / 2
如:[0,1,2,3,4,5,6]就是
{
value:0,
left:{
value:1,
left:{
value:3,
left:null,
right:null
},
right:{
value:4,
left:nill,
right:null
}
},
right:{
value:2,
left:{
value:5,
left:null,
right:null
},
right:{
value:6,
left:null,
right:null
}
}
}
比如节点1的index是1,它的左子树的index应该是2*1+1=3,此时index为3的元素也是3
3. 树的常用操作:深度/广度优先遍历,先中后序遍历(二叉树)
深度优先遍历:尽可能深的搜索树的分支。Depth First Search(DFS)
1. 访问根节点;
2. 对根节点的children挨个进行深度优先遍历
代码:
const tree = {
val:'a',
children:[
{
val:'b',
children:[
{
val:'d',
children:[]
},
{
val:'e',
children:[]
},
]
},
{
val:'c',
children:[
{
val:'f',
children:[]
},
{
val:'g',
children:[]
},
]
},
]
}
const dfs = (root)=>{
console.log(root.val)
root.children.forEach(dfs)
}
dfs(tree)
对于此处为什么可以不写 if (!root) return
是因为forEach对于空数组,不会有任何操作,如下例
const a = []
a.forEach(item=>{console.log(item.val)})
a.forEach(item=>{console.log('xxx')})
均不会输出任何值,而且forEach中用return没用
深度优先遍历类似于二叉树的先序遍历,但是先序遍历会进行判断if (!root) return,因为使用的不是forEach
广度优先遍历:先访问离根节点最近的节点。Breadth First Search(BFS)
1. 新建一个队列,把根节点入队;
2. 把队头出队并访问;
3. 把队头的children挨个入队;
4. 重复二三步,直到队列为空
代码:
const bfs = (root)=>{
const q = [root]
while(q.length>0){
const n = q.shift()
console.log(n.val)
n.children.forEach(child=>q.push(child))
}
}
bfs(tree)
记录当时的一个疑惑:为什么dfs中的forEach可以直接写dfs,但是bfs中却要写child=>q.push(child)
暂时还未解决,用console.log验证了半天也没结果,先保留,继续往下看,后面再解决,不然浪费时间
解决了。。。太蠢了,dfs是整个函数进行递归,bfs只是利用队列进行递归
const dfs = (root) => {
console.log(root.val)
/*
为什么这里可以直接写dfs,但是bfs中不能直接写q.push
因为forEach的三个参数是function(currentValue, index, arr)
currentValue 必需。当前元素
index 可选。当前元素的索引值。
arr 可选。当前元素所属的数组对象。
在bfs中用q.push,就会像console.log一样默认传入三个参数,所以会报错bfs.html:49 Uncaught TypeError: Cannot convert undefined or null to object
但问题是为什么dfs没有传入3个参数,有可能是传了,但是没接收后面两个,
bfs中写全传入三个参数进push,报错不会是简写方式,所以是为什么
*/
// root.children.forEach(dfs)
root.children.forEach((currentValue, index, arr)=>{dfs(currentValue, index, arr)})
// root.children.forEach(console.log)
}
dfs(tree)
--------分割线-------------问题未解决---------------
二叉树:
树的每个节点最多只有两个子节点
js中通常用Object来模拟二叉树
const binaryTree = {
val:1,
left:{
val:2,
left:null,
right:null
},
right{
val:3,
left:null,
right:null
}
}
三种遍历方式:(递归版)//递归虽然没有用到什么结构,但是隐藏了栈的应用,非递归版就是用栈来实现递归
一) 先序遍历
1. 访问根节点;
2. 对根节点的左子树进行先序遍历;
3. 对根节点的右子树进行先序遍历
const bt = {
val:1,
left:{
val:2,
left:{
val:4,
left:null,
right:null
},
right:{
val:5,
left:null,
right:null
},
},
right:{
val:3,
left:{
val:6,
left:null,
right:null
},
right:{
val:7,
left:null,
right:null
},
}
}
const preorder = (root)=>{
if(!root) return
console.log(root.val)
preorder(root.left)
preorder(root.right)
}
preorder(bt)
//结果为1245367
非递归版(调用栈,都是对栈顶元素进行操作,所以是push和pop)
const preorder = (root)=>{
if(!root) return
const stack = [root]
while(stack.length>0){
const n = stack.pop()
console.log(n.val)
if(n.right) stack.push(n.right)
if(n.left) stack.push(n.left)
}
}
preorder(bt)
二) 中序遍历
1. 对根节点的左子树进行中序遍历;
2. 访问根节点;
3. 对根节点的右子树进行中序遍历
const inorder = (root)=>{
if(!root) return
inorder(root.left)
console.log(root.val)
inorder(root.right)
}
inorder(bt)
//结果为4251637
非递归版(调用栈,都是对栈顶元素进行操作,所以是push和pop)
const inorder = (root)=>{
if(!root) return
const stack = []
let p = root
while(stack.length || p){
while(p){
stack.push(p)
p = p.left
}
const n = stack.pop()
console.log(n.val)
p = n.right
}
}
inorder(bt)
三) 后序遍历
1. 对根节点的左子树进行后序遍历;
2. 对根节点的右子树进行后序遍历
3. 访问根节点;
const postorder = (root)=>{
if(!root) return
inorder(root.left)
inorder(root.right)
console.log(root.val)
}
postorder(bt)
//结果为4526731
非递归版(调用栈,都是对栈顶元素进行操作,所以是push和pop)
//因为先序遍历和后续遍历是相似的(不是说输出结果),是遍历顺序,先序遍历的非递归版的入栈顺序稍微修改一下,
//所以用先序遍历的非递归版,每次入栈,再该顺序最后出栈就是后续遍历了
const postorder = (root)=>{
if(!root) return
const stack = [root]
const outputStack = []
while(stack.length>0){
const n = stack.pop()
outputStack.push(n)
if(n.left) stack.push(n.left)
if(n.right) stack.push(n.right)
}
while(outputStack.length){
const n = outputStack.pop()
console.log(n.val)
}
}
postorder(bt)
遍历JSON的所有节点值(深度优先遍历)
const json = {
a: { b: { c: 1 } },
d: [ 1, 2],
}
const dfs = (n)=>{
console.log(n)
Object.keys(n).forEach(k =>{
dfs(n[k])
})
}
dfs(json)
加上path后
const json = {
a: { b: { c: 1 } },
d: [ 1, 2],
}
const dfs = (n, path)=>{
console.log(n, path)
Object.keys(n).forEach(k =>{
dfs(n[k], path.concat(k))
})
}
dfs(json, [])
--------------------------------------------------------------------------------------------- (2021.3.23)
图
1. 图的应用场景
图是网络结构的抽象模型。是一组由边连接的节点,图可以表示任何二元关系,如航班网络,道路网路等
2. 如何创建一个图
js中没有图,但是可以用Object和Array来构建一个图
常用的表示方法为:邻接矩阵、邻接表、关联矩阵...


图的常用操作,利用队列完成深度优先遍历和广度优先遍历
类似于树的dfs和bfs
深度优先遍历:
1. 访问根节点
2. 对根节点的没访问过的相邻节点挨个进行深度优先遍历

const graph = {
0:[1, 2],
1:[2],
2:[0,3],
3:[3]
}
const visited = new Set()
const dfs = (n)=>{
console.log(n)
visited.add(n)
graph[n].forEach(c =>{
if (!visited.has(c)){
dfs(c)
}
})
}
dfs(2) --->2013
广度优先遍历:
1. 新建一个队列,把根节点入队
2. 把队头出队并访问
3. 把队头的没访问过的相邻节点入队
4. 重复二、三步,知道队列为空

const graph = {
0:[1, 2],
1:[2],
2:[0,3],
3:[3]
}
const visited = new Set()
visited.add(2)
const q = [2]
while(q.length) {
const n = q.shift()
console.log(n)
graph[n].forEach(c =>{
if (!visited.has(c)){
q.push(c)
visited.add(c)
}
})
}
--->2031
3. 见 leetcode题目 65. 417.
有效的数字
太平洋大西洋水流问题
--------------------------------------------------------------------------------------------- (2021.3.26)
堆
1. 堆的应用场景
堆是一种特殊的完全二叉树。
所有节点都大于等于(最大堆)或小于等于(最小堆)它的子节点
求前k个最大(小)的元素,设定堆的高度来解决
2. 如何创建一个堆
用数组Array[]可以模拟堆结构,相对于数组也可以表示二叉树
js中用Object来表示二叉树,但是可以用数组来表示堆(完全二叉树)
- 左侧子节点的位置是 2 * index + 1
- 右侧子节点的位置是 2 * index + 2
- 父侧子节点的位置是 (index - 1) / 2
- 队列的常用操作:创建最小堆类
3. 见 leetcode题目 215. 347. 实现最小堆类
最小堆类
class MinHeap {
constructor() {
this.heap = []
}
swap(i1, i2) {
const temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
getParentIndex(i) {
return (i - 1) >> 1//用二进制右移一位,来达到除以2求商
}
getLeftIndex(i) {
return i * 2 + 1
}
getRightIndex(i) {
return i * 2 + 2
}
shiftUp(index) {
if (index == 0) return
const parentIndex = this.getParentIndex(index)
if (this.heap[parentIndex] > this.heap[index]) {
this.swap(parentIndex, index)
this.shiftUp(parentIndex)
}
}
shiftDown(index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if (this.heap[leftIndex] < this.heap[index]) {
this.swap(leftIndex, index)
this.shiftDown(leftIndex)
}
if (this.heap[rightIndex] < this.heap[index]) {
this.swap(rightIndex, index)
this.shiftDown(rightIndex)
}
}
insert(value) {
//把新的放到最后,这样不会丢失之前的值,再进行排序
this.heap.push(value)
this.shiftUp(this.heap.length - 1)
}
pop() {
// 把最大的数放到弹出的顶部,替换掉最小值,然后再排序,就可以了
this.heap[0] = this.heap.pop()
this.shiftDown(0)
}
peek() {
return this.heap[0]
}
size() {
return this.heap.length
}
}
const h = new MinHeap()
h.insert(3)
h.insert(2)
h.insert(1)
--------------------------------------------------------------------------------------------- (2021.3.28)

本文详细介绍了数据结构的时间复杂度和空间复杂度,包括大O表示法,常见时间复杂度和空间复杂度的比较。讨论了栈、队列、链表、集合、字典、树和图等数据结构的创建、应用场景及操作时间复杂度。同时,通过leetcode题目展示了具体应用,探讨了栈、队列、链表、树和图的操作,以及堆的实现和应用。
1038

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



