1. 程式人生 > >AVL樹插入刪除演算法詳解(有圖) -- C++語言實現

AVL樹插入刪除演算法詳解(有圖) -- C++語言實現

一:AVL樹介紹

AVL樹本質上還是一棵二叉搜尋樹,它的特點是: 1.本身首先是一棵二叉搜尋樹。 2.帶有平衡條件:每個結點的左右子樹的高度之差的絕對值(平衡因子)最多為1。在本文中用分別用-1,0,1定義左邊樹高,等高,右邊樹高。平衡因子用m_bf表示。 也就是說,AVL樹,本質上是帶了平衡功能的二叉查詢樹(二叉排序樹,二叉搜尋樹)。

二:插入演算法

由於AVL樹具有BST樹的特性,所以它的插入演算法思路上和BST樹基本步驟是相同的,不過在BST樹插入演算法的基礎上增加了由於平衡因子的變動,平衡化平衡因子的步驟,使得整棵樹達到平衡因子絕對值不超過1的高度平衡。 在AVL樹插入的過程中,為了使樹再次平衡,需要作相應的旋轉,分為單旋轉和雙旋轉。又細分為左旋轉,右旋轉,左右旋轉,右左旋轉。 下面將就其中的主要情況加以說明:

(1)左旋轉

左旋轉的情況如圖所示: 當我們插入元素出現類似的情況時,就需要左旋轉來出馬了。ptr是傳給旋轉函式的引數,我們先讓ptr走到它的右孩子,用ptr表示旋轉後的最上面的節點(有可能為根節點),用sub_left表示旋轉後位置在左側的節點,sub_right表示旋轉後位置在右側的節點(該圖還未出現)。下文同此處。當出現上圖情況是,我們只需用sub_left替換ptr的左孩子即可,並修改相應的平衡因子。不過有一點很重要,ptr的左孩子並不一定像圖中那樣為空,所以若要有左孩子,需要先將左孩子賦給sub_left的右孩子。 程式碼體現如下:
template <typename T>
void avl_tree<T>::rotate_left(node_type *&ptr)
{
	auto sub_left = ptr;      //先指定sub_left
	ptr = ptr->right_child;   //ptr走到它的右孩子

	sub_left->right_child = ptr->left_child;   //將ptr的左孩子覆蓋sub_left的右孩子
	ptr->left_child = sub_left;         //旋轉
	sub_left->m_bf = ptr->m_bf = 0;    //調整平衡因子
}

(2)右旋轉

右旋轉和左旋轉程式碼只是映象關係,就不重述一遍了。

(3)先左旋,後右旋


我們插入時可能碰到上面的情況,可以針對3、7進行左旋轉,sub_left替換ptr的左孩子,再對7、16進行右旋轉,sub_right替換ptr的右孩子。注意,若ptr有左右孩子,要將ptr的左孩子掛在sub_left的右孩子上,將ptr的右孩子掛在sub_left的左孩子上。 不過雙旋轉需要注意的是,平衡因子的情況不會像單旋轉那麼簡單了,還可能出現下面的情況: 在這種情況中,2和18結點都是必須的,否則新插入9元素會直接導致3、7、9不平衡,程式直接執行左單旋轉,而我們現在要討論的是雙旋轉。 圖中新增元素9或5,則3、7先左旋轉,7、16後右旋轉。分情況如下: 1.ptr的平衡因子為0:   ptr平衡因子為0時,如圖2.1,經過雙旋轉後,參與旋轉的節點平衡因子都為0。 2.ptr的平衡因子為1:   ptr平衡因子為1時,如圖2.2,經過旋轉後,sub_right的平衡因子為0,sub_left的平衡因子為-1。 3.ptr的平衡因子為-1:   ptr的平衡因子為-1時,如圖2.3,經過旋轉後,sub_right的平衡因子為1,sub_left的平衡因子為0。 上述三種情況概括了雙旋轉平衡因子的變化情況,程式碼體現如下:
template <typename T>
void avl_tree<T>::rotate_left_right(node_type *&ptr)
{
	auto sub_right = ptr;
	auto sub_left = ptr->left_child;
	ptr = sub_left->right_child;

	sub_left->right_child = ptr->left_child;
	ptr->left_child = sub_left;
	if(ptr->m_bf <= 0)               //根據調整平衡因子
		sub_left->m_bf = 0;
	else
		sub_left->m_bf = -1;

	sub_right->left_child = ptr->right_child;
	ptr->right_child = sub_right;
	if(ptr->m_bf >= 0)               //根據ptr調整平衡因子
		sub_right->m_bf = 0;
	else
		sub_right->m_bf = 1;
	
	ptr->m_bf = 0;    //此句千萬不能忘,否則平衡因子出錯!
}

