1. 程式人生 > >【資料結構與演算法】之二叉查詢樹 --- 第十三篇

【資料結構與演算法】之二叉查詢樹 --- 第十三篇

樹是一種非線性資料結構,這種資料結構要比線性資料結構複雜的多,因此分為三篇部落格進行講解:

第一篇:樹的基本概念及常用操作的Java實現(二叉樹為例)

第二篇:二叉查詢樹

第三篇:紅黑樹


本文目錄

1、二叉查詢樹的基本概念

2、二叉查詢樹的查詢操作

3、二叉查詢樹的插入操作

4、二叉查詢樹的刪除操作    ---- 重要

4.1  待刪除結點沒有子結點

4.2  待刪除結點存在左子結點或右子結點時

4.3  待刪除結點同時存在左右子結點時

4.4  三種情況程式碼整合   

5、支援重複資料的二叉查詢樹

6、二叉查詢樹的時間複雜度分析

7、二叉查詢樹和散列表的對比

8、二叉查詢樹的常用操作全程式碼實現

推薦及參考:


第二篇:二叉查詢樹BST

1、寫在前面:二叉查詢樹最大的特點就是:支援動態資料集合的快速插入、刪除和查詢操作

2、文末附:二叉查詢樹的常用操作。

3、這裡需要進行說明下,前面講二叉查詢樹的時候,我們預設都是樹中結點儲存的都是數字。很多時候,在實際的軟體開發中,我們在二叉查詢樹中儲存的,是一個包含很多欄位的物件。我們利用物件的某個欄位作為鍵值(key)來構建二叉查詢樹。我們把物件中的其他欄位叫做衛星資料。

1、二叉查詢樹的基本概念

二叉查詢樹BST(Binary  Sort  Tree):又稱為二叉排序樹/二叉搜尋樹。顧名思義,二叉查詢樹是為了實現快速查詢而生的。不過它不僅僅支援快速查詢一個數據,還支援快速插入、刪除一個數據。

具有以下性質的二叉樹稱為二叉查詢樹:

1、若左子樹不為空,則左子樹上所有結點的值均小於它的根結點的值;

2、若右子樹不為空,則右子樹上所有的結點的值均大於或等於它的根結點的值;

3、左、右子樹也分別為二叉查詢樹。

 

從上圖可以看出來,二叉查詢樹中,左子樹都比父結點小,右子樹都比父結點大,遞迴定義。

所以,根據這個特點,二叉查詢樹的中序遍歷的結果肯定是有序的,上圖中中序遍歷結果為:1、3、4、6、7、8、10、13、14。


2、二叉查詢樹的查詢操作

根據二叉查詢樹的定義,我們可以將二叉查詢樹的查詢操作大概分為以下幾個步驟:

1、先比較它與根節點,相等就返回;或者根節點為空,說明樹為空,也返回;

2、如果它比根節點小,就從根節點的左子樹中進行遞迴查詢;

3、如果它比根節點大,就從根節點的右子樹中進行遞迴查詢。

【可以看出來,二叉查詢樹的查詢操作與二分查詢十分相似】

public class BinarySearchTree {

	private Node root;  // 根節點
	
	// 二叉查詢樹的查詢操作
	public Node find(int data){
		Node node = root;
		while(node != null){
			if(data < node.data)       node = node.leftChild;
			else if(data > node.data)  node = node.rightChild;
			else return node;
		}
		return null;
	}
	
	// Node內部類
	public static class Node{
		private int data;
		private Node leftChild;
		private Node rightChild;
		
		public Node(int data){
			this.data = data;
		}
	}
}

3、二叉查詢樹的插入操作

二叉查詢樹的插入操作有點類似於查詢操作。先查詢到合適的位置再插入,新插入的資料一般都在葉子結點上,所以我們只需要從根結點開始,依次比較要插入的資料和結點的大小關係。

二叉查詢樹的插入操作的關鍵幾個步驟如下:

1、如果要插入的資料比根結點的資料大,並且根結點的右子樹為空,就將新資料直接插入到右子結點的位置;如果不為空,就再遞迴遍歷右子樹,查詢插入位置;

2、同理,如果要插入的資料比根結點數值小,並且結點的左子樹為空,就將新資料插入到左子結點的位置;如果不為空,則遍歷左子樹,查詢插入位置。

