文章目錄
1、樹
樹(tree)是一種抽象數據類型(ADT),用來模擬具有樹狀結構性質的數據集合。它是由n(n>0)個有限節點通過連接它們的邊組成一個具有層次關係的集合。把它叫做“樹”是因爲它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。
①、節點:上圖的圓圈,比如A,B,C等都是表示節點。節點一般代表一些實體,在java面向對象編程中,節點一般代表對象。
②、邊:連接節點的線稱爲邊,邊表示節點的關聯關係。一般從一個節點到另一個節點的唯一方法就是沿着一條順着有邊的道路前進。在Java當中通常表示引用。
樹有很多種,向上面的一個節點有多餘兩個的子節點的樹,稱爲多路樹,後面會講解2-3-4樹和外部存儲都是多路樹的例子。而每個節點最多只能有兩個子節點的一種形式稱爲二叉樹,這也是本篇博客講解的重點。
樹的常用術語
①、路徑:順着節點的邊從一個節點走到另一個節點,所經過的節點的順序排列就稱爲“路徑”。
②、根:樹頂端的節點稱爲根。一棵樹只有一個根,如果要把一個節點和邊的集合稱爲樹,那麼從根到其他任何一個節點都必須有且只有一條路徑。A是根節點。
③、父節點:若一個節點含有子節點,則這個節點稱爲其子節點的父節點;B是D的父節點。
④、子節點:一個節點含有的子樹的根節點稱爲該節點的子節點;D是B的子節點。
⑤、兄弟節點:具有相同父節點的節點互稱爲兄弟節點;比如上圖的D和E就互稱爲兄弟節點。
⑥、葉節點:沒有子節點的節點稱爲葉節點,也叫葉子節點,比如上圖的H、E、F、G都是葉子節點。
⑦、子樹:每個節點都可以作爲子樹的根,它和它所有的子節點、子節點的子節點等都包含在子樹中。
⑧、節點的層次:從根開始定義,根爲第一層,根的子節點爲第二層,以此類推。
⑨、深度:對於任意節點n,n的深度爲從根到n的唯一路徑長,根的深度爲0;
⑩、高度:對於任意節點n,n的高度爲從n到一片樹葉的最長路徑長,所有樹葉的高度爲0;
2、二叉搜索樹 BST
二叉樹:樹的每個節點最多只能有兩個子節點
上圖的第一幅圖B節點有DEF三個子節點,就不是二叉樹,稱爲多路樹;而第二幅圖每個節點最多隻有兩個節點,是二叉樹,並且二叉樹的子節點稱爲“左子節點”和“右子節點”。上圖的D,E分別是B的左子節點和右子節點。
如果我們給二叉樹加一個額外的條件,就可以得到一種被稱作二叉搜索樹(binary search tree)的特殊二叉樹。
二叉搜索樹要求:
若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 它的左、右子樹也分別爲二叉排序樹。
二叉搜索樹作爲一種數據結構,那麼它是如何工作的呢?它查找一個節點,插入一個新節點,以及刪除一個節點,遍歷樹等工作效率如何,下面我們來一一介紹。
2.1 二叉樹的節點類 Node<AnyType>
private static class Node<AnyType> {
AnyType element;
Node<AnyType> leftChild;
Node<AnyType> rightChild;
boolean isDelete;
Node(AnyType theElement) {
this(theElement, null, null);
}
Node(AnyType theElement, Node<AnyType> lt, Node<AnyType> rt) {
element = theElement;
leftChild = lt;
rightChild = rt;
isDelete = false;
}
}
private Node<AnyType> root;
2.2 查找節點 find(x)
查找某個節點,我們必須從根節點開始遍歷。
①、查找值比當前節點值大,則搜索右子樹;
②、查找值等於當前節點值,停止搜索(終止條件);
③、查找值小於當前節點值,則搜索左子樹;
//查找結點
public Node<AnyType> find(AnyType x) {
Node<AnyType> current = root;
int compareResult = x.compareTo(current.element);
while (current != null) {
if (compareResult < 0)
current = current.leftChild;
else if (compareResult > 0)
current = current.rightChild;
else return current;
}
return null;
}
用變量current來保存當前查找的節點,參數key是要查找的值,剛開始查找將根節點賦值到current。接在在while循環中,將要查找的值和current保存的節點進行對比。如果key小於當前節點,則搜索當前節點的左子節點,如果大於,則搜索右子節點,如果等於,則直接返回節點信息。當整個樹遍歷完全,即 current==null
,那麼說明沒找到查找值,返回null。
樹的效率:查找節點的時間取決於這個節點所在的層數,每一層最多有2n-1個節點,總共N層共有2n-1個節點,那麼時間複雜度爲 O(logN)
,底數爲2。最壞的時間複雜度 O(n)
2.3 查找節點 contains(x)
如果在樹T中存在含有項X的節點,那麼contains方法返回true,否則返回false.如果樹T是空集,則返回false.
/**
* contain
* 如果在樹T中存在含有項X的節點,那麼contains方法返回true
* 否則返回false.如果樹T是空集,則返回false.
* @param x 待檢測的元素
* @param node 樹,一般爲根節點
* @return 存在含有項X的節點 返回true
*/
public boolean contains(AnyType x,BinaryNode<AnyType> node){
if(x == null)
return false;
int compareResult = x.compareTo(node.element);
if(compareResult < 0)
return contains(x, node.leftChild);
else if(compareResult > 0)
return contains(x, node.rightChild);
else return true;
}
2.4 插入節點 insert(x)
二叉搜索樹的插入總是在最後一層插入!下圖所示,
首先currentNode = 41 ,當要插入的節點是42時,
比較 41 < 42 ----> 右子樹 —> currentNode =65;
比較 65 > 42 —> 左子樹 —> currentNode=50
比較 50 > 42 ---->左子樹 ----> currentNode = Null,所以找到了插入的位置!在此處插入42
//插入新節點
public boolean insert(AnyType x){
return insert(x,root);
}
private boolean insert(AnyType x,BinaryNode<AnyType> node)
{
BinaryNode newNode = new BinaryNode<>(x);
if (root == null) { //如果樹爲空,則把新插入的節點設爲根節點
root = newNode;
return true;
}
else {
BinaryNode<AnyType> current = node;
BinaryNode<AnyType> parentNode = null;
while(current != null) {
parentNode = current;
int compareResult = x.compareTo(current.element);
if (compareResult < 0) {
current = current.leftChild;
if(current == null) {
parentNode.leftChild = newNode;
return true;
}
}
else if (compareResult > 0) {
current = current.rightChild;
if(current == null) {
parentNode.rightChild = newNode;
return true;
}
} else return false;
}
}
}
2.5 找最大/最小值
對於findMin方法,思路是一直找當前節點的左子樹,直到currentNode == null!最後尋到的非空節點就是最小值。
對於findMin方法,思路是一直找當前節點的右子樹,直到currentNode == null!最後尋到的非空節點就是最大值。
/**
* findMin() findMax()
* 對於findMin方法,思路是一直找當前節點的左子樹,直到currentNode == null!最後尋到的非空節點就是最小值。
* 對於findMin方法,思路是一直找當前節點的右子樹,直到currentNode == null!最後尋到的非空節點就是最大值。
* @return 最大值/最小值
*/
public AnyType findMin(){
if(isEmpty())
System.out.println("The tree is empty,can't find the minimum node!");
return findMin(root);
}
private AnyType findMin(BinaryNode<AnyType> node){
if(node != null)
while(node.leftChild != null)
node = node.leftChild;
return node.element;
}
public AnyType findMax(){
if(isEmpty())
System.out.println("The tree is empty,can't find the minimum node!");
return findMax(root);
}
private AnyType findMax(BinaryNode<AnyType> node){
if(node != null)
while(node.rightChild != null)
node = node.rightChild;
return node.element;
}
2.6 刪除節點 remove() ——方法1
刪除節點是二叉搜索樹中最複雜的操作,刪除的節點有三種情況,前兩種比較簡單,但是第三種卻很複雜。
- 該節點是葉節點(沒有子節點)
- 該節點有一個子節點
- 該節點有兩個子節點
對於刪除,最複雜的是該將被刪除的節點A具有兩個兒子的情況,一般是該節點的右子樹最小的節點B代替節點A,然後在B原來的位置刪除掉節點B。
如果刪除的次數不多,通常使用懶惰刪除:當一個元素要被刪除時,它仍在樹中,而只是標記爲刪除。 特別是刪除算法會使得左子樹比右子樹深,因爲我們總是用右子樹的一個來代替刪除的節點。會造成二叉查找樹,嚴重的不平衡。
①、刪除沒有子節點的節點
要刪除葉節點,只需要改變該節點的父節點引用該節點的值,即將其引用改爲 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;
}
2.7 刪除節點 remove() ——方法2
上面討論過三種不同的情況,最難的刪除節點有兩個子樹。例如圖中的50,思路是:找右子樹的最小值代替刪除節點!
public BinaryNode<AnyType> remove(AnyType t, BinaryNode<AnyType> node) {
if (node == null) {
return node;
}
int compareResult = t.compareTo(node.element);
if (compareResult > 0) {
node.rightChild = remove(t, node.rightChild);
} else if (compareResult < 0) {
node.leftChild = remove(t, node.leftChild);
} else if (node.leftChild != null && node.rightChild != null) {
node.element = findMin(node.rightChild).element;
node.rightChild = remove(node.element, node.rightChild);
} else {
node = (node.leftChild != null) ? node.leftChild : node.rightChild;
}
return node;
}
3、遍歷
//中序遍歷
private void infixOrder(BinaryNode<AnyType> current){
if(current != null){
infixOrder(current.leftChild);
System.out.print(current.element+" ");
infixOrder(current.rightChild);
}
}
//前序遍歷
private void preOrder(BinaryNode<AnyType> current){
if(current != null){
System.out.print(current.element+" ");
infixOrder(current.leftChild);
infixOrder(current.rightChild);
}
}
//後序遍歷
private void postOrder(BinaryNode<AnyType> current){
if(current != null){
infixOrder(current.leftChild);
infixOrder(current.rightChild);
System.out.print(current.element+" ");
}
}