數(shù)據(jù)結(jié)構(gòu)-二叉搜索樹的實(shí)現(xiàn)

定義

二叉搜索樹(Binary Search Tree,BST),也稱為二叉排序樹或二叉查找樹。

相較于普通的二叉樹勇劣,非空的二叉搜索樹有如下性質(zhì):

  1. 非空左子樹的所有鍵值小于其根結(jié)點(diǎn)的鍵值随夸;
  2. 非空右子樹的所有鍵值大于其根結(jié)點(diǎn)的鍵值默刚;
  3. 左右子樹均為二叉搜索樹
  4. 樹中沒有鍵值相等的結(jié)點(diǎn)逃魄。

可以看到,二叉搜索樹的性質(zhì)很鮮明澜搅,這也使得二叉樹也有了實(shí)際意義伍俘。

二叉搜索樹的常用操作

對(duì)于二叉搜索樹,除了常規(guī)的4種遍歷之外勉躺,還有如下一些關(guān)鍵的操作值得我們?nèi)リP(guān)注癌瘾。

二叉搜索樹ADT

二叉樹的存儲(chǔ)結(jié)構(gòu)實(shí)現(xiàn)

對(duì)于二叉樹,我們還是習(xí)慣的選擇采用鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)實(shí)現(xiàn)饵溅。

二叉樹結(jié)點(diǎn)定義

二叉搜索樹最大的特點(diǎn)妨退,就是他的元素是可以比較大小的。這一點(diǎn)是需要注意的地方。

/**
 * Created by engineer on 2017/10/26.
 * <p>
 * 二叉搜索樹樹結(jié)點(diǎn)定義
 */

public class TreeNode<T extends Comparable<T>> {

    // 數(shù)據(jù)域
    private T data;
    // 左子樹
    public TreeNode<T> leftChild;
    // 右子樹
    public TreeNode<T> rightChild;


    public TreeNode(T data) {
        this(null, data, null);
    }

    public TreeNode(TreeNode leftChild, T data, TreeNode rightChild) {
        this.leftChild = leftChild;
        this.data = data;
        this.rightChild = rightChild;
    }

    public T getData() {
        return data;
    }

    public TreeNode<T> getLeftChild() {
        return leftChild;
    }

    public TreeNode<T> getRightChild() {
        return rightChild;
    }

    public void setData(T data) {
        this.data = data;
    }

}

二叉搜索樹插入

有了根節(jié)點(diǎn)咬荷,我們就可以根據(jù)二叉樹的性質(zhì)冠句,從根節(jié)點(diǎn)出發(fā),構(gòu)建出一顆二叉樹幸乒。

/**
     * 樹中插入元素
     *
     * @param value
     */
    void insert(T value) {
        if (value == null) {
            return;
        }
        root = insert(root, value);
    }

    private TreeNode<T> insert(TreeNode<T> node, T value) {
        if (node == null) {
            // 樹為空,則創(chuàng)建根節(jié)點(diǎn)
            return new TreeNode<>(value);
        } else {
            if (compare(node, value) < 0) { // 插入值比根節(jié)點(diǎn)小懦底,在左子樹繼續(xù)創(chuàng)建二叉搜索樹
                node.leftChild = insert(node.getLeftChild(), value);
            } else if (compare(node, value) > 0) { // 插入值比根節(jié)點(diǎn)大,在右子樹繼續(xù)創(chuàng)建二叉搜索樹
                node.rightChild = insert(node.getRightChild(), value);
            }
        }

        return node;
    }
    private int compare(TreeNode<T> node, T value) {
        return value.compareTo(node.getData());
    }

根據(jù)二叉搜索樹的特性罕扎,我們很容易使用遞歸實(shí)現(xiàn)二叉樹的插入操作聚唐;總的來說,就是每次插入一個(gè)結(jié)點(diǎn)腔召,從根節(jié)點(diǎn)出發(fā)作比較杆查,小的就往左子樹插,大的就往右子樹插臀蛛。這和二叉搜索樹的定義時(shí)完全一致的亲桦。

我們可以簡(jiǎn)單測(cè)試一下,這個(gè)insert方法的正確性掺栅。

測(cè)試二叉搜索樹插入操作

public class BinarySearchTreeTest {

    private static Integer[] arrays = new Integer[]{10, 8, 3, 12, 9, 4, 5, 7, 1,11, 17};

    public static void main(String[] args) {
        BinarySearchTree<Integer> mSearchTree = new BinarySearchTree<>();

        for (Integer data : arrays) {
            mSearchTree.insert(data);
        }
        // 打印二叉樹的三種遍歷順序
        mSearchTree.printTree();

    }
}

關(guān)于樹的遍歷已在上文中詳細(xì)分析烙肺,此處不再做深入探討

