C++高級數據結構算法 | AVL(自平衡二叉查找樹)

之前我們向大家介紹了 高級數據結構算法中的 BST 樹,即二叉查找樹:
《C++高級數據結構算法 | Binary Search Tree(二叉查找樹)》
我們使用遞歸和非遞歸的方式實現了BST樹的插入、刪除、查詢、四種遍歷等操作,也對BST樹的相關典型題目做了分析和代碼實現。

本篇博文將講解AVL樹的基本概念和相關操作與題目分析。



AVL樹的引入

通過之前的講解我們知道,二叉查找樹是基於折半查找思想設計的一種數據結構。通過分析,二叉查找樹的確能在很大程度上提高查找的效率。然而,儘管當二叉查找樹處於平衡狀態時,其操作的時間複雜度爲 O(log2n)O(log_2n),但是當二叉查找樹是單支樹時,其搜索效率將爲 O(n)O(n),將退化成爲一條鏈表,白白浪費掉另一個結點域,如下圖所示

基於上述問題,我們可以發現,二叉搜索樹的平衡性是影響其操作效率的關鍵,因此學者們設計了第一個平衡二叉搜索樹,即AVL樹,AVL樹得名於它的發明者前蘇聯數學家格奧爾吉·阿杰爾鬆-韋利斯基 (G.M.AdelsonVelsky)(G. M. Adelson-Velsky) 和 葉夫吉尼·蘭迪斯 (E.M.Landis)(E. M. Landis)


AVL樹的概念

AVL樹就是一棵二叉查找樹,其準確的定義如下:一棵AVL樹或者是空樹,或者是具有下列性質的二叉查找樹——它的左子樹和右子樹都是AVL樹,且左子樹和右子樹的高度之差的絕對值不超過 11。也就是說,AVL樹本質上是帶了平衡功能的二叉查找樹。下圖即爲一棵AVL樹,樹中略去了各結點的關鍵碼。

注意:AVL樹的平衡性是一種相對的平衡,而非一種絕對的平衡。與絕對平衡相比,這種所謂的相對平衡滿足的是一個較弱的平衡條件,即它不要求左子樹和右子樹的高度完全相等,而僅僅是左子樹和右子樹的高度之差的絕對值不超過 11 即可。之所以將平衡條件降低是因爲絕對的平衡很難實現。

結點的平衡因子是它的左子樹的高度減去它的右子樹的高度(可也爲右子樹的高度減去它的左子樹的高度)。帶有平衡因子 1、0 或 -1 的節點被認爲是平衡的。平衡因子的絕對值大於 1 的結點被認爲是不平衡的,並需要重新平衡這個樹。平衡因子可以直接存儲在每個結點中,或從可能存儲在結點中的子樹高度計算出來。

對於有n個結點的AVL樹,其高度可保持在 log2n\lfloor log_2n \rfloor左右,其查找、插入和刪除在平均和最壞情況下都是 O(log2n)O(log_2n) 增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹


AVL樹的結構定義

我們知道AVL樹就是一棵二叉查找樹,因此依舊採用二叉鏈表的方式進行結構的定義,與普通二叉查找樹不同的是,我們需要爲每個結點添加一個結點高度域,負責存儲該結點的高度,因此我們有需要提供兩個API接口負責返回該結點的高度及計算該結點左右子樹的高度差。

template<typename T>
class AVL
{
public:
	AVL() { _root = nullptr; }
	···
private:
	struct AVLNode
	{
		AVLNode(T data = T())
			:_data(data)
			, _left(nullptr)
			, _right(nullptr)
			, _height(1) 
		{}
		T _data;
		AVLNode *_left;
		AVLNode *_right;
		int _height; // 存儲的就是節點的高度
	};

	// 返回節點的高度
	int height(AVLNode *node)const
	{
		return node == nullptr ? 0 : node->_height;
	}

