javascript数据结构之树(二叉搜索树,平衡二叉树,红黑树)

一种非顺序数据结构--树。

树是一种分层数据的抽象模型,一个树结构包含一系列存在父子关系的节点,每个节点都有一个父节点(除了顶部第一个节点)以及零个或多个子节点,位于树顶部的节点叫根节点。

二叉树和二叉搜索树

二叉树中的节点最多只能有两个节点,一个左侧的节点,一个右侧节点。
二叉搜索树(BST)是二叉树的一种,但是只允许在左侧储存比父节点小的数据,在右侧储存比父节点大的数据。
image

和链表一样,通过引用来表示节点之间的关系,在双向链表里,每个节点有俩引用,一个指向上一个节点,一个指向下一个节点,对于二叉搜索树也使用同样方式,不同的地方是一个指向左侧节点,一个指向右侧节点(树中会称节点为键)。

  1. insert(key) 树里插入键
  2. search(key) 树里查找一个键
  3. inOrderTraverse() 中序遍历所有(上行顺序遍历,先左,自身,右)
  4. preOrderTraverse() 先序遍历所有(先自身,左,右)
  5. postOrderTraverse() 后序遍历所有(先左,右,自身)
  6. min() 获取树最小值
  7. max() 获取树最大值
  8. remove(key) 删除树里某个键

中序,先序,后序,都是递归实现,描述可能不太具体,具体还是看代码把。

const Compare = {
    LESS_THAN:-1,
    BIGGER_THAN:1
}
function defaultCompare(a,b){
    if(a===b){
        return 0;
    }
    return a<b?Compare.LESS_THAN:Compare.BIGGER_THAN;
}
class Node{
    constructor(key){
        this.key = key;
        this.left = null;
        this.right = null;
    }
}

class BinarySearchTree{
    constructor(compareFn = defaultCompare){
        this.compareFn = compareFn;
        this.root = null;
    }
    insert(key){
        if(!this.root){
            this.root = new Node(key);
        }else{
            this.insertNode(this.root,key);
        }
    }
    insertNode(node,key){//查找对比键大小插入,最后生成结构如上图
        if(this.compareFn(key,node.key)===Compare.LESS_THAN){
            if(!node.left){
                node.left = new Node(key);
            }else{
                this.insertNode(node.left,key);
            }
        } else {
            if(!node.right){
                node.right = new Node(key);
            }else{
                this.insertNode(node.right,key);
            }
        }
    }
    search(key){
        return this.searachNode(this.root,key);
    }
    searachNode(node,key){
        if(!node){
            return false;
        }
        if(this.compareFn(key,node.key)===Compare.LESS_THAN){
            return this.searachNode(node.left,key);
        }else if(this.compareFn(key,node.key)===Compare.BIGGER_THAN){
            return this.searachNode(node.right,key);
        }else{
            return true;//可以根据自己需要返回
        }
    }
    min(){
        return this.minNode(this.root);
    }
    minNode(node){
       let current = node;
       while(current&&current.left){
            current = current.left;
       }
       return current;
    }
    max(){
        return this.maxNode(this.root);
    }
    maxNode(node){
       let current = node;
       while(current&&current.right){
            current = current.right;
       }
       return current;
    }
    remove(key){
        this.root = this.removeNode(this.root,key)
    }
    removeNode(node,key){
        if(!node){
            return null;
        }
        if(this.compareFn(key,node.key)===Compare.LESS_THAN){
        //向左查找
            node.left = this.removeNode(node.left,key);
            return node;
        }else if(this.compareFn(key,node.key)===Compare.BIGGER_THAN){
        //向右查找
            node.right = this.removeNode(node.right,key);
            return node;
        }else{//找到了
            if(!node.left&&!node.right){
                node = null;
                return node;
            }
            if(!node.left){
                node = node.right;
                return node;
            }else if(!node.right){
                node = node.left;
                return node;
            }
            //第三种情况,移除的节点有两个子节点,先找到要移除节点右边子树里最小的节点,然后移到当前删除位置,然后这会会有两个相同的键(当前删除位置===右侧子树最小节点),所以把右侧子树最小节点删掉。
            const aux = this.minNode(node.right);
            node.key = aux.key;
            node.right = this.removeNode(node.right,aux.key);
            return node;
        }
    }
    inOrderTraverse(cb){
        this.inOrderTraverseNode(this.root,cb);
    }
    inOrderTraverseNode(node,cb){
        if(node){
            this.inOrderTraverseNode(node.left,cb);
            cb(node.key);
            this.inOrderTraverseNode(node.right,cb);
        }
    }
    preOrderTraverse(cb){
        this.preOrderTraverseNode(this.root,cb)
    }
    preOrderTraverseNode(node,cb){
        if(node){
            cb(node.key);
            this.preOrderTraverseNode(node.left,cb);
            this.preOrderTraverseNode(node.right,cb);
        }
    }
    postOrderTraverse(cb){
        this.postOrderTraverseNode(this.root,cb);
    }
    postOrderTraverseNode(node,cb){
        if(node){
            this.postOrderTraverseNode(node.left,cb);
            this.postOrderTraverseNode(node.right,cb);
            cb(node.key);
        }
    }
}