這里定義了一個(gè)隨機(jī)數(shù)組,這個(gè)將這個(gè)數(shù)組的按序插入到樹中氧卧,并按照樹的三種遍歷結(jié)構(gòu)打印樹桃笙。按照這個(gè)數(shù)組我們將構(gòu)建出如下所示的一顆二叉搜索樹:

手繪二叉樹

看一下程序輸出的遍歷結(jié)果。

前序遍歷:10 8 3 1 4 5 7 9 12 11 17 
中序遍歷:1 3 4 5 7 8 9 10 11 12 17 
后序遍歷:1 7 5 4 3 9 8 11 17 12 10 

可以看到沙绝,遍歷結(jié)果和我們畫出來二叉樹是一致的搏明,因此可以驗(yàn)證插入方法是正確的。

查找

通過插入操作闪檬,我們已經(jīng)實(shí)現(xiàn)了一顆二叉搜索樹星著,下面就來看看如何從樹中查找元素。

  • 查找最大值與最小值

根據(jù)二叉搜索樹的特點(diǎn)粗悯,我們知道在一顆二叉搜索樹上虚循,最小的值一定在最最左邊的結(jié)點(diǎn)上,而最大值一定在最最右邊的結(jié)點(diǎn)上样傍。因此横缔,查找二叉樹最值就變得非常容易了。


/**
     * 查找最大值
     *
     * @return
     */
    public T findMax() {
        if (isEmpty()) return null;
        return findMax(root);
    }

    /**
     * 從特定結(jié)點(diǎn)開始尋找最大值
     *
     * @param node
     * @return
     */
    private T findMax(TreeNode<T> node) {
        TreeNode<T> temp = node;
        while (temp.getRightChild() != null) {
            temp = temp.getRightChild();
        }
        return temp.getData();
    }


    /**
     * 查找最小值
     *
     * @return
     */
    public T findMin() {
        if (isEmpty()) return null;
        return findMin(root);
    }

    /**
     * 從特定結(jié)點(diǎn)開始尋找最小值
     *
     * @param node
     * @return
     */
    private T findMin(TreeNode<T> node) {
        TreeNode<T> temp = node;
        while (temp.getLeftChild() != null) {
            temp = temp.getLeftChild();
        }
        return temp.getData();
    }

可以看到衫哥,算法實(shí)現(xiàn)非常簡(jiǎn)單茎刚,就是不斷后移結(jié)點(diǎn)找到?jīng)]有子樹的結(jié)點(diǎn),就是最邊界位置的結(jié)點(diǎn)了撤逢。

  • 查找特定值

在二叉搜索樹中膛锭,怎樣快速找到一個(gè)值為特定元素的結(jié)點(diǎn)呢粮坞?想想我們是怎樣實(shí)現(xiàn)結(jié)點(diǎn)插入的?這個(gè)問題就很簡(jiǎn)單了初狰。

遞歸實(shí)現(xiàn)莫杈,查找特定結(jié)點(diǎn)

**
/**
     * find 特定值 遞歸實(shí)現(xiàn)
     *
     * @param value
     * @return
     */
    public TreeNode<T> find(T value) {
        if (isEmpty()) {
            return null;
        } else {
            return find(root, value);
        }
    }

    private TreeNode<T> find(TreeNode<T> node, T value) {
        if (node == null) {
            // 當(dāng)查找一個(gè)不在樹中元素時(shí),拋出異常
            throw  new RuntimeException("the value must not in the tree");
        }

        if (compare(node, value) < 0) {
            // 小于根節(jié)點(diǎn)時(shí)跷究,從去左子樹找
            return find(node.getLeftChild(), value);
        } else if (compare(node, value) > 0) {
            // 大于根節(jié)點(diǎn)時(shí)姓迅,從右子樹找
            return find(node.getRightChild(), value);
        } else {
            // 剛好等于,找到了
            return node;
            
            // 剩下還有一種情況俊马,就是不等于丁存,也就是所查找的元素不在樹中
        }
    }

查找的實(shí)現(xiàn)思路,總體上和插入是一致的柴我;無非就是做不同的操作解寝;這里需要注意的是,為了程序的健壯性艘儒,我們還得考慮如果查找的元素不在樹中這種情況聋伦。

迭代實(shí)現(xiàn),查找特定值

有了前面查找最大值界睁、最小值的經(jīng)驗(yàn)觉增,我們也可以考慮使用迭代算法實(shí)現(xiàn)查找指定元素的算法。