// 二叉查詢樹的插入操作
public void insert(int data){
	if(root == null){
		root = new Node(data);
	}
		
	Node node = root;
	while(node != null){
		if(data > node.data){
			if(node.rightChild == null){
				node.rightChild = new Node(data);  // 右子結點為空,直接將新結點設定為當前結點的右子結點
			}
			node = node.rightChild;  // 右子結點不為空,則繼續遍歷當前結點的右子樹
		}else{   // data < node.data
 			if(node.leftChild == null){
 				node.leftChild = new Node(data);   // 左子結點為空,直接將新結點設定為當前結點的左子結點
 			}
 			node = node.leftChild;   // 左子結點不為空,則繼續遍歷當前結點的左子樹
		}
	}
}

4、二叉查詢樹的刪除操作

二叉查詢樹的刪除操作不是很好理解,該章節出自https://blog.csdn.net/isea533/article/details/80345507

二叉查詢樹的刪除操作相對於它的查詢和插入操作來說複雜的多。針對要刪除的結點的子結點個數的不同,我們需要分成三種情況來處理:

1、要刪除結點沒有左右子結點,可以直接刪除;

2、存在左結點或者右結點中的一個,刪除後需要對子結點移動;

3、同時存在左右子結點時,不能簡單地刪除,但是可以通過和後繼結點交換後轉換為前兩種情況。

說明:實際上,還有一個特例,就是刪除根節點,實現程式碼中會體現。

下面就正式對刪除操作的三種情況進行說明:

一棵查詢二叉樹的初始狀態如下圖所示:

4.1  待刪除結點沒有子結點

如圖中值為20的結點。這種情況是最簡單的,我們只需要刪除該結點和它父結點的關係即可。刪除的時候需要判斷自己和父結點的關係是左側還是右側,程式碼如下:

// 這裡忽略了父結點不存在的情況,三種情況整合時會處理這種情況
if(node.parent.left == node){
    node.parent.left = null;    // 如果父結點的左子結點是自己,就將其設定為null
}else{
    node.parent.right = null;   // 如果父結點的右子結點是自己,就將其設定為null
}

4.2  待刪除結點存在左子結點或右子結點時

如圖中值為70的結點,刪除它時,需要斷開兩個關係:70與它子結點的關係已經70與它父結點的關係,同時建立70子結點與70父結點之間的父子結點關係。

// 先找到要刪除結點的子結點,不需要管他是左還是右
BSTNode<T> child = null;   // 表示待刪除結點的子結點
if(node.left != null){
    child = node.left;
}else{
    child = node.right;
}

// 這裡忽略了父結點不存在的情況,三種情況整合的時候,會處理
// 將代刪除結點的父結點和待刪除結點的子結點建立父子結點關係
if(node.parent.left == node){
    // 如果待刪除結點node是它父結點的左子結點,則將待刪除結點node的子結點設定為node父結點的左子結點,即取代它的位置    
    node.parent.left = child;  
}else{
     // 如果待刪除結點node是它父結點的右子結點,則將待刪除結點node的子結點設定為node父結點的右子結點,即取代它的位置 
    node.parent.right = child;
}

child.parent = node.parent;  // 建立node的子結點child中的父結點指標

4.3  待刪除結點同時存在左右子結點時

如上圖中值為30的結點。如上圖中二叉查詢樹中序遍歷的結果,當我們要刪除30結點時,整個中序遍歷結果從32位都要往前移動一位。而32是30的後繼結點(即比30大的結點中最小的結點)。當某個結點存在右結點時,則它的後繼結點就是它右子樹中最小值,由於左側結點總比右側結點和父結點小,所以後繼結點一定不會是左子樹中的。從這一特點也能想到,待刪除結點的後繼結點可能存在右結點或者沒有任何子結點。30的後繼結點還有一個特點就是,他比30的左子樹中的結點大,比30的右子樹中的結點都小,因此刪除30的時候,可以之間將後繼結點32轉移到30結點上,然後再刪除後繼結點32。又因為後繼結點最多隻有一個子結點(最多隻有一個比它大的右子結點),因此刪除後繼結點的時候,就轉換成了3種情況的前兩種情況。

因此,當待刪除結點同時存在左子結點和右子結點時,刪除操作被分為了兩步:

1、將待刪除結點的後繼節點“轉移”到待刪除結點上;

