7 二分搜尋樹的原理與Java原始碼實現
1 折半查詢法
瞭解二叉查詢樹之前,先來看看折半查詢法,也叫二分查詢法
在一個有序的整數陣列中(假如是從小到大排序的),如果查詢某個元素,返回元素的索引。
如下:
int[] arr = new int[]{1,3,4,6,8,9};
在 arr 陣列中查詢6這個元素,查到返回對應的索引,沒有找到就返回-1
思想很簡單:
1 先找到陣列中間元素target與6比較
2 如果target比6大,就在陣列的左邊查詢
3 如果target比6小,就在陣列的右邊查詢
java實現程式碼如下:
private static int binarySearch(int[] data, int target) { int l = 0; int r = data.length - 1; while (l <= r) { //int mid = (l + r) / 2; //這句程式碼理論上是沒有問題的,但是是有bug的 //如果因為 l + r 會超過整數的最大值,就會溢位 //所以換成下面的寫法,最小邊界,加上差的一半,就是中間索引 //最小邊界,加上差的一半,就是中間值 int mid = l + (r - l) / 2; if (data[mid] > target) { //如果中間的值比target大,r向右移動。 r = mid - 1; } else if (data[mid] < target) { //如果中間的值比target小,l向左移動 l = mid + 1; } else { return mid; //如果中間的值與target相等,就返回下標 } } //沒有找到就返回-1 return -1; }
測試程式碼如下:
public static void main(String[] args) {
int[] data = new int[]{1,3,4,6,8,9};
System.out.println(binarySearch(data, 6));
}
輸出
3
折半查詢的關鍵是陣列必須有序,一次過濾掉一半的資料,時間複雜度為O(logN)。
上面是以2為底的,N為陣列的元素個數.
折半查詢和下面的要講的二分搜尋樹是有一樣的思想
2 二分搜尋樹定義
二分搜尋樹定義雙叫二分查詢樹,其定義如下
1 若它的左子樹不為空,則左子樹上所有的節點的值均小於根結點的值
2 若它的右子樹不為空,則右子樹上所有的節點的值均大於根結點的值
3 它的左右子樹也分別為二分搜尋樹
由二叉搜尋樹的定義可知,它前提是二叉樹,並且採用了遞迴的定義方式
。再得,它的節點滿足一定的關係,左子樹的節點一定比父節點的小,
右子樹的節點一定比父節點的大。
構造一棵二叉搜尋樹的目的,其實目的不是為了排序,是為了提高查詢,刪除,插入關鍵字的速度。
下面我們用圖和程式碼來解釋二叉樹的查詢,插入,和刪除。比如下圖就是一個二叉搜尋樹
2.0 二叉搜尋樹的定義和節點的定義
二叉搜尋樹中存放的都是key。先看下二叉樹的定義
//key必須繼承Comparable,可以比較大小的 public class QBST<K extends Comparable<K>, V> { ... }
二叉樹中節點的定義
//QNode是作為QBST的內部類的。後面會有完整的原始碼
class QNode {
//key,也相當於上圖中的數字,只不過不一定是數字
//只要能比較大小就行了。這裡的key,是繼承Comparable的
K key;
//節點中的value
V value;
//左子樹
QNode left;
//右子樹
QNode right;
//根據key,value構造一個節點
QNode(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
}
//根據一個節點,構造另一個新節點
QNode(QNode node){
this.key = node.key;
this.value = node.value;
this.left = node.left;
this.right = node.right;
}
}
類的定義和類中節點的定義都有了。
二分搜尋樹的定義如下:
/**
* 二分搜尋樹,也叫二分查詢樹
*/
public class QBST<K extends Comparable<K>, V> {
class QNode {
K key;
V value;
QNode left;
QNode right;
QNode(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
}
QNode(QNode node){
this.key = node.key;
this.value = node.value;
this.left = node.left;
this.right = node.right;
}
}
//樹的根
private QNode root;
//樹中節點的個數
private int count;
//構造一棵空的二分搜尋樹
public QBST() {
root = null;
count = 0;
}
//返回二分搜尋樹中的個數
public int size() {
return count;
}
//樹是否為空
public boolean isEmpty() {
return count == 0;
}
}
2.1 二叉搜尋樹的插入
1 如果這棵樹為空,新建一個節點,作為根
2 如果要插入的key比根節點大,就插入到右子樹中
3 如果要插入的key比根節點小,就插入到左子樹中
4 如果要插入的key和根節點相等,就更新當前節點的value
程式碼如下:
public void insert(K key, V value) {
root = insert(root, key, value);
}
// 向以node為根的二叉搜尋樹中,插入節點(key,value)
// 返回插入新節點後的二叉搜尋樹的根
private QNode insert(QNode node, K key, V value) {
//查檢條件
checkNotNull(key,"key is null");
//如果node為空,直接new一個節點返回
if (node == null) {
count++;
return new QNode(key, value);
}
//如果key比根節點大,插入到node的右子樹中
if (key.compareTo(node.key) == 1) {
node.right = insert(node.right, key, value);
//如果key比根節點小,插入到node的左子樹中
} else if (key.compareTo(node.key) == -1) {
node.left = insert(node.left, key, value);
//如果key和根節點相等,更新根節點的value
} else {
node.value = value;
}
//返回根
return node;
}
2.2 二叉搜尋樹的查詢
和上面向一棵二叉搜尋樹插入一個節點一樣。
向一棵二叉搜尋樹中查詢一個節點也是類似
1 如果根節點為空,不用查找了,返回null
2 如果key比根節點的key要大,在右子樹中查詢
3 如果key比根節點的key要小,在左子樹中查詢
4 如果key和根節點的key相等,返回根節點
程式碼實現如下:
//搜尋key結果的value
public V search(K key){
return search(root,key);
}
// 向以node為根的二叉搜尋樹中,以key為鍵,返回V
private V search(QNode node,K key){
checkNotNull(key,"key is null");
//如果當前節點為null,返回null
if(node == null){
return null;
}
//如果key比根節點的key大,在右子樹中查詢
if(key.compareTo(node.key) == 1){
return search(node.right,key);
//如果key比根節點的key小,在左子樹中查詢
}else if(key.compareTo(node.key) == -1){
return search(node.left,key);
//如果key與根節點的key值相等,就返回節點的value值
}else {
return node.value;
}
}
2.3 二叉搜尋樹的遍歷
二叉樹的遍歷有前序遍歷,中序遍歷,後序遍歷,層序遍歷(也叫做廣度優先遍歷)
如下圖的二叉搜尋樹。
根據根節點的訪問順序,可以把遍歷分為前序遍歷,中序遍歷,後序遍歷
前序遍歷:先訪問根節點,再前序遍歷左右子樹
中序遍歷:先中序遍歷左子樹,再訪問根節點,後中序遍歷右子樹
後序遍歷:先後序遍歷左子樹,再後序遍歷右子樹,再訪問根節點
程式碼實現分別如下:
// 前序遍歷 O(n)
public void preOrder(){
//後序遍歷以root為根的二叉搜尋樹
preOrder(root);
}
private void preOrder(QNode node){
if(node != null){
//先遍歷根節點
System.out.println(node.key);//這裡的訪問只是列印
//前序遍歷左子樹
preOrder(node.left);
//後序遍歷右子樹
preOrder(node.right);
}
}
// 中序遍歷 O(n)
public void middleOrder(){
middleOrder(root);
}
private void middleOrder(QNode node){
if(node != null){
middleOrder(node.left);
System.out.println(node.key);
middleOrder(node.right);
}
}
// 後序遍歷 O(n)
public void postOrder(){
postOrder(root);
}
private void postOrder(QNode node){
if(node != null){
postOrder(node.left);
postOrder(node.right);
System.out.println(node.key);
}
}
其中層序遍歷就是一層一層的從左到右遍歷
上圖中層序遍歷的結果是 13 6 15 3 7 10 18
程式碼實現需要藉助佇列,程式碼實現如下:
// 層序遍歷,也叫做廣度優先遍歷
public void levelOrder(){
if(root == null){
return;
}
LinkedList<QNode> queue = new LinkedList<>();
queue.addLast(root);
while (!queue.isEmpty()){
QNode node = queue.removeLast();
//這裡我們只打印
System.out.println(node.key);
queue.addLast(node.left);
queue.addLast(node.right);
}
}
2.4 二叉搜尋樹的刪除
二叉搜尋樹最麻煩的就是刪除節點,刪除任意二叉樹中的節點之前,我們來先刪除特殊的節點。
- 刪除二叉搜尋樹中最小的節點
- 刪除二叉搜尋樹中最大的節點
- 查詢二叉搜尋樹中最小的節點
- 查詢二叉搜尋樹中最大的節點
我們先來實現這些操作。
如下圖
根據二叉搜尋樹的定義,可以得出以下結論
- 在一個二叉搜尋樹中,最小的節點一定是最左邊的節點,也就是圖中的節點 3
- 在一個二叉搜尋樹中,最大的節點一定是最右邊的節點,也就是圖中的節點 18
總之:
最小節點去左子樹中找,直到節點的左孩子為空,則當前節點就是最小節點
最大節點去右子樹中找,直到節點的右孩子為空,則當前節點就是最大節點
1 先來實現查詢二叉搜尋樹中最小的節點
如下程式碼
//查詢一棵樹中最小的節點,返回 K
public K minimum(){
checkNotNull(root,"the tree is empty");
//在以根為root的二叉搜尋樹中返回最小節點的鍵值
QNode minNode = minimum(root);
//返回最小節點的鍵值
return minNode.key;
}
// 在以node為根的二叉搜尋樹中,返回最小鍵值的節點
private QNode minimum(QNode node){
//如果node.left == null,說明當前node節點就是最小的節點
//返回當前節點node
if(node.left == null){
return node;
}
//如果當前節點不是最小的節點
//繼承往左子樹中查詢
return minimum(node.left);
}
同理,查詢最大節點也是一樣
2 實現查詢二叉搜尋樹中最大的節點
程式碼如下:
public K maximum(){
checkNotNull(root,"the tree is empty");
QNode maxNode = maximum(root);
return maxNode.key;
}
// 在以node為根的二叉搜尋樹中,返回最大鍵值的節點
private QNode maximum(QNode node){
if(node.right == null){
return node;
}
return maximum(node.right);
}
上面實現了查詢最小節點和最大節點,下面我們再來實現刪除最小節點和刪除最大節點
3 實現刪除二叉搜尋樹中最小的節點
一直往左孩子中刪除,當某一個節點node沒有左孩子時,說明當前節點就是最小節點
這時候分兩種情況
- 當前節點有右孩子
如果是這種情況,直接把右孩子返回,作為當前節點 - 當前節點沒有右孩子
如果是這種情況,直接返回null。此時返回右孩子也行,因為右孩子也是null
程式碼實現如下
// 刪除二叉搜尋樹中最小的節點
public void removeMin(){
if(root != null){
root = removeMin(root);
}
}
// 刪除掉以node為根的二分搜尋樹中的最小的節點
// 返回刪除節點後新的二分搜尋樹的根
private QNode removeMin(QNode node){
//如果當前當前沒有左孩子,則當前節點就是最小節點
if(node.left == null){
//儲存當前節點的右孩子,這句程式碼把上面兩種情況都包含了
QNode rightNode = node.right;
node = null; //釋放當前節點
count--; //記得數量要減1
return rightNode;//返回右孩子,有可能為空或者不為空
}
//遞迴呼叫刪除以當前節點的左孩子為根的二叉搜尋中最小的節點
node.left = removeMin(node.left);
//別忘了返回當前節點
return node;
}
同理,刪除二叉搜尋樹中最大的節點的程式碼如下:
// 刪除二叉搜尋樹中最大的節點
public void removeMax(){
if(root != null){
root = removeMax(root);
}
}
// 刪除掉以node為根的二分搜尋樹中的最大的節點
// 返回刪除節點後新的二分搜尋樹的根
private QNode removeMax(QNode node){
if(node.right == null){
QNode leftNode = node.left;
count--;
node = null;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
下面來分析一下刪除任意一個節點。
刪除任意一個節點node,那麼可以分為以下幾種情況
- node 沒有孩子
- node 只有一個孩子
- node 有兩個孩子
如下圖一棵二叉搜尋樹,我們來分析
第一種情況:node沒有孩子
這種情況最簡單,直接刪除就行了,剩下的還是一棵二叉搜尋樹
比如圖中的 節點5,節點13,節點27,節點50
,刪除任意一個節點之後
剩下的還是滿足一棵二叉搜尋樹
第二種情況:node只有一個孩子
這種情況又分兩種
- node節點有一個左孩子
- node節點有一個右孩子
上面兩種情況其實不影響,比如圖中的節點10,節點45
,分別有一個左孩子和一個右孩子。
也好辦,節點10刪除後,它的左孩子節點5,放在節點10的位置
同理知,節點45刪除後,它的右孩子節點50,放在節點45的位置
這樣一來,剩下的節點還是一棵二叉搜尋樹
第三種情況:node有兩個孩子
還是上圖為準,以節點17
為例,節點17
有左右兩個孩子,分別是10,19
要刪除節點17
,怎麼辦呢?
或者說節點17
刪除 後,哪個節點應該放在節點17
的位置上呢?
我們節點17
滿足兩個性質 :
- 17大於它的左孩子10
- 17小於它的右孩子19
那麼我們找到一個這樣的節點,只要滿足上面這兩條性質,不就是可以了嗎。
so easey
我們就來先找一個大於10而且小於19的節點
- 大於 10 的節點,只要在 17 的右子樹
也就是以 19 為根節點的樹中找不就行了嗎
因為17的右子樹中所有的節點都比 17 大 - 小於 19 的節點,只要在以 19 為根的樹中找左孩子不就得了嗎
經過上面的分析,這樣的節點就是 13 啊,將17刪除 ,把13放到17的位置 ,如圖
其實,把10放到17的位置也是可以的。如下圖
10和13兩個節點都滿足條件,所以我們可以得出結論
刪除一個有兩個孩子節點,可以找這個節點左子樹中的最大節點,或者右子樹中的最小節點來放到當前位置
虛擬碼:
刪除左右都 有孩子的節點 d
找到 s = min(d.right)
s 可以叫作 d 的後繼
s.right = deledeMin(d->right)
s.left = d.left;
刪除 d, s 是新的子樹的根
翻譯成程式碼如下:
public void remove(K key) {
root = remove(root, key);
}
// 刪除掉以node為根的二分搜尋樹中鍵值為key的節點
// 返回刪除節點後新的二分搜尋樹的根
// O(logN)
private QNode remove(QNode node, K key) {
//如果樹為null,返回null
if (node == null) {
return null;
}
//想要刪除某個節點,必須先要找到這個節點
//所以下面的程式碼包含了查詢
if (key.compareTo(node.key) == -1) {//如果key小於根節點的key
//到node的左子樹查詢並刪除鍵值為key的節點
node.left = remove(node.left, key);
//返回刪除節點後新的二分搜尋樹的根
return node;
} else if (key.compareTo(node.key) == 1) {//如果key大於根節點的key
//到node的右子樹查詢並刪除鍵值為key的節點
node.right = remove(node.right, key);
//返回刪除節點後新的二分搜尋樹的根
return node;
} else { //key == node.key,也就是找到了這個節點
//當前節點的左孩子為null
if (node.left == null) {
//儲存右孩子節點
QNode rightNode = node.right;
//個數減1
count--;
//刪除
node = null;
//右節點作為新的根
return rightNode;
}
//當前節點的右孩子為null
if (node.right == null) {
//儲存左孩子的節點
QNode leftNode = node.left;
//個數減1
count--;
//刪除
node = null;
//左節點作為新的根
return leftNode;
}
//上面的情況也包括了左右兩個孩子都是null
//這樣的情況就走第一種,node.left==null的條件中。也滿足
//下面是 node.left != null && node.right != null的情況
//找到右子樹中最小節點
QNode min = minimum(node.right);
//用最小節點新建一個節點,因為等會要刪除最小的節點,所以這裡我們要新建一個最小節點
QNode s = new QNode(min);
//s的右孩子,就是刪除node右子樹中最小節點返回的根
s.right = removeMin(node.right);
//s的左孩子,就是刪除節點的左孩子
s.left = node.left;
//返回新的根
return s;
}
}
同過上面的分析,我們瞭解了二叉搜尋樹的性質,以及插入,查詢,查詢最大節點,查詢最小節點,刪除最大節點,刪除最小節點,以及最後分析出來刪除一個任意節點。
下面我們粘出完整程式碼 。如下
/**
* 二分搜尋樹,也叫二分查詢樹
*/
public class QBST<K extends Comparable<K>, V> {
class QNode {
K key;
V value;
QNode left;
QNode right;
QNode(K key, V value) {
this.key = key;
this.value = value;
this.left = null;
this.right = null;
}
QNode(QNode node) {
this.key = node.key;
this.value = node.value;
this.left = node.left;
this.right = node.right;
}
}
private QNode root;
private int count;
public QBST() {
root = null;
count = 0;
}
public int size() {
return count;
}
public boolean isEmpty() {
return count == 0;
}
public void insert(K key, V value) {
root = insert(root, key, value);
}
// 向以node為根的二叉搜尋樹中,插入節點(key,value)
// 返回插入新節點後的二叉搜尋樹的根
private QNode insert(QNode node, K key, V value) {
checkNotNull(key, "key is null");
if (node == null) {
count++;
return new QNode(key, value);
}
if (key.compareTo(node.key) == 1) {
node.right = insert(node.right, key, value);
} else if (key.compareTo(node.key) == -1) {
node.left = insert(node.left, key, value);
} else {
node.value = value;
}
return node;
}
public boolean contain(K key) {
return contain(root, key);
}
// 向以node為根的二叉搜尋樹中,查詢是否包含key的節點
private boolean contain(QNode node, K key) {
checkNotNull(key, "key is null");
if (node == null) {
return false;
}
if (key.compareTo(node.key) == 1) {
return contain(node.right, key);
} else if (key.compareTo(node.key) == -1) {
return contain(node.left.key);
} else {
return true;
}
}
public V search(K key) {
return search(root, key);
}
// 向以node為根的二叉搜尋樹中,
private V search(QNode node, K key) {
checkNotNull(key, "key is null");
if (node == null) {
return null;
}
if (key.compareTo(node.key) == 1) {
return search(node.right, key);
} else if (key.compareTo(node.key) == -1) {
return search(node.left, key);
} else {
return node.value;
}
}
// 前序遍歷 O(n)
public void preOrder() {
preOrder(root);
}
private void preOrder(QNode node) {
if (node != null) {
System.out.println(node.key);
preOrder(node.left);
preOrder(node.right);
}
}
// 中序遍歷 O(n)
public void middleOrder() {
middleOrder(root);
}
private void middleOrder(QNode node) {
if (node != null) {
middleOrder(node.left);
System.out.println(node.key);
middleOrder(node.right);
}
}
// 後序遍歷 O(n)
public void postOrder() {
postOrder(root);
}
private void postOrder(QNode node) {
if (node != null) {
postOrder(node.left);
postOrder(node.right);
System.out.println(node.key);
}
}
// 層序遍歷,也叫做廣度優先遍歷
public void levelOrder() {
if (root == null) {
return;
}
LinkedList<QNode> queue = new LinkedList<>();
queue.addLast(root);
while (!queue.isEmpty()) {
QNode node = queue.removeLast();
System.out.println(node.key);
queue.addLast(node.left);
queue.addLast(node.right);
}
}
public void destroy() {
destroy(root);
}
// 銷燬操作就是後序遍歷的一次應用
private void destroy(QNode node) {
if (node != null) {
destroy(node.left);
destroy(node.right);
node = null;
count--;
}
}
public K minimum() {
checkNotNull(root, "the tree is empty");
QNode minNode = minimum(root);
return minNode.key;
}
// 在以node為根的二叉搜尋樹中,返回最小鍵值的節點
private QNode minimum(QNode node) {
if (node.left == null) {
return node;
}
return minimum(node.left);
}
public K maximum() {
checkNotNull(root, "the tree is empty");
QNode maxNode = maximum(root);
return maxNode.key;
}
// 在以node為根的二叉搜尋樹中,返回最大鍵值的節點
private QNode maximum(QNode node) {
if (node.right == null) {
return node;
}
return maximum(node.right);
}
// 刪除二叉搜尋樹中最小的節點
public void removeMin() {
if (root != null) {
root = removeMin(root);
}
}
// 刪除掉以node為根的二分搜尋樹中的最小的節點
// 返回刪除節點後新的二分搜尋樹的根
private QNode removeMin(QNode node) {
if (node.left == null) {
QNode rightNode = node.right;
node = null;
count--;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
// 刪除二叉搜尋樹中最大的節點
public void removeMax() {
if (root != null) {
root = removeMax(root);
}
}
// 刪除掉以node為根的二分搜尋樹中的最大的節點
// 返回刪除節點後新的二分搜尋樹的根
private QNode removeMax(QNode node) {
if (node.right == null) {
QNode leftNode = node.left;
count--;
node = null;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
public void remove(K key) {
root = remove(root, key);
}
// 刪除掉以node為根的二分搜尋樹中鍵值為key的節點
// 返回刪除節點後新的二分搜尋樹的根
// O(logN)
private QNode remove(QNode node, K key) {
//如果樹為null,返回null
if (node == null) {
return null;
}
//想要刪除某個節點,必須先要找到這個節點
//所以下面的程式碼包含了查詢
if (key.compareTo(node.key) == -1) {//如果key小於根節點的key
//到node的左子樹查詢並刪除鍵值為key的節點
node.left = remove(node.left, key);
//返回刪除節點後新的二分搜尋樹的根
return node;
} else if (key.compareTo(node.key) == 1) {//如果key大於根節點的key
//到node的右子樹查詢並刪除鍵值為key的節點
node.right = remove(node.right, key);
//返回刪除節點後新的二分搜尋樹的根
return node;
} else { //key == node.key,也就是找到了這個節點
//當前節點的左孩子為null
if (node.left == null) {
//儲存右孩子節點
QNode rightNode = node.right;
//個數減1
count--;
//刪除
node = null;
//右節點作為新的根
return rightNode;
}
//當前節點的右孩子為null
if (node.right == null) {
//儲存左孩子的節點
QNode leftNode = node.left;
//個數減1
count--;
//刪除
node = null;
//左節點作為新的根
return leftNode;
}
//上面的情況也包括了左右兩個孩子都是null
//這樣的情況就走第一種,node.left==null的條件中。也滿足
//下面是 node.left != null && node.right != null的情況
//找到右子樹中最小節點
QNode min = minimum(node.right);
//用最小節點新建一個節點,因為等會要刪除最小的節點,所以這裡我們要新建一個最小節點
QNode s = new QNode(min);
//s的右孩子,就是刪除node右子樹中最小節點返回的根
s.right = removeMin(node.right);
//s的左孩子,就是刪除節點的左孩子
s.left = node.left;
//返回新的根
return s;
}
}
private <E> void checkNotNull(E e, String message) {
if (e == null) {
throw new IllegalArgumentException(message);
}
}
}