前端演算法入門 - 走近紅黑樹

JsRicardo發表於2019-12-09

二叉搜尋樹

假設有一萬個數,需要查詢某個數存在不存在

按照以往的方法,暴力迴圈

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--

相關文章