(4)先右旋,再左旋

和上面只是映象關係,不再贅述。

三:刪除演算法

我將刪除演算法的核心也分以下幾幅圖來描述。

(1)當刪除節點之後,它的父節點pre_tmp的平衡因子為+1/-1,表明該節點的子樹高度並沒有改變,只有有一條分支短了一截。如圖所示,這種情況說明刪除元素後,整棵樹依舊平衡,直接退出即可。

(2)當刪除節點之後,該父節點pre_tmp平衡因子為0,那麼只能保證以父節點為根的子樹平衡,整棵樹的平衡還需要繼續回溯其祖父節點ppre_tmp,迴圈執行,直至棧空。 (3)當刪除節點之後,該父節點pre_tmp平衡因子為-2/+2(以下以-2為例,與+2為映象關係,不贅述),有三種情況:     i.pre_tmp的較高的子樹的頭個節點(如下圖90)平衡因子為0,注意:為0!,此時我們可以直接進行一次右旋轉。但是,在這種情況下,一定要修改平衡因子!因為該情況下,pre_tmp較高的子樹的頭個節點(如下圖90),它的左右子樹個數一定為2!當執行右旋轉時,我們的旋轉函式會將轉函式內部預設將ptr和sub_right的平衡因子修改為0,所以在此處執行完單旋轉函式後,必須令ptr的平衡因子為1,令sub_right的平衡因子為-1。下圖的映象情況為左旋轉的情況,左旋轉結束後令ptr平衡因子為-1,令sub_left的平衡因子為1即可,不再贅述。
    ii.pre_tmp的較高的子樹的頭個節點平衡因子為1,由於與pre_tmp的平衡因子-2反號,如下圖,直接進行先左旋轉,再右旋轉即可。 注意:我們的旋轉函式已經討論過類似形式的樹的平衡化及平衡因子的修改,此處不必再修改平衡因子。如果下圖中結點2若存在孩子節點,那麼就轉化為情況 i了,就必須追加修改平衡因子。     iii.pre_tmp的較高的子樹的頭個節點的平衡因子為-1,與pre_tmp同號,直接進行單旋轉即可。不必再修改平衡因子。

四:AVL樹程式碼

#ifndef _AVLTREE_H
#define _AVLTREE_H

#include <iostream>
#include <assert.h>
#include <stack>
using namespace std;

template <typename T>
class avl_tree;

template <typename T>
class avl_node{
	friend class avl_tree<T>;
public:
	avl_node() : m_data(T()), left_child(nullptr), right_child(nullptr)
	{}
	avl_node(T data, avl_node<T> *left = nullptr, avl_node<T> *right = nullptr)
			: m_data(data), left_child(left), right_child(right)
	{}
	~avl_node() = default;
private:
	T m_data;
	int m_bf;
	avl_node<T> *left_child;
	avl_node<T> *right_child;
};

template <typename T>
class avl_tree{
	typedef avl_node<T> node_type;
public:
	avl_tree() : root(nullptr)
	{}
	~avl_tree();
public:
	bool insert_elem(const T&);
	bool remove_elem(const T&);
	void inorder_traverse()const;
protected:
	void destroy_tree(node_type *&);
	bool insert_elem(node_type *&, const T&);
	bool remove_elem(node_type *&, const T&);
	void inorder_traverse(node_type *)const;
protected:
	void rotate_left(node_type *&);
	void rotate_right(node_type *&);
	void rotate_left_right(node_type *&);
	void rotate_right_left(node_type *&);
private:
	node_type *root;
};