const printNode = (value)=>console.log(value);//可以用此做回掉测试

image

image

image

二叉搜索树(BST)存在一个问题,在添加或删除后,可能会某一侧会很深,有的节点可能就几层,有的可能有很多层。

自平衡二叉树(AVL树)

AVL是一颗自平衡树,在添加或者移除节点的时候会尝试保证自平衡,任何一个节点(不论深度),左侧字树和右侧字数最多相差1。

AVL树需要根据节点高度来计算平衡因子,根据平衡因子来判断,是否需要旋转平衡。

计算一个节点高度的方法:getNodeHeight(node)
计算平衡因子方法:getBalanceFactor(node)
平衡因子的概念:对每个节点计算左子树和右子树的高度的差值,该值(left-right)应为0,1,-1,如果结果不是这三个值之一,则需要平衡AVL树。
LL:向右的单旋转
image
RR:向左的单旋转
image
LR:先左后右,先RR转再LL转,这种情况出现于不平衡节点的左侧节点高度大于右侧节点高度,并且左侧节点右侧较重,这种情况可以先对左侧节点进行左旋转修复,然后在对不平衡节点进行右旋转修复。
image
RL:先右后左,先LL转再RR转,这种情况出现于不平衡节点的右侧节点高度大于左侧节点高度,并且右侧节点左侧较重,这种情况可以先对右侧节点进行右旋转修复,然后对不平衡节点进行一个左旋转修修复。
image

