徹底弄懂二叉排序樹

徹底弄懂二叉排序樹

前言

在之前學習數(shù)據(jù)結(jié)構(gòu)的時候,就學過二叉排序樹,不過,由于但是只是紙上談兵,雖然知道二叉排序樹的插入,刪除等的操作過程,不過由于沒有具體實現(xiàn)過,所以當想要實現(xiàn)的時候,就出現(xiàn)了“道理都懂,卻無法做到”的尷尬局面,趁著最近有空,抽了個時間認真學習二叉排序樹,并且手動編寫了實現(xiàn)的代碼,真正理解了二叉排序樹的操作過程

二叉排序樹簡介

二叉排序樹,二叉樹的一個變種,主要的特點在于,該樹的值在分布的時候具有非常明顯的特征,左子樹的值小于根節(jié)點的值,而根節(jié)點的值小于右子樹的值,這在進行搜索,查找的時候是非常有利的,因為它的平均操作時間接近O(h),h為樹的高度,而且,二叉排序樹本身是具有動態(tài)性的,可以動態(tài)地進行節(jié)點的刪除,插入等的操作,接下來我們就通過具體的例子來學習二叉排序樹

深入學習二叉排序樹

首先先構(gòu)造一個二叉排序樹,然后我們來通過代碼演示如何生成該樹

二叉排序樹舉例

從圖中可以看到,生成的樹相對是比較平衡的

樹的生成過程


class Tree{

    private Node root;

    public Node getRoot() {
        return root;
    }

    // 構(gòu)造樹,構(gòu)造的過程相當于將data中的數(shù)據(jù)插入
    public void construct(int[] data){

        for (int i = 0; i < data.length; i++){
            insert(data[i]);
        }
    }

    /**
     * 插入節(jié)點
     * @param value 節(jié)點的值
     */
    public void insert(int value){
        Node pre = null;
        Node current = root;
        // 插入的過程:
        //      每次都從根節(jié)點開始查找
        //      如果值比根節(jié)點小,則插入的節(jié)點應(yīng)該在根節(jié)點的左側(cè)
        //      否則,應(yīng)該在根節(jié)點的右側(cè)
        while (current != null){
            pre = current;
            // 如果根節(jié)點的值比value大,
            // 則新節(jié)點應(yīng)該插入在根節(jié)點的左側(cè)
            if (current.value > value){
                current = current.left;
            //否則,應(yīng)該插入在右側(cè)
            }else {
                current = current.right;
            }
        }
        current = new Node(value, pre, null, null);
        // 如果pre是null,說明此時是空樹
        if (pre == null){
            root = current;
        }else {
            // 如果current的值比pre大,則current是pre的右節(jié)點
            if (current.value > pre.value){
                pre.right = current;
            // 否則,current是pre的左節(jié)點
            }else {
                pre.left = current;
            }
        }
    }

    /**
     * 中序遍歷樹,可以用于校驗樹是否成功創(chuàng)建
     * @param current 當前節(jié)點
     */
    public void show(Node current){
        if (current != null){
            show(current.left);
            System.out.print(current.value + " ");
            show(current.right);
        }
    }

    /**
     * 樹的節(jié)點
     */
    private class Node{
        int value;
        Node parent;
        Node left;
        Node right;

        public Node(int value, Node parent, Node left, Node right) {
            this.value = value;
            this.parent = parent;
            this.left = left;
            this.right = right;
        }
    }
}

經(jīng)過上面的步驟,我們就成功的創(chuàng)建了一個二叉排序樹了,可以通過簡單的測試來判斷樹的建立是否正確,其中一個比較簡單的方法就是通過中序遍歷該樹,由于二叉排序樹的特點,中序遍歷的結(jié)果應(yīng)當是一系列從小到大排序好的值

獲取樹中的最小值以及最大值

由于二叉排序樹的特點,其最小值必定在最左子樹,最大值必然在最右子樹,當然,如果是空樹那就沒有了,如果是只有跟節(jié)點的樹,那么最小值以及最大值都是根節(jié)點本身


    // 獲取樹中的最小值
    public Integer getMin(){
        Node current = root;
        if (current == null){
            return null;
        }
        // 尋找最左子樹
        while (current.left != null){
            current = current.left;
        }
        return current.value;
    }

    // 獲取樹中的最大值
    public Integer getMax(){
        Node current = root;
        if (current == null){
            return null;
        }
        // 尋找最右子樹
        while (current.right != null){
            current = current.right;
        }
        return current.value;
    }

查找包含某一值的節(jié)點


    // 查找包含某一值的節(jié)點
    public Node search(int value){
        Node current = root;
        while (current != null){
            // 如果該值比當前節(jié)點的值小,則
            // 找當前節(jié)點的左子樹
            if (current.value > value){
                current = current.left;
            // 如果該值比當前節(jié)點的值大,則
            // 找當前節(jié)點的右子樹
            }else if (current.value < value){
                current = current.right;
            }else {
                return current;
            }
        }
        // 找不到則返回null
        return null;
    }

查找某一個節(jié)點的前驅(qū)和后繼

根據(jù)二叉排序樹的特點,某一個節(jié)點的前驅(qū)只可能是

  • 如果該節(jié)點有左子樹,則該節(jié)點的前驅(qū)為其左子樹的最右子樹
  • 如果沒有左子樹,則該節(jié)點的前驅(qū)為,沿著該節(jié)點的路徑往上走,第一個該節(jié)點不是其祖先的左節(jié)點則為其前驅(qū)(此處畫個圖比較好理解)
尋找前驅(qū)示例圖