//public interface

template <typename T>
avl_tree<T>::~avl_tree()
{
	//destroy_tree(root);
}

template <typename T>
bool avl_tree<T>::insert_elem(const T& val)
{
	insert_elem(root, val);
}

template <typename T>
bool avl_tree<T>::remove_elem(const T& val)
{
	remove_elem(root, val);
}

template <typename T>
void avl_tree<T>::inorder_traverse()const
{
	inorder_traverse(root);
}

//protected interface

template <typename T>
void avl_tree<T>::destroy_tree(node_type *&t)
{
	if(t != NULL){
		destroy_tree(t->left_child);
		destroy_tree(t->right_child);
		delete t;
	}
}

template <typename T>
void avl_tree<T>::inorder_traverse(node_type *t)const
{
	if(t != nullptr){
		inorder_traverse(t->left_child);
		cout << t->m_data << " ";
		inorder_traverse(t->right_child);
	}
}

template <typename T>
bool avl_tree<T>::insert_elem(node_type *&t, const T& val)
{
	node_type *tmp = t;
	node_type *pre_tmp = nullptr;
	stack<node_type* > st;   //用棧來儲存路徑,當插入元素後,通過出棧回溯,平衡化祖先節點
	int lable;              //標籤

	while(tmp != nullptr){     //首先查詢要插入的地方
		pre_tmp = tmp;
		st.push(pre_tmp);
		if(val == tmp->m_data)
			return false;
		else if(val < tmp->m_data)
			tmp = tmp->left_child;
		else
			tmp = tmp->right_child;
	}

	tmp = new node_type(val);      //新節點生成
	
	if(t == nullptr){              //如果t為空,根節點
		t = tmp;
	}
	else{
		if(tmp->m_data < pre_tmp->m_data){         //左子樹
			pre_tmp->left_child = tmp;
		}
		else{
			pre_tmp->right_child = tmp;             //右子樹
		}

		while(!st.empty()){
			pre_tmp = st.top();
			st.pop();
			
			if(pre_tmp->left_child == tmp)
				pre_tmp->m_bf--;                    //平衡因子-1
			else
				pre_tmp->m_bf++;                    //平衡因子+1

			if(pre_tmp->m_bf == 0)
				break;
			else if(pre_tmp->m_bf == -1 || pre_tmp->m_bf == 1)
				tmp = pre_tmp;
			else{
				lable = pre_tmp->m_bf > 0 ? 1 : -1;     //標籤,用來記錄父節點平衡因子符號
				if(tmp->m_bf == lable){       //same symbol,同號,單旋轉
					if(lable == 1){      //(L -- \)           
						rotate_left(pre_tmp);
					}
					else{	             //(R -- /)
						rotate_right(pre_tmp);
					}
				}
				else{
					if(lable == -1){     //(LR -- <)       //不同號,雙旋轉
						rotate_left_right(pre_tmp);            
					}
					else{                //(RL -- >)
						rotate_right_left(pre_tmp);
					}
				}
				break;            //!!! don't forget!   由於修改後已經平衡,必須break,否則會繼續回溯,可能導致segmentation fault
			}
		}		
		if(st.empty())
			t = pre_tmp;        //如果棧空,即位頭節點
		else{                   //不空,連結
			auto pre_tmp_ = st.top();
			if(pre_tmp->m_data < pre_tmp_->m_data)
				pre_tmp_->left_child = pre_tmp;
			else
				pre_tmp_->right_child = pre_tmp;
		}
	}
	return true;
}

