二叉樹是數(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)如下圖所示:
每個(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:
這是一棵二叉查找樹,它的任何一個(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ǔ)的樹的操作方法makeEmpty
和isEmpty
,將樹變?yōu)榭諛浜团袛鄻涫欠駷榭铡?/p>
- 現(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è)初步的了解。
- 接下來(lái)我們要編寫的是
findMin
和findMax
方法,分別是找出樹中最小值和最大值的方法。由于我們的樹是一棵二叉查找樹,左子樹的值要小于當(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)了findMin
和findMax
,一個(gè)使用了遞歸,另一個(gè)使用了while循環(huán),其實(shí)這兩種方式也是互通的,能用遞歸的方法也可以用while循環(huán)去實(shí)現(xiàn),反之亦然。
- 接下來(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)做操作就可以了,這里不再贅述。
- 上面我們做了節(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è)也不難想象,如下:
這和鏈表沒(méi)有什么區(qū)別了呀,查找的性能和鏈表一樣了,并沒(méi)有提升。這就引出了下一篇的內(nèi)容:平衡二叉樹,小伙伴們,敬請(qǐng)期待~~