手?jǐn)]二叉樹——二叉查找樹

二叉樹是數(shù)據(jù)結(jié)構(gòu)中非常重要的一種數(shù)據(jù)結(jié)構(gòu),它是的一種,但是每個(gè)節(jié)點(diǎn)的子節(jié)點(diǎn)不能多余兩個(gè),可以是0,1,2個(gè)子節(jié)點(diǎn),0個(gè)子節(jié)點(diǎn)代表沒(méi)有子節(jié)點(diǎn)。常見的二叉樹結(jié)構(gòu)如下圖所示:

image-20241012100212420.png

每個(gè)節(jié)點(diǎn)的子節(jié)點(diǎn)不多于2個(gè),其中3,4,5沒(méi)有子節(jié)點(diǎn),2有一個(gè)子節(jié)點(diǎn),0,1都有兩個(gè)子節(jié)點(diǎn)。

基礎(chǔ)概念

根節(jié)點(diǎn):樹的其實(shí)節(jié)點(diǎn),沒(méi)有父節(jié)點(diǎn)。

葉子節(jié)點(diǎn):沒(méi)有子節(jié)點(diǎn)的節(jié)點(diǎn)叫做葉子節(jié)點(diǎn)。

節(jié)點(diǎn)深度:從根節(jié)點(diǎn)到該節(jié)點(diǎn)的距離叫做深度,如上圖:節(jié)點(diǎn)3的深度是2,節(jié)點(diǎn)1的深度是1。

節(jié)點(diǎn)高度:該節(jié)點(diǎn)到距離最長(zhǎng)的葉子節(jié)點(diǎn)的距離。

二叉查找樹

二叉樹最重要的一個(gè)應(yīng)用是在查詢方面的應(yīng)用,很多的索引結(jié)構(gòu)都是二叉查找樹,還有向HashMap里也使用到了紅黑樹,紅黑樹也是二叉查找樹的一種。二叉查找樹的一個(gè)重要性質(zhì),就是任何一個(gè)節(jié)點(diǎn),它的左子樹中的節(jié)點(diǎn)都小于該節(jié)點(diǎn),它的右子樹中的節(jié)點(diǎn)都大于該節(jié)點(diǎn)。最開始我們的例圖它不是一棵二叉查找樹,它不符合我們剛才說(shuō)的性質(zhì)。我們?cè)倏纯聪旅娴睦龍D:

image-20241012103245333.png

這是一棵二叉查找樹,它的任何一個(gè)節(jié)點(diǎn)的子節(jié)點(diǎn)都小于該節(jié)點(diǎn),右子樹的節(jié)點(diǎn)都大于該節(jié)點(diǎn)。這樣我們?cè)诓檎覕?shù)據(jù)的時(shí)候,就可以從根節(jié)點(diǎn)開始查找,如果查找的值小于該節(jié)點(diǎn),就去左子樹中查找,如果大于該節(jié)點(diǎn),就去右子樹中查找,如果等于,那就不用說(shuō)了,直接返回就可以了。這種可以大大提升我們的查找效率,它的時(shí)間復(fù)雜度是O(logN)。

手?jǐn)]二叉查找樹

首先我們要抽象出節(jié)點(diǎn)類,每個(gè)節(jié)點(diǎn)可以有左子節(jié)點(diǎn),和右子節(jié)點(diǎn),當(dāng)然節(jié)點(diǎn)要存儲(chǔ)一個(gè)值,這個(gè)值的類型我們不做限制,可以是數(shù)字型,也可以是字符串,還可以是自己定義的類,但是這里要加一個(gè)前提條件,就是這個(gè)值是可比較的,因?yàn)閮蓚€(gè)節(jié)點(diǎn)比較后才能確定位置,所以節(jié)點(diǎn)值的類型要實(shí)現(xiàn)Comparable接口。好了,滿足上面的條件,我們就可以抽象出二叉樹節(jié)點(diǎn)的類了,如下:

public class BinaryNode<T extends Comparable<T>> {
    //節(jié)點(diǎn)數(shù)據(jù)
    @Setter@Getter
    private T element;
    //左子節(jié)點(diǎn)
    @Setter@Getter
    private BinaryNode<T> left;
    //右子節(jié)點(diǎn)
    @Setter@Getter
    private BinaryNode<T> right;

    //構(gòu)造函數(shù)
    public BinaryNode(T element) {
        this(element,null,null);
    }
    //構(gòu)造函數(shù)
    public BinaryNode(T element, BinaryNode<T> left, BinaryNode<T> right) {
        if (element == null) {
            throw new RuntimeException("二叉樹節(jié)點(diǎn)元素不能為空");
        }
        this.element = element;
        this.left = left;
        this.right = right;
    }
}

