天天看點

[資料結構] 搜尋樹與AVL樹

文章目錄

    • 1. 搜尋樹
      • 1.1. 了解二叉搜尋樹
      • 1.2. 搜尋樹性能分析
    • 2. AVL樹
      • 2.1. 了解AVL樹
      • 2.2. AVL樹節點的資料結構
      • 2.3. AVL樹的插入
      • 2.4. AVL樹的旋轉*
      • 2.5. AVL樹的删除
      • 2.6. AVL樹性能分析

1. 搜尋樹

1.1. 了解二叉搜尋樹

二叉搜尋樹又稱二叉排序樹,它或者是一棵空樹,或者是具有以下性質的二叉樹:

  • 若它的左子樹不為空,則左子樹上所有節點的值都小于根節點的值
  • 若它的右子樹不為空,則右子樹上所有節點的值都大于根節點的值
  • 它的左右子樹也分别為二叉搜尋樹

如圖所示

[資料結構] 搜尋樹與AVL樹
  • 當進行資料搜尋時
    [資料結構] 搜尋樹與AVL樹
  • 進行資料插入時過程與資料搜尋相同, 找到一個适合自己的空節點
  • 資料删除
    [資料結構] 搜尋樹與AVL樹

1.2. 搜尋樹性能分析

  • 插入和删除操作都必須先查找,查找效率代表了二叉搜尋樹中各個操作的性能.
  • 理想情況下, 插入的樹是完全二叉樹
    [資料結構] 搜尋樹與AVL樹
  • 最壞情況下, 插入的樹成了單鍊樹
    [資料結構] 搜尋樹與AVL樹
  • 最優情況下,二叉搜尋樹為完全二叉樹,其平均比較次數為:logn
  • 最差情況下,二叉搜尋樹退化為單支樹,其平均比較次數為:n / 2

2. AVL樹

2.1. 了解AVL樹

二叉搜尋樹雖可以縮短查找的效率,但如果資料有序或接近有序二叉搜尋樹将退化為單支樹,查找元素相當于在順序表中搜尋元素,效率低下. 是以,兩位俄羅斯的數學家G.M.Adelson-Velskii和E.M.Landis在1962年發明了一種解決上述問題的方法:當向二叉搜尋樹中插入新結點後,如果能保證每個結點的左右子樹高度之差的絕對值不超過1(需要對樹中的結點進行調整),即可降低樹的高度,進而減少平均搜尋長度。

一棵AVL樹或者是空樹,或者是具有以下性質的二叉搜尋樹:

  • 它的左右子樹都是AVL樹
  • 左右子樹高度之差(簡稱平衡因子)的絕對值不超過1(-1/0/1)
  • 如果一棵二叉搜尋樹是高度平衡的,它就是AVL樹。如果它有n個結點,其高度可保持在 ,搜尋時間複雜度O(logn)
    [資料結構] 搜尋樹與AVL樹

2.2. AVL樹節點的資料結構

template <class T>
class AVLTreeNode
{
public:
	AVLTreeNode(T data)
		: _data(data)
		, _pLeft(nullptr)
		, _pRight(nullptr)
		, _pParent(nullptr)
		, _bf(0)
	{ }

	T _data;
	AVLTreeNode<T>* _pLeft;
	AVLTreeNode<T>* _pRight;
	AVLTreeNode<T>* _pParent;
	int _bf;	// 該節點平衡因子
};
           

2.3. AVL樹的插入

AVL樹的插入過程可以分為兩步:

1. 按照二叉搜尋樹的方式插入新節點

2. 調整節點的平衡因子

  • 新增節點的平衡因子是0, 新增節點的祖先平衡因子可能受影響

    1. 新增在parent的右子樹, parent->bf + = 1

    2. 新增在parent的左子樹, parent->bf - = 1

  • 通過cur的位置更新parent的平衡因子, 更新完成後

    1. 如果parent->bf == 1 / -1, 說明parent為根的子樹的高度變了, 繼續向上更新

    2. 如果parent->bf == 2 / -2, 說明parent為根的子樹已經不平衡, 需要旋轉處理

    3. 如果parent->bf == 0, 說明parent所在的子樹原來高度是 1/-1,現在把矮的補齊, parent所在的子樹高度不變, 停止更新

bool insert(const T& data)
	{
		// 如果樹内無節點
		if (_pRoot == nullptr)
		{
			_pRoot = new Node(data);
			return true;
		}
		// 1. 按照二叉搜尋樹的方式插入新節點
		Node* pCur = _pRoot;
		Node* parent = nullptr;
		while (pCur)
		{
			parent = pCur;
			if (data < pCur->_data)
			{
				pCur = pCur->_pLeft;
			}
			else if (data > pCur->_data)
			{
				pCur = pCur->_pRight;
			}
			else
			{
				// 插入相同節點, 傳回失敗
				return false;
			}
		}
		pCur = new Node(data);
		if (parent->_data < data)
		{
			parent->_pRight = pCur;
		}
		else
		{
			parent->_pLeft = pCur;
		}
		pCur->_pParent = parent;

		// 2. 調整節點的平衡因子
		while (parent != nullptr)
		{
			if (parent->_pLeft == pCur)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				pCur = parent;
				parent = pCur->_pParent;
			}
			else if (parent->_bf == -2 || parent->_bf == 2)
			{
				// 旋轉操作
			}
		}
		return true;
	}

           