const BalanceFactor = {//平衡因子的常量
    UNBALANCED_RIGHT:1,
    SLIGHTLY_UNBALANCED_RIGHT:2,
    BALANCED:3,
    SLTGHTLY_UNBALANCED_LEFT:4,
    UNBALANCED_LEFT:5
}
class AVLTree extends BinarySearchTree{
    constructor(compareFn=defaultCompare){
        super(compareFn);
        this.compareFn=compareFn;
        this.root = null;
    }
    getNodeHeight(node){
        if(!node){
            return -1
        }
        return Math.max(this.getNodeHeight(node.left),
        this.getNodeHeight(node.right))+1
    }
    getBalanceFactor(node){
        const heightDifference = this.getNodeHeight(node.left)-this.getNodeHeight(node.right);
        switch(heightDifference){
            case -2:
            return BalanceFactor.UNBALANCED_RIGHT;
            case -1:
            return BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT;
            case 1:
            return BalanceFactor.SLTGHTLY_UNBALANCED_LEFT;
            case 2:
            return BalanceFactor.UNBALANCED_LEFT;
            default:
            return BalanceFactor.BALANCED;
        }
    }
    rotationLL(node){//右单旋转
       const tmp = node.left;
       node.left = tmp.right;
       tmp.right = node;
       return node;
    }
    rotationRR(node){//左单旋转
        const tmp = node.right;
        node.right = tmp.left;
        tmp.left = node;
        return tmp;
    }
    rotationLR(node){
        node.left = this.rotationRR(node.left);
        return this.rotationLL(node);
    }
    rotationRL(node){
        node.right = this.rotationLL(node.right);
        return this.rotationRR(node);
    }
    insert(key){
        this.root = this.insertNode(this.root,key)
    }
    insertNode(node,key){
        if(!node){
            return new Node(key);
        }else if(this.compareFn(key,node.key)===Compare.LESS_THAN){
        //向左继续
            node.left = this.insertNode(node.left,key);
        }elso if(this.compareFn(key,node.key)===Compare.BIGGER_THAN){
        //向右继续
            node.right = this.insertNode(node.right,key);
        }else{//不比当前key大也不比当前key小,等于当前重复
            return node;//重复的键
        }
        //递归插入完成后,根据函数依次弹栈,来计算当前的节点的平衡因子,查看是否失衡
        const balanceFactor = this.getBalanceFactor(node);
        if(balanceFactor === BalanceFactor.UNBALANCED_LEFT){//等于2,左侧子节点失衡
            if(this.compareFn(key,node.left.key)===Compare.LESS_THAN){//查看插入的节点是插到了当前节点左子节点的下左侧,符合LL,详细可以配上面图看
                node = this.rotationLL(node);
            }else {//右,符合LR
                return this.rotationLR(node);
            }
        }
        if(balanceFactor === BalanceFactor.UNBALANCED_RIGHT){//-2右侧失衡
            if(this.compareFn(key,node.right.key)===Compare.BIGGER_THAN){//查看插入的节点是插到了当前节点右子节点的下右侧,符合RR,详细可以配上面图看
                node = this.rotationRR(node);
            }else{//下左,符合RL
                return this.rotationRL(node);
            }
        }
        return node;
    }
    removeNode(node){
        node = super.removeNode(node,key);
        if(!node){
            return node;
        }
        const balanceFactor = this.getBalanceFactor(node);
        if(balanceFactor === BalanceFactor.UNBALANCED_LEFT){//左子节点失衡
            const balanceFactorLeft = this.getBalanceFactor(node.left);
            //判断左边是那种情况,是进行LL转还是LR转
            if(balanceFactorLeft===BalanceFactor.SLTGHTLY_UNBALANCED_LEFT||balanceFactorLeft.BALANCED){//差值为1或者其他情况的时候进行符合LL
                return this.rotationLL(node);
            }
            if(balanceFactorLeft===BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT){//左侧差值为-1,符合LR
                return this.rotationLR(node);
            }
        }
        if(balanceFactor === BalanceFactor.UNBALANCED_RIGHT){//差值为-2,右侧子节点不平衡
            const balanceFactorRight = this.getBalanceFactor(node.right);
            if(balanceFactorRight === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT||balanceFactorRight===BalanceFactor.BALANCED){//和左相反,差值-1或其他符合RR
                return return this.rotationRR(node);
            }
            if(balanceFactorRight === BalanceFactor.SLTGHTLY_UNBALANCED_LEFT){//差值为1符合RL
                return this.rotationRL(node);
            }
        }
        return node;
    }
}

红黑树

最先发明的平衡二叉查找树是AVL树(它严格符合平衡二叉查找树的定义,即任何节点的左右子树高度相差不超过 1,是一种高度平衡的二叉查找树。)但是在工程中,我们经常听到的通常是红黑树,而不是AVL树.那么为什么工程中都喜欢用红黑树,而不是其他平衡二叉查找树呢?
其实在这里,我们应该能有一些想法了.既然他严格按照规定执行,每次的插入,删除,都就会引发树的调整.调整的多了,自然会影响树的效率.那么红黑树又是怎样解决这个问题的呐?

其实,平衡二叉查找树中“平衡”的意思,其实就是让整棵树左右看起来比较“对称”、比较“平衡”,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些。
所以红黑树就是这种设计思路(近似平衡)了.

如果我们需要一个包含多次插入和删除的自平衡树,红黑树是比较好的,如果插入和删除频率较低,更多是进行搜索操作,那么AVL树比红黑树更好。

在红黑树中,每个节点遵循以下规则。

  1. 每个节点不是红的就是黑的。
  2. 树的根节点都是黑的。
  3. 所有叶节点都是黑的([注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!])
  4. 如果一个节点是红的,那么他的两个子节点都是黑的。
  5. 不能有两个相邻的红节点,一个红节点不能有红的父节点或子节点也就是说,红色节点是被黑色节点隔开的
  6. 从给定的节点到他的后代节点(null节点)的所有路径包含相同数量的黑色节点。

image
和AVL树一样,在进行节点的插入和删除时,就会破坏红黑树的一些规则.在红黑树中主要破坏的是以下两点:

  1. 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
  2. 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;

插入

红黑树规定,插入的节点必须是红色的。而且,二叉查找树中新插入的节点都是放在叶子节点上。所以,关于插入操作的平衡调整,有这样两种特殊情况,但是也都非常好处理。
  1. 如果插入节点的父节点是黑色的,那我们什么都不用做,它仍然满足红黑树的定义。
  2. 如果插入的节点是根节点,那我们直接改变它的颜色,把它变成黑色就可以了。
  3. 其他:都会违背红黑树的定义,于是我们就需要进行调整,调整的过程包含两种基础的操作:左右旋转和改变颜色。
