資料結構與演算法之二叉搜尋樹插入、查詢與刪除
阿新 • • 發佈:2018-12-24
1 二叉搜尋樹(BSTree)的概念
二叉搜尋樹又被稱為二叉排序樹,那麼它本身也是一棵二叉樹,那麼滿足以下性質的二叉樹就是二叉搜尋樹,如圖:
- 若左子樹不為空,則左子樹上所有節點的值都小於根節點的值;
- 若它的右子樹不為空,則它的右子樹上所有節點的值都大於根節點的值;
- 它的左右子樹也要分別是二叉搜尋樹。
2 二叉搜尋樹的插入
2.1 搜尋
插入之前我們先來說說它的搜尋,像上圖這樣的一棵二叉搜尋樹,我們要查詢某一個元素是很簡單的。因為它的節點分佈是有規律的,所以查詢一棵元素只需要如下的步驟就可以了:
2.2 插入
由於二叉搜尋樹的特殊性質確定了二叉搜尋樹中每個元素只可能出現一次,所以在插入的過程中如果發現這個元素已經存在於二叉搜尋樹中,就不進行插入。否則就查詢合適的位置進行插入。
2.2.1 第一種情況:root為空
直接插入,return true;
2.2.2 第一種情況:要插入的元素已經存在
如上面所說,如果在二叉搜尋樹中已經存在該元素,則不再進行插入,直接return false;
2.2.3 第三種情況:能夠找到合適位置
3 二叉搜尋樹的刪除
對於二叉搜尋樹的刪除操作,主要是要理解其中的幾種情況,寫起來還是比較簡單的。當然一開始還是需要判斷要刪除的節點是否存在於我們的樹中,如果要刪除的元素都不在樹中,就直接返回false;否則,再分為以下四種情況來進行分析:
- 要刪除的節點無左右孩子;
- 要刪除的節點只有左孩子;
- 要刪除的節點只有右孩子;
- 要刪除的節點有左、右孩子。
3.1 第一種情況:刪除沒有子節點的節點
對於第一種情況,我們完全可以把它歸為第二或者第三種情況,就不用再單獨寫一部分程式碼進行處理;
3.2 第二種情況:刪除有一個子節點的節點
3.2.1 如果要刪除的節點只有左孩子,那麼就讓該節點的父親結點指向該節點的左孩子,然後刪除該節點,返回true;
3.2.2 如果要刪除的節點只有右孩子,那麼就讓該節點的父親結點指向該節點的右孩子,然後刪除該節點,返回true;
對於上面這兩種情況我們還應該在之前進行一個判斷,就是判斷這個節點是否是根節點,如果是根節點的話,就直接讓根節點指向這個節點的左孩子或右孩子,然後刪除這個節點。
3.3 第三種情況: 刪除有兩個子節點的節點,即左右子節點都非空
(1)找到該節點的右子樹中的最左孩子(也就是右子樹中序遍歷的第一個節點,分兩種情況)
- 此節點是有右子樹:
- 當這個節點沒有右子樹的情況下,即node.rchild == null,如果這個節點的父節點的左子樹與這個節點相同的話,那麼就說明這個父節點就是後續節點了
(2)把它的值和要刪除的節點的值進行交換;
(3)然後刪除這個節點即相當於把我們想刪除的節點刪除了,返回true。
4 參考連結
5 原始碼
package Tree;
import java.util.ArrayList;
import java.util.List;
public class BinarySearchTree {
private TreeNode root = null;// 樹的根節點
// 用於儲存節點的列表
private static List<TreeNode> nodeList = new ArrayList<TreeNode>();
private class TreeNode {
private int key;// 節點關鍵字
private TreeNode parent;
private TreeNode lchild;
private TreeNode rchild;
public TreeNode(int key, TreeNode parent, TreeNode lchild,
TreeNode rchild) {
this.key = key;
this.parent = parent;
this.lchild = lchild;
this.rchild = rchild;
}
public String toString() {
String lKey = (lchild == null) ? "" : String.valueOf(lchild.key);
String rKey = (rchild == null) ? "" : String.valueOf(rchild.key);
return "(" + lKey + "<-" + key + "->" + rKey + ")";
}
}
/**
* 判斷是否為空
*/
public boolean isEmpty() {
if (root == null) {
return true;
} else {
return false;
}
}
/**
* 如果樹是空的情況下就丟擲異常
*/
public void TreeEmpty() throws Exception {
if (isEmpty()) {
throw new Exception("這棵樹是空樹!");
}
}
/**
* 插入操作
*
* @param key
*/
public void insert(int key) {
TreeNode parentNode = null;
TreeNode newNode = new TreeNode(key, null, null, null);
TreeNode pNode = root;
if (root == null) {
root = newNode;
return;
}
while (pNode != null) {
parentNode = pNode;
if (key > pNode.key) {
pNode = pNode.rchild;
} else if (key < pNode.key) {
pNode = pNode.lchild;
} else {
// 樹中已經存在此值,無需再次插入
return;
}
}
if (key > parentNode.key) {
parentNode.rchild = newNode;
newNode.parent = parentNode;
} else if (key < parentNode.key) {
parentNode.lchild = newNode;
newNode.parent = parentNode;
}
}
/**
* 搜尋關鍵字
*
* @param key
* @return
*/
public TreeNode search(int key) {
TreeNode pNode = root;
while (pNode != null) {
if (key == pNode.key) {
return pNode;
} else if (key > pNode.key) {
pNode = pNode.rchild;
} else if (key < pNode.key) {
pNode = pNode.lchild;
}
}
return null;// 如果沒有搜尋到結果那麼就只能返回空值了
}
/**
* 獲取二叉樹的最小關鍵位元組點
*
* @param node
* @return
* @throws Exception
*/
public TreeNode minElemNode(TreeNode node) throws Exception {
if (node == null) {
throw new Exception("此樹為空樹!");
}
TreeNode pNode = node;
while (pNode.lchild != null) {
pNode = pNode.lchild;
}
return pNode;
}
/**
* 獲取二叉樹的最大關鍵位元組點
*
* @param node
* @return
* @throws Exception
*/
public TreeNode maxElemNode(TreeNode node) throws Exception {
if (node == null) {
throw new Exception("此樹為空樹!");
}
TreeNode pNode = node;
while (pNode.rchild != null) {
pNode = pNode.rchild;
}
return pNode;
}
/**
* 獲取給定節點在中序遍歷下的後續第一個節點
*
* @param node
* @return
* @throws Exception
*/
public TreeNode successor(TreeNode node) throws Exception {
if (node == null) {
throw new Exception("此樹為空樹!");
}
// 分兩種情況考慮,此節點是否有右子樹
// 當這個節點有右子樹的情況下,那麼右子樹的最小關鍵位元組點就是這個節點的後續節點
if (node.rchild != null) {
return minElemNode(node.rchild);
}
// 當這個節點沒有右子樹的情況下,即 node.rchild == null
// 如果這個節點的父節點的左子樹 與 這個節點相同的話,那麼就說明這個父節點就是後續節點了
// 難道這裡還需要進行兩次if語句嗎?不需要了,這裡用一個while迴圈就可以了
TreeNode parentNode = node.parent;
while (parentNode != null && parentNode.rchild == node) {
node = parentNode;
parentNode = parentNode.parent;
}
return parentNode;
}
/**
* 獲取給定節點在中序遍歷下的前趨結
*
* @param node
* @return
* @throws Exception
*/
public TreeNode precessor(TreeNode node) throws Exception {
// 查詢前趨節點也是分兩種情況考慮
// 如果這個節點存在左子樹,那麼這個左子樹的最大關鍵字就是這個節點的前趨節點
if (node.lchild != null) {
return maxElemNode(node.lchild);
}
// 如果這個節點不存在左子樹,那麼這個節點的父節點
TreeNode parentNode = node.parent;
while (parentNode != null && parentNode.lchild == node) {
node = parentNode;
parentNode = parentNode.lchild;
}
return parentNode;
}
// 從二叉樹當中刪除指定的節點
public void delete(int key) throws Exception {
TreeNode pNode = search(key);
if (pNode == null) {
throw new Exception("此樹中不存在要刪除的這個節點!");
}
delete(pNode);
}
/**
* 這個方法可以算是一個遞迴的方法,適用於 要刪除的節點的兩個子節點都非空,並且要刪除的這個節點的後續節點也有子樹的情況下
*
* @param pNode
* @throws Exception
*/
private void delete(TreeNode pNode) throws Exception {
// 第一種情況:刪除沒有子節點的節點
if (pNode.lchild == null && pNode.rchild == null) {
if (pNode == root) {// 如果是根節點,那麼就刪除整棵樹
root = null;
} else if (pNode == pNode.parent.lchild) {
// 如果這個節點是父節點的左節點,則將父節點的左節點設為空
pNode.parent.lchild = null;
} else if (pNode == pNode.parent.rchild) {
// 如果這個節點是父節點的右節點,則將父節點的右節點設為空
pNode.parent.rchild = null;
}
}
// 第二種情況: (刪除有一個子節點的節點)
// 如果要刪除的節點只有右節點
if (pNode.lchild == null && pNode.rchild != null) {
if (pNode == root) {
root = pNode.rchild;
} else if (pNode == pNode.parent.lchild) {
pNode.parent.lchild = pNode.rchild;
pNode.rchild.parent = pNode.parent;
} else if (pNode == pNode.parent.rchild) {
pNode.parent.rchild = pNode.rchild;
pNode.rchild.parent = pNode.parent;
}
}
// 如果要刪除的節點只有左節點
if (pNode.lchild != null && pNode.rchild == null) {
if (pNode == root) {
root = pNode.lchild;
} else if (pNode == pNode.parent.lchild) {
pNode.parent.lchild = pNode.lchild;
pNode.lchild.parent = pNode.parent;
} else if (pNode == pNode.parent.rchild) {
pNode.parent.rchild = pNode.lchild;
pNode.lchild.parent = pNode.parent;
}
}
// 第三種情況: (刪除有兩個子節點的節點,即左右子節點都非空)
// 方法是用要刪除的節點的後續節點代替要刪除的節點,並且刪除後續節點(刪除後續節點的時候需要遞迴操作)
// 解析:這裡要用到的最多也就會發生兩次,即後續節點不會再繼續遞迴的刪除下一個後續節點了,
// 因為,要刪除的節點的後續節點肯定是:要刪除的那個節點的右子樹的最小關鍵字,而這個最小關鍵字肯定不會有左節點;
// 所以,在刪除後續節點的時候肯定不會用到(兩個節點都非空的判斷 ),如有有子節點,肯定就是有一個右節點。
if (pNode.lchild != null && pNode.rchild != null) {
// 先找出後續節點
TreeNode successorNode = successor(pNode);
if (pNode == root) {
root.key = successorNode.key;
} else {
pNode.key = successorNode.key;// 賦值,將後續節點的值賦給要刪除的那個節點
}
delete(successorNode);// 遞迴的刪除後續節點
}
}
/**
* 中序遍歷二叉樹,並獲得節點列表
*
* @return
*/
public List<TreeNode> inOrderTraverseList() {
if (nodeList != null) {
nodeList.clear();
}
inOrderTraverse(root);
return nodeList;
}
/**
* 進行中序遍歷
*
* @param node
*/
private void inOrderTraverse(TreeNode node) {
if (node != null) {
inOrderTraverse(node.lchild);
nodeList.add(node);
inOrderTraverse(node.rchild);
}
}
/**
* 獲取二叉查詢樹中關鍵字的有序列表
*
* @return
*/
public String toStringOfOrderList() {
StringBuilder sb = new StringBuilder("[");
for (TreeNode pNode : nodeList) {
sb.append(pNode.key + " ");
}
sb.append("]");
return sb.toString();
}
public static void main(String[] args) {
BinarySearchTree tree = new BinarySearchTree();
// 新增資料測試
tree.insert(10);
tree.insert(40);
tree.insert(20);
tree.insert(3);
tree.insert(49);
tree.insert(13);
tree.insert(123);
// 中序排序測試
tree.inOrderTraverse(tree.root);
System.out.println(tree.toStringOfOrderList());
// 查詢測試
if (tree.search(10) != null) {
System.out.println("找到了");
} else {
System.out.println("沒找到");
}
// 刪除測試
try {
tree.delete(tree.search(40));
} catch (Exception e) {
e.printStackTrace();
}
// 檢測刪除節點
if (tree.search(40) != null) {
System.out.println("找到了");
} else {
System.out.println("沒找到");
}
// 重新遍歷
nodeList.clear();
tree.inOrderTraverse(tree.root);
System.out.println(tree.toStringOfOrderList());
}
}