AVL 樹的旋轉

github 地址:https://github.com/duchenlong/Cpp/tree/master/avl_tree/avl_tree

什麼是AVL樹

對於爲什麼會有AVL樹這個概念,名字來源於他的發明者G. M. Adelson-Velsky和E. M. Landis兩位大佬。

AVL樹是最先發明的平衡二叉查找樹,在AVL樹中任何節點的兩個子樹的高度最大差別爲1,所以它也被稱爲高度平衡樹

平衡二叉樹的搜索效率確實也挺高的,但是由於他的根節點是由第一次插入的元素的決定的,一旦根節點確定,那麼以後這個節點就是整棵樹的根了,是不會變化的,這也就出現了一些意外情況:

  • 當我們按照升序進行插入的時候,整個樹就會變成一個只有右子樹的結構

在這裏插入圖片描述

  • 另一種就是我們以降序進行插入的時候,又會出現只有左子樹的情況

在這裏插入圖片描述

對於這樣的情況,當我們進行查找的時候,其實總的效率還是O(n)的,但是由於樹中的節點都是不一定連續的地址空間,然後用指針在邏輯上連接起來的,所以當時間複雜度爲O(n)的時候,可能還不如直接在數組中遍歷找數據。

而AVL樹的出現,很好的解決了這樣的情況。既然樹中節點的地址都是不連續的空間,那麼就可以通過移動指針變量,來對樹的節點進行旋轉,從而讓整棵樹趨於平衡的狀態,讓查找的效率真正達到O(log n)

AVL樹的概念

AVL 樹相比於平衡二叉樹,除了多了一個旋轉的操作,還在成員變量中多了一個平衡因子的概念。

平衡因子

當樹的一個節點是根節點的時候,他的平衡因子 = 右子樹的最大深度 - 左子樹的最大深度
在這裏插入圖片描述

而一顆AVL樹,他一定需要具備以下的幾個特徵:

  1. 他的左右子樹都是AVL樹
  2. 左右子樹的高度之差的絕對值不超過1 (1,0,-1),也就是每一個根節點的平衡因子都是(1,0,-1)中的一個。

那麼,當一顆樹是AVL樹的時候,就可以保證他是高度平衡的。
如果它有n個結點,其高度可保持在O(log n),搜索時間複雜度也爲O(log n)

AVL樹節點的定義

在AVL樹的節點的結構中,爲了調節平衡因子,所有就需要利用孩子節點找到父親節點,所以說爲了方便,就增加了一個父親節點的指針,用來指向他的父親節點。

而樹中節點所保存的數據類型是一個pair的結構體,他是C++中的一個只有兩個元素的結構體模板,可以通過first,second兩個指針來訪問兩個結構體的元素。

template<class Key,class Val>
struct tree_node
{
	typedef tree_node<Key, Val> node;
	typedef pair<Key, Val> data_type;

	node* _parent;	//雙親節點
	node* _left;	//左孩子節點
	node* _right;	//右孩子節點
	data_type _data;//節點的值
	int _bf;		//平衡因子

	tree_node(const data_type& data)
		:_data(data), _bf(0)
	{
		_parent = _left = _right = nullptr;
	}
};

對於整個AVL樹的結構,可以這樣來

template<class key, class val>
class avl_tree
{
public:
	typedef key key_type;
	typedef val val_type;
	typedef tree_node<key_type,val_type> node;
	avl_tree()
		:root(nullptr)
	{}
private:
	node* root;//根節點
};

AVL數的插入

在這裏插入圖片描述

首先,我們需要明確一點,就是AVL樹在插入之前,他一定是一顆平衡的二叉樹。只是在插入這個新的節點之後,纔可能會變得不平衡,所以說平衡因子的取值只會是(-2,-1,0,1,2)。所以說,只有出現(-2,2)中的任何一個數,就說明以該節點爲根節點的子樹不平衡,需要調整

對於前兩步而言,程序部分都是和平衡二叉樹一樣的,只是多了一個更新父親節點的操作

		//如果樹中沒有節點
		if (root == nullptr)
		{
			root = new node(data);
			return true;
		}

		node* parent = nullptr;
		node* cur = root;
		
		//找到要插入的位置
		while (cur)
		{
			if (cur->_data.first == data.first) return false;
			parent = cur;
			cur = (cur->_data.first < data.first) ? cur->_right : cur->_left;
		}

		cur = new node(data);
		//插入 cur 到指定位置
		if (parent->_data.first < data.first)
			parent->_right = cur;
		else
			parent->_left = cur;
		cur->_parent = parent;

在這裏插入圖片描述
接下來就是判斷是否需要旋轉,也就是插入節點後,這棵樹是否平衡?

這個時候,我們維護了兩個指針變量,一個是當前新插入的節點(cur),另一個是該節點的父親節點(parent)。所以說,我們可以循環的取更新平衡因子,直到父親節點(parent)是空的時候,就說明更新完畢了

		//更新平衡因子
		while (parent)
		{
		}

