對於樹這個數據結構,第一次看到這個樹肯定是一臉蒙逼,瑪德,樹?種樹的那個樹么?哈哈哈,當然不是,前面我們說過數組添加、刪除數據很慢,查詢數據很快;而鏈表添加、刪除數據很快,但是查找數據很慢,我們就想啊,有沒有一種數據結構取二者之精華,那不就是一個添加,刪除,查詢都很快的數據結構嗎?那用起來多舒服啊!
這個取二者之精華的數據結構就是樹(tree),而且隨着各種大佬對樹這種結構的改進,就有了很多種樹,常見的有二叉樹,紅黑樹,2-3-4樹等各種樹,我們就一起看看這幾種簡單樹到底是什么鬼!
1.樹的基本概念
樹的基本結構就是由一個個的節點組成,如下圖所示,然后每一個節點都通過邊相連,那么有人要問了,這些節點是什么啊?emmm...上篇博客實現了鏈表,就是類似鏈表的那個節點一樣的東西,本質上就是一個Node的實例,在這個實例中,有幾個屬性,分別保存幾個子節點的引用和保存的數據;
這里注意一下,任意一個節點的父節點只能有一個,子節點可肯能有多個;這很好理解,現實中,你可以有多個孩子,但是你能有多個親爹嗎???
比如對於B節點來說,A是父節點,D,E,F都是子節點,而對於沒有子節點的那種節點,叫做葉節點,這里的D、E、F、G、H都是葉節點;由於一切節點都是從A出發的,所以A叫做根節點
注:第一次看這個圖是不是不覺得像一棵樹啊,其實你要把這個圖旋轉180度,倒過來看就比較像一棵樹了,哈哈哈!話說用過linux操作系統的人應該知道linux的根目錄"/"就是樹結構。。。
那么什么是二叉樹呢?這很簡單,每個節點最多只能兩個子節點,我們看看下圖,這就是一個二叉樹的基本結構
根據上圖,我們說一下樹的基本術語:
路徑:從任意一個節點到另外任意一個節點所經過的節點的順序排列就是路徑;
根節點:一棵樹只有一個根節點,要保證根到任意一個節點只有一條路徑,否則就不是樹了,比如下圖這個就不是樹
父節點:與當前節點連接的上一層節點就是父節點
子節點:與當前節點連接的下一層節點就是子節點
葉節點:沒有子節點的節點就是葉結點
子樹:上圖中在樹中隨便找一個節點B當作根節點,然后B的所有子節點,子節點的子節點等等就構成了一個子樹;
左/右子節點:由於二叉樹的每一個節點都只有兩個節點,於是左邊的子節點叫做左子節點,右邊的就叫做右子節點;
2.二叉搜索樹
什么是二叉搜索樹呢?其實就是一種特殊的二叉樹,只是我們向其中添加數據的時候定義了一種規則,比如下圖B中存了數據20,現在我們要添加數據10和30,應該怎么放呢?我們將小於20的數據放在左子節點,大於20的數據放在右子節點,這就是二叉搜索樹,樹如其名,搜索起來特別快;
順便提一下平衡樹和非平衡樹,數據在左右子節點中分布比較平均就是平衡樹,不怎么平均的就是非平衡樹,下圖所示,76這個節點只有一個右子節點,而且還連着這么多數據,有點不平衡....
下面我們簡單用java代碼來表示樹的節點(還是用靜態內部類的形式):
2.1.添加節點
添加節點的時候要准備兩個指針,parentNode=null和current=root,首先我們要判斷root是不是為null,如果是的話直接將我們要添加的節點newNode放到第一個節點就ok了;
假如有root節點之后再要添加新的節點,先讓parentNode指向current節點(就是root節點),current這個指針指向哪里就必須要判斷newNode和root中數據的大小,如果newNode大,則current就指向左子節點,反之則指向右子節點;同時會判斷左子結點或者右子節點是否存在,不存在的話直接將newNode放到該位置即可,存在的話繼續執行while循環;具體代碼如下:
public boolean add(int value){ Node newNode = new Node(value); if (root==null) { root = newNode; return true; }else{ Node parentNode = null; Node current = root; while(current!=null){ parentNode = current; if (value<current.key) { current = parentNode.leftChild; if (current==null) { parentNode.leftChild = newNode; return true; } }else{ current = parentNode.rightChild; if (current==null) { parentNode.rightChild = newNode; return true; } } } } return false; }
2.2.遍歷樹
我們要查看一下樹中的所有節點中的數據,就需要我們實現對樹中所有節點的遍歷,這個遍歷方式有很多種,每個節點最多有兩個子節點,可想而知最容易想到的方式就是遞歸;
最常見的三種遍歷方式:前序、中序、后序,其中重點是中序,最后會按照從小到大的順序打印出來:
前序:根節點-----左子樹-------右子樹
中序:左子樹-----根節點--------右子樹
后序:右子樹-------根節點--------左子樹
三種方式分別用代碼來實現為,最重要的是中序;
//中序遍歷 public void infixOrder(Node current){ if(current != null){ infixOrder(current.leftChild); System.out.print(current.key+" "); infixOrder(current.rightChild); } } //前序遍歷 public void preOrder(Node current){ if(current != null){ System.out.print(current.key+" "); preOrder(current.leftChild); preOrder(current.rightChild); } } //后序遍歷 public void postOrder(Node current){ if(current != null){ postOrder(current.leftChild); postOrder(current.rightChild); System.out.print(current.key+" "); } }
好好想想這里中序的遞歸。。。。
2.3.查找節點
比如下面這個圖中要查找57這個節點是否存在,我們首先將57比63小,我們就把57和左子結點27比較,57大;然后57在和51比較,再就是和58比較,小於58再和左子結點57比較,相等的話就返回這個57的節點
用代碼來實現原理:
public Node find(Integer value){ Node current = root; while(current!=null){ if (value<current.key) { current = current.leftChild; }else if (value>current.key) { current = current.rightChild; }else{ return current; } } return null; }
2.4.最大值和最小值
假如我們要找樹中的最大值和最小值還是很容易的,因為樹中的數據都是按照了規則放的,最小值應該就是最左邊的子節點,最大值應該就是最右邊的字節點,我們也用代碼來看看:
//查詢樹中最大值 public Node findMax(){ Node current = root; Node max=null; while(current!=null){ max = current; current = current.leftChild; } return max; } //查詢書中最小值 public Node findMin(){ Node current = root; Node min = null; while(current!=null){ min = current; current = current.rightChild; } return min; }
這里的max和min兩個指針比較關鍵,因為當跳出while循環的時候,curent肯定是為null,但是我們想要打印出這個current的父節點,於是我們可以用這兩個指着保存一下;
其實到這里一個樹的基本結構和功能就差不多了,可以自己測試一下;
2.5.刪除節點
刪除節點最后說,為什么呢?因為刪除節點最復雜,你想啊,節點是分為很多種的,假如刪除的是葉節點那很容易,直接將這個葉節點的父節點對它的引用變為null就行了,但假如要刪除的節點是中間的節點呢?這就比較麻煩了,這個中間節點又分為有一個子節點,兩個子節點,對於有一個子節點的很好處理,但是兩個子節點的就最麻煩!
我們重點看看第三個圖,刪除的節點又兩個子節點的時候,肯定要想一個新的節點去代替那個6節點,使得整個樹不破壞結構,還是可以正常使用,這種方式叫做找后繼節點,顧名思義就是找6那個節點后面的節點來代替6節點,而且必須是6節點的右子節點(想想為什么呢?),我們慢慢看有哪幾種后繼節點滿足要求;
第一種:被刪除節點的右子節點的左節點,下圖所示的30就滿足條件啊;而且這給了我們一個啟發,這種的后繼節點就是找一個比被刪除節點大一點點的節點;換句話來說,就是在被刪除節點的右子節點中找最小的節點;
第二種:被刪除節點的右子節點只有一個右子節點,說起來很繞,看圖,我們直接將35作為新的節點放在被刪除節點25的位置就可以了,其他的不動;
現在我們總結一下刪除節點所需要的重點:
(1).刪除的節點是葉節點,我們找到該葉節點的父節點,修改父節點指向葉結點的引用為null即可;
(2).刪除的節點有一個子節點
2.1.這個子節點是左子結點
2.2.這個子節點是右子節點
(3).刪除的節點有兩個子節點,這種就要找后繼節點來補上被刪除節點的那個位置,防止樹的結構被破壞,找后繼節點就是找被刪除節點的右子節點中最小的值
3.1.被刪除的節點的右子節點只有右子節點的話,就直接將右子節點變為后繼節點;
3.2.被刪除的節點的右子節點有兩個子節點的話,找這兩個子節點中的最小值即可;即使這兩個子節點后面還有子節點,也是一樣的找最小值
既然思路已經理清楚了,那就用代碼來表達出來,比較多:
//根據數據刪除對應的節點 public boolean delete(int value){ Node parent = null; Node current = root; Boolean isLeftChild = null; //當根節點不存在的時候,執行刪除操作會拋出異常 if (root==null) { try { throw new Exception("樹中沒有數據,你刪除空氣啊!"); } catch (Exception e) { e.printStackTrace(); } return false; } //這里只是移動了parent和current的指針,首先是判斷節點是在根節點的左邊還是右邊,確定了之后再慢慢往下找,最后將current移動到被刪除的節點那里, //后面我們就可以通過current這個指針獲取刪除節點的信息; //如果最后current==null了,說明最后沒有找到該節點,就返回false while(value!=current.key){ parent = current; if (value<current.key) { isLeftChild = true; current = current.leftChild; }else{ isLeftChild = false; current = current.rightChild; } if (current==null) { return false; } } //如果當前被刪除的節點沒有子節點 if (current.leftChild==null && current.rightChild==null) { if (current==root) { root = null; }else if (isLeftChild) { parent.leftChild = null; }else { parent.rightChild = null; } return true; } //假如當前被刪除的節點有一個子節點,這個時候要區分子節點是左子節點還是右子節點 //假如是左子節點 if(current.leftChild!=null && current.rightChild==null){ if (current == root) { root = current.leftChild; }else if (isLeftChild) { parent.leftChild = current.leftChild; }else{ parent.rightChild = current.rightChild; } return true; } if(current.leftChild==null && current.rightChild!=null){ //假如是右子節點,相當於條件是current.leftChild==null && current.rightChild!=null if (current==root) { root = current.rightChild; }else if (isLeftChild) { parent.leftChild = current.rightChild; }else{ parent.rightChild = current.rightChild; } return true; } //假如被刪除的節點有兩個子節點,這個時候我們首先就要找后繼節點,我們寫一個找后繼節點的方法getAfterNode() if (current.leftChild!=null && current.rightChild!=null) { Node success = getAfterNode(current); if (current == root) { root = success; }else if(isLeftChild){ parent.leftChild = success; }else{ parent.rightChild = success; } return true; } return false; } //根據刪除節點尋找后繼節點,注意,這里的話delNode肯定要有兩個子節點,假如沒有,那就是前面的兩種刪除節點的情況了 public Node getAfterNode(Node delNode){ Node successParent = delNode; Node success = delNode; Node current = delNode.leftChild; while(current!=null){ successParent = success; success = current; current = current.leftChild; } if (success!=delNode) { successParent.leftChild = success.rightChild; success.rightChild = delNode.rightChild; } return success; }
所有的邏輯就這么多,我們可以把所有的代碼整理一下,並且測試一下結果,成功;

