看官,不要生氣,我沒有罵你也沒有鄙視你的意思,今天就是想單純的給大伙分享一下樹的相關知識,但是我還是想說作為一名程序員,自己心里有沒有點樹?你會沒點數嗎?言歸正傳,樹是我們常用的數據結構之一,樹的種類很多有二叉樹、二叉查找樹、平衡二叉樹、紅黑樹、B樹、B+樹等等,我們今天就來聊聊二叉樹相關的樹。
什么是樹?
首先我們要知道什么是樹?我們平常中的樹是往上長有分支的而卻不會形成閉環,數據結構中的樹跟我們我們平時看到的樹類似,確切的說是跟樹根長得類似,我畫了一幅圖,讓大家更好的理解樹。
圖1、圖2都是樹,圖3不是樹。每個紅色的圓圈我們稱之為元素也叫節點,用線將兩個節點連接起來,這兩個節點就形成了父子關系,同一個父節點的子節點成為兄弟節點,這跟我們家族關系一樣,同一個父親的叫做兄弟姐妹,在家族里面最大的稱為老子,樹里面也是一樣的,只是不叫老子,叫做跟節點,沒有子節點的叫做葉子節點。我們拿圖1來做示例,A為根節點,B、C為兄弟節點,E、F為葉子節點。
一顆樹還會涉及到三個概念高度
、深度
、層
,我們先來看看這三個名詞的定義:
高度:節點到葉子節點的最長路徑,從0開始計數
深度:跟節點到這個節點所經歷的邊數,從0開始計數
層:節點距離根節點的距離,從1開始計數
知道了三個名詞的概念之后,我們用一張圖來更加形象的表示這三個概念。
以上就是樹的基本概念,樹的種類很多,我們主要來學學二叉樹。
二叉樹
二叉樹就像它的名字一樣,每個元素最多有兩個節點,分別稱為左節點和右節點。當然並不是每個元素都需要有兩個節點,有的可能只有左節點,有的可能只有右節點。就像國家開放二胎一樣,也不是每個人都需要生兩個孩子。下面我們來看看一顆典型的二叉樹。
基於樹的存儲模式的不同,為了更好的利用存儲空間,二叉樹又分為完全二叉樹和非完全二叉樹,我們先來看看什么是完全二叉樹、非完全二叉樹?
完全二叉樹的定義:葉子節點都在最底下兩層,最后一層的葉子節點都靠左排列,並且除了最后一層,其他層的節點個數都要達到最大
也許單看定義會看不明白,我們來看幾張圖,你就能夠明白什么是完全二叉樹、非完全二叉樹。
1、完全二叉樹
2、非完全二叉樹
上面我們說了基於樹的存儲模式不同,而分為完全二叉樹和非完全二叉樹,那我們接下來來看看樹的存儲模式。
二叉樹的存儲模式
二叉樹的存儲模式有兩種,一種是基於指針或者引用的二叉鏈式存儲法,一種是基於數組的順序存儲法
二叉鏈式存儲法
鏈式存儲法相對比較簡單,理解起來也非常容易,每一個節點都有三個字段,一個字段存儲着該節點的值,另外兩個字段存儲着左右節點的引用。我們順着跟字節就可以很輕松的把整棵樹串起來,鏈式存儲法的結構大概長成這樣。
順序存儲法
順序存儲法是基於數組實現的,數組是一段有序的內存空間,如果我們把跟節點的坐標定位i
=1,左節點就是 2 * i
= 2,右節點 2 * i
+ 1 = 3,以此類推,每個節點都這么算,然后就將樹轉化成數組了,反過來,按照這種規則我們也能將數組轉化成一棵樹。看到這里我想你一定看出了一些弊端, 如果這是一顆不平衡的二叉樹是不是會造成大量的空間浪費呢?沒錯,這就是為什么需要分完全二叉樹和非完全二叉樹。分別來看看這兩種樹基於數組的存儲模式。
完全二叉樹順序存儲法
非完全二叉樹順序存儲法
從圖中將樹轉化成數組之后可以看出,完全二叉樹用數組來存儲只浪費了一個下標為0的存儲空間,二非完全二叉樹則浪費了大量的空間。如果樹為完全二叉樹,用數組存儲比鏈式存儲節約空間,因為數組存儲不需要存儲左右節點的信息
上面我們了解了二叉樹的定義、類型、存儲方式,接下來我們一起了解一下二叉樹的遍歷,二叉樹的遍歷也是面試中經常遇到的問題。
二叉樹遍歷
要了解二叉樹的遍歷,我們首先需要實例化出一顆二叉樹,我們采用鏈式存儲的方式來定義樹,實例化樹需要樹的節點信息,用來存放該節點的信息,因為我們才用的是鏈式存儲,所以我們的節點信息如下。
/**
* 定義一棵樹
*/
public class TreeNode {
// 存儲值
public int data;
// 存儲左節點
public TreeNode left;
// 存儲右節點
public TreeNode right;
public TreeNode(int data) {
this.data = data;
}
}
定義完節點信息之后,我們就可以初始化一顆樹啦,下面是初始化樹的過程:
public static TreeNode buildTree() {
// 創建測試用的二叉樹
TreeNode t1 = new TreeNode(1);
TreeNode t2 = new TreeNode(2);
TreeNode t3 = new TreeNode(3);
TreeNode t4 = new TreeNode(4);
TreeNode t5 = new TreeNode(5);
TreeNode t6 = new TreeNode(6);
TreeNode t7 = new TreeNode(7);
TreeNode t8 = new TreeNode(8);
t1.left = t2;
t1.right = t3;
t2.left = t4;
t4.right = t7;
t3.left = t5;
t3.right = t6;
t6.left = t8;
return t1;
}
經過上面步驟之后,我們的樹就長成下圖所示的樣子,數字代表該節點的值。
有了樹之后,我們就可以對樹進行遍歷啦,二叉樹的遍歷有三種方式,前序遍歷、中序遍歷、后續遍歷三種遍歷方式,三種遍歷方式與節點輸出的順序有關系。下面我們分別來看看這三種遍歷方式。
前序遍歷
前序遍歷:對於樹中的任意節點來說,先打印這個節點,然后再打印它的左子樹,最后打印它的右子樹。
為了方便大家的理解,我基於上面我們定義的二叉樹,對三種遍歷方式的執行流程都制作了動態圖,希望對你的閱讀有所幫助,我們先來看看前序遍歷的執行流程動態圖。
理解了前序遍歷的概念和看完前序遍歷執行流程動態圖之后,你心里一定很想知道,在代碼中如何怎么實現樹的前序遍歷?二叉樹的遍歷非常簡單,一般都是采用遞歸的方式進行遍歷,我們來看看前序遍歷的代碼:
// 先序遍歷,遞歸實現 先打印本身,再打印左節點,在打印右節點
public static void preOrder(TreeNode root) {
if (root == null) {
return;
}
// 輸出本身
System.out.print(root.data + " ");
// 遍歷左節點
preOrder(root.left);
// 遍歷右節點
preOrder(root.right);
}
中序遍歷
中序遍歷:對於樹中的任意節點來說,先打印它的左子樹,然后再打印它本身,最后打印它的右子樹。
跟前序遍歷一樣,我們來看看中序遍歷的執行流程動態圖。
中序遍歷的代碼:
// 中序遍歷 先打印左節點,再輸出本身,最后輸出右節點
public static void inOrder(TreeNode root) {
if (root == null) {
return;
}
inOrder(root.left);
System.out.print(root.data + " ");
inOrder(root.right);
}
后序遍歷
后序遍歷:對於樹中的任意節點來說,先打印它的左子樹,然后再打印它的右子樹,最后打印這個節點本身。
跟前兩種遍歷一樣,理解概念之后,我們還是先來看張圖。
后序遍歷的實現代碼:
// 后序遍歷 先打印左節點,再輸出右節點,最后才輸出本身
public static void postOrder(TreeNode root) {
if (root == null) {
return;
}
postOrder(root.left);
postOrder(root.right);
System.out.print(root.data + " ");
}
二叉樹的遍歷還是非常簡單的,雖然有三種遍歷方式,但都是一樣的,只是輸出的順序不一樣而已,經過了上面這么多的學習,我相信你一定對二叉樹有不少的認識,接下來我們來了解一種常用而且比較特殊的二叉樹:二叉查找樹
二叉查找樹
二叉查找樹又叫二叉搜索樹,從名字中我們就能夠知道,這種樹在查找方面一定有過人的優勢,事實確實如此,二叉查找樹確實是為查找而生的樹,但是它不僅僅支持快速查找數據,還支持快速插入、刪除一個數據。那它是怎么做到這些的呢?我們先從二叉查找樹的概念開始了解。
二叉查找樹:在樹中的任意一個節點,其左子樹中的每個節點的值,都要小於這個節點的值,而右子樹節點的值都大於這個節點的值。
難以理解?記不住?沒關系的,下面我定義了一顆二叉查找樹,我們對着樹,來慢慢理解。
根據二叉查找樹的定義,每棵樹的左節點的值要小於這父節點,右節點的值要大於父節點。62節點的 所有左節點的值都要小於 62 ,所有右節點 的值都要大於 62 。對於這顆樹上的每一個節點都要滿足這個條件,我們拿我們樹上的另一個節點 35 來說,它的右子樹上的節點值最大不能超過 47 ,因為 35 是 47 的左子樹,根據二叉搜索樹的規則,左子樹的值要小於節點值。
二叉查找樹既然名字中帶有查找兩字,那我們就從二叉查找樹的查找開始學習二叉查找樹吧。
二叉查找樹的查找操作
由於二叉查找樹的特性,我們需要查找一個數據,先跟跟節點比較,如果值等於跟節點,則返回根節點,如果小於根節點,則必然在左子樹這邊,只要遞歸查找左子樹就行,如果大於,這在右子樹這邊,遞歸右子樹即可。這樣就能夠實現快速查找,因為每次查找都減少了一半的數據,跟二分查找有點相似,快速插入、刪除都是居於這個特性實現的。
下面我們用一幅動態圖來加強對二叉查找樹查找流程的理解,我們需要在上面的這顆二叉查找樹中找出值等於 37 的節點,我們一起來看看流程圖是怎么實現的。
- 1、先用 37 跟 62 比較,37 < 62 ,在左子樹中繼續查找
- 2、左子樹的節點值為 58,37 < 58 ,繼續在左子樹中查找
- 3、左子樹的節點值為 47,37 < 47,繼續在左子樹中查找
- 4、左子樹的節點值為 35,37 > 35,在右子樹中查找
- 5、右子樹中的節點值為 37,37 = 37 ,返回該節點
講完了查找的概念之后,我們一起來看看二叉查找樹的查找操作的代碼實現
/**
* 根據值查找樹
* @param data 值
* @return
*/
public TreeNode find(int data) {
TreeNode p = tree;
while (p != null) {
if (data < p.data) p = p.left;
else if (data > p.data) p = p.right;
else return p;
}
return null;
}
二叉查找樹的插入操作
插入跟查找差不多,也是從根節點開始找,如果要插入的數據比節點的數據大,並且節點的右子樹為空,就將新數據直接插到右子節點的位置;如果不為空,就再遞歸遍歷右子樹,查找插入位置。同理,如果要插入的數據比節點數值小,並且節點的左子樹為空,就將新數據插入到左子節點的位置;如果不為空,就再遞歸遍歷左子樹,查找插入位置。
假設我們要插入 63 ,我們用一張動態圖來看看插入的流程。
- 1、63 > 62 ,在樹的右子樹繼續查找.
- 2、63 < 88 ,在樹的左子樹繼續查找
- 3、63 < 73 ,因為 73 是葉子節點,所以 63 就成為了 73 的左子樹。
我們來看看二叉查找樹的插入操作實現代碼
/**
* 插入樹
* @param data
*/
public void insert(int data) {
if (tree == null) {
tree = new TreeNode(data);
return;
}
TreeNode p = tree;
while (p != null) {
// 如果值大於節點的值,則新樹為節點的右子樹
if (data > p.data) {
if (p.right == null) {
p.right = new TreeNode(data);
return;
}
p = p.right;
} else { // data < p.data
if (p.left == null) {
p.left = new TreeNode(data);
return;
}
p = p.left;
}
}
}
二叉查找樹的刪除操作
刪除的邏輯要比查找和插入復雜一些,刪除分一下三種情況:
第一種情況:如果要刪除的節點沒有子節點,我們只需要直接將父節點中,指向要刪除節點的指針置為 null。比如圖中的刪除節點 51。
第二種情況:如果要刪除的節點只有一個子節點(只有左子節點或者右子節點),我們只需要更新父節點中,指向要刪除節點的指針,讓它指向要刪除節點的子節點就可以了。比如圖中的刪除節點 35。
第三種情況:如果要刪除的節點有兩個子節點,這就比較復雜了。我們需要找到這個節點的右子樹中的最小節點,把它替換到要刪除的節點上。然后再刪除掉這個最小節點,因為最小節點肯定沒有左子節點(如果有左子結點,那就不是最小節點了),所以,我們可以應用上面兩條規則來刪除這個最小節點。比如圖中的刪除節點 88
前面兩種情況稍微簡單一些,第三種情況,我制作了一張動態圖,希望能對你有所幫助。
我們來看看二叉查找樹的刪除操作實現代碼
public void delete(int data) {
TreeNode p = tree; // p指向要刪除的節點,初始化指向根節點
TreeNode pp = null; // pp記錄的是p的父節點
while (p != null && p.data != data) {
pp = p;
if (data > p.data) p = p.right;
else p = p.left;
}
if (p == null) return; // 沒有找到
// 要刪除的節點有兩個子節點
if (p.left != null && p.right != null) { // 查找右子樹中最小節點
TreeNode minP = p.right;
TreeNode minPP = p; // minPP表示minP的父節點
while (minP.left != null) {
minPP = minP;
minP = minP.left;
}
p.data = minP.data; // 將minP的數據替換到p中
p = minP; // 下面就變成了刪除minP了
pp = minPP;
}
// 刪除節點是葉子節點或者僅有一個子節點
TreeNode child; // p的子節點
if (p.left != null) child = p.left;
else if (p.right != null) child = p.right;
else child = null;
if (pp == null) tree = child; // 刪除的是根節點
else if (pp.left == p) pp.left = child;
else pp.right = child;
}
我們上面了解了一些二叉查找樹的相關知識,由於二叉查找樹在極端情況下會退化成鏈表,例如每個節點都只有一個左節點,這是時間復雜度就變成了O(n),為了避免這種情況,又出現了一種新的樹叫平衡二叉查找樹,由於本文篇幅有點長了,相信看到這里的各位小伙伴已經有點疲憊了,關於平衡二叉查找樹的相關知識我就不在這里介紹了。
最后
打個小廣告,金九銀十跳槽季,平頭哥給大家整理了一份較全面的 Java 學習資料,歡迎掃碼關注微信公眾號:「平頭哥的技術博文」領取,祝各位升職加薪。