	// 返回左右子樹最高的層數
	int maxHeight(AVLNode *node1, AVLNode *node2)
	{
		return height(node1) > height(node2) 
		? height(node1) : height(node2);
	}
	
	AVLNode *_root;
};

接下來我們簡單分析一下下圖中的插入操作:

如上圖,左邊是一棵AVL樹,它是平衡的。當我們給 結點7 插入左孩子時,AVL的平衡特性就被破壞,由於 結點8 失衡了,即 結點8 的左右子樹高度差爲 2,不滿足平衡條件。

再例如,我們將 結點1 刪除,此時左邊的AVL樹的平衡特性也被破壞了,結點2失衡。

我們知道,AVL是平衡的二叉查找樹,因此AVL樹的基本操作與普通的二叉查找樹是相似的,但是我們經過上述分析發現,在我們進行結點的插入和刪除時,有可能會破壞AVL樹的平衡性,實時的保持這種平衡性非常重要。通常可以通過調整樹結構,使之保持平衡,這種用以進行平衡化的操作被稱爲 旋轉

接下來我們具體分析AVL樹的四種旋轉算法並實現。


AVL樹的旋轉算法

如果在一棵平衡的二叉排序樹中插入一個新的結點,就可能造成其失衡,這種失衡可能歸結爲四種基本情況。

上述的四種失衡情況相對應的處理方式被分爲兩類:單旋和雙旋。其中,單旋又分爲左旋和右旋,而雙旋又分爲先左後右和先右後左兩種

通常每次向AVL樹中插入一個新節點時,AVL樹中相關結點的平衡狀態就會發生改變。因此,在插入一個新節點後,就需要從插入位置沿通向根的路徑回溯,檢查各結點的左右子樹高度差。如果在某一點發現高度不平衡,則停止回溯,從發生不平衡的節點起,沿剛纔回溯的路徑取直接下兩層的節點。這時就有兩種情況,

  • 如果這三個節點處在同一條直線上,那麼採用單旋進行平衡化
  • 如果這三個節點不處於同一條直線上,那麼採用雙旋進行平衡化

具體採用的操作如下表所示:

插入方式 描述 旋轉方式
LL 在某節點的左子樹中插入一個左孩子 右旋轉
RR 在某節點的右子樹中插入一個右孩子 左旋轉
LR 在某節點的左子樹中插入一個右孩子 先左旋後右旋
RL 在某節點的右子樹中插入一個左孩子 先右旋後左旋

接下來我們具體講解一下它們的這四類旋轉算法及其代碼實現。


右單旋轉

由於向某節點的左子樹中插入一個左孩子導致該結點失衡,我們需要使用右旋操作來維護AVL樹的平衡。
在這裏插入圖片描述
下圖是具體情境分析:

右單旋轉的方法是以3個成直線排列的節點中的的中間節點爲軸,進行順時針旋轉該中間節點的原父節點變成該節點的右子節點,該中間節點的右子樹則變成其原父節點的左子樹

// 右旋轉操作
AVLNode* rightRotate(AVLNode* node)
{
	AVLNode* child = node->_left; // 拿到中間節點
	node->_left = child->_right; //該中間節點的右子樹則變成其原父節點的左子樹
	child->_right = node;// 該中間節點的原父節點變成該節點的右子節點
	node->_height = maxHeight(node->_left, node->_right) + 1; // 更新節點高度
	child->_height = maxHeight(child->_left, child->_right) + 1;// 更新節點高度
	return child;//返回旋轉後的根節點
}

左單旋轉

由於向某節點的右子樹中插入一個右孩子導致該結點失衡,我們需要使用左旋操作來維護AVL樹的平衡。
在這裏插入圖片描述
下圖是具體情境分析:

左單旋轉的方法是以3個成直線排列的節點的中間節點爲軸,進行逆時針旋轉該中間節點的原父節點變成該節點的左子節點,該中間節點的左子樹則變成其原父節點的右子樹。