2、刪除待刪除結點的後繼結點。【將刪除指定結點轉化為了刪除它的後繼結點】

// 獲取當前結點的後繼結點
Node<T> successor = successor(ndoe);  // successor()函式的具體實現,後文中會給出

// 1、轉移值
node.data = successor.data

// 2、刪除後繼結點successor
node = successor;

4.4  三種情況程式碼整合   ---- 重要【這樣寫比較容易理解】

程式碼說明:為了體現出其他二叉查詢樹的操作,抽象出很多功能函式,相互配合調呼叫。後面會貼上二叉查詢樹的所有常用操作的程式碼。

public class BSTree<T extends Comparable<T>>{

	BSTNode<T> root;  // 根節點
	
	/**
	 * 刪除指定的data結點
	 */
	public void delete(T data){
		// 先查詢到要刪除的結點
		BSTNode<T> node = search(root, data);
		// 如果存在就刪除
		if(node != null){
			delete(node);
		}
	}

	/**
	 * 刪除指定的node
	 */
	public BSTNode<T> delete(BSTNode<T> node){
		// 第3種情況,如果同時存在左右子結點
		if(node.left != null && node.right != null){
			// 獲取待刪除結點的後繼結點
			BSTNode<T> successor = successor(node);
			// 轉移後繼節點值到當前待刪除結點上
			node.data = successor.data;
			// 把要刪除的當前結點設定為它的後繼結點
			node = successor;
		}
		// 經過前面一步的處理,下面只有兩種情況,即:待刪除結點有一個子結點或者沒有子結點
		// 不管有沒有子結點,都獲取子結點
		BSTNode<T> child = null;
		if(node.left != null){
			child = node.left;   // 待刪除結點存在左子結點
		}else{ 
			child = node.right;  // 待刪除結點存在右子結點
		}
		
		// 如果child != null,就說明待刪除結點存在一個子結點
		if(child != null){
			// 將待刪除結點的子結點和待刪除結點的父結點關聯上
			child.parent = node.parent;
		}
		// 如果當前待刪除結點沒有父結點,說明要刪除的結點是根結點
		if(node.parent == null){
			// 將根節點的子節點設定為根節點,按照前面的邏輯下來,根節點只有一個子結點或者沒有子結點
			root = child;
		}
		// 如果待刪除結點有父結點,並且待刪除結點是它父結點的左結點時
		else if(node == node.parent.left){
			// 將父結點的左子結點設定為child
			node.parent.left = child;
		}
		// 如果待刪除結點有父結點,並且待刪除結點是它父結點的右結點時
		else{
			// 將父結點的右子結點設定為child
			node.parent.right = child;
		}
		
		// 返回被刪除的結點
		return node;
	}
	
	/**
	 * 找到結點node的後繼節點。即:查詢二叉樹中資料大於該結點的最小結點
	 */
	private BSTNode<T> successor(BSTNode<T> node) {
		BSTNode<T> successor = null;
		// 如果存在右子結點,則node的後繼結點為以其右子結點為根的右子樹中的最小結點
		if(node.right != null){
			while(node.right.left != null){
				successor = node.right.left;  // 最小結點肯定在右子樹中的左子結點中
			}
		}
		
		// 如果node沒有右子結點,則node有以下兩種情況
		BSTNode<T> nodeParent = node.parent;
		// 1.node是“一個左子結點”,則node的後繼結點就為node的父結點
		if(nodeParent != null && node == node.parent.left){
			successor = node.parent;
		}
		// 2.node是“一個右子結點”,則查詢node的“最低”父結點,並且該父結點要有左子結點,那麼這個“最低”的父結點就是node的後繼結點
		// 2.1 如果nodeParent是nodeParent.parent的左子結點,則nodeParent.parent就是它的後繼節點
		// 2.2如果nodeParent是nodeParent.parent的右子結點,則還需要往上面找,直到node作為它的左子結點時,此時的nodeParent.parent就是它的後繼節點
		while(nodeParent != null && node == node.parent.right){
			node = node.parent;
			nodeParent = nodeParent.parent;
			successor = nodeParent;
		}
		
		return successor;
	}