/**
     * 查找特定值-非遞歸實(shí)現(xiàn)
     *
     * @param value
     * @return 結(jié)點(diǎn)
     */
    public TreeNode<T> findIter(T value) {
        TreeNode<T> current = root;
        while (current != null) {
            if (compare(current, value) < 0) {
                current = current.getLeftChild();
            } else if (compare(current, value) > 0) {
                current = current.getRightChild();
            } else {
                return current;
            }
        }
        // current為null,說明所查找的元素不在tree里
        return null;
    }

這里同樣測(cè)試一下翻斟,查找方法的正確性:

        System.out.printf("\nfind value %d in mSearchTree \n", 12);
        TreeNode mTreeNode = mSearchTree.find(12);
        TreeNode mTreeNode_1 = mSearchTree.findIter(12);
        System.out.println("遞歸實(shí)現(xiàn)結(jié)點(diǎn)  = :" + mTreeNode + ", value=" + mTreeNode.getData());
        System.out.println("非遞歸實(shí)現(xiàn)結(jié)點(diǎn)= :" + mTreeNode_1 + ", value=" + mTreeNode_1.getData());

        System.out.println("\nfind the max value in mSearchTree = " + mSearchTree.findMax());
        System.out.println("find the min value in mSearchTree = " + mSearchTree.findMin());

輸出:

find value 12 in mSearchTree 
遞歸實(shí)現(xiàn)結(jié)點(diǎn)  = :com.avaj.datastruct.tree.bst.TreeNode@4b67cf4d, value=12
非遞歸實(shí)現(xiàn)結(jié)點(diǎn)= :com.avaj.datastruct.tree.bst.TreeNode@4b67cf4d, value=12

find the max value in mSearchTree = 17
find the min value in mSearchTree = 1

我們分別用遞歸和迭代兩種方式去查找 12逾礁,可以看到兩次找到是同一個(gè)對(duì)象,這個(gè)對(duì)象的值為12访惜;找到的最大值和最小值也是正確的嘹履;因此查找功能的實(shí)現(xiàn)是正確的。

刪除結(jié)點(diǎn)

從二叉搜索樹中债热,刪除一個(gè)結(jié)點(diǎn)可以算是最復(fù)雜的操作了砾嫉,主要是因?yàn)樗獎(jiǎng)h除的結(jié)點(diǎn),所處的位置被刪除后窒篱,依然需要保持整棵樹依然為二叉樹焕刮,因此需要就不同的情況就像分析。

手繪二叉樹

就拿我們上面創(chuàng)建的這顆二叉樹來說墙杯,如果要?jiǎng)h除的結(jié)點(diǎn)是1,7,11,17 這樣的葉子結(jié)點(diǎn)济锄,就很容易了;讓其父結(jié)點(diǎn)指向?yàn)閚ull即可霍转;而如果是4,5 這樣包含一顆子樹的結(jié)點(diǎn),換個(gè)角度來說一汽,這其實(shí)就是單向鏈表避消,從單向鏈表中間位置刪除一個(gè)結(jié)點(diǎn)也比較容易低滩;最麻煩的就是如果要?jiǎng)h除的結(jié)點(diǎn)是10,8,3,12 這類結(jié)點(diǎn)包含左右子樹,我們就需要從左子樹中找一個(gè)最大值岩喷,或者是右子樹中的最小值來替代這個(gè)值恕沫。總結(jié)一下刪除結(jié)點(diǎn)的操作:

  • 葉子結(jié)點(diǎn):直接刪除纱意,其父結(jié)點(diǎn)指向null
  • 包含一個(gè)孩子的結(jié)點(diǎn) :父結(jié)點(diǎn)指向要?jiǎng)h除結(jié)點(diǎn)的自結(jié)點(diǎn)(相當(dāng)于鏈表中間刪除一個(gè)元素)婶溯;
  • 包含左右子樹的結(jié)點(diǎn):右子樹最小值或左子樹最大值替換此結(jié)點(diǎn)

結(jié)合以上分析,得出從二叉搜索樹中刪除結(jié)點(diǎn)的實(shí)現(xiàn)偷霉。

/**
     * 從樹中刪除值為value 的特定結(jié)點(diǎn)
     *
     * @param value
     */
    public void delete(T value) {
        if (value == null || isEmpty()) {
            return;
        }

        root = delete(root, value);
    }


    private TreeNode<T> delete(TreeNode<T> node, T value) {

        // 結(jié)點(diǎn)為空迄委,要出刪除的元素不在樹中
        if (node == null) {
            return node;
        }

        if (compare(node, value) < 0) { // 去左子樹刪除
            node.leftChild = delete(node.getLeftChild(), value);
        } else if (compare(node, value) > 0) { // 去右子樹刪除
            node.rightChild = delete(node.getRightChild(), value);
        } else { // 要?jiǎng)h除的就是當(dāng)前結(jié)點(diǎn)
            if (node.getLeftChild() != null && node.getRightChild() != null) {// 被刪除的結(jié)點(diǎn),包含左右子樹
                T temp = findMin(node.getRightChild()); // 得到右子樹的最小值
                node.setData(temp); //右子樹最小值替換當(dāng)前結(jié)點(diǎn)
                node.rightChild = delete(node.getRightChild(), temp); // 從右子樹刪除這個(gè)最小值的結(jié)點(diǎn)
            } else {// 被刪除的結(jié)點(diǎn)类少,包含一個(gè)子樹或沒有子樹
                if (node.getLeftChild() != null) {
                    node = node.getLeftChild();
                } else {
                    node = node.getRightChild();
                }
            }
        }

        return node;
    }

