接下來咱們將會介紹另一種數據結構——樹。二叉樹是樹這種數據結構的一員,後面咱們還會介紹紅黑樹,2-3-4樹等數據結構。那麼爲何要使用樹?它有什麼優勢?java
前面咱們介紹數組的數據結構,咱們知道對於有序數組,查找很快,並介紹能夠經過二分法查找,可是想要在有序數組中插入一個數據項,就必須先找到插入數據項的位置,而後將全部插入位置後面的數據項所有向後移動一位,來給新數據騰出空間,平均來說要移動N/2次,這是很費時的。同理,刪除數據也是。算法
而後咱們介紹了另一種數據結構——鏈表,鏈表的插入和刪除很快,咱們只須要改變一些引用值就好了,可是查找數據卻很慢了,由於無論咱們查找什麼數據,都須要從鏈表的第一個數據項開始,遍歷到找到所需數據項爲止,這個查找也是平均須要比較N/2次。數據庫
那麼咱們就但願一種數據結構能同時具有數組查找快的優勢以及鏈表插入和刪除快的優勢,因而 樹 誕生了。編程
樹(tree)是一種抽象數據類型(ADT),用來模擬具備樹狀結構性質的數據集合。它是由n(n>0)個有限節點經過鏈接它們的邊組成一個具備層次關係的集合。把它叫作「樹」是由於它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。數組
①、節點:上圖的圓圈,好比A,B,C等都是表示節點。節點通常表明一些實體,在java面向對象編程中,節點通常表明對象。數據結構
②、邊:鏈接節點的線稱爲邊,邊表示節點的關聯關係。通常從一個節點到另外一個節點的惟一方法就是沿着一條順着有邊的道路前進。在Java當中一般表示引用。ide
樹有不少種,向上面的一個節點有多餘兩個的子節點的樹,稱爲多路樹,後面會講解2-3-4樹和外部存儲都是多路樹的例子。而每一個節點最多隻能有兩個子節點的一種形式稱爲二叉樹,這也是本篇博客講解的重點。post
①、路徑:順着節點的邊從一個節點走到另外一個節點,所通過的節點的順序排列就稱爲「路徑」。this
②、根:樹頂端的節點稱爲根。一棵樹只有一個根,若是要把一個節點和邊的集合稱爲樹,那麼從根到其餘任何一個節點都必須有且只有一條路徑。A是根節點。編碼
③、父節點:若一個節點含有子節點,則這個節點稱爲其子節點的父節點;B是D的父節點。
④、子節點:一個節點含有的子樹的根節點稱爲該節點的子節點;D是B的子節點。
⑤、兄弟節點:具備相同父節點的節點互稱爲兄弟節點;好比上圖的D和E就互稱爲兄弟節點。
⑥、葉節點:沒有子節點的節點稱爲葉節點,也叫葉子節點,好比上圖的H、E、F、G都是葉子節點。
⑦、子樹:每一個節點均可以做爲子樹的根,它和它全部的子節點、子節點的子節點等都包含在子樹中。
⑧、節點的層次:從根開始定義,根爲第一層,根的子節點爲第二層,以此類推。
⑨、深度:對於任意節點n,n的深度爲從根到n的惟一路徑長,根的深度爲0;
⑩、高度:對於任意節點n,n的高度爲從n到一片樹葉的最長路徑長,全部樹葉的高度爲0;
二叉樹:樹的每一個節點最多隻能有兩個子節點
上圖的第一幅圖B節點有DEF三個子節點,就不是二叉樹,稱爲多路樹;而第二幅圖每一個節點最多隻有兩個節點,是二叉樹,而且二叉樹的子節點稱爲「左子節點」和「右子節點」。上圖的D,E分別是B的左子節點和右子節點。
若是咱們給二叉樹加一個額外的條件,就能夠獲得一種被稱做二叉搜索樹(binary search tree)的特殊二叉樹。
二叉搜索樹要求:若它的左子樹不空,則左子樹上全部結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上全部結點的值均大於它的根結點的值; 它的左、右子樹也分別爲二叉排序樹。
二叉搜索樹做爲一種數據結構,那麼它是如何工做的呢?它查找一個節點,插入一個新節點,以及刪除一個節點,遍歷樹等工做效率如何,下面咱們來一一介紹。
二叉樹的節點類:
package com.ys.tree; public class Node { private Object data; //節點數據 private Node leftChild; //左子節點的引用 private Node rightChild; //右子節點的引用 //打印節點內容 public void display(){ System.out.println(data); } }
二叉樹的具體方法:
package com.ys.tree; public interface Tree { //查找節點 public Node find(Object key); //插入新節點 public boolean insert(Object key); //刪除節點 public boolean delete(Object key); //Other Method...... }
查找某個節點,咱們必須從根節點開始遍歷。
①、查找值比當前節點值大,則搜索右子樹;
②、查找值等於當前節點值,中止搜索(終止條件);
③、查找值小於當前節點值,則搜索左子樹;
//查找節點 public Node find(int key) { Node current = root; while(current != null){ if(current.data > key){//當前值比查找值大,搜索左子樹 current = current.leftChild; }else if(current.data < key){//當前值比查找值小,搜索右子樹 current = current.rightChild; }else{ return current; } } return null;//遍歷完整個樹沒找到,返回null }
用變量current來保存當前查找的節點,參數key是要查找的值,剛開始查找將根節點賦值到current。接在在while循環中,將要查找的值和current保存的節點進行對比。若是key小於當前節點,則搜索當前節點的左子節點,若是大於,則搜索右子節點,若是等於,則直接返回節點信息。當整個樹遍歷徹底,即current == null,那麼說明沒找到查找值,返回null。
樹的效率:查找節點的時間取決於這個節點所在的層數,每一層最多有2n-1個節點,總共N層共有2n-1個節點,那麼時間複雜度爲O(logN),底數爲2。
我看評論有對這裏的時間複雜度不理解,這裏解釋一下,O(logN),N表示的是二叉樹節點的總數,而不是層數。
要插入節點,必須先找到插入的位置。與查找操做類似,因爲二叉搜索樹的特殊性,待插入的節點也須要從根節點開始進行比較,小於根節點則與根節點左子樹比較,反之則與右子樹比較,直到左子樹爲空或右子樹爲空,則插入到相應爲空的位置,在比較的過程當中要注意保存父節點的信息 及 待插入的位置是父節點的左子樹仍是右子樹,才能插入到正確的位置。
//插入節點 public boolean insert(int data) { Node newNode = new Node(data); if(root == null){//當前樹爲空樹,沒有任何節點 root = newNode; return true; }else{ Node current = root; Node parentNode = null; while(current != null){ parentNode = current; if(current.data > data){//當前值比插入值大,搜索左子節點 current = current.leftChild; if(current == null){//左子節點爲空,直接將新值插入到該節點 parentNode.leftChild = newNode; return true; } }else{ current = current.rightChild; if(current == null){//右子節點爲空,直接將新值插入到該節點 parentNode.rightChild = newNode; return true; } } } } return false; }
遍歷樹是根據一種特定的順序訪問樹的每個節點。比較經常使用的有前序遍歷,中序遍歷和後序遍歷。而二叉搜索樹最經常使用的是中序遍歷。
①、中序遍歷:左子樹——》根節點——》右子樹
②、前序遍歷:根節點——》左子樹——》右子樹
③、後序遍歷:左子樹——》右子樹——》根節點
//中序遍歷 public void infixOrder(Node current){ if(current != null){ infixOrder(current.leftChild); System.out.print(current.data+" "); infixOrder(current.rightChild); } } //前序遍歷 public void preOrder(Node current){ if(current != null){ System.out.print(current.data+" "); preOrder(current.leftChild); preOrder(current.rightChild); } } //後序遍歷 public void postOrder(Node current){ if(current != null){ postOrder(current.leftChild); postOrder(current.rightChild); System.out.print(current.data+" "); } }
這沒什麼好說的,要找最小值,先找根的左節點,而後一直找這個左節點的左節點,直到找到沒有左節點的節點,那麼這個節點就是最小值。同理要找最大值,一直找根節點的右節點,直到沒有右節點,則就是最大值。
//找到最大值 public Node findMax(){ Node current = root; Node maxNode = current; while(current != null){ maxNode = current; current = current.rightChild; } return maxNode; } //找到最小值 public Node findMin(){ Node current = root; Node minNode = current; while(current != null){ minNode = current; current = current.leftChild; } return minNode; }
刪除節點是二叉搜索樹中最複雜的操做,刪除的節點有三種狀況,前兩種比較簡單,可是第三種卻很複雜。
一、該節點是葉節點(沒有子節點)
二、該節點有一個子節點
三、該節點有兩個子節點
下面咱們分別對這三種狀況進行講解。
要刪除葉節點,只須要改變該節點的父節點引用該節點的值,即將其引用改成 null 便可。要刪除的節點依然存在,可是它已經不是樹的一部分了,因爲Java語言的垃圾回收機制,咱們不須要非得把節點自己刪掉,一旦Java意識到程序不在與該節點有關聯,就會自動把它清理出存儲器。
@Override public boolean delete(int key) { Node current = root; Node parent = root; boolean isLeftChild = false; //查找刪除值,找不到直接返回false while(current.data != key){ parent = current; if(current.data > 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; } return false; }
刪除節點,咱們要先找到該節點,並記錄該節點的父節點。在檢查該節點是否有子節點。若是沒有子節點,接着檢查其是不是根節點,若是是根節點,只須要將其設置爲null便可。若是不是根節點,是葉節點,那麼斷開父節點和其的關係便可。
刪除有一個子節點的節點,咱們只須要將其父節點本來指向該節點的引用,改成指向該節點的子節點便可。
//當前節點有一個子節點 if(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; }else{ //current.leftChild != null && current.rightChild == null if(current == root){ root = current.leftChild; }else if(isLeftChild){ parent.leftChild = current.leftChild; }else{ parent.rightChild = current.leftChild; } return true; }
當刪除的節點存在兩個子節點,那麼刪除以後,兩個子節點的位置咱們就沒辦法處理了。既然處理不了,咱們就想到一種辦法,用另外一個節點來代替被刪除的節點,那麼用哪個節點來代替呢?
咱們知道二叉搜索樹中的節點是按照關鍵字來進行排列的,某個節點的關鍵字次高節點是它的中序遍歷後繼節點。用後繼節點來代替刪除的節點,顯然該二叉搜索樹仍是有序的。(這裏用後繼節點代替,若是該後繼節點本身也有子節點,咱們後面討論。)
那麼如何找到刪除節點的中序後繼節點呢?其實咱們稍微分析,這實際上就是要找比刪除節點關鍵值大的節點集合中最小的一個節點,只有這樣代替刪除節點後才能知足二叉搜索樹的特性。
後繼節點也就是:比刪除節點大的最小節點。
算法:程序找到刪除節點的右節點,(注意這裏前提是刪除節點存在左右兩個子節點,若是不存在則是刪除狀況的前面兩種),而後轉到該右節點的左子節點,依次順着左子節點找下去,最後一個左子節點便是後繼節點;若是該右節點沒有左子節點,那麼該右節點即是後繼節點。
須要肯定後繼節點沒有子節點,若是後繼節點存在子節點,那麼又要分狀況討論了。
①、後繼節點是刪除節點的右子節點
這種狀況簡單,只須要將後繼節點表示的子樹移到被刪除節點的位置便可!
②、後繼節點是刪除節點的右子節點的左子節點
public Node getSuccessor(Node delNode){ Node successorParent = delNode; Node successor = delNode; Node current = delNode.rightChild; while(current != null){ successorParent = successor; successor = current; current = current.leftChild; } //將後繼節點替換刪除節點 if(successor != delNode.rightChild){ successorParent.leftChild = successor.rightChild; successor.rightChild = delNode.rightChild; } return successor; }
經過上面的刪除分類討論,咱們發現刪除實際上是挺複雜的,那麼其實咱們能夠不用真正的刪除該節點,只須要在Node類中增長一個標識字段isDelete,當該字段爲true時,表示該節點已經刪除,反正沒有刪除。那麼咱們在作好比find()等操做的時候,要先判斷isDelete字段是否爲true。這樣刪除的節點並不會改變樹的結構。
public class Node { int data; //節點數據 Node leftChild; //左子節點的引用 Node rightChild; //右子節點的引用 boolean isDelete;//表示節點是否被刪除 }
從前面的大部分對樹的操做來看,都須要從根節點到下一層一層的查找。
一顆滿樹,每層節點數大概爲2n-1,那麼最底層的節點個數比樹的其它節點數多1,所以,查找、插入或刪除節點的操做大約有一半都須要找到底層的節點,另外四分之一的節點在倒數第二層,依次類推。
總共N層共有2n-1個節點,那麼時間複雜度爲O(logn),底數爲2。
在有1000000 個數據項的無序數組和鏈表中,查找數據項平均會比較500000 次,可是在有1000000個節點的二叉樹中,只須要20次或更少的比較便可。
有序數組能夠很快的找到數據項,可是插入數據項的平均須要移動 500000 次數據項,在 1000000 個節點的二叉樹中插入數據項須要20次或更少比較,在加上很短的時間來鏈接數據項。
一樣,從 1000000 個數據項的數組中刪除一個數據項平均須要移動 500000 個數據項,而在 1000000 個節點的二叉樹中刪除節點只須要20次或更少的次數來找到他,而後在花一點時間來找到它的後繼節點,一點時間來斷開節點以及鏈接後繼節點。
因此,樹對全部經常使用數據結構的操做都有很高的效率。
遍歷可能不如其餘操做快,可是在大型數據庫中,遍歷是不多使用的操做,它更經常使用於程序中的輔助算法來解析算術或其它表達式。
用數組表示樹,那麼節點是存在數組中的,節點在數組中的位置對應於它在樹中的位置。下標爲 0 的節點是根,下標爲 1 的節點是根的左子節點,以此類推,按照從左到右的順序存儲樹的每一層。
樹中的每一個位置,不管是否存在節點,都對應於數組中的一個位置,樹中沒有節點的在數組中用0或者null表示。
假設節點的索引值爲index,那麼節點的左子節點是 2*index+1,節點的右子節點是 2*index+2,它的父節點是 (index-1)/2。
在大多數狀況下,使用數組表示樹效率是很低的,不滿的節點和刪除掉的節點都會在數組中留下洞,浪費存儲空間。更壞的是,刪除節點若是要移動子樹的話,子樹中的每一個節點都要移到數組中新的位置,這是很費時的。
不過若是不容許刪除操做,數組表示可能會頗有用,尤爲是由於某種緣由要動態的爲每一個字節分配空間很是耗時。
Node.java
package com.ys.tree; public class Node { int data; //節點數據 Node leftChild; //左子節點的引用 Node rightChild; //右子節點的引用 boolean isDelete;//表示節點是否被刪除 public Node(int data){ this.data = data; } //打印節點內容 public void display(){ System.out.println(data); } }
Tree.java
package com.ys.tree; public interface Tree { //查找節點 public Node find(int key); //插入新節點 public boolean insert(int data); //中序遍歷 public void infixOrder(Node current); //前序遍歷 public void preOrder(Node current); //後序遍歷 public void postOrder(Node current); //查找最大值 public Node findMax(); //查找最小值 public Node findMin(); //刪除節點 public boolean delete(int key); //Other Method...... }
BinaryTree.java
package com.ys.tree; public class BinaryTree implements Tree { //表示根節點 private Node root; //查找節點 public Node find(int key) { Node current = root; while(current != null){ if(current.data > key){//當前值比查找值大,搜索左子樹 current = current.leftChild; }else if(current.data < key){//當前值比查找值小,搜索右子樹 current = current.rightChild; }else{ return current; } } return null;//遍歷完整個樹沒找到,返回null } //插入節點 public boolean insert(int data) { Node newNode = new Node(data); if(root == null){//當前樹爲空樹,沒有任何節點 root = newNode; return true; }else{ Node current = root; Node parentNode = null; while(current != null){ parentNode = current; if(current.data > data){//當前值比插入值大,搜索左子節點 current = current.leftChild; if(current == null){//左子節點爲空,直接將新值插入到該節點 parentNode.leftChild = newNode; return true; } }else{ current = current.rightChild; if(current == null){//右子節點爲空,直接將新值插入到該節點 parentNode.rightChild = newNode; return true; } } } } return false; } //中序遍歷 public void infixOrder(Node current){ if(current != null){ infixOrder(current.leftChild); System.out.print(current.data+" "); infixOrder(current.rightChild); } } //前序遍歷 public void preOrder(Node current){ if(current != null){ System.out.print(current.data+" "); infixOrder(current.leftChild); infixOrder(current.rightChild); } } //後序遍歷 public void postOrder(Node current){ if(current != null){ infixOrder(current.leftChild); infixOrder(current.rightChild); System.out.print(current.data+" "); } } //找到最大值 public Node findMax(){ Node current = root; Node maxNode = current; while(current != null){ maxNode = current; current = current.rightChild; } return maxNode; } //找到最小值 public Node findMin(){ Node current = root; Node minNode = current; while(current != null){ minNode = current; current = current.leftChild; } return minNode; } @Override public boolean delete(int key) { Node current = root; Node parent = root; boolean isLeftChild = false; //查找刪除值,找不到直接返回false while(current.data != key){ parent = current; if(current.data > 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; //當前節點有一個子節點,右子節點 }else if(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; //當前節點有一個子節點,左子節點 }else if(current.leftChild != null && current.rightChild == null){ if(current == root){ root = current.leftChild; }else if(isLeftChild){ parent.leftChild = current.leftChild; }else{ parent.rightChild = current.leftChild; } return true; }else{ //當前節點存在兩個子節點 Node successor = getSuccessor(current); if(current == root){ root= successor; }else if(isLeftChild){ parent.leftChild = successor; }else{ parent.rightChild = successor; } successor.leftChild = current.leftChild; } return false; } public Node getSuccessor(Node delNode){ Node successorParent = delNode; Node successor = delNode; Node current = delNode.rightChild; while(current != null){ successorParent = successor; successor = current; current = current.leftChild; } //後繼節點不是刪除節點的右子節點,將後繼節點替換刪除節點 if(successor != delNode.rightChild){ successorParent.leftChild = successor.rightChild; successor.rightChild = delNode.rightChild; } return successor; } public static void main(String[] args) { BinaryTree bt = new BinaryTree(); bt.insert(50); bt.insert(20); bt.insert(80); bt.insert(10); bt.insert(30); bt.insert(60); bt.insert(90); bt.insert(25); bt.insert(85); bt.insert(100); bt.delete(10);//刪除沒有子節點的節點 bt.delete(30);//刪除有一個子節點的節點 bt.delete(80);//刪除有兩個子節點的節點 System.out.println(bt.findMax().data); System.out.println(bt.findMin().data); System.out.println(bt.find(100)); System.out.println(bt.find(200)); } }
咱們知道計算機裏每一個字符在沒有壓縮的文本文件中由一個字節(好比ASCII碼)或兩個字節(好比Unicode,這個編碼在各類語言中通用)表示,在這些方案中,每一個字符須要相同的位數。
有不少壓縮數據的方法,就是減小表示最經常使用字符的位數量,好比英語中,E是最經常使用的字母,咱們能夠只用兩位01來表示,2位有四種組合:00、0一、十、11,那麼咱們能夠用這四種組合表示四種經常使用的字符嗎?
答案是不能夠的,由於在編碼序列中是沒有空格或其餘特殊字符存在的,全都是有0和1構成的序列,好比E用01來表示,X用01011000表示,那麼在解碼的時候就弄不清楚01是表示E仍是表示X的起始部分,因此在編碼的時候就定下了一個規則:每一個代碼都不能是其它代碼的前綴。
二叉樹中有一種特別的樹——哈夫曼樹(最優二叉樹),其經過某種規則(權值)來構造出一哈夫曼二叉樹,在這個二叉樹中,只有葉子節點纔是有效的數據節點(很重要),其餘的非葉子節點是爲了構造出哈夫曼而引入的!
哈夫曼編碼是一個經過哈夫曼樹進行的一種編碼,通常狀況下,以字符:‘0’與‘1’表示。編碼的實現過程很簡單,只要實現哈夫曼樹,經過遍歷哈夫曼樹,規定向左子樹遍歷一個節點編碼爲「0」,向右遍歷一個節點編碼爲「1」,結束條件就是遍歷到葉子節點!由於上面說過:哈夫曼樹葉子節點纔是有效數據節點!
咱們用01表示S,用00表示空格後,就不能用01和11表示某個字符了,由於它們是其它字符的前綴。在看三位的組合,分別有000,001,010,100,101,110和111,A是010,I是110,爲何沒有其它三位的組合了呢?由於已知是不能用01和11開始的組合了,那麼就減小了四種選擇,同時011用於U和換行符的開始,111用於E和Y的開始,這樣就只剩下2個三位的組合了,同理能夠理解爲何只有三個四位的代碼可用。
因此對於消息:SUSIE SAYS IT IS EASY
哈夫曼編碼爲:100111110110111100100101110100011001100011010001111010101110
若是收到上面的一串哈夫曼編碼,怎麼解碼呢?消息中出現的字符在哈夫曼樹中是葉節點,也就是沒有子節點,以下圖:它們在消息中出現的頻率越高,在樹中的位置就越高,每一個圓圈外面的數字就是頻率,非葉節點外面的數字是它子節點數字的和。
每一個字符都從根開始,若是遇到0,就向左走到下一個節點,若是遇到1,就向右。好比字符A是010,那麼先向左,再向右,再向左,就找到了A,其它的依次類推。
樹是由邊和節點構成,根節點是樹最頂端的節點,它沒有父節點;二叉樹中,最多有兩個子節點;某個節點的左子樹每一個節點都比該節點的關鍵字值小,右子樹的每一個節點都比該節點的關鍵字值大,那麼這種樹稱爲二叉搜索樹,其查找、插入、刪除的時間複雜度都爲logN;能夠經過前序遍歷、中序遍歷、後序遍從來遍歷樹,前序是根節點-左子樹-右子樹,中序是左子樹-根節點-右子樹,後序是左子樹-右子樹-根節點;刪除一個節點只須要斷開指向它的引用便可;哈夫曼樹是二叉樹,用於數據壓縮算法,最常常出現的字符編碼位數最少,不多出現的字符編碼位數多一些。