template <typename T>
bool avl_tree<T>::remove_elem(node_type *&t, const T& val)
{
	assert(t != nullptr);

	node_type *tmp = t;
	node_type *pre_tmp = nullptr;
	node_type *ppre_tmp = nullptr;
	node_type *it_tmp = nullptr;
	stack<node_type*> st; 
	int sign, lable;    //符號標記
	int flag = 0;       //子樹標記,下文具體解釋

	while(tmp != nullptr){
		if(tmp->m_data == val)       //找到跳出迴圈
			break;
		
		pre_tmp = tmp;
		st.push(pre_tmp);
		
		if(tmp->m_data > val)
			tmp = tmp->left_child;
		else
			tmp = tmp->right_child;
	}

	if(tmp == nullptr)               //未找到,返回
		return false;
	else if(tmp->left_child != nullptr && tmp->right_child != nullptr){  
		pre_tmp = tmp;               //將有兩個孩子的節點轉化為只有一個孩子的節點,方法是尋找它中序遍歷的直接前驅或後繼
		st.push(pre_tmp);
		
		it_tmp = tmp->left_child;                             
		while(it_tmp->right_child != nullptr){
			pre_tmp = it_tmp;
			st.push(pre_tmp);
			it_tmp = it_tmp->right_child;
		}
		tmp->m_data = it_tmp->m_data;      //覆蓋要刪除的節點
		tmp = it_tmp;                      //tmp指向要刪除的節點,函式結束前會delete tmp
	}
	
	if(tmp->left_child != nullptr){        //這樣的判斷方式會導致刪除一個節點下兩個沒有孩子節點的節點時,由於左孩子均為空,直接跳入else
		it_tmp = tmp->left_child;
	}
	else{
		it_tmp = tmp->right_child;         
	}

	if(pre_tmp == nullptr)
		t = it_tmp;
	else{
		if(pre_tmp->left_child == tmp){   //上面直接跳入else,但我們在此處加上flag,用來識別它到底是pre_tmp的左孩子還是右孩子。
			flag = 0;
			pre_tmp->left_child = it_tmp;
		}
		else{
			flag = 1;
			pre_tmp->right_child = it_tmp;
		}	

		while(!st.empty()){
			pre_tmp = st.top();
			st.pop();

			if(pre_tmp->left_child == it_tmp && flag == 0)//此處flag=0,防止pre_tmp的左孩子為空,右孩子同樣為空,直接進入else
				pre_tmp->m_bf++;
			else
				pre_tmp->m_bf--;
			
			if(!st.empty())
			{
				ppre_tmp = st.top();
				if(ppre_tmp->left_child == pre_tmp)
				{
					sign = -1;      //sign用來識別是祖父節點的左孩子還是右孩子,下文連結會用上
					flag = 0;
				}
			    else{
					sign = 1;
					flag = 1;
				}
			}
			else
				sign = 0;    //棧空,它的祖先節點不存在,pre_tmp即為根節點,但是這裡也要寫上,否則sign的值會遺留下來

			if(pre_tmp->m_bf == -1 || pre_tmp->m_bf == 1)
				break;           //已經平衡,直接跳出
			if(pre_tmp->m_bf != 0){      //m_bf為+2/-2
				if(pre_tmp->m_bf < 0){   
					lable = -1;          //lable表示父節點符號,下面會用來區分同號異號
					it_tmp = pre_tmp->left_child;
				}
				else{
					lable = 1;
					it_tmp = pre_tmp->right_child;
				}

				if(it_tmp->m_bf == 0){       //pre_tmp的較高子樹的頭節點m_bf為0
					if(lable == -1){
						rotate_right(pre_tmp);
						pre_tmp->m_bf = 1;					
						pre_tmp->right_child->m_bf = -1;
					}
					else{
						rotate_left(pre_tmp);
						pre_tmp->m_bf = -1;
						pre_tmp->left_child->m_bf = 1;
					}
					break;           //直接跳出,並沒有連結,需要下文寫上鍊接
				}

				if(it_tmp->m_bf == lable){       //同號 
					lable == 1 ? rotate_left(pre_tmp) : rotate_right(pre_tmp);
				}
				else{                            //異號
					lable == -1 ? rotate_left_right(pre_tmp) 
										: rotate_right_left(pre_tmp);					
				}
				//sign == -1 ? ppre_tmp->left_child = pre_tmp                //不能寫成這樣,因為sign值可能為0,會直接進入後者
				//						: ppre_tmp->right_child = pre_tmp;   //!!!!! sign maybe 0 ! not only1 or -1 !!! warning!
				if(sign == -1)             
					ppre_tmp->left_child = pre_tmp;
				else if(sign == 1)            //else if正確方式
					ppre_tmp->right_child = pre_tmp;
			}
			it_tmp = pre_tmp;       //回溯
		}
		if(st.empty())              //棧為空,根節點
			t = pre_tmp;
		else{             //這一段else參考書上沒有,書是錯的,如果不寫此處,290break直接跳出while迴圈,會導致連結不上,資料丟失
			ppre_tmp = st.top();
			if(ppre_tmp->m_data > pre_tmp->m_data)
				ppre_tmp->left_child = pre_tmp;
			else
				ppre_tmp->right_child = pre_tmp;
		}
	}
	delete tmp;
	tmp = nullptr;
	return true;
}