2.4. AVL樹的旋轉*

如果在一棵原本是平衡的AVL樹中插入一個新節點,可能造成不平衡,此時必須調整樹的結構,使之平衡化。根據節點插入位置的不同,AVL樹的旋轉分為四種:

  1. 新節點插入較高左子樹的左側—左左:右單旋

                         

    [資料結構] 搜尋樹與AVL樹
    [資料結構] 搜尋樹與AVL樹
  • 注意判斷parent的父節點和subLR是否存在
void RotateR(Node* parent)
	{
		Node* pSubL = parent->_pLeft;
		Node* pSubLR = pSubL->_pRight;

		pSubL->_pRight = parent;
		Node* pParent = parent->_pParent;
		parent->_pParent = pSubL;

		if (pSubLR != nullptr)
		{
			pSubLR->_pParent = parent;
		}
		parent->_pLeft = pSubLR;

		if (parent == _pRoot)
		{
			_pRoot = pSubL;
			pSubL->_pParent = nullptr;
		}
		else
		{
			if (pParent->_pLeft == parent)
			{
				pParent->_pLeft = pSubL;
			}
			else
			{
				pParent->_pRight = pSubL;
			}
			pSubL->_pParent = pParent;
		}
		
		parent->_bf = pSubL->_bf = 0;
	}
           
  1. 新節點插入較高右子樹的右側—右右:左單旋

                     

    [資料結構] 搜尋樹與AVL樹
    [資料結構] 搜尋樹與AVL樹
void RotateL(Node* parent)
	{
		Node* pSubR = parent->_pRight;
		Node* pSubRL = pSubR->_pLeft;

		pSubR->_pLeft = parent;
		Node* pParent = parent->_pParent;
		parent->_pParent = pSubR;

		parent->_pRight = pSubRL;
		if (pSubRL != nullptr)
		{
			pSubRL->_pParent = parent;
		}
		if (parent == _pRoot)
		{
			_pRoot = pSubR;
			pSubR->_pParent = nullptr;
		}
		else
		{
			if (pParent->_pRight == parent)
			{
				pParent->_pRight = pSubR;
			}
			else
			{
				pParent->_pLeft = pSubR;
			}
			pSubR->_pParent = pParent;
		}
		parent->_bf = pSubR->_bf = 0;
	}
           
  1. 新節點插入較高左子樹的右側—左右:先左單旋再右單旋
    [資料結構] 搜尋樹與AVL樹
  • 共會出現三種可能的情況
    [資料結構] 搜尋樹與AVL樹
  • 注意觀察閃電标志的平衡因子, 可以分析得到這三個點不同插入情況下平衡因子的值

  • 完善上方代碼可得
void RotateLR(Node* parent)
	{
		Node* pSubL = parent->_pRight;
		Node* pSubLR = pSubR->_pLeft;
		int bf = pSubLR->_bf;

		RotateL(parent->_pLeft);
		RotateR(parent);

		if (bf == 1)
		{
			parent->_bf = 0;
			pSubL->_bf = 1;
			pSubLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			pSubL->_bf = 0;
			pSubLR->_bf = 0;
		}
		else
		{
			parent->_bf = 0;
			pSubL->_bf = 0;
			pSubLR->_bf = 0;
		}
	}
           
  1. 新節點插入較高右子樹的左側—右左:先右單旋再左單旋

    同情況三代碼如下:

void RotateRL(Node* parent)
	{
		Node* pSubR = parent->_pRight;
		Node* pSubRL = pSubR->_pLeft;
		int bf = pSubRL->_bf;

		RotateR(parent->_pRight);
		RotateL(parent);

		if (bf == 1)
		{
			parent->_bf = -1;
			pSubR->_bf = 0;
			pSubRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			pSubR->_bf = 1;
			pSubRL->_bf = 0;
		}
		else
		{
			parent->_bf = 0;
			pSubR->_bf = 0;
			pSubRL->_bf = 0;
		}
	}
           

2.5. AVL樹的删除

因為AVL樹也是二叉搜尋樹,可按照二叉搜尋樹的方式将節點删除,然後再更新平衡因子,隻不過與删除不同的是,删除節點後的平衡因子更新,最差情況下一直要調整到根節點的位置.

2.6. AVL樹性能分析

AVL樹是一棵絕對平衡的二叉搜尋樹,其要求每個節點的左右子樹高度差的絕對值都不超過1,這樣可以保證查詢時高效的時間複雜度,即 。但是如果要對AVL樹做一些結構修改的操作,性能非常低下,比如:插入時要維護其絕對平衡,旋轉的次數比較多,更差的是在删除時,有可能一直要讓旋轉持續到根的位置。

是以:如果需要一種查詢高效且有序的資料結構,而且資料的個數為靜态的(即不會改變),可以考慮AVL樹,但一個結構經常修改,就不太适合. 此時, 紅黑樹就出場了!

  • 源代碼: https://github.com/Zhangddy/DataStructure/tree/master/AVL%E6%A0%91

完.

繼續閱讀