平衡二叉樹
1、定義:
平衡二叉樹,是一種二叉排序樹,其中每個節點的左子樹和右子樹相差的高度不超過1。它是一種高度平衡的二叉排序樹。高度平衡:意思是說,要麼它是一顆空樹,要麼它的左子樹和右子樹都是平衡二叉樹。
平衡二叉樹的出現是爲了優化二叉順序樹的查找效率,你可以想象下,二叉順序樹如果順序添加一個這樣的數據{5,4,3,2,1},那麼樹成了一個鏈表,查找效率顯然不高 。 平衡二叉樹首先是一個二叉順序樹,只不過他在每次添加數據的時候會進行自我平衡,來優化查找的效率。
2、術語:
最小不平衡子樹:指離插入節點最近且以平衡因子的絕對值大於1的節點作爲根的子樹。
平衡因子(bf):結點的左子樹的深度減去右子樹的深度,那麼顯然-1<=bf<=1;
3、插入操作
在平衡二叉樹中插入結點與二叉查找樹最大的不同在於要隨時保證插入後整棵二叉樹是平衡的。那麼調整不平衡樹的基本方法就是: 旋轉。
4、我理解的旋轉
旋轉是爲了解決平衡樹的失衡,如何做呢?即將子樹高的一邊中的一些節點調整到另一邊,(並不只是把一邊的節點給移到另一邊,只是總體看上去,高的一邊變矮了。)調整時需要保持樹依然 是有序的。
上面這顆樹明顯已經不是平衡數,根節點50的平衡因子(bf) 3 -1 > 1 ,此時我們需要調整該樹讓其再一次到達平衡。我們先把樹拆成兩部分。如下
- 第一部分:根節點深度大的子樹
- 第二部分:根節點與深度小的子樹
然後我們在將這拆開的兩部分組合成一個新的平衡二叉樹
爲了少打點字,在下面我就把分出來的兩部分 一個叫第一部分 , 一個叫第二部分
分開的第一部分,即深度大的子樹的根節點(這裏是40)將會成爲新樹的根節點。然後呢?第一部分(針對這個例子)所有的鍵值是比第二部分的鍵值小的(二叉順序數 左子樹< 跟節點 < 右子樹),所以我們得把第二部分插入到新的根節點的右子樹上。 然後我們發現新的根節點的右子樹已經有值了,那不得把他給拆下來。將拆下來的這個右子樹插到第二部分。我們可以看到第二部分是一定會有一邊(可能會有兩邊)是空的,因爲我們把他深度高的子樹給他拆下來了 。 而我們把剛拆下來的右子樹就可以插到符合順序的子節點上,這個子節點一定會是空的。這樣就完成了旋轉。如圖:
我們來小結一下,我們做了兩個步驟(並不完全,後面還會加東西)
- 將數拆成了兩部分
- 第一部分:根節點深度大的子樹
- 第二部分:根節點與深度小的子樹
- 將第二部分插入到第一部分中: 第二部分插入到第一部分符合二叉順序樹的一邊(子節點),如果那裏已經有值了,那就把它拆出來插入到第二部分空的一方(子節點)。無值的話插就完事了
看到這如果你感覺你理解了辣麼來練習一下:
上面說的就是左旋轉 和 右旋轉,第一個例子是左旋 ,第二個例子是右旋、我在網上找的所有博客都是旋轉方法分左右來介紹,讓我理解的有點難度。
那什麼時候進行左旋 , 什麼時候進行右旋呢 ?通過上面的例子我們可以簡單的看到哪個(左右)子樹深度大就叫哪(左右)旋 。
5、需要進行兩次的旋轉操作
以爲這樣就完了嗎 ?不,還有呢,來看下面這個例子!
按這前面的套路一頓操作,然後發現得到依然還是個不平衡的二叉樹。。可能有些人看到這就開始了,你在寫什麼玩意,我看這麼久是用你的方法來一頓操作然後得不到正確的結果嗎?
呃呃,走遠了。我們來分析下爲什麼對於這個樹按照前面的套路得不到平衡樹。按套路來、
1、首先拆成兩部分,沒有什麼不對的。額,其實這裏沒啥分析的
2、我們在合成新樹的時候,在將第二部分插入到第一部分滿足順序的子節點時,發現該子節點,並不是一個葉子節點(無子節點的節點),而先前成功的案例都是葉子節點。而且我們看結果問題也確實出現在42這個45的子節點上。
現在問題找到了,那怎麼辦呢 ??答案是,將第一部分在給他旋轉一次,旋轉是幹什麼的,就是爲了將左右子樹儘量的平衡。雖然第一部分是平衡樹,但他這個平衡在我們進行旋轉時,我們覺得他這樣子不行,那就給他旋轉一下。
用旋轉後的第一部分在與前面的第二部分進行合成,即可。
這種旋轉也有個名字,叫 XX 旋(X的取值是左右。) 比如上面這個例子,他先是進行左旋,然後右旋 ,所以稱之爲 左右旋 。同理就有右左旋。但是沒有左左旋和右右旋,因爲這樣的一次旋轉就可以達到再次平衡.
貼一個右左旋轉的例子,這裏就不在畫圖演示了,辣麼快去練習一下吧。右邊是旋轉好的結果,
6、旋轉的總結
6.1 步驟
- 將樹拆成兩部分
- 第一部分:根節點深度大的子樹
- 第二部分:根節點與深度小的子樹
- 將第二部分插入到第一部分中
第二部分插入到第一部分符合二叉順序樹的一邊(子節點),然後
-
-
- a、如果那個節點是空節點,直接加進去就完成了
- b、如果那個節點不是空節點,且是個葉子節點,那就把它拆出來插入到第二部分空的子節點上(滿足順序的一方)
- c、如果那個節點不是空節點,且是個非葉子節點,那就需要對第一部分進行一次旋轉,然後在進行上面的操做。
-
6.2 分類
- 左旋 :深度大的一邊是左子樹,且左子樹的右節點是葉子節點或空節點 步驟:舊根節點的左子節點成爲新的根節點,新根節點的右子節點(若存在)成爲舊根節點的的左子節點,舊根節點成爲新根節點的右子節點
- 右旋: 深度大的一邊是右子樹,且右子樹的左節點是葉子節點或空節點 步驟:舊根節點的右子節點成爲新的根節點,新根節點的左子節點(若存在)成爲舊根節點的的右子節點,舊根節點成爲新根節點的左子節點
- 左右選旋: 深度大的一邊是左子樹,且左子樹的右節點是非葉子節點 步驟:我不知道怎麼描述。。哈哈
- 右左旋: 深度大的一邊是右子樹,且左子樹的左節點是非葉子節點 步驟:我不知道怎麼描述。。哈哈
7、插入的什麼時候去執行旋轉,執行什麼旋轉,旋轉哪些節點?
每次進行插入操作的時候,我們都需要進行一次檢查,檢查插入後是否還是平衡樹,如果不是平衡樹,那麼需要找到最小不平衡子樹(可以是整個樹),通常就是找到最小不平衡子樹的根節點,然後對最小不平衡子樹進行旋轉操作。
如何判斷是不是平衡樹:挨個遍歷(採用後序遍歷)非葉子節點,如過有節點 左子樹 - 右子樹 的絕對值大於1,那麼這棵樹就是非平衡的了,找到的這個節點就是最小不平衡子樹的根節點。
8、刪除操作
當樹進行刪除操作時,如果待刪除節點有子節點,那麼會導致樹會被拆開成兩個部分,而我們則需要合成這兩部分,並保證該樹依然是個平衡二叉樹。即三個步驟
拆開->合併->調整
拆開沒什麼講的主要是合併與再次調整。
合併分爲幾種情況。
- 沒有子節點 : 直接去除即可,不需要合併
- 只有一個子節點 :用待刪除節點的子節點替代他
- 兩個子節點 :有兩種方式,用刪除節點的左子樹最右側節點代替刪除節點、用刪除節點的右子樹的最左側的節點代替刪除節點。
然後在繼續進行自我平衡。使用前面插入平衡既可以。
JAVA 實現代碼
自己寫,看到這還寫不出來那就是XX 。 我自己也沒寫 mmp
寫了半天 , 媽的 被 左 右搞煩了 跟我Q E Q E Q E 站着的是 ?
package com.ss.study.tree.binary;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
/**
* 平衡二叉樹
* @author xia17
* @date 2019/12/19 11:02
*/
public class BalancedBinaryTree {
private Node root ;
/**
* 給樹添加
* @param key 值
*/
public void push(int key){
if (root == null){
//空樹直接加到根節點
root = new Node(key,null);
}else {
//否則遞歸插入相應位置
push(root,key);
}
}
/**
* 遞歸方法 插入到滿足順序的地方
* @param node 比較節點
* @param key 值
*/
private void push(@NotNull Node node , int key){
if (key > node.getKey()){
//大於則加到右子樹
if (node.getRight() == null){
//右子節點空則插入 , 否則繼續比較
node.setRight(new Node(key,node));
//檢查是否破壞了平衡
if (this.balanced()){
System.out.println("在添加鍵值:" + key +"的時候造成非平衡,自動調整完成");
}
}else {
//繼續遞歸比較
push(node.getRight(),key);
}
}else if (key < node.getKey()){
//小於則加入左子節點
if (node.getLeft() == null){
node.setLeft(new Node(key,node));
//檢查是否破壞了平衡
if (this.balanced()){
System.out.println("在添加鍵值:" + key +"的時候造成非平衡,自動調整完成");
}
}else {
//檢查是否破壞了平衡
push(node.getLeft(),key);
}
}else {
// 等於則報錯
throw new RuntimeException("樹中已經有了該鍵值:" + key);
}
}
/**
* 刪除
* @param key 值
* @return 是否刪除
*/
public boolean delete(int key){
if (root == null){
//空樹直接加到根節點
return false;
}
return delete(root,key);
}
/**
* 刪除
* @param node 節點
* @param key 值
* @return 是否刪除
*/
public boolean delete(Node node , int key){
if (key > node.getKey()){
//大於則去右子樹
if (node.getRight() == null){
//沒有這個節點
return false;
}else {
//繼續遞歸比較
delete(node.getRight(),key);
}
}else if (key < node.getKey()){
//小於則去左子樹
if (node.getLeft() == null){
//沒有這個節點
return false;
}else {
//繼續遞歸比較
delete(node.getLeft(),key);
}
}
// 等於則刪除
Node father = node.getFather();
if (node.isLeaf()){
//沒有子節點 : 直接去除即可,不需要合併
if (father == null){
this.root = null;
}else if (father.getKey() > key){
father.setLeft(null);
}else {
father.setRight(null);
}
}else if (node.getLeft() != null && node.getRight() == null){
//只有一個子節點 :用待刪除節點的子節點替代他
//判斷該節點是左節點還是右節點
if (father == null){
this.root = node.getLeft();
}else if (father.getKey() > key){
father.setLeft(node.getLeft());
}else {
father.setRight(node.getLeft());
}
//維持父子關係
node.getLeft().setFather(father);
}else if (node.getRight() != null && node.getLeft() == null){
//只有一個子節點 :用待刪除節點的子節點替代他
if (father == null){
this.root = node.getRight();
}else if (father.getKey() > key){
father.setLeft(node.getRight());
}else {
father.setRight(node.getRight());
}
node.getRight().setFather(father);
}else {
// 有兩種方式,1、用刪除節點的左子樹最右側節點代替刪除節點、2、用刪除節點的右子樹的最左側的節點代替刪除節點。
// 這裏採用方式 1
// 獲取刪除節點左子樹的最大的值(即最右側節點)
Node max = this.findMax(node.getLeft());
if (father == null){
this.root = max;
}else if (father.getKey() > key){
father.setLeft(max);
}else {
father.setRight(max);
}
max.setFather(father);
max.setLeft(node.getLeft());
max.setRight(node.getRight());
node.getLeft().setFather(max);
node.getRight().setFather(max);
}
//調整
if (this.balanced()){
System.out.println("在刪除節點:" + key +"時造成非平衡,自動調整完成。");
}
return true;
}
/**
* 獲取一個節點最小值節點
* @param node 節點
* @return 節點
*/
private Node findMin(@NotNull Node node){
if (node.getLeft()==null){
return node;
}
return findMin(node.getLeft());
}
/**
* 獲取一個節點最大值節點
* @param node 節點
* @return 節點
*/
private Node findMax(@NotNull Node node){
if (node.getRight() == null){
return node;
}
return this.findMax(node.getRight());
}
/**
* 自動調整平衡
* @return 返回是否進行了調整操作
*/
private boolean balanced(){
if (root == null){
return false;
}
//採用前序遍歷是找的第一個,可能不是最小非平衡樹,所以這裏採用後續遍歷
Iterator<Node> iterator = backIterator();
while (iterator.hasNext()){
Node next = iterator.next();
// 平衡因子的絕對值 > 1 , 則需要調整
int bf = next.getLeftLength() - next.getRightLength();
if (bf < -1 || bf > 1){
//調整 , 傳入最小非平衡樹的根節點,與平衡因子,傳入平衡因子是因爲不想計算兩次 getLeftLength()方法也是採用遞歸的、
rotate(next, bf);
return true;
}
}
return false;
}
/**
* 旋轉
* @param node 最小非平衡子樹的根節點
* @param bf 平衡因子 爲了不再次去算
* @return 新的根節點
*/
private Node rotate(@NotNull Node node , int bf){
// one 是第一部分(根節點深度大的子樹) , node 是第二部分(根節點與深度小的子樹)
// two 是第二部分插入第一部分可能會覆蓋的節點,(其實就是 one的一個子節點 ,左右取決於 one 的左右【與 one 的左右相反】 )
// 注意下面左右旋的差別 即if塊裏的差別 (主要是看 左 右)
Node one , two ;
// bf(平衡因子 ) 大於0 說明根節點深度大的子樹在左邊 , 即 左旋
if (bf>0){
// 左旋
one = node.getLeft();
if (one.getRight() != null && !one.getRight().isLeaf()){
// 如果不是葉子節點 , 進行第二次旋轉
// 第二次旋轉是用第一部分的根節點進行旋轉 , 第二次是右旋
one = rotate(one, one.getBf());
}
// 截取two , 左旋取右節點 反之
two = one.getRight();
// 修改節點位置 , 重新合併one node two
// two 成爲node 的 左節點
node.setLeft(two);
// node 成爲 one 的右節點
one.setRight(node);
}else {
//右旋
one = node.getRight();
if (one.getLeft() != null && !one.getLeft().isLeaf()){
// 如果不是葉子節點 , 進行第二次旋轉
// 第二次旋轉是用第一部分的根節點進行旋轉 , 第二次是左旋
one = rotate(one, one.getBf());
}
two = one.getLeft();
// 修改節點位置 , 重新合併one node two
// two 成爲node 的 右節點
node.setRight(two);
// node 成爲 one 的左節點
one.setLeft(node);
}
//維護父子關係 (修改了節點位置 , 子節點記錄的父節點需要重新定位)
// 這裏 two 節點可能是空的
if (two !=null){
two.setFather(node);
}
if (node.getFather()==null){
// 如果傳入的最小非平衡子樹的根節點 是整棵樹的根節點 那麼需要修改 樹對象 記錄的根節點
one.setFather(null);
root = one;
}else {
// 不是上面這個條件時 , 傳入的最小非平衡子樹的根節點的父節點的 左 或者 右 節點需要修改成one 。
one.setFather(node.getFather());
if (bf > 0){
node.getFather().setLeft(one);
}else {
node.getFather().setRight(one);
}
}
// node 的 父親是 新樹根節點
node.setFather(one);
//返回one
return one;
}
/**
* 前序遍歷 先訪問根節點,再訪問左子樹,最後訪問右子樹
* ----------- 這裏採用的方法時順序加入到list中 , 這裏感覺應該用數組效率會好一點, 因爲這裏的數據其實是固定大小的
* @return 迭代器
*/
public Iterator<Node> frontIterator(){
ArrayList<Node> nodes = new ArrayList<>();
if (this.root != null){
addToFrontNodeList(nodes,this.root);
}
return nodes.iterator();
}
/**
* 前序遍歷 的 遞歸方法
* @param nodes 結果
* @param node 節點
*/
private void addToFrontNodeList(List<Node> nodes,Node node){
nodes.add(node);
if (node.getLeft()!=null){
addToFrontNodeList(nodes,node.getLeft());
}
if (node.getRight()!=null){
addToFrontNodeList(nodes,node.getRight());
}
}
/**
* 後序遍歷 先左子樹,再右子樹,最後根節點
* ----------- 這裏採用的方法時順序加入到list中 , 這裏感覺應該用數組效率會好一點, 因爲這裏的數據其實是固定大小的
* @return 迭代器
*/
public Iterator<Node> backIterator(){
ArrayList<Node> nodes = new ArrayList<>();
if (this.root != null){
addToBackNodeList(nodes,this.root);
}
return nodes.iterator();
}
/**
* 後續遍歷的遞歸方法
* @param nodes 結果
* @param node 節點
*/
private void addToBackNodeList(List<Node> nodes,Node node){
if (node.getLeft()!=null){
addToBackNodeList(nodes,node.getLeft());
}
if (node.getRight()!=null){
addToBackNodeList(nodes,node.getRight());
}
nodes.add(node);
}
/**
* 查找
* @param key 兼職
* @return 節點
*/
public Optional<Node> find(int key){
if (root == null){
return Optional.empty();
}else {
return find(root,key);
}
}
/**
* 查找的遞歸方法
* @param node 查找子樹的根節點
* @param key 值
* @return 節點
*/
private Optional<Node> find(@NotNull Node node , int key){
if (key > node.getKey()){
//大於則加到右子樹
if (node.getRight() == null){
return Optional.empty();
}else {
return find(node.getRight() , key);
}
}else if (key < node.getKey()){
if (node.getLeft() == null){
return Optional.empty();
}else {
return find(node.getLeft() , key);
}
}
return Optional.of(node);
}
/**
* 測試
* @param args 😄
*/
public static void main(String[] args) {
BalancedBinaryTree tree = new BalancedBinaryTree();
tree.push(3);
tree.push(2);
tree.push(1);
tree.push(4);
tree.push(5);
tree.push(7);
tree.push(8);
int leftLength = tree.root.getLeftLength();
int rightLength = tree.root.getRightLength();
tree.find(8).ifPresent(System.out::println);
tree.find(9).ifPresent(System.out::println);
System.out.println();
}
}
/**
* 這個節點的 set 方法應該不讓外部操作。
*/
@Data
class Node{
Node(int key , Node father){
this.key = key;
this.father = father;
}
/**
* 鍵值
*/
private int key;
/**
* 左節點
*/
private Node left ;
/**
* 右節點
*/
private Node right;
/**
* 父節點
*/
private Node father;
/**
* 遞歸求一個節點的最大子樹的深度
* ---------------注意 : 最大子樹的深度不包括該節點。
* 這個方法感覺可以獨立出去成爲一個靜態方法
* @param node 節點
* @return int 最大子樹深度
*/
private int findMaxLength(@NotNull Node node){
int left = node.left == null ? 0 : findMaxLength(node.left);
int right = node.right == null ? 0 : findMaxLength(node.right);
return Math.max(left,right) + 1;
}
/**
* 獲取左子樹的深度
* @return 成
*/
public int getLeftLength(){
return this.left == null ? 0 : this.findMaxLength(this.left);
}
/**
* 獲取右子樹的深度
* @return int 只有跟節點是0
*/
public int getRightLength(){
return this.right == null ? 0 : this.findMaxLength(this.right);
}
/**
* 平衡因子
* @return int
*/
public int getBf(){
return this.getLeftLength() - this.getRightLength();
}
/**
* 是否是葉子節點 , 左節點和右節點都是空說明是葉子節點
* ps : 葉子節點 是沒有孩子節點的節點
* @return boolean
*/
public boolean isLeaf(){
return left == null && right == null;
}
@Override
public String toString(){
return String.valueOf(key);
}
}