// 左旋轉操作 以node爲根節點進行左旋轉,返回旋轉後的根節點
AVLNode* leftRotate(AVLNode* node)
{
	AVLNode* child = node->_right;// 拿到中間節點
	node->_right = child->_left;// 該中間節點的左子樹則變成其原父節點的右子樹
	child->_left = node;// 該中間節點的原父節點變成該節點的左子節點
	node->_height = maxHeight(node->_left, node->_right) + 1;// 更新節點高度
	child->_height = maxHeight(child->_left, child->_right) + 1;// 更新節點高度
	return child; //返回旋轉後的根節點
}

上面我們介紹了AVL樹的兩種單向旋轉方式。那麼接下來我們首先看一下如下圖所示的這類情況。顯然經過一次單旋轉的修復後無論是X或者W作爲根結點都無法符合AVL樹的性質,此時就需要用雙旋轉算法來實現了。

由於子樹Y是在插入某個結點後導致X結點的左右子樹失去平衡,那麼就說明子樹Y肯定是非空的,因此爲了易於理解,我們可以把子樹Y看作一個根結點和兩棵子樹,如下圖所示:

下面我們就來講解雙旋轉算法。


左-右雙旋轉

由於向某節點的左子樹中插入一個右孩子導致該結點失衡,我們需要使用先左後右雙向旋轉操作來維護AVL樹的平衡。

先左後右雙旋轉的處理方法是以3個成折線排列的節點中的末節點爲軸,進行逆時針旋轉(左旋),使得末節點代替中間節點的位置,也就是讓末節點成爲原中間節點的父節點,而末節點的左子樹變爲原中間節點的右子樹。這時,這3個節點將成一直線排列,原來的末節點變成了3條直線的中間節點,而原來的中間節點變成了3條直線中的末節點。這時再以新的中間節點爲旋轉軸做右單旋轉,即可完成平衡操作。

簡而言之:首先對原失衡節點的左子樹進行左旋操作,再對原失衡節點做右旋操作即可。

// 左平衡  左-右旋轉
AVLNode* leftBalance(AVLNode* node)
{
	node->_left = leftRotate(node->_left);
	return rightRotate(node);
}

右-左雙旋轉

由於向某節點的右子樹中插入一個左孩子導致該結點失衡,我們需要使用先右後左雙向旋轉操作來維護AVL樹的平衡。

先右後左雙旋轉的處理方法是以3個成折線排列的節點中的末節點爲軸,進行順時針旋轉(右旋),使得末節點代替中間節點的位置,也就是讓末節點成爲原中間節點的父節點,而末節點的右子樹變爲原中間節點的左子樹。這時,這3個節點將成一直線排列,原來的末節點變成了3條直線的中間節點,而原來的中間節點變成了3條直線中的末節點。這時再以新的中間節點爲旋轉軸做左單旋轉,即可完成平衡操作。

簡而言之:首先對原失衡節點的右子樹進行右旋操作,再對原失衡節點做左旋操作即可。

// 右平衡  右-左旋轉
AVLNode* rightBalance(AVLNode* node)
{
	node->_right = rightRotate(node->_right);
	return leftRotate(node);
}

AVL樹的插入和刪除

AVL樹的插入

/**
 * AVL樹的插入操作與普通的二叉查找樹基本類似,區別就是在我們每次插入了新節點後
 * 能會導致AVL樹失去平衡特性,我們使用遞歸回溯的特性,每當創建好新節點回溯到父
 * 節點插入後,判斷該結點的左右子樹高度差,然後進行相應操作。
 * 具體的實現是我們給某結點的左子樹插入新節點後,那麼該結點的左子樹高度便增加1,
 * 此時有可能失衡,但是我們需要判斷是由於向左子樹插入左孩子導致失衡(需要右旋)
 * 還是向左子樹插入右孩子導致失衡(需要左-右旋轉)。判斷方法就是判斷當前結點
 * 與插入的val的大小關係,大於當前結點肯定是插到了當前結點的右邊,小於當前結
 * 點肯定是插入到了當前結點的左邊,然後進行相應的旋轉操作即可。
 * 那麼對於在某節點的右子樹插入新節點的情況與上邊的分析過程類似,這裏不再贅述。
 */
