1. 程式人生 > >沒事兒就學習(1):AVL樹插入

沒事兒就學習(1):AVL樹插入

       AVL樹是最先發明的自平衡二叉樹,這是個啥樹呢,為啥要平衡呢?我們可以在分析其原理的時候慢慢用C++實現它一下。

       故名思意,自平衡二叉樹是一種可以實現自我平衡的二叉樹,分開看是自動化的、平衡的、二叉樹。資料結構中的二叉樹是一種特殊的樹,因為其限定了樹中的每個節點最多隻能包含兩個子節點,那麼根據其特性就不難發現在二叉樹的第n層,就最多僅有2^(n-1)個子節點。二叉樹可以沿任意一側不斷延伸到任意的深度,導致了二叉樹可以具有各種各樣的形狀,比如下面畫的巨醜的一張圖,左邊是一棵滿二叉樹,而右邊則是一棵普通二叉樹。

 

       規整的二叉樹和不規整的二叉樹差別在什麼地方呢?從直觀上來說,一棵樹越趨向於滿二叉樹,那麼它的通過遞迴查詢的次數可能越少,效率就越高,因此趨向於將資料向滿二叉樹排列。那麼一個簡單的排序樹是什麼樣的呢?我們先隨便說一串數字,比如{1,2,4,8,5,7,3,9,6},在構建樹的時候,先把第一個元素作為根節點,然後後續插入時將比節點中數值大的作為其右子節點,否則為左子節點,具體的流程可以從下圖的步驟中看出。

      按順序構建的二叉樹長的和北斗七星一樣,9個數值構建出的深度竟然達到了7層之多,如果按滿二叉樹的方式構建,最多隻需要4層就足夠進行擺放。那麼有沒有什麼方法可以將狂野生長的二叉樹變成比較規矩的,每一層都有足夠節點的二叉樹呢?平衡的概念就此提出。我們很明顯的看出上圖中最後生成的樹一點都不平衡,根節點左側啥也沒有,右側延伸出一個北斗七星,只有在左右兩側節點深度和數量差不多的情況下樹才能平衡啊,不然就是一邊倒了,所以我們要給出一些限定條件來轉換節點之間的位置和順序。


       平衡有相對的平衡和絕對的平衡,自然界中沒有絕對的平衡存在,所有的平衡都有一定的緩衝範圍,也就是所謂的閾值,超過這個閾值的限定即為不平衡,需要通過一定的手段對環境進行再平衡。這個閾值在自平衡二叉樹中體現在每個結點的左子樹和右子樹的深度相差不能大於1

,否則即為不平衡的狀態,否則需要通過一系列的手段,左旋右旋將樹轉換到穩定態。我們先拿一個最簡單的例子{1,2,3}進行一下平衡性實驗,如下圖所示,首先生成的樹如上方所示,1作為根節點,2是1的右子節點,3是2的右子節點,那麼根節點1的左子樹深度為0,右子樹深度為2,相差2>1已經處於不平衡的狀態。那麼怎麼樣可以使其變成平衡態呢?很明顯,將2作為根節點可以有效解決這一問題。那麼怎麼將2作為根節點呢?我們可以把這三個節點向左旋轉,1作為2的左子節點,3作為2的右子節點,旋轉完畢後根節點2左子樹深度為1,右子樹深度為1,實現了平衡。這樣的操作我們就稱之為左旋同樣的如果是左側子樹的深度大於右側子樹的深度,就需要對其進行
右旋

       所以我們可以先定義一下二叉樹節點的資料結構,每個節點可以儲存一個值,儲存左孩子和右孩子,儲存當前平衡的狀態,因此定義如下。

//列舉:平衡狀態
enum HeightType
{
	LEFTHEIGHT,//左深
	RIGHTHEIGHT,//右深
	EQUALHEIGHT//平衡
};

class TreeNode {
public:
	int value;//儲存值
	TreeNode *leftChild;//左枝
	TreeNode *rightChild;//右枝
	HeightType heightType;//平衡狀態
	//建構函式
	TreeNode(int value) {
		this->value = value;
		this->leftChild = nullptr;
		this->rightChild = nullptr;
		this->heightType = EQUALHEIGHT;
	};
};

    然後定義一下左旋操作,1的左孩子連向2的右孩子,2的左孩子連向1,再把2作為根節點,於是乎函式可以這樣寫。

bool TurnLeft(TreeNode* &node) {
	TreeNode* tempNode = node->rightChild;
	node->rightChild = tempNode->leftChild;
	tempNode->leftChild = node;
	node = tempNode;
	return true;
}

    然後再定義一下右旋操作,與左旋類似。