template <typename T>
void avl_tree<T>::rotate_left(node_type *&ptr)             //左旋轉
{
	auto sub_left = ptr;
	ptr = ptr->right_child;

	sub_left->right_child = ptr->left_child;
	ptr->left_child = sub_left;
	sub_left->m_bf = ptr->m_bf = 0;
}

template <typename T>
void avl_tree<T>::rotate_right(node_type *&ptr)            //右旋轉
{
	auto sub_right = ptr;
	ptr = ptr->left_child;

	sub_right->left_child = ptr->right_child;
	ptr->right_child = sub_right;
	sub_right->m_bf = ptr->m_bf = 0;
}

template <typename T>
void avl_tree<T>::rotate_left_right(node_type *&ptr)       //左右旋轉
{
	auto sub_right = ptr;
	auto sub_left = ptr->left_child;
	ptr = sub_left->right_child;

	sub_left->right_child = ptr->left_child;
	ptr->left_child = sub_left;
	if(ptr->m_bf <= 0)
		sub_left->m_bf = 0;
	else
		sub_left->m_bf = -1;

	sub_right->left_child = ptr->right_child;
	ptr->right_child = sub_right;
	if(ptr->m_bf >= 0)
		sub_right->m_bf = 0;
	else
		sub_right->m_bf = 1;
	
	ptr->m_bf = 0;            //不要漏寫
}

template <typename T>
void avl_tree<T>::rotate_right_left(node_type *&ptr)       //右左旋轉
{
	auto sub_left = ptr;
	auto sub_right = ptr->right_child;
	ptr = sub_right->left_child;

	sub_right->left_child = ptr->right_child;
	ptr->right_child = sub_right;
	if(ptr->m_bf >= 0)
		sub_right->m_bf = 0;
	else
		sub_right->m_bf = 1;
	
	sub_left->right_child = ptr->left_child;
	ptr->left_child = sub_left;
	if(ptr->m_bf <= 0)
		sub_left->m_bf = 0;
	else
		sub_left->m_bf = -1;

	ptr->m_bf = 0;
}

#endif /*avl_tree.h*/

五:測試程式碼

#include "avl_tree.h"
#define ElemType int

int main()
{
	//ElemType array[] = {50, 60, 79};   //rotate_left
	
	//ElemType array[] = {50, 20, 10};   //rotate_right
	
	//ElemType array[] = {16, 3, 7};     //rotate_left_right
	//ElemType array[] = {16, 3, 18, 2, 7, 9};
	//ElemType array[] = {16, 3, 18, 2, 7, 5};
	
	//ElemType array[] = {16, 30, 12, 31, 20, 21};    //rotate_right_left
	//ElemType array[] = {16, 30, 12, 31, 20, 17};
    
	//ElemType array[] = {16,3,7,11,9,26,18,14,15};
	ElemType array[] = {80, 50, 100, 40, 60, 90, 140, 45, 83, 95, 150, 81, };
	//ElemType array[] = {16, 12};
	avl_tree<int> avl;
	
	size_t len = sizeof(array) / sizeof(ElemType);
	for(size_t i=0; i<len; ++i){
		avl.insert_elem(array[i]);
	}
	avl.inorder_traverse();
	cout << endl;
	
	avl.remove_elem(140);
	//avl.remove_elem(3);
	//avl.remove_elem(9);
	avl.inorder_traverse();
	cout << endl;
	
	return 0;
}
測試結果: (寫文不易,轉載請註明出處)