void insert(const T& val)
{
	_root = insert(_root, val);
}
AVLNode* insert(AVLNode* node, const T& val)
{
	if (node == nullptr)
	{
		return new AVLNode(val);
	}

	if (val < node->_data)
	{
		// 回溯到父節點插入
		node->_left = insert(node->_left, val);
		// 插入左子樹 做AVL旋轉操作
		if (height(node->_left) - height(node->_right) > 1)
		{
			// 左孩子的左子樹失衡 右旋轉操作
			if (node->_data > val)
			{
				node = rightRotate(node);
			}
			else // 左孩子的右子樹失衡 左-右旋轉操作
			{
				node = leftBalance(node);
			}
		}
	}
	else if (val > node->_data)
	{
		node->_right = insert(node->_right, val);
		// 插入左子樹 做AVL旋轉操作
		if (height(node->_right) - height(node->_left) > 1)
		{
			// 右孩子的的右子樹失衡  左旋轉操作
			if (node->_data < val)
			{
				node = leftRotate(node);
			}
			else // 右孩子的左子樹失衡 右-左旋轉操作
			{
				node = rightBalance(node);
			}
		}
	}
	else
	{
		;
	}
	// 更新節點高度
	node->_height = maxHeight(node->_left, node->_right) + 1;
	return node;
}

AVL樹的刪除

/**
 * AVL樹的刪除操作基本框架和普通的二叉查找樹也是類似的,我們在分析BST樹的刪除
 * 情況時分了三種情況,在AVL樹中同樣是適用的,但是對於第三種情況,也就是待刪除
 * 結點同時擁有左右孩子的情況,我們之前講解的是可以使用前驅或後繼節點來替代當前
 * 節點並將其前驅或後繼刪除,我們只採用了其中一種方法,但是在AVL樹的刪除中,有
 * 可能導致失衡從而需要使用旋轉操作來維護平衡,但是在這裏直接判斷當前待刪除結點
 * 的左右子樹高度,若左子樹高我們刪除前驅,若右子樹高或者高度相同我們刪除後繼,
 * 這樣就不用進行旋轉操作,簡化編程流程。
 * 但是對於普通的情況,即待刪除結點只有一個孩子的情況,我們就必須要進行相應判斷
 * 和旋轉操作來維護AVL樹的平衡特性。
 * 如果我們待刪除的結點在左子樹中找到,那麼刪除後,左子樹高度降低,那麼失衡肯定
 * 是由於右子樹的高度過高導致的。因此我們直接判斷右子樹的右孩子與右子樹的左孩子
 * 的高度大小,若右子樹的右孩子高度更高,那麼我們進行左旋操作維護平衡,否則我們
 * 進行右-左雙旋轉維護平衡。
 * 如果我們待刪除的結點在右子樹中找到,分析過程與上述類似,這裏不再贅述。
 */
