二叉树
最多只有两个节点,一个左侧节点,一个是右侧节点
二叉搜索树
是二叉树的一种,只能左侧节点存储比父节点小,右侧比父节点大的值
- 创建一个左右都为空的node节点类的构造函数
-
class Node{ constructor(key){ this.right=null this.left=null } }
- 创建二叉树BST类并定义根节点root为空
constructor(){ this.root=null } }
- 因为让所有大的数放在右边节点,所以要遍历循环,封装一个每次自己和自己的前一个节点比的方法
class BST{ constructor(){ this.root=null } insert(key){ if(this.root===null){ this.root=new Node(key) }else{ this.insetNode(this.root,key) } } insertNode(Node,key){ if(小于){ this.insertNode(node.left,key) }else{ this.insertNode(node.left,key) } } } var mytree=new BST()
-
.为便于比较,建立一个对象将对比结果返回0,-1,1
const Compare={
less:-1,
bigger:1,
equ:0
}
CompareFn(){
if(a===b){
return Compare.equ
}
return a<b?less:bigger
}
再递归比较的插入函数中,当传入两个参数后,先与根节点比,小往左,大往右,知道没有最左、右边的节点它就归位。
insertNode(node,key){
if(this.CompareFn(key, node.key)===Compare.less){
if(node.left===null){
node.left=new Node(key)
}
else{
this.insertNode(node.left, key)
}
}else{
if(node.right===null){
node.right===new Node(right)
}
else{
this.insertNode(node.right,key)
}
}
}
遍历
中序遍历:按从小到大
//中序遍历
inOrderMap(callback) {
this.inOrderMapNode(this.root, callback)
}
inOrderMapNode(node, callback) {
if(node!=null){
this.inOrderMapNode(node.left,callback)
callback(node,key)
this.inOrderMapNode(node.right,callback)
}
}
}
-
递归遍历左子树:
this.inOrderMapNode(node.left, callback)
会首先被调用,它会递归地遍历当前节点的左子树,直到到达最左边的叶子节点。在这个过程中,不会执行callback
函数,因为还在深入左子树。 -
到达最左边的叶子节点:当递归到达最左边的叶子节点时,这个叶子节点没有左子树,所以递归调用会停止。此时,开始执行
callback
函数。 -
执行
callback
函数:callback(node.key)
被调用,其中node
是当前正在处理的节点,node.key
是该节点的键值。callback
函数可以执行任何操作,比如打印节点的键值、将节点添加到数组中、更新节点的数据等。 -
回溯并处理父节点:在处理完最左边的叶子节点后,递归函数会回溯到其父节点,并执行
callback(node.key)
来处理父节点。 -
递归遍历右子树:在处理完当前节点后,
this.inOrderMapNode(node.right, callback)
会被调用,递归地遍历当前节点的右子树。 -
那么在中序遍历过程中,每访问一个节点,就会打印出该节点的
key
值。如果树的结构和键值如下: -
var mytree=new BST() mytree.inOrderMap((value)=>{ console.log(value); })
复制
1 / \ 2 3 / \ 4 5
中序遍历的输出将会是:
4 2 5 1 3
先序遍历:按先父后子访问每个节点,如:打印一个结构化文档
- 执行过程:
- 检查节点是否为空:
if(node != null)
,如果节点为空,则不进行任何操作,直接返回。 - 执行回调函数:
callback(node.key)
,首先对当前节点执行回调函数,传入当前节点的key
作为参数。 - 递归遍历左子树:
this.inOrderMapNode(node.left, callback)
,递归地遍历当前节点的左子树。 - 递归遍历右子树:
this.inOrderMapNode(node.right, callback)
,递归地遍历当前节点的右子树。
- 检查节点是否为空:
具体执行步骤
假设我们有一个如下的二叉树
1
/ \
2 3
/ \
4 5
步骤 1:从根节点开始
- 当前节点:1
- 执行
callback(1)
。 - 递归遍历左子树(节点 2)。
- 递归遍历右子树(节点 3)。
- 执行
步骤 2:遍历左子树(节点 2)
- 当前节点:2
- 执行
callback(2)
。 - 递归遍历左子树(节点 4)。
- 递归遍历右子树(节点 5)。
- 执行
步骤 3:遍历左子树(节点 4)
- 当前节点:4
- 执行
callback(4)
。 - 节点 4 没有左子树,返回。
- 节点 4 没有右子树,返回。
- 执行
步骤 4:遍历右子树(节点 5)
- 当前节点:5
- 执行
callback(5)
。 - 节点 5 没有左子树,返回。
- 节点 5 没有右子树,返回。
- 执行
步骤 5:返回到节点 2,继续遍历右子树(节点 3)
- 当前节点:3
- 执行
callback(3)
。 - 节点 3 没有左子树,返回。
- 节点 3 没有右子树,返回。
- 执行
最终输出
如果 callback
函数的定义如下:
function callback(key) {
console.log(key);
}
那么先序遍历的输出将会是:
1
2
4
5
3
因为先序遍历会先访问根节点,然后遍历左子树,最后遍历右子树,而 callback
函数在每个节点被访问时执行,打印出节点的 key
值。
后序遍历:用于计算大小
//后序遍历
PostOrderMap(callback) {
this.postOrderMapNode(this.root, callback)
}
PostOrderMapNode(node, callback) {
if(node!=null){
this.postOrderMapNode(node.left,callback)
this.postOrderMapNode(node.right,callback)
callback(node.key)
}
}
}
思想和先序大差不差
查询:
查找最大值和最小值,也就是二叉树中最左边,最右边一列
用指针一点点去覆盖最右、左边的数值
min(){
return this.minNode(this.root)
}
minNode(){
let current=node
while(current!=null&¤t.left!=null){
current=current.left
}
return current
}
max(){
return this.maxNode(this.root)
}
maxNode(node){
let current=node
while(current!==null&¤t.right!==null){
current==current.right
}
return current
}
search(key){
return this.searchNode(this.root,key)
}
searchNode(node,key){
if(node!=null)
{
return false
}
else if(this.CompareFn(key,node.key)===CompareFn.less){
return searchNode(node.left)
}
else if(this.CompareFn(key,node.key)===CompareFn.bigger){
return searchNode(node.right)
}
return true
}
其他找最大最小和按值搜索方法如上
如果按key一次找会很慢,所以按照二叉树这样比较大小找较为容易。
移除
将被删除的节封为三种情况:
- 如果没有子代:直接移除
- 有一个子代节点:直接跳过和和爷节点连接
- 有很多子节点:从众多子节点的右端中找一个最小的节点(就算最小也比左节点大),与被删的父节点兑换。
- 思想就是从根节点往下找,不断将目标节点与树中遍历到的节点比较,来判断进入左右哪个分支,如果相等就不再执行比较函数,证明找到了,直接俄看目标节点的子代情况。
-
第一种——节点没有子代
remove(key){
this.root= this.removeNode(this.root,key)
}
removeNode(node,key){
if(node==null){
return null//找到空节点
}
else if(this.CompareFn(key,node.key)===less){
node.left= this.removeNode(node.left,key)//将左边的节点把当前节点覆盖,不然原节点不会受影响
}
else if(this.CompareFn(node.key,key)===bigger){
node.right= this.CompareFn(node.right,key)
}
else
{
if (node.right === null && node.left === null){
return null//找到没有子代的节点
return node
}
}
- 比如传入root是2,左节点是1,右节点是3的二叉树。想删除1
- 从remove进入删除,2作为root进入removeNode,node是root2,key是1;因为1比2小;再进入 node.left= this.removeNode(node.left,key)而node.left1作为第二次执行函数中的参数node;这样目标节点1,就等于node.l节点1所以不会执行compareFn比较函数,直接进入else{}
- 第二次执行removeNode函数后node.left是2 的左节点1,key还是1,1左节点是null;
- 在第二次removeNode参数中node是没有子代的1;进入条件node.right === null && node.left === null,将1赋值为null,返回null.
- 再回到node.left= this.removeNode(node.left,key)中node.left,在这一步中node还是之前的root2.
- 就是removeNode函数执行后返回的null赋值给node.left1,也就实现1的删除。执行下一行返回node2,跳出removeNode。
- 回到remove函数将removeNode结果返回root也就是node2。
第二种——要么左节点为空,要么右节点为空
比较方法和CompareFn方法一样不同的是在else{}中,
- 如果node的右节点为空,就用左节点替换想删除的节点。
- 如果node的左节点为空,就用右节点替换想删除的节点。
-
if(node.right===null){ node=node.left return node } else if(node.left===null){ node=node.right return node }
第三种——左右都有节点
-
当目标节点被删除后必须有比它左子代节点的值大的来替换被删除的位置,所以在目标节点的右分支上找最小(仅对于右分支)的节点来替换被删除节点的位置,并把这个最小的节点删除(防止重复)
二叉堆
是二叉数的一种,不同于二叉搜索树必须保证左边小于有右边
最小堆
子节点比父节点大,最小是根节点
这里用数组管理数据,那么按照索引值找到相应的大小顺序?
如上图:【1,2,3,4,5,6,7】下标为0,1,2,3,4,5,6
规律:左节点的下标为父代的2x+1,右节点的下标为父代下标的2x+2
class MinHeap{
heap=[]//数组管理数据
getLeftIndex(index){
return 2*index+1
}
getRightIndex(index){
return 2*index+2
}
}
插入insert
push一个数,但是要保证最顶上的节点最小,父节点永远小于等于子节点。用上溯的一级级与上层节点比较,遇到大的就替换
从最后往上找的方法shiftUp()
找到父节点的方法getParentIndex():用(index-1)/2向下取整。
- 暂时将push的新元素下标定为数组最后的下标
- 用getParentIndex方法找到父元素的索引值,
- 将父的数值与新数值比较,如果新的比父节点小就换位置,再递归爷节点等上层节点比对
insert(value){
if(value){
this.heap.push(value)
this.shiftUp(this.heap.length-1)
return true
}
return false
}
CompareFn() {
if (a === b) {
return Compare.equ
}
return a < b ? less : bigger
}
shiftUp(index){
let parent=getParentIndex(index)
while(index>0&&CompareFn(this.heap[parent],this.heap[index])===Compare.bigger){
this.swap(this.heap,parent,index)//父子交换
index=parent//索引值交换
parent=this.getParentIndex(index)//再得到爷节点
}
}
swap(arry,a,b){
const temp=arry[a]
arry[a]=arry[b]
arry[b]=temp
}
移除
为保证结构的完整,删除一个元素后让最后面的元素顶到被删的位置上去,再用逐层比较的方法调整到最小堆的位置去。
//删除
extarct(){
if(this.isEmpty()){
return
}
if(this.size()===1){
return this.heap.shift
}
const remove =this.heap[0]
this.heap[0]=this.heap.pop()//最后一个顶到第一个位置去
this.shiftDown()//向下溯
return remove
}
- 封装向下溯的shiftDown方法,将当前节点于子左节点比,如果比子代大,就将索引返回子代节点,记录为current后,
- 再与右节点比,还是大就索引返回右节点,
- 最后再用封装好的swap方法交换
最大堆
保证最大的元素在最前面
大部分方法可以继承最小堆,但是插入和回溯的方法相反,为了方便,直接更改comparefn
class MaxHeap extends MinHeap{
constructor(){
super()
}
CompareFn() {
if (a === b) {
return Compare.equ
}
return a>b ? Compare.less : Compare. bigger
}
}