看官,不要生氣,我沒有罵你也沒有鄙視你的意思,今天就是想單純的給大夥分享一下樹的相關知識,可是我仍是想說做爲一名程序員,本身內心有沒有點樹?你會沒點數嗎?言歸正傳,樹是咱們經常使用的數據結構之一,樹的種類不少有二叉樹、二叉查找樹、平衡二叉樹、紅黑樹、B樹、B+樹等等,咱們今天就來聊聊二叉樹相關的樹。java
首先咱們要知道什麼是樹?咱們日常中的樹是往上長有分支的而卻不會造成閉環,數據結構中的樹跟咱們咱們平時看到的樹相似,確切的說是跟樹根長得相似,我畫了一幅圖,讓你們更好的理解樹。程序員
一顆樹還會涉及到三個概念高度
、深度
、層
,咱們先來看看這三個名詞的定義:面試
高度:節點到葉子節點的最長路徑,從0開始計數算法
深度:跟節點到這個節點所經歷的邊數,從0開始計數數組
層:節點距離根節點的距離,從1開始計數微信
知道了三個名詞的概念以後,咱們用一張圖來更加形象的表示這三個概念。數據結構
以上就是樹的基本概念,樹的種類不少,咱們主要來學學二叉樹。post
二叉樹就像它的名字同樣,每一個元素最多有兩個節點,分別稱爲左節點和右節點。固然並非每一個元素都須要有兩個節點,有的可能只有左節點,有的可能只有右節點。就像國家開放二胎同樣,也不是每一個人都須要生兩個孩子。下面咱們來看看一顆典型的二叉樹。學習
基於樹的存儲模式的不一樣,爲了更好的利用存儲空間,二叉樹又分爲徹底二叉樹和非徹底二叉樹,咱們先來看看什麼是徹底二叉樹、非徹底二叉樹?測試
徹底二叉樹的定義:葉子節點都在最底下兩層,最後一層的葉子節點都靠左排列,而且除了最後一層,其餘層的節點個數都要達到最大
也許單看定義會看不明白,咱們來看幾張圖,你就可以明白什麼是徹底二叉樹、非徹底二叉樹。
上面咱們說了基於樹的存儲模式不一樣,而分爲徹底二叉樹和非徹底二叉樹,那咱們接下來來看看樹的存儲模式。
二叉樹的存儲模式有兩種,一種是基於指針或者引用的二叉鏈式存儲法,一種是基於數組的順序存儲法
鏈式存儲法相對比較簡單,理解起來也很是容易,每個節點都有三個字段,一個字段存儲着該節點的值,另外兩個字段存儲着左右節點的引用。咱們順着跟字節就能夠很輕鬆的把整棵樹串起來,鏈式存儲法的結構大概長成這樣。
順序存儲法是基於數組實現的,數組是一段有序的內存空間,若是咱們把跟節點的座標定位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 + " ");
}
複製代碼
二叉樹的遍歷仍是很是簡單的,雖然有三種遍歷方式,但都是同樣的,只是輸出的順序不同而已,通過了上面這麼多的學習,我相信你必定對二叉樹有很多的認識,接下來咱們來了解一種經常使用並且比較特殊的二叉樹:二叉查找樹
二叉查找樹又叫二叉搜索樹,從名字中咱們就可以知道,這種樹在查找方面必定有過人的優點,事實確實如此,二叉查找樹確實是爲查找而生的樹,可是它不只僅支持快速查找數據,還支持快速插入、刪除一個數據。那它是怎麼作到這些的呢?咱們先從二叉查找樹的概念開始瞭解。
二叉查找樹:在樹中的任意一個節點,其左子樹中的每一個節點的值,都要小於這個節點的值,而右子樹節點的值都大於這個節點的值。
難以理解?記不住?不要緊的,下面我定義了一顆二叉查找樹,咱們對着樹,來慢慢理解。
二叉查找樹既然名字中帶有查找兩字,那咱們就從二叉查找樹的查找開始學習二叉查找樹吧。
因爲二叉查找樹的特性,咱們須要查找一個數據,先跟跟節點比較,若是值等於跟節點,則返回根節點,若是小於根節點,則必然在左子樹這邊,只要遞歸查找左子樹就行,若是大於,這在右子樹這邊,遞歸右子樹便可。這樣就可以實現快速查找,由於每次查找都減小了一半的數據,跟二分查找有點類似,快速插入、刪除都是居於這個特性實現的。
下面咱們用一幅動態圖來增強對二叉查找樹查找流程的理解,咱們須要在上面的這顆二叉查找樹中找出值等於 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 ,咱們用一張動態圖來看看插入的流程。
咱們來看看二叉查找樹的插入操做實現代碼
/** * 插入樹 * @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),爲了不這種狀況,又出現了一種新的樹叫平衡二叉查找樹,因爲本文篇幅有點長了,相信看到這裏的各位小夥伴已經有點疲憊了,關於平衡二叉查找樹的相關知識我就不在這裏介紹了。
打個小廣告,歡迎掃碼關注微信公衆號:「平頭哥的技術博文」,一塊兒進步吧。