概念分析
- 平衡二叉樹也叫平衡二叉搜索樹,又被稱爲AVL樹,它能保證查詢效率較高
- 他具有以下特點:它是一顆空樹或者它的左右兩顆子樹的高度差的絕對值不大於一,並且左右子樹都是平衡二叉樹,其也滿足二叉排序樹的特點
小練習
看過上述的概念之後,我們應該可以分辨哪些是平衡二叉樹。
爲什麼出現平衡二叉樹
上一章講的是二叉排序樹,其效率一般是大於鏈表的,那麼如果我們添加的數列是{1,2,3,4,5,6}呢?
我們會發現一個問題,我們構建的BST成爲了一個鏈表形狀,但是效率卻不如鏈表,因爲進行遍歷查找還需要判斷是否有左子結點。
建立節點類和AVL類
結點類
class Node{
int value;
Node left;
Node right;
public Node(int value){
this.value = value;
}
/**
* 查找待刪除的節點
* @param value 待刪除節點的值
* @return 返回被刪除的節點 不存在該節點則返回null
*/
public Node search(int value){
if(this.value == value){
return this;
} else if (value < this.value){ // 二叉排序樹的左子結點都小於其父結點的值
if (this.left == null){
return null;
}else {
return this.left.search(value);
}
}else { // 如果不小於其父結點的值 那麼可以判斷在右子結點
if (this.right == null){
return null;
}else {
return this.right.search(value);
}
}
}
/**
* 查找待刪除節點的父結點
* @param value 待刪除節點的值
* @return 待刪除節點的父結點
*/
public Node searchParent(int value){
if((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)){
return this;
} else {
if (value < this.value && this.left != null){
return this.left.searchParent(value);
} else if (value >= this.value && this.right != null){
return this.right.searchParent(value);
}else {
return null;
}
}
}
/**
* BST添加節點
* @param node 待添加的節點
*/
public void add(Node node){
if(node == null){
return;
}
// 添加的節點小於此節點的值 我們將其添加到左子結點處
if (node.value < this.value){
if (this.left == null){
this.left = node;
}else {
// 如果其左子結點不爲空,那麼我們將要插入的節點與其左子結點進行比較
this.left.add(node);
}
}else { // 相等的或者大於的都添加到右子結點
if (this.right == null){
this.right = node;
}else {
this.right.add(node);
}
}
// 左旋轉發生在添加之後,如果右子樹高度比左子樹的高度超過一
if (this.rightHeight() - 1 > this.leftHeight()){
if (this.right != null && this.right.leftHeight() > this.right.rightHeight()){
this.right.rightRotate();
}
this.leftRotate();
}
// 左子樹高度比右子樹的高度超過一
if (this.leftHeight() - 1 > this.rightHeight()){
if (this.left != null && this.left.rightHeight() > this.left.leftHeight()){
this.left.leftRotate();
}
this.rightRotate();
}
}
/**
* BST中序遍歷
*/
public void inorderTraversal(){
if (this.left != null){
this.left.inorderTraversal();
}
System.out.print(this.value + "\t");
if (this.right != null){
this.right.inorderTraversal();
}
}
/**
* 以當前節點爲根節點的樹的高度
* @return 高度
*/
public int height(){
return Math.max(this.left == null ? 0 : this.left.height(), this.right == null ? 0 : this.right.height()) + 1;
}
/**
* 以當前結點爲根節點的右子樹的高度
* @return 右子樹的高度
*/
public int rightHeight(){
if (this.right == null){
return 0;
}else {
return this.right.height();
}
}
/**
* 以當前節點爲根節點的左子樹的高度
* @return 左子樹的高度
*/
public int leftHeight(){
if (this.left == null){
return 0;
}else {
return this.left.height();
}
}
// 左旋轉
public void leftRotate(){
// 1. 新建一個節點 值等於當前結點
Node newNode = new Node(this.value);
// 2. 新結點的左子結點指向當前結點的左子結點
newNode.left = this.left;
// 2. 新結點的右子結點指向當前結點的右子結點的左子結點
newNode.right = this.right.left;
// 4. 當前結點的值等於當前結點的右子結點
this.value = this.right.value;
// 5. 當前結點的有右子樹等於右子結點的右子樹
this.right = this.right.right;
// 6. 當前結點的左子樹設置爲新建的結點
this.left = newNode;
}
// 右旋轉
public void rightRotate(){
// 1. 新建一個結點 值等於當前結點
Node newNode = new Node(this.value);
// 2. 新結點的右子結點指向當前結點的右子結點
newNode.right = this.right;
// 3. 新結點的左子結點等於當前結點的左子結點的右子結點
newNode.left = this.left.right;
// 4. 當前結點的值等於當前結點的左子結點
this.value = this.left.value;
// 5. 當前結點的左子樹等於當前結點的左子樹的左子樹
this.left = this.left.left;
// 6. 當前結點的右子樹等於新建的節點
this.right = newNode;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
AVL類
class AVLTree{
Node root;
/**
* 找到左子樹中最大的值
* @param node 左子樹的根節點
* @return 返回左子樹中最大的值
*/
public int deleteLeftMax(Node node){
Node temp = node;
while (temp.right != null){
temp = temp.right;
}
// 找到左側最大 將其刪除
temp = delete(temp.value);
return temp.value;
}
/**
* 找到右子樹中最小的值
* @param node 右子樹的根節點
* @return 返回右子樹中最小的值
*/
public int deleteRightMin(Node node){
Node temp = node;
while (temp.left != null){
temp = temp.left;
}
// 找到右側最小 將其刪除
temp = delete(temp.value);
return temp.value;
}
public Node delete(int value){
// 如果當前BST爲空 直接結束流程 返回空
if (root == null){
return null;
} else {
// 1. 先去找到待刪除的節點
Node targetNode = search(value);
// 沒有找到待刪除節點
if (targetNode == null){
return null;
}
// 如果可以找到待刪除節點 並且BST只有一個root
if (root.left == null && root.right == null){
Node temp = root;
root = null;
return temp;
}
// 如果可以找到待刪除節點 並且BST不只有一個root,那麼待刪除節點必然有父結點
Node parent = searchParent(value);
// 判斷待刪除節點是否是葉子節點
if(targetNode.left == null && targetNode.right == null){
if (parent.left != null &&parent.left.value == value){
Node temp = parent.left;
parent.left = null;
return temp;
} else if (parent.right != null && parent.right.value == value){
Node temp = parent.right;
parent.right = null;
return temp;
}else {
return null;
}
}else if (targetNode.left != null && targetNode.right != null){
// 這種情況是待刪除節點有兩個子結點
int target;
// target = deleteRightMin(targetNode.right);
target = deleteLeftMax(targetNode.left);
targetNode.value = target;
}else {
// 待刪除節點只有一個子結點
// 待刪除節點的子結點是左子結點
if(targetNode.left != null){
if (parent != null){
if (parent.left.value == value){
parent.left = targetNode.left;
}else {
parent.right = targetNode.left;
}
}else {
root = targetNode.left;
}
}else {
if (parent != null){
if (parent.left.value == value){
parent.left = targetNode.right;
}else {
parent.right = targetNode.right;
}
}else {
root = targetNode.right;
}
}
return targetNode;
}
return null;
}
}
public Node search(int value){
if (root == null){
return null;
}else {
return root.search(value);
}
}
public Node searchParent(int value){
if (root == null){
return null;
}else {
return root.searchParent(value);
}
}
/**
* BST添加節點
* @param node 待被添加的節點
*/
public void add(Node node){
if (root == null) {
root = node;
}else {
root.add(node);
}
}
/**
* BST實現中序遍歷
*/
public void inorderTraversal(){
if (root == null){
System.out.println("BST爲空");
}else {
root.inorderTraversal();
}
}
}
一個小問題
我們怎麼得到當前結點爲根節點的子樹的高度呢?
遞歸解決,取左邊的最高值和右邊最高值,取到最大值以後加上結點本身即爲高度
/**
* 以當前節點爲根節點的樹的高度
* @return 高度
*/
public int height(){
return Math.max(this.left == null ? 0 : this.left.height(), this.right == null ? 0 : this.right.height()) + 1;
}
取左子樹的高度:
/**
* 以當前節點爲根節點的右子樹的高度
* @return 右子樹的高度
*/
public int leftHeight(){
if (this.left == null){
return 0;
}else {
return this.left.height();
}
}
取右子樹的高度:
/**
* 以當前結點爲根節點的右子樹的高度
* @return 右子樹的高度
*/
public int rightHeight(){
if (this.right == null){
return 0;
}else {
return this.right.height();
}
}
左旋轉
根據數列 {4, 3, 6, 5, 7, 8}創建一顆二叉排序樹,我們發現樹是這個樣子的
這個是不滿足二叉排序樹的,我們要將其構建爲二叉排序樹,就需要進行左旋轉。
步驟如下:
1. 我們新建一個結點,其值等於根節點
2. 將新建的結點的左子結點指向根節點的左子結點
3. 將新建節點的右子結點指向根節點的右子結點的左子結點
4. 將根節點的值置爲根節點的右子結點的值
5. 將根節點的右子結點置爲根節點的右子結點的右子結點
6. 將根節點的左子結點的值置爲新建的節點
圖解:
我們新建一個結點,值爲4,其左子結點指向3,右子結點是7,之後將根節點的值置爲6,根節點的右子結點置爲根節點的右子結點的右子結點,那麼就是我上圖中右邊畫的這個二叉樹,此時滿足平衡二叉樹。
代碼實現:
public void leftRotate(){
// 1. 新建一個節點 值等於當前結點
Node newNode = new Node(this.value);
// 2. 新結點的左子結點指向當前結點的左子結點
newNode.left = this.left;
// 2. 新結點的右子結點指向當前結點的右子結點的左子結點
newNode.right = this.right.left;
// 4. 當前結點的值等於當前結點的右子結點
this.value = this.right.value;
// 5. 當前結點的有右子樹等於右子結點的右子樹
this.right = this.right.right;
// 6. 當前結點的左子樹設置爲新建的結點
this.left = newNode;
}
右旋轉
根據數列{10, 12, 8, 9, 7, 6}創建一顆二叉排序樹,我們發現樹是這個樣子的
此時左子樹的高度大於了右子樹的高度,且大於的值超過了1,我們需要對其進行右旋
步驟如下:
1. 新建一個結點,其值等於根節點
2. 新建結點的右子節點等於根節點右子節點
3. 新建結點的左子結點等於根結點的左子結點的右子結點
4. 根節點的值等於根節點的左子結點的值
5. 根節點的左子結點等於根結點的左子結點的左子結點
6. 根節點的右子結點等於新建的結點
圖解:
代碼如下
public void rightRotate(){
// 1. 新建一個結點 值等於當前結點
Node newNode = new Node(this.value);
// 2. 新結點的右子結點指向當前結點的右子結點
newNode.right = this.right;
// 3. 新結點的左子結點等於當前結點的左子結點的右子結點
newNode.left = this.left.right;
// 4. 當前結點的值等於當前結點的左子結點
this.value = this.left.value;
// 5. 當前結點的左子樹等於當前結點的左子樹的左子樹
this.left = this.left.left;
// 6. 當前結點的右子樹等於新建的節點
this.right = newNode;
}
雙旋轉
可是有的時候單旋轉並不能解決問題,如下:
int[] arr = {10, 11, 7, 6, 8, 9};
使用此數列進行右旋轉,會產生什麼情況呢?
這就尷尬了,怎麼右旋轉完了感覺還得左旋轉一下呢?
問題分析:
因爲根節點的左子結點的右子樹的高度大於其左子結點的左子樹的高度,我們進行右旋轉時,需要將根節點的左子樹的右子結點設置爲新結點的左子結點(看不懂的往上翻),根節點的左子結點設置爲根節點的左子結點的左子結點,那問題來了,現在根結點的左子節點的左子樹的高度已然小於根節點的右子結點的右子樹的高度了,我們再將其旋轉,只不過是將其左子樹高變成右子樹高而已。
解決辦法:
我們可以先將根節點的左子結點進行一次左旋轉,之後對根節點進行右旋轉
如果是右子樹出現這種情況,那我們讓其右旋,之後左旋即可
情況限制:
必須是滿足根節點的左右子樹高度差的絕對值大於一,並且較高的那顆子樹如果是左子樹,這棵子樹的根節點的右子樹的高度大於其左子樹的高度,或者較高的那顆子樹是右子樹,那麼就是右子樹的左子樹的高度大於右子樹的右子樹的高度,這時候我們需要雙旋轉!!!
代碼分析:
// 插入在add方法的最後面,每次插入我們都進行一次判斷是否需要旋轉
// 左旋轉發生在添加之後,如果右子樹高度比左子樹的高度超過一
if (this.rightHeight() - 1 > this.leftHeight()){
if (this.right != null && this.right.leftHeight() > this.right.rightHeight()){
this.right.rightRotate();
}
this.leftRotate();
}
// 左子樹高度比右子樹的高度超過一
if (this.leftHeight() - 1 > this.rightHeight()){
if (this.left != null && this.left.rightHeight() > this.left.leftHeight()){
this.left.leftRotate();
}
this.rightRotate();
}
小結
平衡二叉樹是爲了改進二叉排序樹而設計的,重點是左旋轉和右旋轉,雙旋轉是指在滿足左旋轉或右旋轉的基礎上,再進行一次判斷,然後將其調整爲單次旋轉可以調整爲平衡二叉樹的程度。
星光不符趕路人。