二叉搜索樹定義
二叉搜索樹,是指一棵空樹或者具有下列性質的二叉樹:
- 若任意節點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
- 若任意節點的右子樹不空,則右子樹上所有節點的值均大於它的根節點的值;
- 任意節點的左,右子樹也分別為二叉搜索樹;
- 沒有鍵值相等的節點。
用Java來表示二叉樹
public class BinarySearchTree { // 二叉搜索樹類 private class Node { // 節點類 int data; // 數據域 Node right; // 右子樹 Node left; // 左子樹 } private Node root; // 樹根節點 }
首先,需要一個節點對象的類。這個對象包含數據域和指向節點的兩個子節點的引用。
其次,需要一個樹對象的類。這個對象包含一個根節點root。
創建樹(insert)
public void insert(int key) { Node p=new Node(); //待插入的節點 p.data=key; if(root==null) { root=p; } else { Node parent=new Node(); Node current=root; while(true) { parent=current; if(key>current.data) { current=current.right; // 右子樹 if(current==null) { parent.right=p; return; } } else //本程序沒有做key出現相等情況的處理,暫且假設用戶插入的節點值都不同 { current=current.left; // 左子樹 if(current==null) { parent.left=p; return; } } } } }
創建樹的時候,主要用到了parent,current來記錄要插入節點的位置。哪么怎么檢驗自己是否正確地創建了一顆二叉搜索樹呢,我們通過遍歷來輸出各個節點的值
遍歷樹(travel)
遍歷指的是按照某種特定的次序來訪問二叉搜索樹中的每個節點,主要有三種遍歷的方法:
- 前序遍歷,“中左右”
- 中序遍歷,“左中右”
- 后續遍歷,“左右中”
上面的口訣“中左右”表示的含義是,先訪問根節點,再訪問左子,最后訪問右子。舉個例子:

- 前序遍歷:39 24 23 30 64 53 60
- 中序遍歷:23 24 30 39 53 60 64
- 后序遍歷:23 30 24 60 53 64 39
你會發現,按照中序遍歷的規則將一個二叉搜索樹輸入,結果為按照正序排列。
public void preOrder(Node root) { // 前序遍歷,"中左右" if (root != null) { System.out.print(root.data + " "); preOrder(root.left); preOrder(root.right); } } public void inOrder(Node root) { // 中序遍歷,"左中右" if (root != null) { inOrder(root.left); System.out.print(root.data + " "); inOrder(root.right); } } public void postOrder(Node root) { // 后序遍歷,"左右中" if (root != null) { postOrder(root.left); postOrder(root.right); System.out.print(root.data + " "); } } public void traverse(int traverseType) { // 選擇以何種方式遍歷 switch (traverseType) { case 1: System.out.print("preOrder traversal "); preOrder(root); System.out.println(); break; case 2: System.out.print("inOrder traversal "); inOrder(root); System.out.println(); break; case 3: System.out.print("postOrder traversal "); postOrder(root); System.out.println(); break; } }
以上的代碼采用遞歸的方式實現三種遍歷,為了方便我們使用,又寫了一個traverse函數來實現選擇哪種方式進行樹的遍歷。
這會兒就可以寫單元測試了,我們首先創建一個二叉搜索樹,然后分別使用“前序”,“中序”,“后序”來遍歷輸出樹的所有節點。
public static void main(String[] args) //unit test { BinarySearchTree tree=new BinarySearchTree(); tree.insert(39); tree.insert(24); tree.insert(64); tree.insert(23); tree.insert(30); tree.insert(53); tree.insert(60); tree.traverse(1); tree.traverse(2); tree.traverse(3); }
運行該單元測試,可以看到如下的結果:

查找節點(find)
public Node find(int key) { // 從樹中按照關鍵值查找元素 Node current = root; while (current.data != key) { if (key > current.data) current = current.right; else current = current.left; if (current == null) return null; } return current; } public void show(Node node) { //輸出節點的數據域 if(node!=null) System.out.println(node.data); else System.out.println("null"); }
查找節點比較簡單,如果找到節點則返回該節點,否則返回null。為了方便在控制台輸出,我們有添加了一個show函數,用來輸出節點的數據域。
刪除節點(delete)
刪除節點是二叉搜索樹中,最復雜的一種操作,但是也不是特別難,我們分類討論:
- 要刪除節點有零個孩子,即葉子節點

如圖所示,只需要將parent.left(或者是parent.right)設置為null,然后Java垃圾自動回收機制會自動刪除current節點。
- 要刪除節點有一個孩子

如圖所示,只需要將parent.left(或者是parent.right)設置為curren.right(或者是current.left)即可。

- 要刪除節點有兩個孩子
這種情況比較復雜,首先我們引入后繼節點的概念,如果將一棵二叉樹按照中序周游的方式輸出,則任一節點的下一個節點就是該節點的后繼節點。例如:上圖中24的后繼節點為25,64的后繼節點為70.找到后繼節點以后,問題就變得簡單了,分為兩種情況:
1.后繼節點為待刪除節點的右子,只需要將curren用successor替換即可,注意處理好current.left和successor.right.
注意:這種情況下,successor一定沒有左孩子,一但它有左孩子,哪它必然不是current的后繼節點。


2.后繼節點為待刪除結點的右孩子的左子樹,這種情況稍微復雜點,請看動態圖片演示。


算法的步驟是:
- successorParent.left=successor.right
- successor.left=current.left
- parent.left=seccessor
弄懂原理后,我們來看具體的代碼實現:
private Node getSuccessor(Node delNode) //尋找要刪除節點的中序后繼結點 { Node successorParent=delNode; Node successor=delNode; Node current=delNode.right; //用來尋找后繼結點 while(current!=null) { successorParent=successor; successor=current; current=current.left; } //如果后繼結點為要刪除結點的右子樹的左子,需要預先調整一下要刪除結點的右子樹 if(successor!=delNode.right) { successorParent.left=successor.right; successor.right=delNode.right; } return successor; } public boolean delete(int key) // 刪除結點 { Node current = root; Node parent = new Node(); boolean isRightChild = true; while (current.data != key) { parent = current; if (key > current.data) { current = current.right; isRightChild = true; } else { current = current.left; isRightChild = false; } if (current == null) return false; // 沒有找到要刪除的結點 } // 此時current就是要刪除的結點,parent為其父結點 // 要刪除結點為葉子結點 if (current.right == null && current.left == null) { if (current == root) { root = null; // 整棵樹清空 } else { if (isRightChild) parent.right = null; else parent.left = null; } return true; } //要刪除結點有一個子結點 else if(current.left==null) { if(current==root) root=current.right; else if(isRightChild) parent.right=current.right; else parent.left=current.right; return true; } else if(current.right==null) { if(current==root) root=current.left; else if(isRightChild) parent.right=current.left; else parent.left=current.left; return true; } //要刪除結點有兩個子結點 else { Node successor=getSuccessor(current); //找到要刪除結點的后繼結點 if(current==root) root=successor; else if(isRightChild) parent.right=successor; else parent.left=successor; successor.left=current.left; return true; } }
大家注意哪個私有函數getSuccessor的功能,它不僅僅是用來找后繼結點的。
總結
二叉搜索樹其實不是特別難,理解以后,多練習幾次,應該可以掌握。以下是全部的代碼:
package org.yahuian; public class BinarySearchTree { // 二叉搜索樹類 private class Node { // 節點類 int data; // 數據域 Node right; // 右子樹 Node left; // 左子樹 } private Node root; // 樹根節點 public void insert(int key) { Node p = new Node(); // 待插入的節點 p.data = key; if (root == null) { root = p; } else { Node parent = new Node(); Node current = root; while (true) { parent = current; if (key > current.data) { current = current.right; // 右子樹 if (current == null) { parent.right = p; return; } } else // 本程序沒有做key出現相等情況的處理,暫且假設用戶插入的節點值都不同 { current = current.left; // 左子樹 if (current == null) { parent.left = p; return; } } } } } public void preOrder(Node root) { // 前序遍歷,"中左右" if (root != null) { System.out.print(root.data + " "); preOrder(root.left); preOrder(root.right); } } public void inOrder(Node root) { // 中序遍歷,"左中右" if (root != null) { inOrder(root.left); System.out.print(root.data + " "); inOrder(root.right); } } public void postOrder(Node root) { // 后序遍歷,"左右中" if (root != null) { postOrder(root.left); postOrder(root.right); System.out.print(root.data + " "); } } public void traverse(int traverseType) { // 選擇以何種方式遍歷 switch (traverseType) { case 1: System.out.print("preOrder traversal "); preOrder(root); System.out.println(); break; case 2: System.out.print("inOrder traversal "); inOrder(root); System.out.println(); break; case 3: System.out.print("postOrder traversal "); postOrder(root); System.out.println(); break; } } public Node find(int key) { // 從樹中按照關鍵值查找元素 Node current = root; while (current.data != key) { if (key > current.data) current = current.right; else current = current.left; if (current == null) return null; } return current; } public void show(Node node) { //輸出節點的數據域 if(node!=null) System.out.println(node.data); else System.out.println("null"); } private Node getSuccessor(Node delNode) //尋找要刪除節點的中序后繼結點 { Node successorParent=delNode; Node successor=delNode; Node current=delNode.right; //用來尋找后繼結點 while(current!=null) { successorParent=successor; successor=current; current=current.left; } //如果后繼結點為要刪除結點的右子樹的左子,需要預先調整一下要刪除結點的右子樹 if(successor!=delNode.right) { successorParent.left=successor.right; successor.right=delNode.right; } return successor; } public boolean delete(int key) // 刪除結點 { Node current = root; Node parent = new Node(); boolean isRightChild = true; while (current.data != key) { parent = current; if (key > current.data) { current = current.right; isRightChild = true; } else { current = current.left; isRightChild = false; } if (current == null) return false; // 沒有找到要刪除的結點 } // 此時current就是要刪除的結點,parent為其父結點 // 要刪除結點為葉子結點 if (current.right == null && current.left == null) { if (current == root) { root = null; // 整棵樹清空 } else { if (isRightChild) parent.right = null; else parent.left = null; } return true; } //要刪除結點有一個子結點 else if(current.left==null) { if(current==root) root=current.right; else if(isRightChild) parent.right=current.right; else parent.left=current.right; return true; } else if(current.right==null) { if(current==root) root=current.left; else if(isRightChild) parent.right=current.left; else parent.left=current.left; return true; } //要刪除結點有兩個子結點 else { Node successor=getSuccessor(current); //找到要刪除結點的后繼結點 if(current==root) root=successor; else if(isRightChild) parent.right=successor; else parent.left=successor; successor.left=current.left; return true; } } public static void main(String[] args) // unit test { BinarySearchTree tree = new BinarySearchTree(); tree.insert(39); tree.insert(24); tree.insert(64); tree.insert(23); tree.insert(30); tree.insert(53); tree.insert(60); tree.traverse(1); tree.traverse(2); tree.traverse(3); tree.show(tree.find(23)); tree.show(tree.find(60)); tree.show(tree.find(64)); tree.delete(23); tree.delete(60); tree.delete(64); tree.show(tree.find(23)); tree.show(tree.find(60)); tree.show(tree.find(64)); } }
動態圖片來自於:https://visualgo.net/en/bst
