- 什麼是二叉查找樹
樹的概念這裏不做說明,在假設我們都知道二叉樹的情況下,那麼二叉查找樹就是符合以下情況的一種特殊的二叉樹:對於二叉查找樹的某個節點X,他的左子樹中所有的節點值都小於X節點的值,右子樹所有節點的值都大於X節點的值。例如下面這顆樹,就是一個二叉搜索樹。該樹結構中,16左邊的節點都小於16,16右邊的節點大於16,其他節點同理。那下面我們分別討論二叉查找樹的增刪改查幾個重要方法。
- 查找
因爲二叉搜索樹的特點,所以我們查找一個節點是不是在二叉搜索樹中,一般有兩步,以上面的二叉搜索樹爲例,查找元素9:1. 訪問根節點,如果根節點的值正好和查詢的元素相等,那麼直接返回,這裏9不等於16,接續第二步。2. 判斷9是否大於16,是則從16的右邊開始查,否則,從16的左邊查,這裏9小於16,從16的左邊查,定位到8,發現9大於8,從8的右邊查,找到9。很顯然,二叉搜索樹的查詢時間複雜度,是優於鏈表的。下面給出代碼實現。
/**
* 以node爲根的二叉搜索樹是否包含元素e
* @param node
* @param e
* @return
*/
private boolean contains(Node node, E e) {
// 遞歸終止條件
if (null == node) {
return false;
}
if (e.compareTo(node.e) == 0) {
return true;
} else if (e.compareTo(node.e) < 0) {
return contains(node.left, e);
} else {
return contains(node.right, e);
}
}
- 插入
瞭解了查詢的過程,插入就是可以用contains的方法遍歷樹,如果找到對應的節點,在做一次更新,否則,就插入到遍歷的路徑上的最後一個空節點。下面給出代碼。
/**
* 遞歸添加元素
* @param node
* @param e
* @return 返回插入新節後二叉搜索樹的根
*/
private Node add(Node node, E e) {
if (null == node) {
size ++;
return new Node(e);
}
if (e.compareTo(node.e) < 0) {
// 添加返回值將new出來的節點掛載到樹上
node.left = add(node.left, e);
} else if (e.compareTo(node.e) > 0) {
node.right = add(node.right, e);
}
return node;
}
- 刪除
刪除相對於增加和查找元素,比較複雜,因爲要考慮到這樣幾種情況。
case1:以下圖爲例,刪除元素30,那麼我們直接刪除30就可以
case2:刪除元素8,這個時候8沒有左孩子,所以刪除元素8以後,我們可以直接讓8的右孩子替代8,成爲16的左孩子。
case3:還是刪除元素8,這個時候8沒有右孩子,那麼我們可以讓8的左孩子替換8成爲16的左孩子。
case4:還是刪除元素8,這次8既有左孩子,也有右孩子。這種情況我們一般的策略是找到元素8右孩子中的最小的元素,也就是9,讓他替代8的位置。代碼如下。
/**
* 刪除指定元素
* @param node 節點
* @param e 待刪除的元素
* @return 刪除後的根節點
*/
private Node remove(Node node, E e) {
if (null == node) {
return null;
}
if (e.compareTo(node.e) < 0) {
node.left = remove(node.left, e);
return node;
} else if (e.compareTo(node.e) > 0) {
node.right = remove(node.right, e);
return node;
} else {
// 左子樹爲空
if (null == node.left) {
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
// 右子樹爲空
if (null == node.right) {
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
// 待刪除節點左右子樹都不爲空
// 找到待刪除元素的右子樹的最小節點,替換待刪除元素
Node successor = minNum(node.right);
// successor現在替換了node的位置,就要將successor從原來的位置刪除
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = null;
node.right = null;
return successor;
}
}
- 關於二叉搜索樹的其他操作,例如深度優先遍歷、廣度優先遍歷、查詢最大最小元素等代碼,基本都是用了遞歸的方法,在這裏做一個記錄,就不贅述了。
package com.tree.BST;
import java.util.LinkedList;
import java.util.Queue;
public class BST<E extends Comparable<E>> {
private class Node {
E e;
Node right;
Node left;
public Node(E e) {
this.e = e;
this.right = null;
this.left = null;
}
}
private Node root;
private int size;
public BST() {
this.root = null;
this.size = 0;
}
private int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/**
* 添加元素
* @param e
*/
public void add(E e) {
root = add(root, e);
}
/**
* 遞歸添加元素
* @param node
* @param e
* @return 返回插入新節後二叉搜索樹的根
*/
private Node add(Node node, E e) {
if (null == node) {
size ++;
return new Node(e);
}
if (e.compareTo(node.e) < 0) {
// 添加返回值將new出來的節點掛載到樹上
node.left = add(node.left, e);
} else if (e.compareTo(node.e) > 0) {
node.right = add(node.right, e);
}
return node;
}
/**
* 是否包含元素e
* @param e
* @return
*/
public boolean contains(E e) {
return contains(root, e);
}
/**
* 以node爲根的二叉搜索樹是否包含元素e
* @param node
* @param e
* @return
*/
private boolean contains(Node node, E e) {
if (null == node) {
return false;
}
if (e.compareTo(node.e) == 0) {
return true;
} else if (e.compareTo(node.e) < 0) {
return contains(node.left, e);
} else {
return contains(node.right, e);
}
}
/**
* 前序遍歷
*/
public void preOrder() {
preOrder(root);
}
/**
* 前序遍歷 先訪問根節點,在訪問左右子樹, 遞歸算法
* @param node
*/
private void preOrder(Node node) {
if (null == node) {
return;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
/**
* 中序遍歷
*/
public void inOrder() {
inOrder(root);
}
/**
* 中序遍歷 先訪問左子樹,在訪問根節點和右子樹, 遞歸算法
* @param node
*/
private void inOrder(Node node) {
if (null == node) {
return;
}
inOrder(node.left);
//Gson gson = new Gson();
//String json = gson.toJson(node);
System.out.println(node.e);
inOrder(node.right);
}
/**
* 後序遍歷
*/
public void postOrder() {
postOrder(root);
}
/**
* 後序遍歷 先訪問左子樹,在訪問右子樹和根節點, 遞歸算法
* @param node
*/
private void postOrder(Node node) {
if (null == node) {
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
/**
* 層序遍歷
*/
public void levelOrder() {
if (null == root) {
return;
}
Queue<Node> queue = new LinkedList<>();
Queue<Node> levelQueue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
Node cur = queue.remove();
System.out.print(cur.e + " ");
if (null != cur.left) {
queue.add(cur.left);
}
if (null != cur.right) {
queue.add(cur.right);
}
if(queue.isEmpty()){
System.out.println();
queue.addAll(levelQueue);
levelQueue.clear();
}
}
}
/**
* 獲取二叉搜索樹的最大節點
*/
public E maxNum() {
if (size == 0) {
throw new IllegalArgumentException("BSI is empty");
}
return maxNum(root).e;
}
/**
* 遞歸獲取最大元素,從樹的右邊遍歷
* @param node
* @return
*/
private Node maxNum(Node node) {
if (null == node.right) {
return node;
}
return maxNum(node.right);
}
/**
* 獲取二叉搜索樹的最小節點
*/
public E minNum() {
if (size == 0) {
throw new IllegalArgumentException("BSI is empty");
}
return minNum(root).e;
}
/**
* 遞歸獲取最小元素,從樹的左邊遍歷
* @param node 根節點
* @return 以node爲根的最小節點
*/
private Node minNum(Node node) {
if (null == node.left) {
return node;
}
return minNum(node.left);
}
/**
* 移除最大元素
* @return 最大元素值
*/
public E removeMax() {
E e = maxNum();
removeMax(root);
return e;
}
/**
* 移除最大元素
* @param node 根節點
* @return 移除後根節點
*/
private Node removeMax(Node node) {
// 如果右子樹爲空,則根節點爲最大節點,刪除根節點
if (null == node.right) {
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
/**
* 刪除最小元素
* @return 最小元素的值
*/
public E removeMin() {
E e = minNum();
removeMin(root);
return e;
}
/**
* 刪除最小元素
* @param node
* @return 刪除最小節點後的新的二叉搜索樹
*/
private Node removeMin(Node node) {
// 如果左子樹爲空,則根節點爲最小節點,刪除根節點
if (null == node.left) {
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
/**
* 刪除指定元素
* @param e
*/
public void remove(E e) {
root = remove(root, e);
}
/**
* 刪除指定元素
* @param node 節點
* @param e 待刪除的元素
* @return 刪除後的根節點
*/
private Node remove(Node node, E e) {
if (null == node) {
return null;
}
if (e.compareTo(node.e) < 0) {
node.left = remove(node.left, e);
return node;
} else if (e.compareTo(node.e) > 0) {
node.right = remove(node.right, e);
return node;
} else {
// 左子樹爲空
if (null == node.left) {
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
// 右子樹爲空
if (null == node.right) {
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
// 待刪除節點左右子樹都不爲空
// 找到待刪除元素的右子樹的最小節點,替換待刪除元素
Node successor = minNum(node.right);
// successor現在替換了node的位置,就要將successor從原來的位置刪除
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = null;
node.right = null;
return successor;
}
}
/**
* 查找key的floor值
* @param key
* @return
*/
public E floor(E key) {
if (key.compareTo(minNum()) < 0) {
return null;
}
return floor(root, key);
}
/**
* 查找key的floor值
* @param node
* @param key
* @return
*/
private E floor(Node node, E key) {
if (null == node) {
return null;
}
// 如果key和node值相等,node就是key對應的floor節點
if (key.compareTo(node.e) == 0) {
return key;
}
// 如果key比node的值小,那麼對應的floor節點肯定在node的左子樹
if (key.compareTo(node.e) < 0) {
return floor(node.left, key);
}
// 如果key比node的值大,node有可能是key對應的floor節點,也有可能不是
E floor = floor(node.right, key);
if (floor != null) {
return floor;
}
return node.e;
}
/**
* 查找key的ceil值
* @param key
* @return
*/
public E ceil(E key) {
if (key.compareTo(maxNum()) > 0) {
return null;
}
return ceil(root, key);
}
/**
* 查找key的ceil值
* @param node
* @param key
* @return
*/
private E ceil(Node node, E key) {
if (null == node) {
return null;
}
// 如果key和node值相等,node就是key對應的ceil節點
if (key.compareTo(node.e) == 0) {
return key;
}
// 如果key比node的值大,那麼對應的ceil節點肯定在node的右子樹
if (key.compareTo(node.e) > 0) {
return ceil(node.right, key);
}
// 如果key比node的值小,node有可能是key對應的ceil節點,也有可能不是
E ceil = ceil(node.left, key);
if (ceil != null) {
return ceil;
}
return node.e;
}
/**
* 求樹的深度
* @return 樹的深度
*/
public int depth() {
return depth(root);
}
/**
* 求樹的深度
* @return node爲節點的書的樹的深度
*/
private int depth(Node node){
if (null == node) {
return 0;
}
int leftDepth = depth(node.left);
int rightDepth = depth(node.right);
return Math.max(leftDepth, rightDepth) + 1;
}
}
文章代碼參考《數據結構與算法分析》和慕課網liuyubobobo老師的算法大師帶你玩轉數據結構的課程。這裏作爲學習筆記整理。推薦一篇寫的很不錯的博客:淺談算法和數據結構: 七 二叉查找樹