JS数据结构和算法
零 基础介绍
- 数据结构:数据的逻辑结构、数据的物理/存储结构、数据的运算与实现。
- 逻辑结构的分类:----抽象的特点
1 线性结构:有且只有一个开始和结束节点,所有节点最多只有一个直接前驱和直接后继。例如线性表、栈、队列、串
非线性结构:多对多,例如树、图
2 集合、线性结构、树、图
3 ?逻辑结构有很多,还有通过不同的逻辑定义更多的数据类型,但是底层的存储结构是有限的
- 存储结构分类:---实现的方式
1 顺序存储结构:数组
链接存储结构:链表
索引存储结构:建立一个索引表
散列/哈希存储结构:
- 抽象数据类型:数据(数据对象,对象之间的关系)以及操作方法
- 算法的特性:有穷性、确定性(相同输入必定相同输出)、可行性、输入、输出
- 算法的效率:
时间复杂度--------有时间需要多练习
1、定义:又称"渐进式时间复杂度",表示代码执行时间与数据规模n之间的增长关系。
假设执行基本语句的时间相同,执行时间T(n)可以表示为所有语句的执行次数
为了比较不同算法之间时间效率,我们用f(n)表示执行次数和数据规模n之间的关系
当n趋近于无穷大时,忽略低阶项和系数的影响,当f(n)/t(n)的比值是一个常数时,可以使用O(fn)来表示T(n),被称为大O表示法
2、基本方法:先找出执行最多的基本语句,找到f(n),最后大O表示
空间复杂度--------有时间需要多练习
算法1 只需要新建一个变量,而算法2新建一个长度n的数组
1 线性表----结合线性结构理解,注意前驱和后继的理解(前后只有一个元素)
1.1 线性表的链式实现
针对具体的问题分析逻辑结构和存储结构,?实际上就是类
- 循环链表,首尾相接的连接。空链表时,头指针的next指向自己 this.head.next=this.head?
- 顺序表示随机存储,链表是顺序存储?
- 头插法和尾插法
尾插法:每次插入在元素的尾部 1、append方法 2 使用尾指针 this.tail
2 栈和队列
栈和队列是限定插入和删除只能在端点进行的线性表,
- 栈按照存储方式也有顺序栈和链式栈,链栈的插入删除只在栈顶的头部进行(修改指向)
3 串、数组和广义表
串就是字符串,里面的内容都是字符
串的模式匹配算法(找匹配的子串):
1 BF算法(暴力算法)
计算next的方法可以参考王卓的视频,计算nextval的方法可以参考链接
// 计算nextval数组 function nextVal(str) { len = str.length let next = [0,1] let nv=[0] for (let i = 2; i <len; i++) { let tem=str.slice(0,i) let n=1 let k=1 for (k=1;k<tem.length;k++){ if (tem.slice(0,k)==tem.slice(tem.length-k,tem.length)){ n=k+1 } } next.push(n) } for(let j=1;j<len;j++){ let idx=next[j] if(str[j]==str[idx-1]){ nv.push(next[idx-1]) }else{ nv.push(next[j]) } } return nv; } // source 源字符串 // match 要匹配的字符串 // res 保存匹配位置的数组 function search(source, match) { let next = nextVal(match), m = source.length, n = match.length, i = 0, j = 0, res = []; while (i < m - n && j< n) { if (source[i] === match[j]) { i++; j++; } else if(j>0){ j = next[j - 1]; }else{ i++ } } if (j==n) {return true} else {return false}; } let source = '1234456', match = '121212'; let res = search(source, match); console.log(res)广义表:又称列表lists,它有长度和深度
4 树和二叉树
二叉树不是树的特殊情况,二叉树哪怕只有一个节点仍要区分左右子树,但是树只剩下一个节点则不需要
- 哈夫曼树获取不等长的前缀码?
- 二叉树求解表达式的值
在n个节点的二叉链表中,必有2n个链域(左右节点),有n-1个链域存放指针,剩下n+1个空指针域
已知先序和中序/中序和后序 确定二叉树
进行遍历的时候,除了使用递归以外,也可以尝试栈+循环的方式
- 二叉树的层序遍历
- 线索二叉树,利用链表中的空指针域,如果某个节点的左指向为空,可以将其指向其前驱。如果某个节点的右指向为空,可以将其指向其后继,
- 树的双亲表示法,孩子表示法,儿子兄弟表示法(将树转换成二叉树,加线,抹线,旋转)
- 哈夫曼树,带权路径长度最短的树
带权路径长度:节点的带权路径长度(路径长度×权),树的带权路径长度(从树根到每个叶子节点的路径长度之和)
满二叉树不一定是哈夫曼树,权值越大的叶子离根越近,哈夫曼树不唯一
?构造哈夫曼树
利用哈夫曼树进行编码:根据权重构建哈弗曼树,左分支的边代表0,右分支的边代表1
哈夫曼编码可以保证前缀编码,从树的角度,被表示的字符都位于叶子节点,不会路过其他字符代表的编码
能够保证字符编码总长最短
5 图
网:有权图
联通图:对任意两个顶点U、V都存在你从U到V的路径
图没有顺序存储结构,但是可以使用二维数组构成的领接矩阵表示:顶点数据Vexs,以及矩阵Edge[i][j](如果i,j之间存在边则为1.否则为0,如果是有向图发出时才为1)
链式的领接表:对于有向图,找出度使用领接表;找入度使用逆领接表
图的遍历:从某一节点出发,遍历所有节点,且每个节点只被访问一次
图的应用:
*** 最小生成树:所有顶点均由边联通,但是不存在回路;生成树是图的极小联通子图,n个节点有n-1条边;深度/广度优先生成树(按遍历顺序不重复即可)
权重之和最小的生成树被称为最小代价生成树。--------------针对城市之间修铁路之间的问题
?MST性质:
普里姆prim算法:每次找到距离最小生成树节点最近(最小生成树中所有节点都参与比较)的那个节点;针对点,稠密图
克鲁斯卡尔算法:先不要边,把边按照从小到大排序,在不形成环的条件下,添加进去;针对边,稀疏图
*** 最短路径:A到B权重和最小的路径(可以不用包含所有的点)
?迪杰斯特拉算法:单源最短路径
?弗洛伊德算法:所有顶点间的最短路径
*** 拓扑排序:有向无环图------工作流程等问题
AOV :有向,无回路;顶点表示排序的网,用于拓扑排序 ;如果经过拓扑排序还有结点剩下,说明有环存在
AOE 边表示排序的网,用于关键路径
关键活动:活动的最早发生时间和最晚发生时间相等
6 散列表/哈希表
散列表:用散列方法构造的表
散列方法:选取某个函数(散列函数),依据该函数按关键字计算元素存储的位置,并按此存放。
冲突:不同的关键码映射到同一个散列地址
散列函数:尽可能简单,尽可能均匀分布,可能尽量解决冲突
一 定义一个栈
- 在给函数定义一个方法时,可以使用引用方式,也可以使用原型的方式,但是由于使用引用的方式会导致每次new对象时都会占用内存,因此推荐原型的方式。
- 定义原型时使用Stack.prototype而不是this.protoType
- 有些方法需要有返回值
function Stack(){ this.items=[] //push Stack.prototype.push=function(element){ this.items.push(element) } //push Stack.prototype.pop=function(element){ return this.items.pop() } //peek 输出栈顶元素 Stack.prototype.peek=function(){ return this.items[this.items.length-1] } //判断是否为空 Stack.prototype.isEmpty=function(){ return this.items.length==0 } //toString方法 Stack.prototype.toString=function(){ return this.items.join("") } }
1 利用栈的性质实现十进制转化为二进制
function dev2bin(n){ var res=[] while(n>0){ res.unshift(n%2) n=Math.floor(n/2) } return res.join("") } // 对循环条件的认识
2 括号匹配的检验
左括号的话就入栈,右括号的话就要看和栈顶的元素是否匹配,如果不匹配就失败,匹配的话将栈顶的元素出栈,继续下一轮比较
3 表达式求值
二 定义一个队列
1 实现击鼓传花
function jg(arr,n){ while(arr.length>1){ for(let i=0;i<n-1;i++){ arr.push(arr.shift()) } arr.shift() } return arr[0] }核心点:将队列中的第一个删除的同时,放入队列的尾部
三 优先级队列
- 每个元素不再只是数据,而是包含数据的优先级
- 在添加时根据优先级添加到正确的位置
- 使用场景:飞机的头等舱、具有优先级的线程
- 定义一个数组,数组里面是定义的一个内部类,有值和优先级两种属性,插入的时候如果为空直接插入,不为空需要根据优先级splice
- 可以用二维数组代替
四 链表
1 append方法
function linkedList(){
function node(data){
this.data=data
this.next=null
}
this.header=null
this.length=0
//append方法
linkedList.prototype.append=function(data){
var newNode=new node(data)
if (this.length==0){
this.header=newNode
}else{
var current=this.header
while(current.next){
current=current.next
}
current.next=newNode
}
this.length +=1 //注意不要漏掉了
}
//tostring方法
linkedList.prototype.toString=function(){
var res=""
var current=this.header
while(current){ //注意此处的判断条件
res=res+current.data
current=current.next
}
return res
}
//insert方法
linkedList.prototype.insert=function(pos,data){
if (pos<0 || pos>this.length) return false
var newNode=new node(data)
var current=this.header
if (pos==0){
newNode.next=this.header
this.header=newNode
}else{ for (let i=0;i<pos-1;i++){ //次数的判定:1从特殊点0判定等,如果是找到当前是pos,找到当前的一个则是pos-1
current=current.next
}
newNode.next=current.next
current.next=newNode
}
this.length+=1
}
//removeAt方法
linkedList.prototype.removeAt=function(pos){
if (pos<0 || pos>=this.length) return false
var current=this.header
if (pos==0) {this.header=this.header.next}
else{
for (let i=0;i<pos-1;i++){
current=current.next
}
current.next=current.next.next // 也可以定义两个指针,将前面一个节点定位为previous
}
this.length-=1
}
}
五 双向链表
对链表的理解要摆脱之前数组的影响,彼此之间的关系主要是通过引用
function doulyLinkedlist(){ this.head=null this.tail=null this.length=0 function node(data){ this.data=data this.next=null this.prev=null } doulyLinkedlist.prototype.append=function(data){ var newNode=new node(data) if (this.length==0){ this.head=newNode this.tail=newNode }else{ newNode.prev=this.tail this.tail.next=newNode this.tail=newNode } this.length+=1 } doulyLinkedlist.prototype.insert=function(pos,data){ if (pos<0 || pos>this.length) { return false } var newNode=new node(data) if (pos==0){ newNode.next=this.head this.head.prev=newNode this.head=newNode }else if(pos==this.length){ newNode.prev=this.tail this.tail.next=newNode this.tail=newNode }else{ var current=this.head for (let i=0;i<pos;i++){ current=current.next } newNode.next=current newNode.prev=current.prev//先给新节点添加属性,更加安全 current.prev.next=newNode current.prev=newNode } this.length+=1 } doulyLinkedlist.prototype.toString=function(){ var res="" var current=this.head while(current){ res+=current.data current=current.next } return res } }
六 集合
1 j集合的操作:set的常用方法
- add remove has clear size values
-
function Set(){ this.items={} Set.prototype.has=function(data){ return this.items.hasOwnProperty(data) } Set.prototype.add=function(data){ if (this.has(data)) return false else{ this.items[data]=data } } Set.prototype.remove=function(data){ if (!this.has(data)) return false else{ delete this.items[data] //this.items[data]=undefined } } Set.prototype.clear=function(){ this.items={} } Set.prototype.size=function(){ return Object.keys(this.items).length // Object.values(this.items).length } Set.prototype.values=function(){ return Object.values(this.items) //返回的是字符串类型 // Object.values(this.items).length 返回的是数字类型 } }
2 集合之间的操作
function Set(){ this.items={} ... // 并集 Set.prototype.union=function(otherSet){ var res=new Set() //需要返回新的集合元素 for(let i of this.values()){ res.add(i) //add方法包含是否重复 } for(let j of otherSet.values()){ res.add(j) } return res } // 交集 //充分利用写好的has,add等属性 ... }
七 字典
对象中的键只能是string或者是symbol,但是字典里的key可以是任意的类型
八 哈希表
哈希化
哈希函数
哈希表
解决哈希冲突的方法:
1 链地址法:数组中的每个元素的类型是链表或者数组,这样当通过哈希函数得到的索引重复时,将重复的元素按照链表或者数组的形式放置
2 开放地址法 :当索引重复时,在数组中搜素空白的位置,然后将重复的值放在空白处
ps:在真实开发中使用链地址法比较多,因为不会随着添加地址后性能下降
function hashTable(){ this.storage=[] this.count=0 this.limit=7 hashTable.prototype.hashFunc=function(str,size){ var hashCode=0 for (let i=0;i<str.length;i++){ hashCode=hashCode*37+str.charCodeAt(i) } var res=hashCode%size return res } hashTable.prototype.put=function(key,value){ //根据key获取对应的index var index=this.hashFunc(key,this.limit) //根据Index获取对应的bucket var bucket=this.storage[index] //判断bucket是否为空 if (bucket==null){ bucket=[] this.storage[index]=bucket } //判断是否是修改数据 for(let i=0;i<bucket.length;i++){ var tuple=bucket[i] if(tuple[0]==key){ tuple[1]=value return } } //进行添加操作 bucket.push([key,value]) this.count+=1 } hashTable.prototype.get=function(key){ var index=this.hashFunc(key,this.limit) var bucket=this.storage[index] if (bucket==null) return null for(let i=0;i<bucket.length;i++){ var tuple=bucket[i] if(tuple[0]==key){ return tuple[1] } } return null //最后仍然没找到 } }
判断一个是否是质数
- 常规方法:遍历到n-1,看余数是不是0
- 高效的方法:遍历到sqrt(n)
九 树结构
- 将一个树使用儿子兄弟表示法(节点内只定义左边第一个子结点,和这个子结点的右边的第一个兄弟节点),然后旋转45°,就变成了二叉树
- 所有的树本质上都可以通过二叉树模拟出来
- 重要性质:
- 一个二叉树的第I层的最大子结点的个数是2^(i-1)
- 深度为k的二叉树有最大节点总数为2^k-1
- 对任何非空的二叉树的,n0表示叶子节点的个数,n2表示度为2的非叶结点的个数,则n0=n2+1
- 完美二叉树(满二叉树) ,完全二叉树(每个节点的位置要和满二叉树一样/除第 h 层外,其它各层是满的,第 h 层所有的结点都连续集中在最左边)
- 存储方式:对于完全二叉树,使用数组(可以计算每个节点的索引值),否则使用链表
5. 二叉搜索树:左子树的值小于根节点的值,右子树的值大于根节点,左右子树同时也是二叉搜索树
function binarySearchTree(){ function node(key){ this.key=key this.left=null this.right=null } this.root=null binarySearchTree.prototype.insert=function(key){ var newNode=new node(key) //递归 if (this.root==null){this.root=newNode} else{ this.insertNode(this.root,newNode) } } binarySearchTree.prototype.insertNode=function(node,newNode){ if(node.key>newNode.key){ if(node.left==null){ node.left=newNode }else{ this.insertNode(node.left,newNode) } }else{ if(node.right==null){ node.right=newNode }else{ this.insertNode(node.right,newNode) } } } //先序遍历 binarySearchTree.prototype.xian=function(){ this.xianNode(this.root) return res } var res=[] binarySearchTree.prototype.xianNode=function(node){ if (node!=null){ res.push(node.key) this.xianNode(node.left) this.xianNode(node.right) } } //最小值 binarySearchTree.prototype.min=function(){ var current=this.root while(current.left!=null){ current=current.left } return current.key //为了防止节点里的key为空,可以给出默认值 } //搜索一个值 binarySearchTree.prototype.search=function(key){ var node=this.root while(node!=null){ //while基本可以替代递归 if(key==node.key){ return true }else if(key < node.key){ node=node.left }else{ node=node.right } } return false } }删除节点,分为0节点,一个节点,两个节点三种情况(删除节点靠的是删除节点之间的联系)
function binarySearchTree(){ function node(key){ this.key=key this.left=null this.right=null } this.root=null binarySearchTree.prototype.insert=function(key){ var newNode=new node(key) //递归 if (this.root==null){this.root=newNode} else{ this.insertNode(this.root,newNode) } } binarySearchTree.prototype.insertNode=function(node,newNode){ if(node.key>newNode.key){ if(node.left==null){ node.left=newNode }else{ this.insertNode(node.left,newNode) } }else{ if(node.right==null){ node.right=newNode }else{ this.insertNode(node.right,newNode) } } } //删除节点 binarySearchTree.prototype.remove=function(key){ var current=this.root var parent=null var isLeftChild=true while(current.key!=key){ parent=current if(key<current.key){ current=current.left isLeftChild=true }else{ current=current.right isLeftChild=false } if(current==null) return false } //删除叶子节点 if(current.left==null && current.right==null){ if (current==this.root){ this.root=null //注意根节点 }else if(isLeftChild){ parent.left=null }else{ parent.right=null } } //删除的节点有一个子结点 else if(current.left!=null && current.right==null){ if (current==this.root){ this.root=this.root.left //注意根节点 }else{ if(isLeftChild){parent.left=current.left} else{parent.right=current.left} } }else if(current.left==null && current.right!=null){ if (current==this.root){ this.root=this.root.right }else{ if(isLeftChild){parent.left=current.right} else{parent.right=current.right} } } //删除有两个节点的节点,找到前驱或者后继, else{ var successor=this.getSuccessor(current) if(isLeftChild){ parent.left=successor }else{ parent.right=successor } successor.left=current.left//将删除节点的左子树转移到后继节点,对删除节点父子节点的关系进行调整 } } //找到后继的方法 binarySearchTree.prototype.getSuccessor=function(dnode){ //定义变量找到保存的后继 var successor=dnode var succcessorParent=dnode var current=dnode.right//找到是后继不是前驱 //循环查找 while(current!=null){ successor=current.left succcessorParent=current current=current.left } //判断后继节点是否就是dnode节点的右节点,对后继节点的父子节点关系进行调整 if(successor!=dnode.right){ succcessorParent.left=successor.right successor.right=dnode.right } return successor } }
- 二叉树在某些情况下会变成非平衡树(左右子孙节点数目相差很多),极端时甚至会变成链表
-
- 红黑树的三种变化方式:变色,左旋转,右旋转
十 图
- 图的术语:顶点,边,相邻顶点(由一条边连接起来的顶点),度(相邻顶点的数量),路径(简单路径不包含重复顶点,回路的第一个和最后一个顶点相同)
- 有向图和无向图,有权图和无权图
- 领接矩阵:当是稀疏矩阵时,存在内存浪费
-
- 邻接表:计算出度(指向别人)比较简单,计算入度比较麻烦
-
- 图的遍历方法:广度优先遍历(BFS)和深度优先遍历(DFS)
DFS:基于栈或者递归,通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问
白色表示未被访问,灰色表示被访问过,但是没被探索,黑色表示访问且被探索过
- 广度优先遍历----和树的层序遍历很相似
- 深度优先遍历----和先序遍历很相似
-
function graph(){ this.vert=[] this.edge={} graph.prototype.addv=function(v){ this.vert.push(v) this.edge[v]=[] } graph.prototype.adde=function(v1,v2){ this.edge[v1].push(v2) this.edge[v2].push(v1) } graph.prototype.toString=function(){ var res="" //遍历所有顶点以及顶点对应的边 for (let i=0;i<this.vert.length;i++){ res=res+(this.vert)[i]+"=>" for (let j=0;j<Object.values(this.edge)[i].length;j++){ res=res+Object.values(this.edge)[i][j] } res=res+"\n" } return res } //初始化颜色 graph.prototype.initColor=function(){ var colors={} for(let i=0;i<this.vert.length;i++){ colors[this.vert[i]]="white" } return colors } //广度优先遍历 graph.prototype.bfs=function(initv,handler){ var colors=this.initColor() var queue=[] queue.push(initv) //循环从队列中取出元素 while(queue.length>0){ //从队列中取出第一个顶点 var v=queue.shift() //获取和顶点相邻的其他顶点 var vlist=this.edge[v] //将v的颜色设置为灰色 colors[v]="gray" //遍历所有的顶点并加到队列中 for(let i=0;i<vlist.length;i++){ var e=vlist[i] if(colors[e]=="white"){ colors[e]="gray" queue.push(e) } } handler(v) colors[v]="black" } } //深度优先遍历 graph.prototype.dfs=function(handler){ var colors=this.initColor() this.dfsVisit(this.vert[0],colors,handler) } graph.prototype.dfsVisit=function(v,colors,handler){ colors[v]="gary" handler(v) //相当于一个容器 //访问V相连的顶点 var vlist=this.edge[v] for(let i=0;i<vlist.length;i++){ var e=vlist[i] if(colors[e]=="white"){ this.dfsVisit(e,colors,handler) } } } } var a=new graph() a.addv("A") a.addv("B") a.addv("C") a.addv("D") a.addv("E") a.addv("F") a.adde("A","B") a.adde("A","C") a.adde("A","D") a.adde("C","D") a.adde("C","E") a.adde("D","F") var res="" a.dfs(function(node){ res += node +" " }) console.log(res)