我們先了解有序數組和鏈表兩種數據結構:有序數組,可以通過二分查找法快速的查詢特定的值,時間復雜度為O(logN),可是插入刪除時效率低,平均要移動N/2個元素,時間復雜度為O(N)。鏈表:查詢效率低,平均要比較N/2個元素,時間復雜度O(N),插入和刪除效率較高,O(1)。二叉樹的特點是結合了有序數組和鏈表的優點,能像有序數組那樣快速的查找,又能像鏈表那樣快速的插入和刪除。操作二叉搜索樹的時間復雜度是O(logN)。
1.定義
二叉樹:樹中的每個節點最多只能有兩個子節點,這樣的樹是二叉樹。
二叉搜索樹:一個節點的左子節點的關鍵字值小於這個父節點,右子節點的關鍵字值大於等於這個父節點。
平衡二叉搜索樹:它是一顆裸空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩棵子樹都是平衡二叉樹。它的實現方式有:紅黑樹,AVL
2.結構圖
二叉樹由節點和邊構成,每個節點包含的有關鍵字值,以及其他數據。接下來我們通過代碼看如何對二叉搜索樹進行插入,刪除,查找,遍歷等操作。
3.查找操作
先定義節點Node
public class Node { int key; //key 關鍵字值 double data; //存儲的數據 Node leftChild; //左子節點的引用 Node rightChild; //右子節點的引用 //顯示該節點內容 public void displayNode(){ System.out.println("key="+key+",data="+data); } }
定義二叉樹Tree
public class Tree { //根節點 public Node root;
}
查找方法:將關鍵字值key與根節點的關鍵字值做比較,小於root節點的關鍵字值,進入root節點的左子樹進行比較;否則如果大於,進入root節點的右子樹進行比較。依次比較下去,直到key的值等於某個節點的關鍵字值,則將該節點Node返回。如果沒有找到
符合條件的節點,那么返回null
//查詢效率和有序數組中的二分查找法一樣,時間復雜度為O(log2N) public Node find(int key){ Node current = root; while (current.key != key) { if(key < current.key) current = current.leftChild; else{ current = current.rightChild; } //表示沒有找到符合條件的節點 if (null == current) return null; } return current; }
4.插入操作
插入操作,先要確定新節點要插入的位置,這個就是查找的過程;然后確定位置后,將新節點newNode作為父節點parent的的左子節點或者右子節點
//插入方法:查找最后一個不為null的節點parent作為要插入節點的父節點,newNode作為它的左子節點或者右子節點 public void insert(int key,double data){ Node newNode = new Node(); newNode.key = key; newNode.data = data; if(null == root){ root = newNode; }else{ Node current = root; Node parent; while(true){ parent = current; if(key < current.key){ //go left current = current.leftChild; if(null == current){ parent.leftChild = newNode; return; } }else{ //go right current = current.rightChild; if(null == current){ parent.rightChild = newNode; return; } } } } }
5.遍歷操作
遍歷操作,是指根據特定的順序訪問樹的每一個節點。遍歷方法有:中序遍歷,前序遍歷,后序遍歷
中序遍歷,會使所有節點按照關鍵字值的升序被訪問到。遍歷時,通常使用遞歸調用的方法,初始參數是樹的根節點。中序遍歷會有三個步驟:
* 調用自身來遍歷該節點的左子樹
* 訪問這個節點
* 調用自身來遍歷該節點的右子樹
//中序遍歷二叉樹:按key值的升序排序 public void orderByMiddle(Node localRoot){ if(null != localRoot){ orderByMiddle(localRoot.leftChild); System.out.println(localRoot.data); orderByMiddle(localRoot.rightChild); } } //前序遍歷二叉樹 public void preOrder(Node localRoot){ if(null != localRoot){ System.out.println(localRoot.data); preOrder(localRoot.leftChild); preOrder(localRoot.rightChild); } } //后序遍歷二叉樹 public void postOrder(Node localRoot){ if(null != localRoot){ postOrder(localRoot.leftChild); postOrder(localRoot.rightChild); System.out.println(localRoot.data); } }
6.查找最大值和最小值
查找最大值,從根節點開始走向右子節點,然后一直走向右子節點,直到最后一個不為null的右子節點,就是最大值。查找最小值,是類似的,一直走向左子節點。
//查找最大值 public Node maxNum() { Node current,last = null; current = root; while (null != current) { last = current; current = current.rightChild; } return last; } //查找最小值 public Node miniNum() { Node current,last = null; current = root; while (null != current) { last = current; current = current.leftChild; } return last; }
7.刪除操作
對於刪除操作的邏輯如下:
* 要先判斷被刪除的節點的情況,然后分別處理,被刪除節點可能是:是葉節點,只有一個子節點,有兩個子節點
* 如果被刪除節點有兩個子節點:找到要刪除節點的后繼節點,然后讓后繼節點替換該節點。由於二叉搜索樹要滿足的特性是一個節點的左子節點的key值要比該節點小,右子節點的key值要比該節點大,所以,一個節點的后繼節點就是比該節點key值大的最小的那個節點。也就是該節點的右子節點的左子節點,再左子節點,直到最后一個不是null的左子節點,就是該節點的后繼節點。找到后繼節點后,就將后繼節點替換當前節點,涉及到各個節點引用的改變,有四個地方的引用改變,也就是代碼中的abcd四個步驟。
如果要刪除的節點有兩個子節點,看圖:
//刪除方法 //1.要刪除的節點是葉節點 2.只有一個字節點 3.有兩個子節點 //步驟:1.先找到要刪除的節點 public boolean delete(int key){ //1.先查找到要刪除的節點 Node current = root; Node parent = root; //標識被刪除的節點是否是左子節點 boolean isLeftChild = true; while(key != current.key){ parent = current; if(key < current.key){ current = current.leftChild; isLeftChild = true; }else{ current = current.rightChild; isLeftChild = false; } //沒有找到要刪除的節點 if(null == current){ return false; } } //2.如果該節點沒有子節點 if(null == current.leftChild && null == current.rightChild){ //判斷該節點是否是根節點 if(current == root){ root = null; }else if(isLeftChild) { parent.leftChild = null; }else{ parent.rightChild = null; } //3.如果該節點只有一個子節點:左子節點 }else if(null == current.rightChild){ //被刪除的節點如果是根節點 if(current == root){ root = current.leftChild; }else if(isLeftChild){ parent.leftChild = current.leftChild; }else{ parent.rightChild = current.leftChild; } //4.如果該節點只有一個子節點:右子節點 }else if(null == current.leftChild){ if(current == root){ root = current.rightChild; }else if(isLeftChild){ parent.leftChild = current.rightChild; }else{ parent.rightChild = current.rightChild; } //5.如果要刪除的節點有兩個子節點:需要找到該節點的后繼節點代替該節點,后繼節點就是key值比當前節點大的那個最小的值 }else{ //先找到后繼節點successor Node successor = getSuccessor(current); if(current == root){ root = successor; }else if(isLeftChild){ //c 將后繼節點賦值給要刪除節點的父節點的左子節點 parent.leftChild = successor; }else{ //c 將后繼節點賦值給要刪除節點的父節點的右子節點 parent.rightChild = successor; } //d 將要刪除節點的左子節點賦值給后繼節點的左子節點 successor.leftChild = current.leftChild; } return true; } //查找后繼節點,並替換部分引用 a,b private 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; //a 把后繼節點的右子節點賦值給后繼節點的父節點的左子節點 successor.rightChild = delNode.rightChild; //b 把要刪除節點的右子節點賦值給后繼節點的右子節點 } return successor; }
8.用數組表示二叉樹
其實,除了以上那種方式表示二叉樹外,還可以用數組表示二叉樹。用數組時,節點存儲在數組中,節點再數組中的位置對應於它在樹中的位置,通過下圖可以理解它的存儲方式。
9.重復關鍵字
由於二叉搜索樹的特性是:右子節點的key值大於等於父節點,所以在insert操作中,關鍵字key值相同的節點插入到與它相同的節點的右子節點處。在查詢操作時,獲取到的是多個相同節點的第一個節點。
注:以上圖片摘自《Java數據結構和算法》這本書