我們定義二叉樹節(jié)點(diǎn)的類為BinaryNode,我們注意一下后面的泛型,它要實(shí)現(xiàn)Comparable接口。然后我們定義節(jié)點(diǎn)數(shù)據(jù)element,左子節(jié)點(diǎn)left,和右子節(jié)點(diǎn)right,并且使用@Setter@Getter注解實(shí)現(xiàn)其set和get方法。接下來(lái)就是定義兩個(gè)構(gòu)造方法,一個(gè)是只傳入節(jié)點(diǎn)元素的,另一個(gè)是傳入節(jié)點(diǎn)元素和左右子樹的。節(jié)點(diǎn)的元素是不能為空的,如果是空則拋出異常。

然后,我們?cè)俣x二叉查找樹類,類中包括一些二叉查找樹的基本操作方法,這些基本的操作方法我們后面講,先看定義的基本元素,如下:

public class BinarySearchTree<T extends Comparable<T>> {
    //根節(jié)點(diǎn)
    private BinaryNode<T> root;

    public BinarySearchTree() {
        this.root = null;
    }
    
    //將樹變?yōu)榭諛?    public void makeEmpty() {
        this.root = null;
    }

    //判斷樹是否為空
    public boolean isEmpty() {
        return this.root == null;
    }
}

類的名字定義為:BinarySearchTree,同樣我們注意一下這里的泛型,它和BinaryNode的泛型是一樣的,因?yàn)檫@個(gè)類型我們傳遞給BinaryNode。類中定義了樹的根節(jié)點(diǎn)root,以及構(gòu)造方法,構(gòu)造方法只是定義了一棵空樹,根節(jié)點(diǎn)為空。然后是兩個(gè)比較基礎(chǔ)的樹的操作方法makeEmptyisEmpty,將樹變?yōu)榭諛浜团袛鄻涫欠駷榭铡?/p>

  1. 現(xiàn)在我們要編寫一些樹的操作方法了,首先我們要編寫的就是contains方法,它會(huì)判斷樹中是否包含某個(gè)元素,比如上面例圖中,我們判斷樹中是否包含3這個(gè)元素。具體實(shí)現(xiàn)如下:
/**
 * 二叉樹是否包含某個(gè)元素
 *
 * @param element 檢查的元素
 * @return true or false
 */
public boolean contains(T element) {
    return contains(root, element);
}

/**
 * 二叉樹是否包含某個(gè)元素
 *
 * @param tree    整棵樹或左右子樹
 * @param element 檢查的元素
 * @return true or false
 */
private boolean contains(BinaryNode<T> tree, T element) {
    if (tree == null) {
        return false;
    }

    int compareResult = element.compareTo(tree.getElement());

    if (compareResult > 0) {
        return contains(tree.getRight(), element);
    }
    if (compareResult < 0) {
        return contains(tree.getLeft(), element);
    }
    return true;
}

這里我們定義了兩個(gè)contains方法,第一個(gè)contains方法調(diào)用第二個(gè)contains方法,第二個(gè)contains方法是私有的,外部不能訪問(wèn)。在調(diào)用第二個(gè)contains方法時(shí),我們將root傳進(jìn)去,也就是整棵樹傳入去查找。在第二個(gè)contains方法中,我們先判斷樹是否為空,如果為空,肯定不會(huì)包含我們要查找的元素,則直接返回false。然后我們用查找的元素和當(dāng)前節(jié)點(diǎn)的元素作比較,這里我們使用compareTo方法,它是Comparable接口中定義好的方法,這也是我們定義泛型時(shí)要實(shí)現(xiàn)Comparable接口的原因了。比較結(jié)果大于0,說(shuō)明查找的值大于當(dāng)前節(jié)點(diǎn)值,我們遞歸調(diào)用contains方法,將右子樹和查找的值傳入;比較結(jié)果小于0,說(shuō)明查找的值小于當(dāng)前節(jié)點(diǎn)值,我們同樣遞歸調(diào)用contains方法,將左子樹和查找的值傳入進(jìn)行查找。最后如果比較結(jié)果等于0,說(shuō)明查找的值和當(dāng)前節(jié)點(diǎn)值是一樣的,我們返回true就可以了。

contains方法算是一個(gè)開胃小菜,其中用到了遞歸,這也讓我們對(duì)二叉樹的編寫方法有了一個(gè)初步的了解。

  1. 接下來(lái)我們要編寫的是findMinfindMax方法,分別是找出樹中最小值和最大值的方法。由于我們的樹是一棵二叉查找樹,左子樹的值要小于當(dāng)前節(jié)點(diǎn),右子樹的值大于當(dāng)前節(jié)點(diǎn),所以,最左側(cè)節(jié)點(diǎn)的值就是最小值,最右側(cè)的值則是最大值。我們用代碼實(shí)現(xiàn)一下,