	/**
	 * 遞迴實現查詢以node為根節點的二叉樹中值為data的結點
	 */
	private BSTNode<T> search(BSTNode<T> node, T data) {
		
		if(node == null){
			return node;
		}
		
		int cmp = data.compareTo(node.data);
		if(cmp < 0)         return search(node.left, data);   // 在左子樹中找
		else if(cmp > 0)    return search(node.left, data);   // 在右子樹中找
		else                return node;
	}


	/**
	 * BSTNode:結點內部類
	 */
	public class BSTNode<T extends Comparable<T>>{
		T data;             // 值
		BSTNode<T> left;    // 左子結點
		BSTNode<T> right;   // 右子結點
		BSTNode<T> parent;  // 父結點

		public BSTNode(T key, BSTNode<T> parent, BSTNode<T> left, BSTNode<T> right){
			this.data= data;
			this.parent = parent;
			this.left = left;
			this.right = right;
		}
		
		public T getData(){
			return this.data;
		}
		
		@Override
		public String toString(){
			return "data:" + data;
		}
	}
}

5、支援重複資料的二叉查詢樹

前面我們講的二叉查詢樹的操作,針對的都是不存在鍵值相同的情況。如果存在儲存兩個物件的鍵值相同的情況,我們可以有兩種解決辦法:

方法1:通過連結串列和支援動態擴容的陣列等資料結構實現二叉樹中的結點,把值相同的資料都儲存在同一個結點上;

方法2:每個結點仍然只儲存一個數據。在查詢插入位置的過程中,如果碰到一個結點的值與要插入資料的值相同,那麼就將這個要插入的資料放到這個結點的右子樹中,也就是說把這個新插入的資料當作大於這個結點的值來處理。

第1種方法比較容易理解,下面對第2種方法,進行下說明:

當我們查詢資料的時候,遇到值相同的結點,我們並不停止查詢操作,而是繼續在右子樹中查詢,直到遇到葉子節點,才停止。這樣就可以把鍵值等於要查詢值得所有結點都找出來了。

對於刪除操作,我們也需要先查詢到每個要刪除得結點,然後再按照前面講得刪除操作的方法,依次刪除。


6、二叉查詢樹的時間複雜度分析

此章節內容出自於:https://time.geekbang.org/column/article/68334

二叉查詢樹的形態多種多樣。比如下圖中,對於同一組資料,我們構造了三種二叉查詢樹。它們的查詢、插入和刪除操作的執行效率都不一樣的。圖中第一種二叉查詢樹,根節點的左右子樹極不平衡,已經退化成了連結串列,所以查詢的時間複雜度為O(n)。這是最糟糕的情況。

上面分析了最糟糕的情況,下面來分析下最理想的情況,即二叉查詢樹是一棵滿二叉樹。

其實從前面的講解中,我們不難發現,不管操作是查詢、插入還是刪除,時間複雜度都跟樹的高度成正比,也就是O(height)。所以,求解二叉查詢樹操作的時間複雜度就轉化為了如何求一棵包含n個結點的完全二叉樹的高度。

樹的高度就等於最大層數減1,為了方便計算,我們轉換為層來表示。從圖中可以看出,包含n個結點的完全二叉樹中,下面一層的結點個數是上面一層的2倍,即第k層包含的結點個數就是2^(k - 1)。不過對於完全二叉樹的最後一層的結點個數就沒有規律了,它包含的結點個數在1~2^(L - 1)個之間(假設最大層數為L)。如果我們把每一層的結點個數加起來就是總的結點個數n。那麼可以得出如下關係:

1 + 2 + 4 + 8 + ... + 2^(L-2) + 1  <= n <= 1 + 2 + 4 + 8 + ... + 2^(L-2) + 2^(L-1)    // L-2:不包含最後一層

藉助等比數列的求和公式,我們可以計算出L的範圍是[\log_{2}(n+1) , \log_{2}n + 1]。完全二叉樹的層數小於等於 \log_{2}n + 1,也就是說,完全二叉樹的高度小於等於\log_{2}n。即理想狀態下的完全二叉樹的查詢、插入和刪除操作的時間複雜度都是O(logn)。

從上面的最壞和最好的二叉查詢樹的時間複雜度分析來看,極度不平衡的二叉查詢樹的查詢效能肯定不能滿足我們的需求。我們需要構建出一種不管怎麼刪除、插入和查詢資料都能保持任意結點的左、右子樹比較平衡的二叉查詢樹,這就是下一篇文章要講的一種特殊的二叉查詢樹,即平衡二叉查詢樹(紅黑樹)。平衡二叉查詢樹的高度接近logn,所以它的查詢、插入和刪除操作的時間複雜度都比較穩定,是O(logn)。


