前端算法入门 - 走近红黑树

158 阅读6分钟

二叉搜索树

假设有一万个数,需要查找某个数存在不存在

按照以往的方法,暴力循环

let arr = new Array(10000)

for(let i = 0; i < 10000; i++) {
    arr[i] = Math.floor(Math.random() * 10000)
}

let num = 0
function search(arr, target) {
    for (let i = 0; i < arr.length; i++) {
        num += 1
        if (arr[i] == target) return true
    }
    return false
}
console.log(search(arr, 1000)) // false / true
console.log(num) // 10000 / 7260

可以看出,这样写,循环了非常多次,这是非常浪费性能的

  • 如果一个算法的性能很烂的话,有两个方面的原因
    1. 数据结构很烂
    2. 算法不对

很明显上方的算法没有什么问题,就是比较嘛。那么问题就只能出现在数据结构上了,这个数据结构很烂!

二叉搜索树

这是一颗二叉树

这颗二叉树有排序效果,左子树的节点都比当前节点小,右子树的节点都比当前节点大

构建二叉搜索树

  1. 任选一个数字做根节点
  2. 将剩下的数与节点比较,比节点小的放左边,比节点大的放右边
  3. 重复第2步

实现二叉搜索树

代码实现

interface INodeType {
    value: number;
    left: INodeType | null;
    right: INodeType | null;
}

type INode = INodeType | null;

class BuildSearchTree {
    private arr: number[]
    readonly root: INode

    constructor() {
        this.arr = this.createArr()
        this.root = this.init()
    }
    // 初始化数组
    private createArr(): number[] {
        let arr: number[] = new Array(10000)
        for (let i = 0; i < 10000; i++) {
            arr[i] = Math.floor(Math.random() * 10000)
        }
        return arr
    }

    private node(value): INode {
        return {
            value,
            left: null,
            right: null,
        }
    }
    /**
     * 连接节点
     * @param root 根节点
     * @param num 需要连接的数
     */
    private addNode(root: INode, num: number): void {
        if (root == null || root.value == num) return // 如果这个数存在,则不作处理
        if (root.value < num) { // 大的数放右边
            if (root.right == null) root.right = this.node(num) 
            this.addNode(root.right, num)
        } else { // 小的数放左边
            if (root.left == null) root.left = this.node(num)
            this.addNode(root.left, num)
        }
    }
    /**
     * 创建二叉搜索树
     */
    private init(): INode {
        if (this.arr == null || this.arr.length == 0) return null
        let root: INode = this.node(arr[0]) // 选定数组第0位作为根节点
        for (let i = 0; i < this.arr.length; i++) {
            this.addNode(root, this.arr[i])
        }
        return root
    }
}
const root = new BuildSearchTree().root

二叉搜索树创建好了之后,搜索其实很简单,很像前序遍历

/**
 * 二叉树搜索
 * @param root 根节点
 * @param target 目标数
 */
let num2 = 0
function searchByTree(root: INode, target: number): boolean{
    if(root == null) return false
    num2 += 1
    if (root.value == target) return true
    if (root.value < target) return searchByTree(root.right, target)
    if (root.value > target) return searchByTree(root.left, target)
}

console.log(searchByTree(root, 1000)) // false
console.log(num2) // 15
console.log(search(arr, 1000)) // false
console.log(num) // 10000

从循环次数上面来看,二叉搜索树的效果简直完爆嘛,二叉搜索树的强大之处,不言而喻。

虽然现在的性能看起来已经很强大了,但是不要忘了,前序遍历的循环次数是受二叉树层数影响的,层数越少,遍历的次数也就越少。也就是说,如果能把这颗二叉树尽量构造成平衡二叉树,那么就还能提升性能,用计算机科学的话来说,就是还未到性能的极致。

优化二叉搜索树 - 平衡二叉树

平衡二叉树概念

  1. 根节点的左子树与右子树的高度差不超过1
  2. 这棵树的每个子树都符合第一条

判断二叉树是否平衡

获取二叉树的深度 从上往下一层一层判断。不平衡就停止,平衡则继续向下判断

class Pingheng {
    // 获取二叉树深度
    public static getDeep(root: INode): number {
        if (root == null) return 0
        let leftDeep = this.getDeep(root.left),
            rightDeep = this.getDeep(root.right);
        return Math.max(leftDeep, rightDeep) + 1; // 当前还有一层, 所以要 + 1
    }

    // 判断是否是平衡二叉树
    public static isBlance(root: INode): boolean {
        if (root == null) return true;
        let leftDeep = this.getDeep(root.left),
            rightDeep = this.getDeep(root.right);
        if (Math.abs(leftDeep - rightDeep) > 1) {
            // 差值大于1 不平衡
            return false;
        } else {
            return this.isBlance(root.right) && this.isBlance(root.left);
        }
    }
}

二叉树的单旋(左单旋,右单旋)

某一节点不平衡,如果左边浅,右边深,进行左单旋。 反之亦然

上面的类里面加一点方法