/**
 * 找出二叉樹的最小元素
 *
 * @return
 */
public T findMin() {
    if (isEmpty()) throw new RuntimeException("二叉樹為空");
    return findMin(root);
}

private T findMin(BinaryNode<T> tree) {
    if (tree.getLeft() != null) {
        return findMin(tree.getLeft());
    }
    return tree.getElement();
}

/**
 * 找出二叉樹的最大元素
 *
 * @return
 */
public T findMax() {
    if (isEmpty()) throw new RuntimeException("二叉樹為空");
    return findMax(root);
}

private T findMax(BinaryNode<T> tree) {
    while (tree.getRight() != null) {
        tree = tree.getRight();
    }
    return tree.getElement();
}

我們先來(lái)看findMin方法,先判斷樹是否為空,空樹沒(méi)有最小值,也沒(méi)有最大值,所以我們這里拋出異常。然后我們將整棵樹傳入第二個(gè)findMin方法,在第二個(gè)findMin方法中,我們一直去尋找左子節(jié)點(diǎn),如果左子節(jié)點(diǎn)不為空,我們就遞歸的再去尋找,直到節(jié)點(diǎn)的左子節(jié)點(diǎn)為空,那么當(dāng)前節(jié)點(diǎn)就是整棵樹的最左節(jié)點(diǎn),那么它的值就是最小的,我們返回就可以了。

我們?cè)賮?lái)看findMax方法,和findMin方法一樣,先判斷樹是否為空,為空則拋出異常。我們要重點(diǎn)看的是第二個(gè)findMax方法,這個(gè)方法中,我們沒(méi)有使用遞歸去尋找最右側(cè)的節(jié)點(diǎn),而是使用了一個(gè)while循環(huán),去找到最右側(cè)的節(jié)點(diǎn)。這里我們使用了兩種不同的方法實(shí)現(xiàn)了findMinfindMax,一個(gè)使用了遞歸,另一個(gè)使用了while循環(huán),其實(shí)這兩種方式也是互通的,能用遞歸的方法也可以用while循環(huán)去實(shí)現(xiàn),反之亦然。

  1. 接下來(lái)我們?cè)賮?lái)看一下二叉查找樹的一個(gè)非常重要的方法,那就是insert插入方法了。當(dāng)我們向二叉查找樹中添加一個(gè)節(jié)點(diǎn)時(shí),要和當(dāng)前節(jié)點(diǎn)做比較,如果小于當(dāng)前節(jié)點(diǎn)值,則在左側(cè)插入,如果大于則在右側(cè)插入,這里我們不討論等于的情況。具體代碼如下:
/**
 * 插入元素
 *
 * @param element
 */
public void insert(T element) {
    if (root == null) {
        root = new BinaryNode<>(element);
        return;
    }
    insert(root, element);
}

private void insert(BinaryNode<T> tree, T element) {
    int compareResult = element.compareTo(tree.getElement());
    if (compareResult > 0) {
        if (tree.getRight() == null) {
            tree.setRight(new BinaryNode<>(element));
        } else {
            insert(tree.getRight(), element);
        }
    }

    if (compareResult < 0) {
        if (tree.getLeft() == null) {
            tree.setLeft(new BinaryNode<>(element));
        } else {
            insert(tree.getLeft(), element);
        }
    }
}