7、二叉查詢樹和散列表的對比

散列表的講解:https://blog.csdn.net/pcwl1206/article/details/83582986

在前面的散列表文章中講過,散列表的查詢、插入和刪除操作的時間複雜度都可以做到常量級的O(1),非常的高效。那麼二叉查詢樹在比較平衡的情況下,查詢、插入和刪除操作的時間複雜度才是O(logn),相對散列表好像沒有什麼優勢,那麼我們為什麼還要用二叉查詢樹呢?

主要原因有以下幾個方面:

1、散列表中的資料是無序儲存的,如果要輸出有序的資料,需要先進行排序。對於二叉查詢樹來說,只需要中序遍歷,就可以在O(n)的時間複雜度內輸出所有的有序資料;

2、散列表擴容耗時很多,而且當遇到雜湊衝突的時候,效能不穩定。在工程中,我們常用的平衡二叉查詢樹的效能非常穩定,時間複雜度穩定在O(logn);

3、籠統的來說,儘管散列表的查詢等操作的時間複雜度是常量級的,但是因為雜湊衝突的存在,這個常量不一定比logn小,所以實際的查詢速度可能不一定比O(logn)快。再加上雜湊函式的耗時,也不一定比平衡二叉查詢樹的效率高;

4、散列表的構造要比二叉查詢樹要複雜的多,需要考慮的東西很多。比如雜湊函式的設計、衝突解決辦法、擴容、縮容等。平衡二叉查詢樹只需要考慮平衡性這一個問題,而且這個問題的解決方案比較成熟、固定。

5、散列表為了避免過多的雜湊衝突,散列表裝載因子不能太大,特別是基於開放定址法解決雜湊衝突的散列表,不然會浪費一定的儲存空間。

綜合以上幾點:平衡二叉查詢樹在某些方面還是由於散列表的,所以,這兩者之間並不存在衝突。我們在實際的開發中,需要結合實際的需求再來選擇使用哪一個。


8、二叉查詢樹的常用操作全程式碼實現

package com.zju.tree;
/** 
    * @author    作者 pcwl
    * @date      建立時間:2018年11月17日 上午9:04:51 
    * @version   1.0 
    * @comment   二叉查詢樹的操作
*/
public class BSTree<T extends Comparable<T>>{

	BSTNode<T> root;  // 根節點
	
	public BSTree(){
		root = null;
	}
	
	/**
	 * BSTNode:結點內部類
	 */
	public class BSTNode<T extends Comparable<T>>{
		T data;             // 值
		BSTNode<T> left;    // 左子結點
		BSTNode<T> right;   // 右子結點
		BSTNode<T> parent;  // 父結點

		public BSTNode(T key, BSTNode<T> parent, BSTNode<T> left, BSTNode<T> right){
			this.data= data;
			this.parent = parent;
			this.left = left;
			this.right = right;
		}
		
		public T getData(){
			return this.data;
		}
		
		@Override
		public String toString(){
			return "data:" + data;
		}
	}
	
	/**
	 * 前序遍歷指定結點下的所有結點
	 */
	public void preOrder(BSTNode<T> node){
		if(node != null){
			System.out.println(root.data + " ");
			preOrder(node.left);
			preOrder(node.right);
		}
	}
	
	/**
	 * 前序遍歷二叉查詢樹下的所有結點
	 */
	public void preOrder(){
		preOrder(root);
	}
	
	/**
	 * 中序遍歷指定結點下的所有結點
	 */
	public void inOrder(BSTNode<T> node){
		if(node != null){
			inOrder(node.left);
			System.out.println(node.data + " ");
			inOrder(node.right);
		}
	}
	
	/**
	 * 中序遍歷二叉查詢樹下的所有結點
	 */
	public void inOrder(){
		inOrder(root);
	}
	
	/**
	 * 後序遍歷指定結點下的所有結點
	 */
	public void lastOrder(BSTNode<T> node){
		if(node != null){
			lastOrder(node.left);
			lastOrder(node.right);
			System.out.println(node.data + " ");
		}
	}
	