這里選擇使用右子樹的最小值替換叙身,是因?yàn)閯h除這個(gè)最小值的結(jié)點(diǎn)會(huì)比較容易,因?yàn)樗欢ㄊ遣粫?huì)是一個(gè)包含左右子樹的結(jié)點(diǎn)硫狞。

同樣信轿,這里測(cè)試一下刪除結(jié)點(diǎn)的功能:

        // 刪除只帶一個(gè)子樹的結(jié)點(diǎn)
        mSearchTree.delete(4);
        mSearchTree.printTree();
        System.out.println();
        // 刪除帶左右子樹的根節(jié)點(diǎn)
        mSearchTree.delete(10);
        mSearchTree.printTree();

輸出:

前序遍歷:10 8 3 1 5 7 9 12 11 17 
中序遍歷:1 3 5 7 8 9 10 11 12 17 
后序遍歷:1 7 5 3 9 8 11 17 12 10 

前序遍歷:11 8 3 1 5 7 9 12 17 
中序遍歷:1 3 5 7 8 9 11 12 17 
后序遍歷:1 7 5 3 9 8 17 12 11 

通過和我們一開始畫出來的樹相比較,發(fā)現(xiàn)是對(duì)應(yīng)的残吩。

二叉搜索樹的高度

最后财忽,再來看看如何計(jì)算一顆二叉搜素樹的度。

public int getTreeHeight() {
        if (isEmpty()) {
            return 0;
        }

        return getTreeHeight(root);

    }

    private int getTreeHeight(TreeNode<T> node) {
        if (node == null) {
            return 0;
        }
        int leftHeight = getTreeHeight(node.getLeftChild());
        int rightHeight = getTreeHeight(node.getRightChild());
        int max = leftHeight > rightHeight ? leftHeight : rightHeight;
        // 得到左右子樹中較大的返回.
        return max + 1;
    }

順便來算一算泣侮,到最后我們創(chuàng)建的樹即彪,經(jīng)過插入刪除操作高度變成了多少。

System.out.println("\n\nTree's height =" + mSearchTree.getTreeHeight());

輸出:

Tree's height =5

可以看到旁瘫,由于結(jié)點(diǎn)4被刪除祖凫,樹由原來的6層變成了5層,結(jié)果是正確的酬凳!


好了惠况,二叉搜索樹的分析就是這些了!文中所有源碼地址.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宁仔,一起剝皮案震驚了整個(gè)濱河市稠屠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌翎苫,老刑警劉巖权埠,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異煎谍,居然都是意外死亡攘蔽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門呐粘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來满俗,“玉大人转捕,你說我怎么就攤上這事∷衾” “怎么了五芝?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辕万。 經(jīng)常有香客問我枢步,道長(zhǎng),這世上最難降的妖魔是什么渐尿? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任醉途,我火速辦了婚禮,結(jié)果婚禮上涡戳,老公的妹妹穿的比我還像新娘结蟋。我一直安慰自己,他們只是感情好渔彰,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布嵌屎。 她就那樣靜靜地躺著,像睡著了一般恍涂。 火紅的嫁衣襯著肌膚如雪宝惰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天再沧,我揣著相機(jī)與錄音尼夺,去河邊找鬼。 笑死炒瘸,一個(gè)胖子當(dāng)著我的面吹牛淤堵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播顷扩,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼拐邪,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了隘截?” 一聲冷哼從身側(cè)響起扎阶,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎婶芭,沒想到半個(gè)月后东臀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡犀农,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年惰赋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呵哨。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡赁濒,死狀恐怖贵扰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情流部,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布纹坐,位于F島的核電站枝冀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏耘子。R本人自食惡果不足惜果漾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谷誓。 院中可真熱鬧绒障,春花似錦、人聲如沸捍歪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)糙臼。三九已至庐镐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間变逃,已是汗流浹背必逆。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留揽乱,地道東北人名眉。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像凰棉,于是被迫代替她去往敵國(guó)和親损拢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容