更新平衡因子 並 判斷是否旋轉

如何更新平衡因子?

對於插入一個節點,非三種情況:

  1. 新插入的節點就是AVL樹 的跟節點,這個時候只需要更新跟節點就可以了
  2. 新插入的節點在右子樹中,根節點的平衡因子 + 1
  3. 新插入的節點在左子樹中,根節點的平衡因子 - 1
		//新插入的節點在右子樹
		if (cur == parent->_right)
		{
			parent->_bf++;
		}
		else if (cur == parent->_left)//新插入的節點在左子樹
		{
			parent->_bf--;
		}

在更新平衡因子的時候,我們選擇從下向上更新,而不是在找插入節點位置時候同時更新平衡因子。

在這裏插入圖片描述
我們需要考慮這樣一種情況,對於某一個根節點,我們插入新的節點後,並沒有使得這個根節點的左右子樹的高度發生變化。那麼我們是不需要更新根節點(以及根節點再向上的父親節點)的平衡因子
在這裏插入圖片描述
所以說,對於更新完畢的平衡因子,我們做以下的處理

  1. 當更新後,父親節點的平衡因子爲0。說明對於父親節點向上的節點來說,他們的高度沒有發生變化,更新完畢了
  2. 父親節點的平衡因子爲 (1,-1)。說明這個父親節點的高度發生了變化,會引起上面節點的高度發生變化,但是沒有失去平衡,所以需要向上迭代
  3. 父親節點的平衡因子爲(-2,2)。說明當前的父親節點已經不平衡了,我們需要進行旋轉操作
	if (parent->_bf == 0)
	{
		break;
	}
	else if (abs(parent->_bf) == 1)
	{
		cur = parent;
		parent = parent->_parent;
	}
	else if (abs(parent->_bf) == 2)
	{
		//此時parent的子樹已經不平衡,需要調整
	}

旋轉

對於一顆AVL樹,當需要進行旋轉的時候,他大體上一定是下面這四種類型:

  1. 當父親節點的平衡因子爲-2的時候,說明左子樹高度比較高

在這裏插入圖片描述
第二種情況經過化簡,其實就是這個模式
在這裏插入圖片描述

  1. 當父親節點的平衡因子爲2的時候,說明右子樹的高度比較高

在這裏插入圖片描述
同樣的,第三種情況化簡後可以是這樣
在這裏插入圖片描述

那麼對於第一種和第四種情況,他們都是隻有一條單邊,那麼我們只需要旋轉一次就可以了,此爲單旋

單旋

對於單旋而言,又根據旋轉方向的不同,分爲左單旋和右單旋兩種

  1. 左單旋,即爲向左進行旋轉

在這裏插入圖片描述
程序的實現

	//右,右,左旋
	void Right_Right(node* parent)
	{
		//保留父親的父親節點
		node* pParent = parent->_parent;
		//父親的右節點
		node* pRight = parent->_right;
		//右節點的左節點
		node* pRLeft = pRight->_left;

		//先把 pRLeft 和 parent 節點連接起來
		parent->_right = pRLeft;
		//更新 pRLeft 的父親節點爲 parent
		if (pRLeft) pRLeft->_parent = parent;

		//開始左旋
		pRight->_left = parent;
		parent->_parent = pRight;

		//pRight 此時作爲新的父親節點,需要判斷是否是新的根節點
		//並連接 pRight 和 pParent 
		if (pParent == nullptr)
			root = pRight;
		else
			(pParent->_left == parent) ? pParent->_left = pRight : pParent->_right = pRight;
		
		pRight->_parent = pParent;
		pRight->_bf = parent->_bf = 0;
	}
  1. 右單旋,即爲向右進行旋轉。

在這裏插入圖片描述
程序實現

	//左,左,右旋
	void Left_Left(node* parent)
	{
		// 保留父親的父親節點
		node* pParent = parent->_parent;
		//父親的左節點
		node* pLeft = parent->_left;
		//左節點的右節點
		node* pLRight = pLeft->_right;

		//先把 pLRight 和 parent 節點連接起來
		parent->_left = pLRight;
		//更新 pLRight 的父親節點爲 parent
		if (pLRight) pLRight->_parent = parent;

		//開始右旋
		pLeft->_right = parent;
		parent->_parent = pLeft;

		//pLeft 此時作爲新的父親節點,需要判斷是否是新的根節點
		//並連接 pLeft 和 pParent 
		if (pParent == nullptr)
			root = pLeft;
		else
			(parent == pParent->_left) ? pParent->_left = pLeft : pParent->_right = pLeft;

		pLeft->_parent = pParent;
		pLeft->_bf = parent->_bf = 0;
	}