	/**
	 * 後序遍歷二叉查詢樹下的所有結點
	 */
	public void lastOrder(){
		lastOrder(root);
	}
	
	
	/**
	 * 遞迴實現查詢以node為根節點的二叉樹中值為data的結點
	 */
	private BSTNode<T> search(BSTNode<T> node, T data) {
		
		if(node == null){
			return node;
		}
		
		int cmp = data.compareTo(node.data);
		if(cmp < 0)         return search(node.left, data);   // 在左子樹中找
		else if(cmp > 0)    return search(node.left, data);   // 在右子樹中找
		else                return node;
	}
	
	/**
	 * 遞迴實現查詢根節點下值為data的結點
	 */
	public BSTNode<T> search(T data){
		return search(root, data);
	}
	
	/**
	 * 刪除指定的data結點
	 */
	public void delete(T data){
		// 先查詢到要刪除的結點
		BSTNode<T> node = search(root, data);
		// 如果存在就刪除
		if(node != null){
			delete(node);
		}
	}

	/**
	 * 刪除指定的node
	 */
	public BSTNode<T> delete(BSTNode<T> node){
		// 第3種情況,如果同時存在左右子結點
		if(node.left != null && node.right != null){
			// 獲取待刪除結點的後繼結點
			BSTNode<T> successor = successor(node);
			// 轉移後繼節點值到當前待刪除結點上
			node.data = successor.data;
			// 把要刪除的當前結點設定為它的後繼結點
			node = successor;
		}
		// 經過前面一步的處理,下面只有兩種情況,即:待刪除結點有一個子結點或者沒有子結點
		// 不管有沒有子結點,都獲取子結點
		BSTNode<T> child = null;
		if(node.left != null){
			child = node.left;   // 待刪除結點存在左子結點
		}else{ 
			child = node.right;  // 待刪除結點存在右子結點
		}
		
		// 如果child != null,就說明待刪除結點存在一個子結點
		if(child != null){
			// 將待刪除結點的子結點和待刪除結點的父結點關聯上
			child.parent = node.parent;
		}
		// 如果當前待刪除結點沒有父結點,說明要刪除的結點是根結點
		if(node.parent == null){
			// 將根節點的子節點設定為根節點,按照前面的邏輯下來,根節點只有一個子結點或者沒有子結點
			root = child;
		}
		// 如果待刪除結點有父結點,並且待刪除結點是它父結點的左結點時
		else if(node == node.parent.left){
			// 將父結點的左子結點設定為child
			node.parent.left = child;
		}
		// 如果待刪除結點有父結點,並且待刪除結點是它父結點的右結點時
		else{
			// 將父結點的右子結點設定為child
			node.parent.right = child;
		}
		
		// 返回被刪除的結點
		return node;
	}

	/**
	 * 查詢以node為根節點下的最小結點
	 */
	public BSTNode<T> minNode(BSTNode<T> node){
		if(node == null){
			return null;
		}
		
		// 其實就是找ndoe下的最後一個左子結點
		while(node.left != null){
			node = node.left;
		}
		return node;
	}

	/**
	 * 查詢整棵二叉查詢樹下的最小結點
	 */
	public BSTNode<T> minNode(){
		return minNode(root);
	}
	
	/**
	 * 查詢以node為根節點下的最大結點
	 */
	public BSTNode<T> maxNode(BSTNode<T> node){
		if(node == null){
			return null;
		}
		
		// 其實就是找ndoe下的最後一個右子結點
		while(node.right != null){
			node = node.right;
		}
		return node;
	}
	 
	/**
	 * 查詢整棵二叉查詢樹下的最大結點
	 */
	public BSTNode<T> maxNode(){
		return maxNode(root);
	}
	