class Change extends Pingheng {
    // 左单旋
    protected static leftRotate(root: INode): INode {
        // 找到新根
        let newRoot = root.right
        // 找到变化分支
        let changeTree = root.right.left
        // 当前旋转节点的右孩子为变化分支
        root.right = changeTree
        // 新根的左孩子为旋转节点
        newRoot.left = root
        // 返回新根
        return newRoot
    }
    // 右单旋
    protected static rightRotate (root: INode):INode {
         // 找到新根
         let newRoot: INode = root.left
         // 找到变化分支
         let changeTree = root.left.right
         // 当前旋转节点的左孩子为变化分支
         root.left = changeTree
         // 新根的右孩子为旋转节点
         newRoot.right = root
         // 返回新根
         return newRoot
    }
    // 旋转二叉树
    public static change(root: INode): INode {
        if (this.isBlance(root)) return root;

        if (root.left != null) root.left = this.change(root.left)

        if (root.right != null) root.right = this.change(root.right)

        let leftDeep = this.getDeep(root.left)
        let rightDeep = this.getDeep(root.right)

        if (Math.abs(leftDeep - rightDeep) < 2) {
            return root
        } else if (leftDeep > rightDeep) { // 左边深, 右单旋
            return this.rightRotate(root)
        } else { // 右边深, 左单旋
            return this.leftRotate(root)
        }
    }
}

二叉树的双旋(右左双旋, 左右双旋)

  • 当要对某个节点进行左单旋时: 如果变化分支是唯一的最深分支,要先对新根进行右单旋,然后进行左单旋,这样的旋转叫做右左双旋
  • 当要对某个节点进行右单旋时: 如果变化分支是唯一的最深分支,要先对新根进行左单旋,然后进行右单旋,这样的旋转叫做左右双旋
class Shuangxuan extends Change {
    public static change(root: INode): INode {
        if (!root) { return null; }

        if (this.isBlance(root) || (root.right == null && root.left == null)) {
            return root;
        }
        if (root.left != null) {
            root.left = this.change(root.left);
        }

        if (root.right != null) {
            root.right = this.change(root.right);
        }

        const leftDeep = this.getDeep(root.left);
        const rightDeep = this.getDeep(root.right);

        if (Math.abs(leftDeep - rightDeep) < 2) {
            return root;
        } else if (leftDeep > rightDeep) { // 左边深, 右单旋
            const changeTreeDeep = this.getDeep(root.right && root.right.left),
                noChangeTreeDeep = this.getDeep(root.right && root.right.right);
            if (changeTreeDeep > noChangeTreeDeep) {
                root.left = this.rightRotate(root.left as INodeType);
            }
            return this.rightRotate(root);
        } else { // 右边深, 左单旋
            const changeTreeDeep = this.getDeep(root.right && root.right.left),
                noChangeTreeDeep = this.getDeep(root.right && root.right.right);
            if (changeTreeDeep > noChangeTreeDeep) {
                root.right = this.rightRotate(root.right as INodeType);
            }
            return this.leftRotate(root);
        }
    }
}

二叉树的双旋

前面经过了二叉树的单旋,左右双旋,右左双旋,二叉树依旧有可能不平衡。那就还需要考虑一种情况:如果变化分支的深度比旋转节点的另一侧高度差距超过2,那么单旋之后依旧不平衡。

那再改造一下change方法

class DoubleRotate extends Change {
    public static change(root: INode): INode {
        if (!root) { return null; }

        if (this.isBlance(root) || (root.right == null && root.left == null)) {
            return root;
        }
        if (root.left != null) {
            root.left = this.change(root.left);
        }

        if (root.right != null) {
            root.right = this.change(root.right);
        }

        const leftDeep = this.getDeep(root.left);
        const rightDeep = this.getDeep(root.right);

        if (Math.abs(leftDeep - rightDeep) < 2) {
            return root;
        } else if (leftDeep > rightDeep) { // 左边深, 右单旋
            const changeTreeDeep = this.getDeep(root.right && root.right.left),
                noChangeTreeDeep = this.getDeep(root.right && root.right.right);
            if (changeTreeDeep > noChangeTreeDeep) {
                root.left = this.rightRotate(root.left as INodeType);
            }
            let newRoot = this.rightRotate(root);

            if (newRoot) {
                newRoot.right = this.change(newRoot.right);
            }
            newRoot = this.change(newRoot);
            return newRoot;
        } else { // 右边深, 左单旋
            const changeTreeDeep = this.getDeep(root.right && root.right.left),
                noChangeTreeDeep = this.getDeep(root.right && root.right.right);
            if (changeTreeDeep > noChangeTreeDeep) {
                root.right = this.rightRotate(root.right as INodeType);
            }
            let newRoot = this.leftRotate(root);
            if (newRoot) {
                newRoot.left = this.change(newRoot.left);
            }
            newRoot = this.change(newRoot);
            return newRoot;
        }
    }
}

234树

思考一下

影响二叉平衡树的性能的点是什么

  • 在于二叉平衡搜索树的叉只有两个,导致在节点铺满时也有很多层。
  • 如果一个节点存多个数,可以提升空间性能
  • 树的层级越少,查找的效率越高

怎么样能使二叉平衡排序树的层数变少

  • 如果不是二叉,层数会更少

叉越多,层数越小,但是叉阅读,树的结构就越复杂,树的叉最多为4层最好

234树

  • 希望一颗树,最多有四个叉(度最大为4)

  • 234树的子节点永远在最后一层,

  • 234树永远是平衡的(每一个路径的高度都相同)

  • 达成的效果

  1. 分支变多了,层数变少了
  2. 节点中存的树变多了,节点变少了
  3. 因为分支变多了, 所以复杂度上升了

期望

  • 希望对二三四树进行简化
  • 简化为二叉树
  • 依旧保留多叉
  • 单节点依旧保留存放多个值

由此出现了红黑树

红黑树

to be continue--