package com.wyq.test; import com.wyq.test.MyTree.Node; public class MyTree { private Node root; public static class Node{ private Integer key; //節點中存的數據 private Node leftChild;//左子結點 private Node rightChild;//右子節點 public Node(Integer key) { this.key = key; this.leftChild = null; this.rightChild = null; } public void displayNode(){ System.out.println("{"+key+"}"); } } public boolean add(int value){ Node newNode = new Node(value); if (root==null) { root = newNode; return true; }else{ Node parentNode = null; Node current = root; while(current!=null){ parentNode = current; if (value<current.key) { current = parentNode.leftChild; if (current==null) { parentNode.leftChild = newNode; return true; } }else{ current = parentNode.rightChild; if (current==null) { parentNode.rightChild = newNode; return true; } } } } return false; } //中序遍歷樹中的所有數據 public void infixOrder(Node node){ if (node!=null) { infixOrder(node.leftChild); node.displayNode(); infixOrder(node.rightChild); } } //根據數據查找對應的節點 public Node find(Integer value){ Node current = root; while(current!=null){ if (value<current.key) { current = current.leftChild; }else if (value>current.key) { current = current.rightChild; }else{ return current; } } return null; } //查詢樹中最大值 public Node findMax(){ Node current = root; Node max=null; while(current!=null){ max = current; current = current.leftChild; } return max; } //查詢書中最小值 public Node findMin(){ Node current = root; Node min = null; while(current!=null){ min = current; current = current.rightChild; } return min; } //根據數據刪除對應的節點 public boolean delete(int value){ Node parent = null; Node current = root; Boolean isLeftChild = null; //當根節點不存在的時候,執行刪除操作會拋出異常 if (root==null) { try { throw new Exception("樹中沒有數據,你刪除空氣啊!"); } catch (Exception e) { e.printStackTrace(); } return false; } //這里只是移動了parent和current的指針,首先是判斷節點是在根節點的左邊還是右邊,確定了之后再慢慢往下找,最后將current移動到被刪除的節點那里, //后面我們就可以通過current這個指針獲取刪除節點的信息; //如果最后current==null了,說明最后沒有找到該節點,就返回false while(value!=current.key){ parent = current; if (value<current.key) { isLeftChild = true; current = current.leftChild; }else{ isLeftChild = false; current = current.rightChild; } if (current==null) { return false; } } //如果當前被刪除的節點沒有子節點 if (current.leftChild==null && current.rightChild==null) { if (current==root) { root = null; }else if (isLeftChild) { parent.leftChild = null; }else { parent.rightChild = null; } return true; } //假如當前被刪除的節點有一個子節點,這個時候要區分子節點是左子節點還是右子節點 //假如是左子節點 if(current.leftChild!=null && current.rightChild==null){ if (current == root) { root = current.leftChild; }else if (isLeftChild) { parent.leftChild = current.leftChild; }else{ parent.rightChild = current.rightChild; } return true; } if(current.leftChild==null && current.rightChild!=null){ //假如是右子節點,相當於條件是current.leftChild==null && current.rightChild!=null if (current==root) { root = current.rightChild; }else if (isLeftChild) { parent.leftChild = current.rightChild; }else{ parent.rightChild = current.rightChild; } return true; } //假如被刪除的節點有兩個子節點,這個時候我們首先就要找后繼節點,我們寫一個找后繼節點的方法getAfterNode() if (current.leftChild!=null && current.rightChild!=null) { Node success = getAfterNode(current); if (current == root) { root = success; }else if(isLeftChild){ parent.leftChild = success; }else{ parent.rightChild = success; } return true; } return false; } //根據刪除節點尋找后繼節點,注意,這里的話delNode肯定要有兩個子節點,假如沒有,那就是前面的兩種刪除節點的情況了 public Node getAfterNode(Node delNode){ Node successParent = delNode; Node success = delNode; Node current = delNode.leftChild; while(current!=null){ successParent = success; success = current; current = current.leftChild; } if (success!=delNode) { successParent.leftChild = success.rightChild; success.rightChild = delNode.rightChild; } return success; } public static void main(String[] args) { MyTree tree = new MyTree(); tree.add(100); tree.add(50); tree.add(200); tree.add(25); tree.add(75); tree.add(150); tree.add(250); tree.delete(200); tree.infixOrder(tree.root); Node find = tree.find(250); System.out.print("查找節點數據:"); find.displayNode(); System.out.print("最大值為:"); tree.findMax().displayNode(); System.out.print("最小值為:"); tree.findMin().displayNode(); } }
3.關於刪除數據的一點思考
上面的刪除方法可謂是很長而且邏輯很容易弄混,那有沒有方法避免這種有點坑的東西呢?
於是啊,我們就想到一個辦法,我們把刪除節點不是真的刪除,是邏輯刪除;比如相當於給這個節點添加一個屬性isDelete,這個狀態默認為false表示這是一個正常的節點,如果我們要刪除某個節點,只需要把isDelete變為true,代表着這個節點已經不屬於這個樹了,這種做法的好處就是不會改變這個樹的結構,可以想想這種做法和之前刪除的做法的區別;但是壞處也很明顯,就是刪除的節點也會保存在樹中,當這種刪除的操作很多的時候,樹中就保存了太多垃圾數據了,所以看情況使用。。。
4.關於節點中數據的一點改進
有沒有看到我們上面實現的樹中的節點中保存的數據都是數字啊,為什么呢?因為簡單唄,很容易理解,如果把樹中節點的數據換成對象其實也是行的,比如下面這樣的:
如果是這樣的話,我們添加數據就必須要按照User對象的某個屬性(比如id)為關鍵字進行比較,然后向樹中插入數據,其實跟我們用Integer類型的差不多,只是寫起來代碼看起來不夠簡潔;
下圖選取部分代碼進行修改:
5.總結
樹這種數據結構還是挺厲害的,雜糅了數組和鏈表的有點於一身,查找數據很快,增加和刪除數據也很快,但就是特么的理解其中的邏輯需要一點點時間去慢慢啃。。。。。后面還有各種樹!
下一篇應該是紅黑樹了,加油加油!.