根據(jù)二叉排序樹的特點,如果target.parent.left == target,則target.parent的值比target的值大,所以應(yīng)該一直往上尋找,如果紅色箭頭所示,當找到第一個target.parent.right == target,如果黑色方框所示,這意味著target.parent的值是剛剛所經(jīng)過的路徑的最小值,而target就是倒數(shù)第二小的值,(還記得,target.parent的右子樹的最左子樹嗎?)

后繼節(jié)點的查找類似,只不過方向應(yīng)該相反,這里就不重復敘述了


    // 前驅(qū)
    public Node getPre(Node target){
        if (target == null){
            return null;
        }
        // 如果左子樹非空,則前驅(qū)為左子樹的最右子樹
        if (target.left != null){
            target = target.left;
            // 尋找最右子樹
            while (target.right != null){
                target = target.right;
            }
            return target;
        }else {
            // 否則,查找該節(jié)點是某個節(jié)點的右子樹的最左子樹的節(jié)點
            // 也就是沿著父親路徑往上走,第一個該節(jié)點不是其父親節(jié)點的左節(jié)點的節(jié)點
            Node parent = target.parent;
            while (parent != null && target == parent.left){
                target = parent;
                parent = parent.parent;
            }
            return parent;
        }
    }

    // 后繼
    public Node getSuc(Node target){

        if (target == null){
            return null;
        }
        // 查找右子樹的最左子樹
        if (target.right != null){
            target = target.right;
            while (target.left != null){
                target = target.left;
            }
            return target;
        }else {
            // 沿著父親路徑向上走,第一個該節(jié)點不是父親節(jié)點的右子樹的節(jié)點
            Node parent = target.parent;
            while (parent != null && target == parent.right){
                target = parent;
                parent = parent.parent;
            }
            return parent;
        }
    }

刪除節(jié)點

刪除節(jié)點是二叉排序樹最復雜的一個地方,主要是由于刪除的時候,存在多種情況

  • 被刪除的節(jié)點沒有左右子樹
  • 被刪除的節(jié)點只有左子樹
  • 被刪除的節(jié)點只有右子樹
  • 被刪除的節(jié)點有左右子樹

前三種情況比較好處理,直接令其父親指向其孩子即可,最后一種比較復雜,直接看代碼結(jié)合注釋比較好理解


    // 刪除節(jié)點
    public void remove(Node target){
        if (target == null){
            return;
        }

        // 只有右子樹
        if (target.left == null){
            // 如果target是其父親的左子樹
            if (target.parent.left == target){
                // 將target的右孩子連接到父親的左孩子,
                // 也就是target的右孩子替代父親
                target.parent.left = target.right;
            }else {
                // 如果target是右孩子,則連接到parent的右孩子
                target.parent.right = target.right;
            }
            // 如果右孩子非空,右孩子的parent指向target.parent
            if (target.right != null){
                target.right.parent = target.parent;
            }
        // 如果target的右子樹為空,而且此時左子樹不為空
        // 操作基本同上
        }else if (target.right == null){
            if (target.parent.left == target){
                target.parent.left = target.left;
            }else {
                target.parent.right = target.left;
            }
            target.left.parent = target.parent;
        // 如果左右子樹都非空,則用右子樹的最左子樹進行替代
        }else {
            // 如果target的右子樹沒有左子樹,直接拿右子樹進行替代
            if (target.right.left == null){
                if (target.parent.left == target){
                    target.parent.left = target.right;
                }else {
                    target.parent.right = target.right;
                }
                // 指向target的parent
                target.right.parent = target.parent;
                // 接管target的左子樹
                target.right.left = target.left;
            }else {
                // 尋找target的右子樹的最左子樹
                Node current = target;
                target = target.right;
                while (target.left != null){
                    target = target.left;
                }
                // 直接替換其值即可
                current.value = target.value;
                // 此時target為右子樹的最左子樹,但是target可能有右子樹
                // 所以刪除只有,target.parent.left需要接管target的右子樹
                target.parent.left = target.right;
            }

        }
    }

總結(jié)

本小節(jié)主要學習了二叉排序樹的基本原理,并且通過代碼的方式,學習了二叉排序樹的創(chuàng)建,插入,查找最大值,查找最小值,查找指定值的節(jié)點,查找指定節(jié)點的前驅(qū),后繼,刪除節(jié)點等,其中刪除節(jié)點可以說最復雜,也是最難理解的一個,在學習的過程中最好結(jié)合具體的圖片,然后手動演示整個過程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 樹的概述 樹是一種非常常用的數(shù)據(jù)結(jié)構(gòu),樹與前面介紹的線性表,棧,隊列等線性結(jié)構(gòu)不同,樹是一種非線性結(jié)構(gòu) 1.樹的定...
    Jack921閱讀 4,754評論 1 31
  • 基于樹實現(xiàn)的數(shù)據(jù)結(jié)構(gòu),具有兩個核心特征: 邏輯結(jié)構(gòu):數(shù)據(jù)元素之間具有層次關(guān)系; 數(shù)據(jù)運算:操作方法具有Log級的平...
    yhthu閱讀 4,465評論 1 5
  • 四、樹與二叉樹 1. 二叉樹的順序存儲結(jié)構(gòu) 二叉樹的順序存儲就是用數(shù)組存儲二叉樹。二叉樹的每個結(jié)點在順序存儲中都有...
    MinoyJet閱讀 1,729評論 0 7
  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,371評論 0 12
  • 1 概述 二叉搜索樹,顧名思義,其主要目的用于搜索,它是二叉樹結(jié)構(gòu)中最基本的一種數(shù)據(jù)結(jié)構(gòu),是后續(xù)理解B樹、B+樹、...
    CodingTech閱讀 3,194評論 5 35

友情鏈接更多精彩內(nèi)容