bool TurnRight(TreeNode* &node) {
	TreeNode* tempNode = node->leftChild;
	node->leftChild = tempNode->rightChild;
	tempNode->rightChild = node;
	node = tempNode;
	return true;
}

     但是僅僅這樣旋轉就結束了嗎,很顯然不是的,當不平衡發生的時候根節點和其子節點的平衡性符號相反時,1的平衡性符號為RH,3的平衡性符號為LH,如果僅僅進行左旋操作的話,得到的結果為:3為根節點,1為3的左孩子,2為3的右孩子,很明顯與要求不符。因此需要先將3和2先調換位置,將2作為1的右孩子,3作為2的右孩子,使1和2的平衡性因子均為RH,然後再進行左旋操作。那麼我們可以將左旋和右旋操作的函式寫的更加完善一點。


bool BigTurnLeft(TreeNode* &node) {
	TreeNode *nextNode = node->rightChild;//轉換節點位置
	if (node->heightType != nextNode->heightType) {
		node->rightChild = nextNode->leftChild;
		node->rightChild->rightChild = nextNode;
		nextNode->leftChild = nullptr;
	}
        //左旋
	TurnLeft(node);
	return true;
}

bool BigTurnRight(TreeNode* &node) {
	TreeNode *nextNode = node->rightChild;//轉換節點位置
	if (node->heightType != nextNode->heightType) {
		node->leftChild = nextNode->rightChild;
		node->leftChild->leftChild = nextNode;
		nextNode->rightChild = nullptr;
	}
        //右旋
	TurnRight(node);
	return true;
}

       在定義完畢基本的左旋和右旋操作後,我們就可以寫AVL樹的插入函數了,不如我們先小看一下程式碼?(沒錯,你沒的選,我就是想讓你看)

bool InsertIntoATLTree(TreeNode* &node, int value, bool &isGrow) {
	//如果沒有根節點,則創造一個根節點
	if (node == nullptr) {
		node = new TreeNode(value);
	}
	else {
		if (value == node->value) { //判斷值是否相同,相同則無需插入
			return false;
		}
		
		if (value < node->value) { //若value值小於當前節點值
			if (node->leftChild == nullptr) {  //左枝是否為空?將value作為該節點的左節點:遞歸向下判斷
				TreeNode *newNode = new TreeNode(value);//新建node,連在當前node左枝
				node->leftChild = newNode;
				switch (node->heightType)//修改當前節點穩定性
				{
				case RIGHTHEIGHT:
					node->heightType = EQUALHEIGHT;
					break;
				case EQUALHEIGHT:
					node->heightType = LEFTHEIGHT;
					isGrow = true;
					break;
				default:
					break;
				}
				return true;
			}
			else {
				if (InsertIntoATLTree(node->leftChild, value, isGrow)) {
					if (isGrow) {
						switch (node->heightType)
						{
						case LEFTHEIGHT:
							BigTurnRight(node);
							node->heightType = EQUALHEIGHT;
							node->rightChild->heightType = EQUALHEIGHT;
							isGrow = false;
							break;
						case RIGHTHEIGHT:
							node->heightType = EQUALHEIGHT;
							break;
						case EQUALHEIGHT:
							node->heightType = LEFTHEIGHT;
							break;
						default:
							break;
						}
					}
				}
				else {
					return false;
				}
			}
		}
		else {
			if (node->rightChild == nullptr) {  //右枝是否為空?將value作為該節點的右節點:遞歸向下判斷
				TreeNode *newNode = new TreeNode(value);//新建node,連在當前node右枝
				node->rightChild = newNode;
				switch (node->heightType)//修改當前節點穩定性
				{
				case LEFTHEIGHT:
					node->heightType = EQUALHEIGHT;
					break;
				case EQUALHEIGHT:
					node->heightType = RIGHTHEIGHT;
					isGrow = true;
					break;
				default:
					break;
				}
				return true;
			}
			else {
				if (InsertIntoATLTree(node->rightChild, value, isGrow)) {
					if (isGrow) {
						switch (node->heightType)
						{
						case LEFTHEIGHT:
							node->heightType = EQUALHEIGHT;
							break;
						case RIGHTHEIGHT:
							BigTurnLeft(node);
							node->heightType = EQUALHEIGHT;
							node->leftChild->heightType = EQUALHEIGHT;
							isGrow = false;
							break;
						case EQUALHEIGHT:
							node->heightType = RIGHTHEIGHT;
							break;
						default:
							break;
						}
					}
				}
				else{
					return false;
				}
			}
		}
	}

	return true;
}
 

      樹的插入演算法是一個遞迴的過程,首先將需要插入的數值與根節點RN 中的數值RV 進行對比,若V<RV 則與其左孩子進行對比,若V>RV則於其右孩子進行對比,直至某個節點沒有左孩子或右孩子為止。構建新節點作為新的葉子,判斷該葉子的父節點平衡狀態,若父節點的平衡狀態與葉子節點的懸掛方向相反則表示樹沒有生長,否則樹發生生長,回溯是判斷各個父節點的平衡狀態,若父節點處於不平衡狀態則進行左旋或右旋操作,旋轉完畢後再往前回溯的節點不再改變平衡狀態。

      以一個小例子作為插入的結束C={23, 16, 47, 53, 62, 11, 17, 8 , 3};