void remove(const T& val)
{
	_root = remove(_root, val);
}
AVLNode* remove(AVLNode* node, const T& val)
{
	if (node == nullptr)
	{
		return nullptr;
	}
	
	if (node->_data > val)
	{
		node->_left = remove(node->_left, val);
		if (height(node->_right) - height(node->_left) > 1)
		{
			if (height(node->_right->_right) 
			> height(node->_right->_left))
			{
				node = leftRotate(node);
			}
			else
			{
				node = rightBalance(node);
			}
		}

	}
	else if (node->_data < val)
	{
		node->_right = remove(node->_right, val);
		if (height(node->_left) - height(node->_right) > 1)
		{
			if (height(node->_left->_left) 
			>= height(node->_left->_right))
			{
				node = rightRotate(node);
			}
			else
			{
				node = leftBalance(node);
			}
		}
	}
	else
	{
		if (node->_left != nullptr && node->_right != nullptr)
		{
			if (height(node->_left) > height(node->_right))
			{
				AVLNode* preNode = node->_left;
				while (preNode->_right != nullptr)
				{
					preNode = preNode->_right;
				}
				node->_data = preNode->_data;
				node->_left = remove(node->_left, preNode->_data);
			}
			else
			{
				AVLNode* lastNode = node->_right;
				while (lastNode->_left != nullptr)
				{
					lastNode = lastNode->_left;
				}
				node->_data = lastNode->_data;
				node->_right = remove(node->_right, lastNode->_data);
			}
		}
		else if (node->_left != nullptr)
		{
			AVLNode* child = node->_left;
			delete node;
			//node->_height = maxHeight(node->_left, node->_right) + 1;
			return child;
		}
		else if (node->_right != nullptr)
		{
			AVLNode* child = node->_right;
			delete node;
			//node->_height = maxHeight(node->_left, node->_right) + 1;
			return child;
		}
		else
		{
			delete node;
			//node->_height = maxHeight(node->_left, node->_right) + 1;
			return nullptr;
		}
	}
	node->_height = maxHeight(node->_left, node->_right) + 1;
	return node;
}

判斷一棵二叉搜索樹是否是平衡樹

/**
 * 判斷一棵二叉搜索樹是否是平衡樹,因爲題目條件已經說明了該樹是一棵二叉搜索樹
 * 了,因此我們直接從二叉搜索樹與平衡二叉搜索樹在性質上的重要區別入手,即一顆
 * 平衡二叉搜索樹任一結點的左右子樹高度差不超過1,因此我們藉助了求層數的函數
 * level(),在函數遞歸前判斷是否滿足該條件即可,若不滿足,我們直接結束,若滿足
 * 繼續遞歸遍歷其他結點即可。
 */
bool isAVL()
{
	return isAVL(_root);
}
bool isAVL(AVLNode *node)
{
	if (node == nullptr)
	{
		return true;
	}

	if (abs(level(node->_left) - level(node->_right)) > 1)
	{
		return false;
	}
	return isAVL(node->_left) && isAVL(node->_right);
}

/**
 * 上邊函數所用到的求樹層數的函數level()的定義如下
 */
int level()
{
	return level(_root);
}

int level(AVLNode* node)
{
	if (node == nullptr)
	{
		return 0;
	}

	int left = level(node->_left);
	int right = level(node->_right);

	return (left > right ? left : right) + 1;
}

判斷一棵二叉樹是否是平衡二叉搜索樹

/**
 * 判斷一顆二叉樹是否是平衡二叉搜索樹,我們之前有寫過判斷一顆二叉樹是否是
 * 二叉搜索樹(BST)的代碼,我們在遞歸函數前進行很多的條件判斷,那麼我們
 * 只需要在這部分繼續添加條件,判斷是否是平衡樹即可。判斷方式和上題是
 * 相同的。
 */
bool isAVLTree()
{
	return isAVLTree(_root);
}
bool isAVLTree(AVLNode* node)
{
	static AVLNode* prev = nullptr;
	if (node == nullptr)
	{
		return true;
	}

	if (!isAVLTree(node->_left))
	{
		return false;
	}

	if (prev != nullptr && node->_data < prev->_data)
	{
		return false;
	}

	if (abs(level(node->_left) - level(node->_right)) > 1)
	{
		return false;
	}

	prev = node;
	return isAVLTree(node->_right);
}

AVL樹的其他操作:查找、遍歷等都與BST二叉查找樹是相同的,包括其他算法面試題目,大家可以參考我之前的博文:
《C++高級數據結構算法 | Binary Search Tree(二叉查找樹)》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章