二叉搜尋樹和平衡二叉樹
寫在前面
前面講了樹的基本概念,這篇文章主要講常見的樹的基本操作,如查詢,新增,刪除等。其中通過動圖的方式使得更加容易理解。
二叉查詢樹
二叉查詢樹(BST,Binary Sort Tree),也稱二叉排序樹,或二叉搜尋樹。一棵二叉查詢樹滿足以下條件:
- 左子樹的所有值均小於根節點的值
- 右子樹的所有值均大於根節點的值
- 左右子樹同時也滿足以上兩點
通俗來說就是一棵二叉樹,節點左小右大。
插入操作
一棵二叉查詢樹的插入操作,如果插入的值 x 從根節點開始:
- x值小於該節點值,在左子樹中繼續
- x值大於該節點值,在右子樹中繼續
- 如果節點是葉節點,X值小於該節點值則插入左子節點,否則插入右節點
在上面的二叉排序樹中,如果我們插入節點的值為 80,具體操作如下:
查詢操作
一棵二叉查詢樹的查詢操作,如果查詢的值 x 從根節點開始:
- 如果x小於根節點,則在左子樹中繼續查詢
- 如果x大於根節點,則在右子樹中繼續查詢
- 如果x的值等於根節點,則返回該節點
- 如果都查詢不到,則返回null
在上面的二叉排序樹中,如果我們需要查詢節點的值為10的節點,具體操作如下:
遍歷樹
樹的遍歷有三種方法。前序(preorder),中序(inorder),後序(postorder)。
前序遍歷
中序遍歷
後序遍歷
最大值和最小值
最小值:找到根節點的左子節點,一直向左查詢直到沒有左子節點的節點即為最小節點
最大值:找到根節點的右子節點,一直向右查詢直到沒有右子節點的節點即為最小節點
刪除操作
該節點是葉子節點
結點為葉子結點時,直接刪除,把父節點指向子節點的引用設為null。
一棵二叉查詢樹的刪除操作,如果刪除的值 x 從根節點開始:
- 如果節點的值等於 x,則刪除
- x值小於該節點值,在左子樹中繼續
- x值大於該節點值,在右子樹中繼續
如果我們刪除節點的值為 80,具體操作如下:
該節點有一個子節點
節點有一個子節點,分兩種情況,判斷是父節點的左子結點還是右子節點,把父節點的引用指向結點的子節點(子節點也要分左右子節點情況,相當於一共四種情況)
左左左:即刪除節點在根節點的左邊,且刪除節點在其父節點的左邊,且刪除節點的子節點為左子節點
左右左:即刪除節點在根節點的左邊,且刪除節點在其父節點的右
右右右:即刪除節點在根節點的右邊,且刪除節點在其父節點的右邊,且刪除節點的子節點為右子節點
右左右:即刪除節點在根節點的右邊,且刪除節點在其父節點的左邊,且刪除節點的子節點為右子節點
該節點有兩個子節點
由於二叉搜尋樹的特性,保證了某個結點的左子樹的值都小於該節點,右子樹的值都大於該節點,只需找到左子樹中的最大值或者右子樹中的最小值(也叫作中續後繼節點)來替換該結點,即可保證節點刪除後任為二叉搜尋樹。
後繼節點
在二叉查詢樹中,節點是按照左小右大的方式排列的,對任意一個節點來說,比該節點的值次高的節點為它的中續後繼,簡稱為後繼節點。由於左側節點總小於右側節點及父節點,所以後繼節點沒有左子節點,可能存在右子節點。通過後繼節點來替換該結點,即可保證節點刪除後仍為二叉搜尋樹。
查詢方式也分為2種
若使用左子樹中的最大值來替換
若使用右子樹的最小值(後繼節點)來替換
程式碼實現
public class BSTTree {
/**
* 節點
*/
public static class Node {
//資料,為簡化程式碼,本程式預設節點裡儲存一個int變數,實際程式裡可以儲存自定義型別變數
int data;
//左子節點
Node leftChild;
//右子節點
Node rightChild;
public Node(int data) {
this.data = data;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", leftChild=" + leftChild +
", rightChild=" + rightChild +
'}';
}
}
/**
* 新增節點 採用遞迴的方式
*
* @param root 根節點
* @param data 插入的資料
* @return
*/
public static Node insert(Node root, int data) {
if (root == null) {
root = new Node(data);
return root;
}
//若插入的資料小於根節點,插入到其左子樹
if (data <= root.data) {
root.leftChild = insert(root.leftChild, data);
} else {
//插入到其右子樹
root.rightChild = insert(root.rightChild, data);
}
return root;
}
/**
* 前序遍歷
*
* @param root
*/
public static void preOrder(Node root) {
if (root != null) {
System.out.println(root.data + "->");
preOrder(root.leftChild);
preOrder(root.rightChild);
}
}
/**
* 中序遍歷
*
* @param root
*/
public static void inOrder(Node root) {
if (root != null) {
inOrder(root.leftChild);
System.out.print(root.data + "->");
inOrder(root.rightChild);
}
}
/**
* 後序遍歷
*
* @param root
*/
public static void postOrder(Node root) {
if (root != null) {
postOrder(root.leftChild);
postOrder(root.rightChild);
System.out.print(root.data + "->");
}
}
/**
* 查詢資料
*
* @param data
* @return
*/
public static Node find(Node root, int data) {
//若查詢的資料小於根節點,則向左查詢(也可以採用遞迴的方式查詢)
Node current = root;
while (current != null) {
if (data < current.data) {
current = current.leftChild;
} else if (data > current.data) {
//向右查詢
current = current.rightChild;
} else {
return current;
}
}
return null;
}
/**
* 最小值
*
* @param root
* @return
*/
public static Node minimum(Node root) {
if (root == null) {
return null;
}
while (root.leftChild != null) {
root = root.leftChild;
}
return root;
}
/**
* 最大值
*
* @param root
* @return
*/
public static Node maximum(Node root) {
if (root == null) {
return null;
}
while (root.rightChild != null) {
root = root.rightChild;
}
return root;
}
/**
* 刪除節點
* 1.該節點是葉子節點,即沒有子節點
* 2.該節點有一個子節點
* 3.該節點有兩個子節點(通過該節點的中續後繼節點來代替需要刪除的節點,
* 因為後繼節點比刪除節點的右節點都小,比刪除節點的左節點都大)
* 中續後繼節點:比該節點值次高的節點為中續後繼節點,如節點2的後繼節點為3
* 4
* / \
* 2 6
* / \ / \
* 1 3 5 8
*
* @param root
* @param data 要刪除的節點的值
*/
public static boolean delete(Node root, int data) {
//用來表示要刪除節點的父節點
Node parent = null;
//需要刪除的節點是否為父節點的左子節點
boolean ifLeftChild = true;
//需要刪除的節點
Node current = root;
//定位刪除節點的位置及其父節點
while (true) {
if (data == current.data) {
break;
} else if (data < current.data) {
ifLeftChild = true;
parent = current;
current = current.leftChild;
} else if (data > current.data) {
ifLeftChild = false;
parent = current;
current = current.rightChild;
}
//若找不到直接返回fasle
if (current == null) {
return false;
}
}
//1.該節點是葉子節點
if (current.leftChild == null && current.rightChild == null) {
//若為根節點,刪除整棵樹
if (current == root) {
root = null; //GC
}
//若為左子節點
if (ifLeftChild) {
parent.leftChild = null;
} else {
parent.rightChild = null;
}
}
//2.該節點有一個子節點
if (current.leftChild != null && current.rightChild == null) {//若刪除節點的左子節點不為null
//如果該節點為根節點,將根節點的左子節點變為根節點
if (current == root) {
root = current.leftChild;
}
if (ifLeftChild) {
//左左左:若該節點為父節點的左子節點,則該節點的左子節點變為父節點的左子節點
parent.leftChild = current.leftChild;
} else {
//左右左:若該節點為父節點的左子節點,則該節點的左子節點變為父節點的右子節點
parent.rightChild = current.leftChild;
}
} else if (current.leftChild == null && current.rightChild != null) {
if (current == root) {
root = current.rightChild;
}
if (ifLeftChild) {
//右左右:若該節點為父節點的左子節點,則該節點的右子節點變為父節點的左子節點
parent.leftChild = current.rightChild;
} else {
//右右右:若該節點為父節點的右子節點,則該節點的右子節點變為父節點的右子節點
parent.rightChild = current.rightChild;
}
}
//3.該節點有兩個子節點,這裡通過後繼節點的方式刪除
if (current.leftChild != null && current.rightChild != null) {
//獲取刪除節點且重新構建的後繼結點
Node successor = getSuccessor(current);
if (root == current) {
root = successor;
} else if (ifLeftChild) {
parent.leftChild = successor;
} else {
parent.rightChild = successor;
}
}
return true;
}
/**
* @param node 要刪除的節點(假設此時該節點存在右子節點)
* @return 刪除節點的後繼節點
*/
public static Node getSuccessor(Node node) {
//node節點的左子節點
Node leftChild = node.leftChild;
//定義後繼節點的父節點
Node successorParent = node;
//定義後繼節點
Node successor = node;
//定義一個臨時變數current,先獲取刪除節點的右子節點,然後再獲取右子節點的最小值
Node current = node.rightChild;
//這一步就是查詢刪除節點的後繼節點
while (current != null) {
//找到後繼幾點的父節點
successorParent = successor;
successor = current;
//獲取右子節點的最小值,直左子樹的左子節點為null說明該節點就是刪除節點的右子節點的最小值
current = current.leftChild;
}
//找到後繼節點之後重新構建後繼節點樹
if (current != node.rightChild) {
/* 後繼節點的父節點的左子節點由原來的後繼節點變為原來後繼點的右子節點(因為左子節點的值始終小於父節點的值)
* 如下55若為後繼節點提出去後,58就變為了60的左子節點
* 60 55
* / \ \
* 55 80 ---重新構建--- 60
* \ / \
* 58 58 80
*/
successorParent.leftChild = successor.rightChild;
successor.rightChild = node.rightChild;
successor.leftChild = leftChild;
}
return successor;
}
public static void main(String[] args) {
/*
* 新增操作
* 4
* / \
* 2 6
* / \ / \
* 1 3 5 8
* /
* 7
*/
Node root = null;
root = insert(root, 4);
root = insert(root, 2);
root = insert(root, 1);
root = insert(root, 3);
root = insert(root, 6);
root = insert(root, 5);
root = insert(root, 8);
root = insert(root, 7);
// root = insert(root, 50);
// root = insert(root, 25);
// root = insert(root, 15);
// root = insert(root, 35);
// root = insert(root, 5);
// root = insert(root, 20);
// root = insert(root, 30);
// root = insert(root, 40);
//delete(root, 25);
inOrder(root);
// System.out.println("---------");
// //查詢操作 4
// Node node = find(root, 4);
// printTree(node);
// System.out.println("---------");
// //刪除操作
// Node delete = delete(root, 4);
// printTree(delete);
}
/**
* 列印
*
* @param root
*/
public static void printTree(Node root) {
System.out.println("根節點" + root.data);
if (root.leftChild != null) {
System.out.print("左子節點:");
printTree(root.leftChild);
}
if (root.rightChild != null) {
System.out.print("右子節點:");
printTree(root.rightChild);
}
}
}
平衡二叉樹(AVL)
平衡二叉樹(AVL),是一個二叉排序樹,同時任意節點左右兩個子樹的高度差(或平衡因子,簡稱BF)的絕對值不超過1,並且左右兩個子樹也滿足。
為什麼使用平衡二叉樹
通過二叉查詢樹的查詢操作可以發現,一棵二叉查詢樹的查詢效率取決於樹的高度,如果使樹的高度最低,那麼樹的查詢效率也會變高。
如下面一個二叉樹,全部由右子樹構成
這個時候的二叉樹其實就類似於連結串列,此時的查詢時間複雜度為O(n),而AVL樹的查詢時間複雜度為O(logn)。之前講過O(logn)耗時是小於O(n)的,如下:
可參考:常見資料結構的時間複雜度
平衡二叉樹的調整
一棵平衡二叉樹的插入操作會有2個結果:
如果平衡沒有被打破,即任意節點的BF=1,則不需要調整
如果平衡被打破,則需要通過旋轉調整,且該節點為失衡節點
一棵失衡的二叉樹通過以下調整可以重新達到平衡:
左旋:以某個結點作為支點(旋轉結點),其右子結點變為旋轉結點的父結點,右子結點的左子結點變為旋轉結點的右子結點,左子結點保持不變
右旋:以某個結點作為支點(旋轉結點),其左子結點變為旋轉結點的父結點,左子結點的右子結點變為旋轉結點的左子結點,右子結點保持不變
通過旋轉最小失衡子樹來實現失衡調整:
在一棵平衡二叉樹新增節點,在新增的節點向上查詢,第一個平衡因子的絕對值超過1(BF>1)的節點為根的子樹稱為最小不平衡子樹。也就是說,一棵失衡的樹,有可能多棵子樹同時失衡,這時只需要調整最小的不平衡子樹即可。
看完下面的旋轉方式之後再回來看最小失衡子樹旋轉就很清晰了
LL旋轉
向左子樹(L)的左孩子(L)插入新節點
插入節點在失衡節點的左子樹的左邊,經過一次右旋即可達到平衡,如下
-
插入新節點5,舊根節點40為失衡節點
-
舊根節點40為新根節點20的右子樹
-
新根節點20的右子樹30為舊根節點40的左子樹
旋轉過程
RR旋轉
插入節點在失衡節點的右子樹的右邊,經過一次左旋即可達到平衡,如下
-
插入新節點60,舊根節點20為失衡節點
-
舊根節點20為新根節點40的左子樹
-
新根節點40的左子樹30為舊根節點20的右子樹
旋轉過程
LR旋轉
插入節點在失衡節點的左子樹的右邊,先左旋,再右旋,如下
旋轉過程
RL旋轉
插入節點在失衡節點的右子樹的左邊,先右旋,再左旋,如下
旋轉過程
程式碼實現
public class AVLTree {
//節點
public static class Node {
int data; //資料
Node leftChild; //左子節點
Node rightChild;//右子節點
int height; // 記錄該節點所在的高度
public Node(int data) {
this.data = data;
}
}
//獲取節點的高度
public static int getHeight(Node p){
return p == null ? -1 : p.height; // 空樹的高度為-1
}
public static void main(String[] args) {
Node root = null;
root = insert(root,40);
root = insert(root,20);
root = insert(root,50);
root = insert(root,10);
root = insert(root,30);
//插入節點在失衡結點的左子樹的左邊
root = insert(root,5);
//列印樹,按照先列印左子樹,再列印右子樹的方式
printTree(root);
}
public static void printTree(Node root) {
System.out.println(root.data);
if(root.leftChild !=null){
System.out.print("left:");
printTree(root.leftChild);
}
if(root.rightChild !=null){
System.out.print("right:");
printTree(root.rightChild);
}
}
// AVL樹的插入方法
public static Node insert(Node root, int data) {
if (root == null) {
root = new Node(data);
return root;
}
if (data <= root.data) { // 插入到其左子樹上
root.leftChild = insert(root.leftChild, data);
//平衡調整
if (getHeight(root.leftChild) - getHeight(root.rightChild) > 1) {
if (data <= root.leftChild.data) { // 插入節點在失衡結點的左子樹的左邊
System.out.println("LL旋轉");
root = LLRotate(root); // LL旋轉調整
}else{ // 插入節點在失衡結點的左子樹的右邊
System.out.println("LR旋轉");
root = LRRotate(root);
}
}
}else{ // 插入到其右子樹上
root.rightChild = insert(root.rightChild, data);
//平衡調整
if(getHeight(root.rightChild) - getHeight(root.leftChild) > 1){
if(data <= root.rightChild.data){//插入節點在失衡結點的右子樹的左邊
System.out.println("RL旋轉");
root = RLRotate(root);
}else{
System.out.println("RR旋轉");//插入節點在失衡結點的右子樹的右邊
root = RRRotate(root);
}
}
}
//重新調整root節點的高度值
root.height = Math.max(getHeight(root.leftChild), getHeight(root.rightChild)) + 1;
return root;
}
/**
* LR旋轉
*/
public static Node LRRotate(Node p){
p.leftChild = RRRotate(p.leftChild); // 先將失衡點p的左子樹進行RR旋轉
return LLRotate(p); // 再將失衡點p進行LL平衡旋轉並返回新節點代替原失衡點p
}
/**
* RL旋轉
*/
public static Node RLRotate(Node p){
p.rightChild = LLRotate(p.rightChild); // 先將失衡點p的右子樹進行LL平衡旋轉
return RRRotate(p); // 再將失衡點p進行RR平衡旋轉並返回新節點代替原失衡點p
}
/*
* LL旋轉
* 右旋示意圖(對結點20進行右旋)
* 40 20
* / \ / \
* 20 50 10 40
* / \ LL旋轉 / / \
* 10 30 5 30 50
* /
* 5
*/
public static Node LLRotate(Node p){ // 40為失衡點
Node lsubtree = p.leftChild; //失衡點的左子樹的根結點20作為新的結點
p.leftChild = lsubtree.rightChild; //將新節點的右子樹30成為失衡點40的左子樹
lsubtree.rightChild = p; // 將失衡點40作為新結點的右子樹
// 重新設定失衡點40和新節點20的高度
p.height = Math.max(getHeight(p.leftChild), getHeight(p.rightChild)) + 1;
lsubtree.height = Math.max(getHeight(lsubtree.leftChild), p.height) + 1;
return lsubtree; // 新的根節點取代原失衡點的位置
}
/*
* RR旋轉
* 左旋示意圖(對結點20進行左旋)
* 20 40
* / \ / \
* 10 40 20 50
* / \ RR旋轉 / \ \
* 30 50 10 30 60
* \
* 60
*/
public static Node RRRotate(Node p){ // 20為失衡點
Node rsubtree = p.rightChild; //失衡點的右子樹的根結點40作為新的結點
p.rightChild = rsubtree.leftChild; //將新節點的左子樹30成為失衡點20的右子樹
rsubtree.leftChild = p; // 將失衡點20作為新結點的左子樹
// 重新設定失衡點20和新節點40的高度
p.height = Math.max(getHeight(p.leftChild), getHeight(p.rightChild)) + 1;
rsubtree.height = Math.max(getHeight(rsubtree.leftChild), getHeight(rsubtree.rightChild)) + 1;
return rsubtree; // 新的根節點取代原失衡點的位置
}
}
總結
能看到這的,都是狠人。其實並不難,主要理解左旋和右旋的概念,我覺得就很清晰了。這篇也花了我一整天時間,基本我也是從0到1去接觸的,如果感興趣可以多研究研究。
更新記錄
修改時間 | 修改內容 |
---|---|
2021-7-20 | 二叉排序樹刪除操作(程式碼邏輯錯誤) |