数据结构与算法(JavaScript) 1 时间复杂度与空间复杂度
时间复杂度:定性描述一个算法的运行时间
空间复杂度: 算法在运行过程中临时占用存储空间大小的量度
一、数据结构 1 栈(stack)
栈的常用操作
入栈 push()
出栈 pop()
查看栈尾元素 stack[stack.length - 1]
1.1 栈的应用场景 十进制转二进制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function tenToTwo (num ) { let stack = [] while (num !== 0 ){ stack.push(num % 2 ) num = Math .floor(num / 2 ) } let res = '' while (stack.length !== 0 ){ res = res.concat(stack.pop()) } return res } const res = tenToTwo(100 )console .log(parseInt (res))
有效的括号
LeetCode:20
函数调用堆栈
2 队列(queue)
队列常用操作
入队: push()
出队: shift()
查看队头元素: queue[0]
2.1 队列的应用场景 js异步中的任务队列
Javascript(0x06)-js异步中的任务队列
计算最近请求次数
LeetCode: 933
3 链表
LeetCode: 237、206、2、83、141
注意:使用指针之前一定判断指针是否存在
3.1 链表遍历、节点插入与删除 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const a = {val : "a" }const b = {val : "b" }const c = {val : "c" }const d = {val : "d" }a.next = b b.next = c c.next = d let p = awhile (p){ console .log(p.val) p = p.next } const e = {val : "e" }c.next = e e.next = d c.next = d
3.2 使用链表指针获取JSON节点值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const json = { a : { b : { c : 1 } }, d : { e : 2 } } const path = ['d' , 'e' ]let p = jsonpath.forEach(k => { p = p[k] }) p = 2
4 集合 一种无序且唯一的数据结构
4.1 集合基本用法 通过集合实现数组去重
1 2 3 const arr = [1 , 1 , 1 , 2 , 3 , 3 , 4 ]const arr2 = [...new Set (arr)]
判断元素是否在集合中
.has()
方法判断,在集合中则返回true,否则返回false
1 2 3 const set = new Set([1,2,3,4])const has = set .has(0)
求交集
先将一个集合转化为数组,遍历数组看是否有元素在另一集合中,将符合条件的元素放在一个数组中
将交集元素的数组转化为集合,则求出了交集
1 2 3 4 5 6 7 const set1 = new Set ([1 ,2 ,4 ,5 ])const set2 = new Set ([5 ,6 ,7 ,8 ])const set3 = new Set ([...set1].filter(item => set2.has(item)))set3的值是5
4.2 ES6中set的使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 let set = new Set()// 2 添加元素 set .add(1)set .add(5)set .add(5) //此次不能添加, 因为集合元素无序且唯一set .add('helo')const obj = {a : 1 , b :2 } set .add(obj)set .add({a : 1 , b :2 }) const has = set .has(obj) //返回trueconst has1 = set .has(5) //返回true const has2 = set .has({a : 1 , b :2 }) set .delete(5) //可以正常删除set .delete({a : 1 , b :2 }) set .delete(obj) //可以正常删除// 5 迭代,即循环集合 set 的val和key一样 for(let item of set ) console.log(item) //d打印values for(let item of set .values()) console.log(item) for(let item of set .keys()) console.log(item) //以上三种方式输出结果一样,集合的values和keys属性一样 for(let [key, value] of set .entries()) console.log(key, value) //同时拿到集合的key和value值 // 6 数组与集合互转 // 6.1 集合转数组 const arr = [...set ] const arr1 = Array.from(set ) // 6.2 数组转集合 const set1 = new Set([1,2,3,4]) // 7 求集合的交集和差集 // 7.1 求集合交集 const intersection = new Set([...set ].filter(item => set1.has(item))) // 7.2 求差集 const difference1 = new Set([...set ].filter(item => !set1.has(item))) const difference2 = new Set([...set1].filter(item => !set .has(item))) const defference = new Set([...difference1].concat([...difference2])) // 8 获取集合元素个数 console.log(set .size)
5 字典
LeetCode: 349、20、1、3、76
存储唯一值的数据结构,以键值对的形式来存储
5.1 ES6中Map(字典)的使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const map = new Map ()map.set('a' , 'aa' ) map.set('a' , 'aa' ) map.set('b' , 'bb' ) console .log(map.get('a' )) map.delete('a' ) map.clear() map.set('b' , 'bbbb' ) map.has('b' ) map.forEach((value,key ) => { console .log(value,key) }) map.size
6 树
LeetCode: 104、111、102、94、112
一种分层数据的抽象模型
6.1 深度优先搜索 案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 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(item => { dfs(item) }); } dfs(tree)
6.2 广度优先搜索 案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 const tree = { val : 'a' , children : [ { val : 'b' , children : [ { val : 'd' , children : [] }, { val : 'e' , children : [] } ] }, { val : 'c' , children : [ { val : 'f' , children : [] }, { val : 'g' , children : [] } ] } ] } const bfs = (root ) => { const q = [root] while (q.length > 0 ){ let item = q.shift() console .log(item.val) item.children.forEach(child => { q.push(child) }) } } bfs(tree)
6.3 二叉树
树中每个节点最多只能有两个子节点
二叉树性质
在二叉树的第i层上最多有2i-1 个节点 。(i>=1)
二叉树中如果深度为k,那么最多有2k-1个节点。(k>=1)
n0=n2+1 n0表示度数为0的节点数,n2表示度数为2的节点数。
在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]是向下取整。
若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性:
(1)若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;
(2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
(3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。
满二叉树
在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
满二叉树的特点有:
1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。
2)非叶子结点的度一定是2。
3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
完全二叉树
对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
特点:
1)叶子结点只能出现在最下层和次下层。
2)最下层的叶子结点集中在树的左部。
3)倒数第二层若存在叶子结点,一定在右部连续位置。
4)如果结点度为1,则该结点只有左孩子,即没有右子树。
5)同样结点数目的二叉树,完全二叉树深度最小。
注 :满二叉树一定是完全二叉树,但反过来不一定成立。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const bt = { val : 1 , left : { val : 2 , left : { val : 3 , left : null , right : null }, right : { val : 4 , left : { val: 5 , left: null , right: null }, right : null } }, right : { val : 6 , left : null , right : { val : 7 , left : null , right : null } } } module .exports = bt
6.3.1 先序遍历 根节点先访问 ,根左右
递归版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const bt = require ('./bt' ) const preorder = (root ) => { if (!root) { return } console .log(root.val) preorder(root.left) preorder(root.right) } preorder(bt)
非递归版
本质是仍然是递归的思想,借鉴了函数调用堆栈,自己模拟了一个栈来存储访问的节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const bt = require ('./bt' )const preorder = (root ) => { if (!root) {return } let stack = [root] while (stack.length){ const node = stack.pop() console .log(node.val) if (node.right) stack.push(node.right) if (node.left) stack.push(node.left) } } preorder(bt)
6.3.2 中序遍历 根节点在中间访问 ,左根右
递归版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const bt = require ('./bt' ) const inorder = (root ) => { if (!root) {return } inorder(root.left) console .log(root.val) inorder(root.right) } inorder(bt)
非递归版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const bt = require ('./bt' )const inorder = (root ) => { if (!root) {return } const stack = [] let p = root while (stack.length || p){ while (p){ stack.push(p) p = p.left } const node = stack.pop() console .log(node.val) p = node.right } } inorder(bt)
6.3.3 后序遍历 根节点在最后访问 ,左右根
递归版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const bt = require ('./bt' )const postorder = (root ) => { if (!root) {return } postorder(root.left) postorder(root.right) console .log(root.val) } postorder(bt)
非递归版
后序遍历压入栈的顺序正好是先序遍历输出的顺序,只是左右节点对调了一下,所以可以利用先序遍历输出算法来构建后续遍历的栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const bt = require ('./bt' )const postorder = (root ) => { const outputStack = [] const stack = [root] while (stack.length){ const node = stack.pop() outputStack.push(node) if (node.left) stack.push(node.left) if (node.right) stack.push(node.right) } while (outputStack.length){ const node = outputStack.pop() console .log(node.val) } } postorder(bt)
7 图
LeetCode:65、417、133
图是网络结构的抽象模型,是一组由边连接的节点
7.1 图的表示法 7.1.1 邻接矩阵
7.1.2 邻接表
7.2 图的常用操作 案例:
1 2 3 4 5 6 7 8 9 const graph = { 0 : [1 , 2 ], 1 : [2 ], 2 : [0 , 3 ], 3 : [3 ] } module .exports = graph
7.2.1 深度优先遍历 算法思路:
访问根节点
对根节点的没有访问过的相邻节点 挨个进行深度优先遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const graph = require ('./graph' )const visited = new Set () const dfs = (node ) => { console .log(node) visited.add(node) graph[node].forEach(item => { if (!visited.has(item)){ dfs(item) } }); } dfs(2 )
7.2.2 广度优先遍历 算法思路:
1 新建一个队列,把根节点入队
2 把队头出列并访问
3 把队头的没有访问过的相邻节点 全部入队
4 重复2、3步,直到队列为空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const graph = require ('./graph' )const visited = new Set ()const bfs = (node ) => { let q = [node] visited.add(node) while (q.length){ let temp = q.shift() console .log(temp) graph[temp].forEach(item => { if (!visited.has(item)){ q.push(item) visited.add(item) } }) } } bfs(2 )
7.3 leetcode 7.3.1 65 有效数字
1 2 3 4 5 6 7 8 9 10 11 12 const graph = { 0 : { 'blank' : 0 , 'sign' : 1 , '.' : 2 , 'digit' : 6 }, 1 : {'digit' : 6 , '.' : 2 }, 2 : {'digit' : 3 }, 3 : {'digit' : 3 , 'e' :4 }, 4 : {'digit' : 5 , 'sign' :7 }, 5 : {'digit' : 5 }, 6 : {'digit' : 6 , '.' : 3 , 'e' :4 }, 7 : {'digit' : 5 } }
1 2 3 4 5 6 7 8 9 10 11 const group = { 0 : {'digit' : 2 , 'sign' : 1 , '.' : 3 }, 1 : {'digit' : 2 ,'.' : 3 }, 2 : {'digit' : 2 , '.' : 4 , 'e' : 5 }, 3 : {'digit' : 4 }, 4 : {'digit' : 4 , 'e' : 5 }, 5 : {'digit' : 7 , 'sign' : 6 }, 6 : {'digit' : 7 }, 7 : {'digit' : 7 } }
8 堆
LeetCode:215、347、23
一种特殊的完全二叉树
每一层节点都完全填满,最后一层如果不是满的,则只缺少右侧的若干节点
最大堆:
最小堆:
8.1 JS中的堆
JS中通常用数组来表示堆,以广度优先遍历 顺序存储到数组中
左侧子节点的位置(数组中的索引值):2*index+1
右侧子节点的位置(数组中的索引值):2*index+2
父节点的位置: Math.floor((index-1)/2)
向下取整
8.2 堆的应用 8.2.1 求最大(小)值 堆能高效地、快速地找出最大值或最小值、时间复杂度:O(1)
8.2.2 求第K个最大(小)值 找出第k个最大的元素
1 构建一个最小堆,并将元素依次加入堆中
2 当堆的容量超过k,就删除堆顶
3 插入结束后,堆顶就是第K个最大的元素
找出第K个最小元素
使用找第K个最大元素的方法,只是换成构建一个最大堆
8.3 构建最小堆 插入操作-算法思路
1 将值插入堆的底部,即数组的尾部
2 然后上移:将这个值和它的父节点交换位置,直到父节点小于等于这个插入的值
插入操作时间复杂度:O(logN)
堆的大小为n(有n个节点),插入的值最小的情况下,则大需要上移logN层
删除堆顶-算法思路
1 用数组尾部的元素替换堆顶(直接删除堆顶,会导致数组所有元素前移,破坏堆的结构)
2 然后下移:将堆顶的元素和它的子节点交换位置,直到它的子节点大于等于它
删除堆顶时间复杂度:O(logN)
堆的大小为n(有n个节点),队尾的值最大,则大需要下移logN层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 class MinHeap { constructor () { this .heap = [] } getParent(index){ return Math .floor((index-1 )/2 ) } getLeft(index){ return index*2 + 1 } getRight(index){ return index*2 + 2 } swap(i1, i2){ let temp = this .heap[i1] this .heap[i1] = this .heap[i2] this .heap[i2] = temp } shiftUp(index){ if (index === 0 ) return let parentIndex = this .getParent(index) if (this .heap[parentIndex] > this .heap[index]){ this .swap(parentIndex, index) this .shiftUp(parentIndex) } } shiftDown(index){ let leftIndex = this .getLeft(index) let rightIndex = this .getRight(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(val){ this .heap.push(val) this .shiftUp(this .heap.length-1 ) } delTop(){ if (this .getSize() === 1 ){ this .heap.pop() return } this .heap[0 ] = this .heap.pop() this .shiftDown(0 ) } getTop(){ return this .heap[0 ] } getSize(){ return this .heap.length } } const h = new MinHeap()h.insert(3 ) h.insert(2 ) h.insert(1 ) h.insert(5 ) h.insert(-1 ) h.delTop()
二、结合前端 1 队列与前端 1.1 js异步中的任务队列
Javascript(0x06)-js异步中的任务队列)
2 链表与前端 2.1 使用链表指针获取JSON节点值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const json = { a : { b : { c : 1 } }, d : { e : 2 } } const path = ['d' , 'e' ]let p = jsonpath.forEach(k => { p = p[k] }) p = 2
3 树与前端 3.1 遍历JSON的所有节点值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const json = { a : { b : { c : 1 }}, d : [110 , 112 ] } const dfs = (root, path ) => { console .log(root, path) Object .keys(root).forEach(item => { dfs(root[item], path.concat(item)) }) } dfs(json, []) { a : { b : { c : 1 } }, d : [ 110 , 112 ] } ------ [] { b : { c : 1 } } ------ [ 'a' ] { c : 1 } ------ [ 'a' , 'b' ] 1 ------ [ 'a' , 'b' , 'c' ][ 110 , 112 ] ------ [ 'd' ] 110 ------ [ 'd' , '0' ]112 ------ [ 'd' , '1' ]
三、算法
算法可视化
1 排序与搜索
LeetCode: 21、374
1.1 排序
JavaScript sort() 方法
1.1.1 冒泡排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Array .prototype.bubbleSort = function ( ) { for (let i = 0 ; i < this .length - 1 ; i++) { for (let j = 0 ; j < this .length - 1 - i; j++) { if (this [j] > this [j + 1 ]) { let temp = this [j] this [j] = this [j + 1 ] this [j + 1 ] = temp } } } } const arr = [5 , 4 , 3 , 2 , 1 ]arr.bubbleSort()
1.1.2 选择排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Array .prototype.selSort = function ( ) { for (let i =0 ;i<this .length-1 ;i++){ let sel = i for (let j = i;j<this .length;j++){ if (this [j] < this [sel]){ sel = j } } if (sel !== i){ let temp = this [i] this [i] = this [sel] this [sel] = temp } } } const arr = [5 , 4 , 3 , 2 , 1 ]arr.selSort() console .log(arr)
1.1.3 插入排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Array .prototype.insertSort = function ( ) { for (let i = 1 ; i < this .length; i++) { let temp = this [i] let j = i while (j > 0 ){ if (this [j-1 ] > temp){ this [j] = this [j-1 ] }else { break } j-- } this [j] = temp } } const arr = [5 , 4 , 3 , 2 , 1 ]arr.insertSort() console .log(arr)
1.1.4 归并排序
火狐浏览器的sort()方法就是用的归并排序
时间复杂度:O(n*logn)
分时间复杂度:O(logn)
合时间复杂度:O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 Array .prototype.mergeSort = function ( ) { const dfs = (arr ) => { if (arr.length === 1 ) return arr const mid = Math .floor(arr.length / 2 ) const left = arr.slice(0 , mid) const right = arr.slice(mid, arr.length) const orderLeft = dfs(left) const orderRight = dfs(right) const res = [] while (orderLeft.length || orderRight.length){ if (orderLeft.length && orderRight.length){ res.push(orderLeft[0 ] < orderRight[0 ] ? orderLeft.shift() : orderRight.shift()) }else if (orderLeft.length){ res.push(orderLeft.shift()) }else if (orderRight.length){ res.push(orderRight.shift()) } } return res } const res = dfs(this ) res.forEach((val , i ) => { this [i] = val }) } const arr = [5 , 4 , 3 , 2 , 1 ]arr.mergeSort() console .log(arr)
1.1.5 快速排序
时间复杂度:O(n*logn)
递归时间复杂度:O(logn)
分区时间复杂度:O(n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Array .prototype.quickSort = function ( ) { const dfs = (arr ) => { if (arr.length === 1 || arr.length === 0 ){ return arr } const left = [] const right = [] const mid = arr[0 ] for (let i = 1 ;i<arr.length;i++){ if (arr[i]<mid){ left.push(arr[i]) }else { right.push(arr[i]) } } return [...dfs(left),mid,...dfs(right)] } const res = dfs(this ) res.forEach((val , i ) => { this [i] = val }) } const arr = [5 , 4 , 3 , 2 , 1 ]arr.quickSort() console .log(arr)
1.2 搜索 1.2.1 顺序搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 Array .prototype.sequentialSearch = function (item ) { for (let i=0 ;i<this .length;i++){ if (this [i] === item){ return i } } return -1 } const res = [5 ,4 ,3 ,2 ,1 ].sequentialSearch(9 )console .log(res)
1.2.2 二分搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Array .prototype.binarySearch = function (item ) { let low = 0 let high = this .length-1 while (low <= high){ let mid = Math .floor((low+high) / 2 ) if (item < this [mid]){ high = mid-1 }else if (item > this [mid]){ low = mid+1 }else { return mid } } return -1 } const res = [1 ,2 ,3 ,4 ,5 ].binarySearch(1 )console .log(res)
2 分而治之
它将一个问题分 成多个和原问题相似的小问题,递归解决 小问题,再将小问题合并 以解决原来的问题
LeetCode:374、226、100、101
3 动态规划
它将一个问题分解为互相重叠 的子问题,通过反复求解子问题,来解决原来的问题
动态规划 VS 分而治之
子问题是相互重叠的(例:斐波拉切数列) 则是动态规划
子问题是相互独立的(例:翻转二叉树 )则是分而治之
LeetCode: 70、198
4 贪心算法
期盼通过每个阶段的局部最优 选择,从而达到全局的最优
leetcode: 455、122
不是最优解案例:
零钱兑换
用最少的硬币数达到要求的兑换总额
coins: 可以使用的面额, amount: 兑换总额
情况1是最优解,但是情况2可以是2个3,不是最优解
5 回溯法
LeetCode:46、78
回溯法是一种渐进式 寻找并构建问题解决方案的策略
回溯法会先从一个可能的动作开始解决问题,如果不行,就回溯并选择另一个动作,直到解决问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var permute = function (nums ) { const res = [] const dfs = (path ) => { if (path.length === nums.length){ res.push(path) return } nums.forEach(n => { if (path.includes(n)){return } dfs(path.concat(n)) }) } dfs([]) return res };