const Color = {
    RED:'red',
    BLACK:'black'
}
class RedBlackNode extends Node{
    constructor(key){
        this.key = key;
        this.color = Color.RED;
        this.parent = null;
    }
    isrRed(){
        return this.color === Color.RED;
    }
}
class RedBlackTree extends BinarySearchTree{
    constructor(compareFn = defaultCompare){
        super(compareFn);
        this.compareFn = compareFn;
        this.root = null;
    }
    insert(key){
        if(!this.root){
            this.root = new RedBlackNode(key);
            this.root.color = Colors.BLACK;
        }else{
            const newNode = this.insertNode(this.root,key);
            this.fixTreeProperties(newNode);
        }
    }
    insertNode(node,key){
       if(this.compareFn(key,node.key)===Compare.LESS_THAN){
            if(!node.left){
                node.left = new RedBlackNode(key);
                node.left.parent = node;
                return node.left;
            }else{
                return this.insertNode(node.left,key);
            }
       }else if(this.compareFn(key,node.key)===Compare.BIGGER_THAN){
            if(!node.right){
                node.right = new RedBlackNode(key);
                node.right.parent = node;
                return node.right;
            }else{
                return this.insertNode(node.right,key);
            }
       }else{//相同
            node.key = key; //key万一是复杂对象。
            return node;
       }
       
    }
    fixTreeProperties(node){
        while(node&&node.parent&&node.parent.isRed()&&node.color===!==Color.BLACK){
            let parent = node.parent;
            const grandParent = parent.parent;
            if(grandParent&&grandParent.left===parent){
            //父节点是左侧子节点
                const uncle = grandParent.right;
                if(uncle&&uncle.color === Color.RED){
                //左侧叔父节点是红色,重新填色,隔开颜色
                    grandParent.color = Color.RED;
                    uncle.color = Color.BALCK;
                    parent.color= Color.BALCK;
                    node = grandParent;
                }else{
                    //节点是右侧子节点-左旋转
                    if(node === parent.right){
                        this.rotationRR(parent);
                        node = parent;
                        parent = node.parent;
                    }
                    //节点是左侧子节点-右旋转
                    this.rotationLL(grandParent);
                    parent.color = Color.BLACK;
                    grandParent.color = Color.RED;
                    node = parent;
                }
            
            }else{//父节点是右侧节点
                const uncle = grandParent.left;
                //左侧叔父节点是红色,重新填色,隔开颜色
                if(uncle&&uncle.color === Color.RED){
                    grandParent.color = Color.RED;
                    uncle.color = Color.BALCK;
                    parent.color= Color.BALCK;
                    node = grandParent;
                }else{
                    if(node === parent.left){
                        //节点是左侧子节点-右旋转
                        this.rotationLLarent);
                        node = parent;
                        parent = node.parent;
                    }
                     //节点是右侧子节点旋转
                    this.rotationRRrandParent);
                    parent.color = Color.BLACK;
                    grandParent.color = Color.RED;
                    node = parent;
                    
                }
            }
        }
        this.root.color = Color.Black;
    }
    rotationLL(node){//多了一步设置对应父级
        const tmp = node.left;
        node.left = tmp.right;
        if(tmp.right&&tmp.right.key){
            tmp.right.parent = node;
        }
        tmp.parent = node.parent;
        if(!node.parent){//顶级了
            this.root =tmp;
        }else{
            if(node === node.parent.left){
                node.parent.left = tmp;
            }else{
                 node.parent.right = tmp;
            }
        }
        tmp.right = node;
        node.parent = tmp;
        
    }
    rotationRR(node){//左旋转
        const tmp = node.right;
        node.right = tmp.left;
        if(tmp.left&&tmp.left.key){
            tmp.left.parent = node;
        }
        tmp.parent = node.parent;
        if(!node.parent){
            this.root = node;
        }else{
            if(node === node.parent.left){
                node.parent.left=tmp;
            }else{
                node.parent.right = tmp;
            }
        }
        tmp.left = node;
        node.parent = tmp;
    }
}

做个笔记,欢迎指出错误,该内容借鉴与学习javascript数据结构与算法

阅读 219

推荐阅读

世界核平

0 人关注
17 篇文章
专栏主页