	/**
	 * 找到結點node的後繼節點。即:查詢二叉樹中資料大於該結點的最小結點
	 */
	private BSTNode<T> successor(BSTNode<T> node) {
		BSTNode<T> successor = null;
		// 如果存在右子結點,則node的後繼結點為以其右子結點為根的右子樹中的最小結點
		if(node.right != null){
			while(node.right.left != null){
				successor = node.right.left;  // 最小結點肯定在右子樹中的左子結點中
			}
		}
		
		// 如果node沒有右子結點,則node有以下兩種情況
		BSTNode<T> nodeParent = node.parent;
		// 1.node是“一個左子結點”,則node的後繼結點就為node的父結點
		if(nodeParent != null && node == node.parent.left){
			successor = node.parent;
		}
		// 2.node是“一個右子結點”,則查詢node的“最低”父結點,並且該父結點要有左子結點,那麼這個“最低”的父結點就是node的後繼結點
		// 2.1 如果nodeParent是nodeParent.parent的左子結點,則nodeParent.parent就是它的後繼節點
		// 2.2如果nodeParent是nodeParent.parent的右子結點,則還需要往上面找,直到node作為它的左子結點時,此時的nodeParent.parent就是它的後繼節點
		while(nodeParent != null && node == node.parent.right){
			node = node.parent;
			nodeParent = nodeParent.parent;
			successor = nodeParent;
		}
		
		return successor;
	}
	
	/**
	 * 找到結點node的前驅節點。即:查詢二叉樹中資料小於該結點的最小結點
	 */
	private BSTNode<T> predecessor(BSTNode<T> node) {
		BSTNode<T> predecessor = null;
		// 如果存在左子結點,則node的前驅結點為以其左子結點為根的左子樹中的最大結點
		if(node.left != null){
			while(node.left.right != null){
				predecessor = node.left.right;  // 最大結點肯定在左子樹中的右子結點中
			}
		}
		
		// 如果node沒有左子結點,則node有以下兩種情況
		BSTNode<T> nodeParent = node.parent;
		// 1.node是“一個右子結點”,則node的前驅結點就為node的父結點
		if(nodeParent != null && node == node.parent.left){
			predecessor = node.parent;
		}
		// 2.node是“一個左子結點”,則查詢node的“最低”父結點,並且該父結點要有右子結點,那麼這個“最低”的父結點就是node的前驅結點
		// 2.1 如果nodeParent是nodeParent.parent的右子結點,則nodeParent.parent就是它的前驅節點
		// 2.2如果nodeParent是nodeParent.parent的左子結點,則還需要往上面找,直到node作為它的右子結點時,此時的nodeParent.parent就是它的前驅節點
		while(nodeParent != null && node == node.parent.left){
			node = node.parent;
			nodeParent = nodeParent.parent;
			predecessor = nodeParent;
		}
		
		return predecessor;
	}
	
	/**
	 * 將結點插入到查詢二叉樹中
	 */
	public void insert(BSTree<T> bst, BSTNode<T> node){
		int cmp;
		BSTNode<T> node1 = null;
		BSTNode<T> root = bst.root;
		
		// 先查詢node的插入位置
		while(root != null){
			node1 = root;
			cmp = node.data.compareTo(root.data);
			if(cmp < 0){
				root = root.left; 
			}else{
				root = root.right;
			}
		}
		
		node.parent = node1;
		if(node1 == null){
			bst.root = node;
		}else{
			cmp = node.data.compareTo(node1.data);
			if(cmp < 0){
				node1.left = node;
			}else{
				node1.right = node;
			}
		}
	}
	
	/**
	 * 銷燬二叉樹
	 */
	public void destroy(BSTNode<T> node){
		if(node == null){
			return ;
		}
		if(node.left != null){
			destroy(node.left);
		}
		if(node.right != null){
			destroy(node.right);
		}
		node = null;
	}
	
	/**
	 * 列印node作為根節點的二叉樹
	 * direction:  0:表示該結點是根結點
	 *            -1:表示該結點是它父結點的左子結點
	 *             1:表示該結點是它父結點的右子結點
	 */
	public void print(BSTNode<T> node, T data, int direction){
		if(node != null){
			if(direction == 0){
				System.out.printf("%2d is root\n", node.data);
			}else{
				System.out.printf("%2d is %2d's %6s child\n", node.data, data, direction == 1 ? "right" : "left");
			}
			print(node.left, node.data, -1);
			print(node.right, node.data, 1);
		}
	}
	
	/**
	 * 列印二叉樹
	 */
	public void print(){
		if(root != null){
			print(root, root.data, 0);
		}
	}
}

推薦及參考:

 

1、重溫資料結構:二叉排序樹的查詢、插入、刪除:https://blog.csdn.net/u011240877/article/details/53242179

2、《資料結構與演算法之美》:https://time.geekbang.org/column/article/68334

3、二叉樹的刪除操作詳解:https://blog.csdn.net/isea533/article/details/80345507【推薦閱讀】