在插入節(jié)點(diǎn)的過(guò)程中,我們先判斷根節(jié)點(diǎn)是否為空,如果為空,說(shuō)明是一棵空樹,我們直接將插入元素給到根節(jié)點(diǎn)就可以了。如果根節(jié)點(diǎn)不為空,我們進(jìn)入到第二個(gè)insert方法,在第二個(gè)insert方法中,我們先將插入的值和當(dāng)前節(jié)點(diǎn)做比較,比較結(jié)果如果大于0,說(shuō)明插入的值比當(dāng)前節(jié)點(diǎn)大,所以我們要在右側(cè)插入,如果當(dāng)前節(jié)點(diǎn)的右子節(jié)點(diǎn)為空,我們直接插入就可以了;如果右子節(jié)點(diǎn)不為空,還要和右子節(jié)點(diǎn)作比較,這里我們用遞歸的方法實(shí)現(xiàn),邏輯比較清晰。同理,如果比較結(jié)果小于0,我們對(duì)左側(cè)節(jié)點(diǎn)做操作就可以了,這里不再贅述。

  1. 上面我們做了節(jié)點(diǎn)的插入,最后再來(lái)看看節(jié)點(diǎn)的刪除remove。要?jiǎng)h除一個(gè)節(jié)點(diǎn),首先我們要找到這個(gè)節(jié)點(diǎn),找到這個(gè)節(jié)點(diǎn)后,要分情況對(duì)這個(gè)節(jié)點(diǎn)進(jìn)行處理,如下:
  • 刪除節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn):我們直接將該節(jié)點(diǎn)刪除,也就是將節(jié)點(diǎn)置為null;

  • 刪除節(jié)點(diǎn)只有左子節(jié)點(diǎn)或右子節(jié)點(diǎn):這種只有一個(gè)子節(jié)點(diǎn)的情況,我們直接將要?jiǎng)h除的節(jié)點(diǎn)改為它的唯一的子節(jié)點(diǎn)就可以了。這里等于是用子節(jié)點(diǎn)覆蓋掉當(dāng)前節(jié)點(diǎn);

  • 刪除節(jié)點(diǎn)有兩個(gè)子節(jié)點(diǎn):這種是最復(fù)雜的情況,要解決這個(gè)問(wèn)題,我們還是要利用二叉查找樹的特性,就是當(dāng)前節(jié)點(diǎn)的左子樹的值都比當(dāng)前節(jié)點(diǎn)小,右子樹的值都比當(dāng)前節(jié)點(diǎn)大。那么我們把當(dāng)前節(jié)點(diǎn)刪除后,用哪個(gè)節(jié)點(diǎn)代替當(dāng)前節(jié)點(diǎn)呢?這里我們可以在左子樹中找到最大的值,或者從右子樹中找到最小的值,代替當(dāng)前要?jiǎng)h除的節(jié)點(diǎn)。這樣替換后,還是可以保證左子樹的值比當(dāng)前值小,右子樹的值比當(dāng)前值大。然后我們?cè)侔烟鎿Q的值,也就是左子樹中的最大值,或者右子樹中的最小值,在左或右子樹中刪掉就可以了。這一段邏輯比較繞,小伙伴們可以多讀幾遍,理解一下。具體實(shí)現(xiàn)如下:

/**
 * 刪除元素
 * @param element
 */
public void remove(T element) {
    remove(root, element);
}

private void remove(BinaryNode<T> tree, T element) {
    if (tree == null) {
        return;
    }
    int compareResult = element.compareTo(tree.getElement());
    if (compareResult > 0) {
        remove(tree.getRight(), element);
        return;
    }
    if (compareResult < 0) {
        remove(tree.getLeft(), element);
        return;
    }
    if (tree.getLeft() != null && tree.getRight() != null) {
        tree.setElement(findMin(tree.getRight()));
        remove(tree.getRight(), tree.getElement());
    } else {
        tree = tree.getLeft() != null ? tree.getLeft() : tree.getRight();
    }
}

第一個(gè)remove方法不說(shuō)了,我們重點(diǎn)看第二個(gè)。方法進(jìn)來(lái)后,節(jié)點(diǎn)是否為空,為空則說(shuō)明是空樹,或者要?jiǎng)h除的節(jié)點(diǎn)沒(méi)有找到,那么直接返回就可以了。然后再用刪除的元素和當(dāng)前節(jié)點(diǎn)作比較,如果大于0,我們用遞歸方法在右子樹中繼續(xù)執(zhí)行刪除方法。同理如果小于0,用左子樹遞歸。再下面就是等于0的情況,也就是找到了要?jiǎng)h除的節(jié)點(diǎn)。我們先處理最復(fù)雜的情況,就是刪除節(jié)點(diǎn)左右子節(jié)點(diǎn)都存在的情況,我們使用上面的邏輯,使用右子樹中最小的節(jié)點(diǎn)覆蓋當(dāng)前節(jié)點(diǎn),然后再在右子樹中,將這個(gè)值刪掉,我們也是遞歸的調(diào)用了remove方法。當(dāng)然這里也可以使用左子樹中的最大值,小伙伴們自己實(shí)現(xiàn)吧。最后就是處理沒(méi)有子節(jié)點(diǎn)和只有一個(gè)子節(jié)點(diǎn)的情況,這兩種情況在代碼中可以合并,如果左子節(jié)點(diǎn)不為空,就用左子節(jié)點(diǎn)覆蓋掉當(dāng)前節(jié)點(diǎn),否則使用右子節(jié)點(diǎn)覆蓋。如果右子節(jié)點(diǎn)也為空,也就是沒(méi)有子節(jié)點(diǎn),那么當(dāng)前節(jié)點(diǎn)也就變?yōu)榭樟恕?/p>

問(wèn)題

到這里,二叉查找樹的基本的操作方法就編寫完了。這里引申一個(gè)問(wèn)題,如果我們順序的向一棵樹中插入1,2,3,4,5,這個(gè)樹會(huì)是什么形狀?這個(gè)也不難想象,如下:

image-20241013100457665.png

這和鏈表沒(méi)有什么區(qū)別了呀,查找的性能和鏈表一樣了,并沒(méi)有提升。這就引出了下一篇的內(nèi)容:平衡二叉樹,小伙伴們,敬請(qǐng)期待~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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