爲何要用到二叉樹?java
先來看看有序數組的優缺點:windows
在有序數組中作查詢,可使用二分查找法,會提升查找效率,縮短查找次數,而用二分查找法的效率是O(logN)。也可使用順序遍歷訪問每一個數據項。數組
然而,在有序數組中,想要插入數據項和刪除數據項,須要先找到數據項的位置,而後將數據項後面的數據移動,平均移動數據項次數是N/2,因此很慢。數據結構
查找插入位置若是用遍歷查找的是O(n),用二分查找是O(log2n)。post
可是數組的插入操做須要將插入位置後的元素所有後移一位,這須要O(n)。編碼
因此總的時間複雜度是O(n)。(O(n)+O(n)=O(n),O(log2n)+O(n)=O(n))。spa
顯而易見,須要屢次插入和刪除數據,不適合使用有序數組。3d
再來看看鏈表的優缺點:code
在鏈表中作刪除和插入操做,須要改變一些引用的值就好了,這些操做的時間複雜度是O(1)。blog
遺憾的是,鏈表中查找數據項可不是那麼容易。查找必須從頭開始,依次訪問鏈表中的每個數據項,直到找到該數據項爲止。所以,須要平均訪問N/2個數據項,而且每次都要和數據項比較,這個過程很慢,費時O(N)。
用樹解決問題:
要是能有一種數據結構,既能像有序數組那樣快速查找數據,又能夠向鏈表同樣快速插入和刪除。
根:樹的頂層,一棵樹只有一個根,一個根能夠有多個子節點。
葉節點:也叫葉子節點,在一棵樹中,沒有子節點的節點,稱爲葉子節點,說明葉子節點已是樹的底部了。
路徑:從一個節點,但願走到另外一個節點,所通過的節點的順序排列就被稱爲「路徑」。好比在windows系統中,咱們要查找一個文件,須要先找到C盤,而後進入C盤後,再查找包含文件的文件夾,打開以後,再一級級打開文件夾,直到找到文件,那麼咱們訪問過的文件夾目錄,就是文件的路徑。
子樹:每一個節點均可以做爲子樹的根節點,這個節點和它包含的子節點,孫節點,組成的樹就是子樹。
層:一棵樹中,能夠把根節點看作是0層,根節點的子節點,看作是1層,根節點的孫節點,看作是2層等等。一棵樹的層數,體現了這棵樹的深度。
二叉樹:每一個節點最多隻有兩個子節點,咱們稱做二叉樹,而且二叉樹的兩個子節點稱做,左子節點,右子節點。
二叉搜索樹:一個二叉樹中,父節點的左子節點小於這個節點,右子節點大於等於父節點,稱做二叉搜索樹。
非平衡樹:樹中大部分節點在根的一邊或者另外一邊,個別子樹也多是非平衡的
二叉樹查找節點:
代碼實現
public class Node { Person data; Node leftNode; Node rightNode; public void displayNode() { } } class Person { int iData; double dData; public Person(int i, double d) { iData = i; dData = d; } } class Tree { Node root; public Node find(int key) { Node current = root; while (current.data.iData != key) { if (key < current.data.iData) { if (current.leftNode != null) { current = current.leftNode; } else { return null; } } else { if (current.rightNode != null) { current = current.rightNode; } else { return null; } } } return current; } }
樹的查找效率:
查找節點的時間,取決於該節點在樹中的層數。最多31個節點,不超過5層----則最多隻須要5次比較,就能夠找到這個節點。他的時間複雜度度是O(logN),更緊缺的說是O(log2N),以2爲底的對數。
插入一個節點:
向樹中插入新節點45
代碼實現:
public void insert(int i, double d){ Node newNode = new Node(i, d); if(root == null){ root = newNode; }else{ Node current = root; while(true){ if(i < current.data.iData){ current = current.leftChild; if(current == null){ current.leftChild = newNode; return; } }else{ current = current.rightChild; if(current == null){ current.rightChild = newNode; return; } } } } }
遍歷樹:
遍歷樹的意思是根據一種特定順序,訪問樹的每個節點。有三種簡單方法:前序(preorder),中序(inorder),後序(postorder)。二叉樹最經常使用的方法是中序遍歷。
中序遍歷:
中序遍歷二叉樹會使全部的節點按關鍵字值升序被訪問到。若是但願二叉樹建立有序的數據序列,這是一種方法。
遍歷數最簡單的方法是遞歸,用遞歸方法遍歷整棵樹要用一個節點做爲參數,初始化這個節點爲根節點,這個方法只須要作三件事:
1.調用自身來遍歷節點的左子樹
2.訪問這個節點
3.調用自身來遍歷節點的右子樹
中序遍歷java代碼實現:
public void inOrder(Node current) { if(current != null){ inOrder(current.leftChild); System.out.println(current.data.iData); inOrder(root.rightChild); } }
前序遍歷:
同中序遍歷同樣,前序遍歷也要通過三個步驟,可是前序遍歷的順序與中序遍歷不一致:
1.訪問自身節點
2.調用自身遍歷該節點的左子樹
3.調用自身遍歷該節點的右子樹
後序遍歷:
後序遍歷,把三個步驟的順序又換了一下:
1.調用自身遍歷節點的左子樹
2.調用自身遍歷節點的右子樹
3.訪問自身節點
中序遍歷輸出結果:A*B+C
前序遍歷輸出結果:*A+BC
後序遍歷輸出結果:ABC+*
查詢最大值和最小值:
在二叉搜索樹中想獲得最大值和最小值是很容易的事情,由於二叉搜索樹已經將最小的節點放在樹的最左葉節點上了,最大節點放在樹的最右葉節點上了。
取最小值,最大值的JAVA代碼實現:
public Node minimum() { Node current = root; Node last = null; while (current != null) { last = current; current = current.leftChild; } return last; } public Node maximum(){ Node current = root; Node last = null; while (current != null) { last = current; current = current.rightChild; } return last; }
刪除節點:
刪除節點是二叉樹經常使用操做,可是也是最複雜的。
刪除節點要從查找開始入手,找到這個節點後,刪除操做要通過下面的步驟:
1.該節點是不是葉子節點
2.該節點有一個子節點
3.該節點有兩個子節點
下面將依次介紹這三種狀況。第一種最簡單:第二種也比較簡單;第三種相對複雜了。
狀況一:刪除沒有子節點的葉節點
要刪除葉節點,只須要改變該節點的父節點的對應子節點的值,好比以前父節點是指向該葉節點的,如今把父節點的這個指向改成null。雖然要刪除的節點還存在,可是它已經不屬於這棵樹了。java的自動垃圾回收機制,發現該節點沒有任何引用了,就會回收掉。
刪除沒有任何子節點的節點JAVA代碼實現:
public boolean delete(int key) { Node parent = root; Node current = root; boolean isLeftChild = true; // 循環查找待刪除結點,並記錄查找到的節點與父節點的關係:左子節點仍是右子節點 while (current.data.iData != key) { if (key < parent.data.iData) { current = parent.leftChild; } else if (key > parent.data.iData) { isLeftChild = false; current = parent.rightChild; } } // 沒找到節點,返回false 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; }
狀況二刪除有一個子節點的節點JAVA代碼實現:
// 刪除有一個子節點的節點 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; } } if(current.rightChild != null && current.leftChild == null){ if(current == root){ root = current.rightChild; }else if(isLeftChild){ parent.leftChild = current.leftChild; }else{ parent.rightChild = current.leftChild; } }
狀況三刪除有兩個子節點的節點:
當咱們要刪除節點25時,剛好節點25包含兩個子節點15和35,且子節點15和35,都包含有各自的子節點,那麼該使用哪一個節點做爲後繼節點?由於搜索二叉樹中,比當前節點大的節點必定在當前節點的右邊,因此咱們須要在刪除節點25的右邊的集合裏,尋找一個最小的值,來當後繼節點。就是說在待刪除節點的右子節點裏,尋找左子節點,若是左子節點還有左子節點,則一路向下返回最後一個左子節點,此時該左子節點就是比刪除節點大的集合裏的最小節點,來當後繼節點。
樹的大部分工做,都是須要從上到下一層一層地查找到某個節點。一棵滿樹中,大約有一半的節點,在樹的最底層(準確的說,一棵滿樹,最底層的節點梳篦上面的節點數多一個),所以,查找、插入或刪除節點的操做大約有一半都須要找到最底層的節點。(另外還有四分之一節點的這些操做要到倒數第二層,以此類推)。
二叉搜索樹比較的次數 L=log2(N+1) 大概是N以2爲底的對數。
在大O表示法中,表示爲O(logN),若是樹不滿,那麼不滿的樹平均查找時間比滿樹要短,由於在它層數較低的子樹上完成查找的次數要比滿樹時少。
樹的優勢是,查找和刪除元素比較快,可是遍歷不如其餘操做快,可是遍歷在大型數據結構中不是經常使用的操做。
用數組的方式存儲樹時,節點存在數組中,而不是由引用相連。節點在數組中的位置對應於它在樹中的位置。下標爲0的節點是根,下標爲1的是左子節點,下標爲的2是右子節點,以此類推,按照從左到右的順序,依次存儲樹的每一層。
樹中的每一個位置,不管節點是否存在,都對應數組中的一個位置。把節點插入樹的一個位置,意味着要在數組的相應的位置插入一個數據項,樹中沒有節點的位置在數組中對應0或null。
基於這種思想,要在數組中查找樹的節點能夠利用簡單的算術計算它們在數組中的索引值。
設節點索引值爲index,則節點的左子節點是:
2*index+1
右子節點是
2*index+2
它的父節點是
(index-1)/2 (「/」表示整除運算)
大多數狀況下,用數組表示樹不是頗有效率。不滿的節點和刪除後的節點,在數組中會留下洞,浪費空間。更壞的是,若是刪除節點時須要移動子樹,子樹中的每一個節點都要移動到新位置上去,這在比較大的樹中很費時。
不過,若是不容許刪除,數組存儲樹會頗有用,特別是某些緣由要動態地爲每一個節點分配存儲空間比較費時。數組表示發在特定的狀況下也頗有用,好比要將樹的節點,畫到屏幕上固定的位置上,就可使用數組先存儲整棵樹,經過遍歷數組,取出樹的節點用於顯示。
和其餘數據結構同樣,重複的關鍵字必需要被提到。
有重複關鍵字的節點都插入到與他關鍵字相同的節點的右子節點處。
問題是,find()方法只能找到兩個(或多個)相同關鍵字節點中的第一個。能夠修改查找方法,區分相同關鍵字的數據項,可是這樣作很耗時。
一種簡單的選擇是禁止重複關鍵字,使用樹來存儲不會具備相同關鍵字的數據,好比員工id。再或者就是在插入操做時,檢查關鍵字是否相同,相同時放棄插入操做。
二叉樹並不全是搜索樹,不少二叉樹用於其餘狀況。
哈夫曼編碼,是使用二叉樹來壓縮數據,1952年被david huffman發現這種方法後,就稱它爲哈弗曼編碼。數據壓縮在不少領域都很重要。