最後,因爲如果只進行單旋,那麼旋轉後的圖形的根節點的平衡因子一定是0,並且原本不平衡的那個節點由於變成了一個葉子節點,所以說平衡因子也爲0。
在這裏插入圖片描述
而最底下的那個節點,因爲在單旋的過程中,高度沒有發生變化,所以說他的平衡因子不變。

雙旋

對於雙旋的情況,我們重新用程序實現也是挺麻煩的,肯定還不簡潔。所以可以嘗試着把雙旋變成單旋來試試。

  1. 我們上面所示的第二種情況,當失衡節點的平衡因子是-2

在這裏插入圖片描述
程序實現

	//左,右,雙旋
	void Left_Right(node* parent)
	{
		node* pLeft = parent->_left;
		node* pLRight = pLeft->_right;
		int pLRbf = pLRight->_bf;
		//右,右,單旋
		Right_Right(pLeft);

		//左,左,單旋
		Left_Left(parent);
		
		//更新平衡因子
	}

對於雙旋而言,我們需要再次更新平衡因子,因爲我們第一次單旋的分支並不一定是一個模板的分支,他可能只是單旋分支的前兩部分

在這裏插入圖片描述
其實這裏又有三種平衡因子變化的情況,就是當根節點的左節點的右節點,的平衡因子是 (0,-1,1)這三種情況的時候,對於雙旋後的平衡因子都有不同的結果

  1. 當該節點的平衡因子爲0時:

在這裏插入圖片描述
2. 當該點的平衡因子爲-1時:

在這裏插入圖片描述

  1. 當該點的平衡因子爲1時

在這裏插入圖片描述
所以說,在修改平衡因子的時候,可以判斷一下這個節點的情況,在進行賦值就可以了

		if (pLRbf == 1)
		{
			parent->_bf = 0;
			pLeft->_bf = -1;
			pLRight->_bf = 0;
		}
		else if (pLRbf == -1)
		{
			parent->_bf = 1;
			pLeft->_bf = 0;
			pLRight->_bf = 0;
		}
		else if (pLRbf == 0)
		{
			parent->_bf = 0;
			pLeft->_bf = 0;
			pLRight->_bf = 0;
		}

這裏需要注意的是,我們在修改平衡因子的過程是在旋轉完畢後的,那麼旋轉後這個點的平衡因子是會發生變化的,所以說我們需要在進行旋轉之前就保留這個點的平衡因子的值,然後用這個變量在最後去修改。(親測一下午)

爲什麼說這個點的取值一定是(0,-1,1)?

因爲我們這是一顆AVL樹,他在插入一個元素之前一定是一顆平衡的樹,我們進行雙旋的時候,這個點一定是一個平衡的節點,所以他的取值是(0,-1,1)

  1. 失衡節點的平衡因子是2的時候,也就是第三種情況

在這裏插入圖片描述
那麼,他的旋轉過程我們可以再進行一次封裝,並且他調節平衡因子的過程和上面是差不多的,也是那三種情況。

void Right_Left(node* parent)
	{
		node* pRight = parent->_right;
		node* pRLeft = pRight->_left;
		int pRLbf = pRLeft->_bf;
		//左,左,單旋
		Left_Left(pRight);

		//右,右,單旋
		Right_Right(parent);

		if (pRLbf == 1)
		{
			parent->_bf = -1;
			pRight->_bf = 0;
			pRLeft->_bf = 0;
		}
		else if (pRLbf == -1)
		{
			parent->_bf = 0;
			pRight->_bf = 1;
			pRLeft->_bf = 0;
		}
		else if (pRLbf == 0)
		{
			parent->_bf = 0;
			pRight->_bf = 0;
			pRLeft->_bf = 0;
		}
	}

AVL樹的驗證

對於驗證,我們只需要對數的結構進行遍歷(前中後序都可以),然後依次判斷每個節點的左右孩子節點的深度可以了。

另外,和插入操作相同,我們在遍歷的時候,需要傳入一個參數,也就是當前節點的指針。而這個樹的節點是被封裝好的私有類型,所以我們需要在類內部調用另一個接口。

public:
	//判斷是否是平衡二叉樹
	bool IsBF()
	{
		return _IsBF(root);
	}
private:
	//判斷是否是平衡
	bool _IsBF(node* root)
	{
		if (root == nullptr) return true;

		int leH = _Height(root->_left);//左子樹高度
		int riH = _Height(root->_right);//右子樹高度

		if (riH - leH != root->_bf)
		{
			cout << root->_data.first << " 平衡因子異常" << endl;
			return false;
		}
			
		return _IsBF(root->_left) && _IsBF(root->_right);
	}

	//由根節點確定子樹高度
	int _Height(node* root)
	{
		if (root == nullptr)
			return 0;
		return 1 + max(_Height(root->_left), _Height(root->_right));
	}

最後在測試的時候,我使用了大量的隨機數來進行測試,程序一切正常
在這裏插入圖片描述

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