資料結構——二叉樹進階

努力的zjk發表於2020-12-28

在我看來,在資料結構中,存在三個最經典的樹型結構,他們分別是二叉排序樹(BST)二叉平衡樹(AVL)紅黑樹(BRT)

二叉排序樹又叫做二叉搜尋樹,它可以是一棵空樹,也可以是具有三個性質的一棵二叉樹。
性質一:若它的左子樹不為空,則左子樹上的所有結點的值均小於它的根結點的值
性質二: 若它的右子樹不為空,則右子樹上的所有結點的值均大於它的根結點的值
性質三: 左右子樹也是二叉排序樹。

構造二叉搜尋樹的目的是提高查詢和插入刪除關鍵字的速度。因為二叉排序樹是一個排序好的有序資料集,查詢的速度肯定是快於無需資料集的,所以在考慮一些具體的問題時,一定要記得二叉排序樹的特性,就比如在100w個數,找到一個給定的值,就非常的快,不過後面有更優化的方法。

這裡只給出二叉搜尋樹的查詢插入程式碼

template<class T>
struct BSTNode
 {
	 BSTNode(const T& data = T())
	 : _pLeft(nullptr) , _pRight(nullptr), _data(data)
	 {}
	 BSTNode<T>* _pLeft;
	 BSTNode<T>* _pRight;
	 T _data;
 };

template<class T>
template<class T>
class BSTree
 {
	 typedef BSTNode<T> Node;
	 typedef Node* PNode;
public:
 	BSTree(): _pRoot(nullptr)
 	{}
 // 同學們自己實現,與二叉樹的銷燬類似
 ~BSTree();
 
PNode find(const T& key)
{
	PNode cur = _pRoot;
	while (cur != nullptr && cur->_data != key)  //當查詢元素不為空且與根節點不同
	{
		if (key < cur->_data)    //小於根節點左子樹找
		{
			cur = cur->_pLeft;
		}
		else   //大於根節點右子樹找
		{
			cur = cur-> _pRight;
		}
	}
	if (cur == nullptr)  //若cur=NULL則說明遍歷完沒有找到,
		return nullptr;
	return cur;  
}

bool Insert(const T& data)
 {
 // 如果樹為空,直接插入
	 if (nullptr == _pRoot)
	 {
		 _pRoot = new Node(data);
		 return true;
 	}
 // 按照二叉搜尋樹的性質查詢data在樹中的插入位置
 	PNode pCur = _pRoot;
 // 記錄pCur的雙親,因為新元素最終插入在pCur雙親左右孩子的位置
 	PNode pParent = nullptr;
	 while (pCur)
	 {
		 pParent = pCur;
		 if (data < pCur->_data)
		 	pCur = pCur->_pLeft;
		 else if (data > pCur->_data)
		 	pCur = pCur->_pRight; // 元素已經在樹中存在
		 else
		 	return false;
		 }
		 // 插入元素
		 pCur = new Node(data);
		 if (data < pParent->_data)
		 	pParent->_pLeft = pCur;
		 else
		 	pParent->_pRight = pCur;
		 return true;
 }
 private:
 	PNode _pRoot;
 };

二叉排序樹是以連結的方式來儲存,對於插入刪除是非常方便的,操作時不需要移動元素的優點,插入刪除的時間效能比較好。而對於查詢的效能就取決於樹的形狀,問題就在二叉排序樹的形狀是不確定的。

例如要查詢結點99,但是左圖只需要兩次比較,右圖要比較10次。

在這裡插入圖片描述
所以對於這種情況,就需要更好的資料結構來進行查詢操作。保證二叉排序的高度平衡,這時就出現了平衡二叉樹。
如何保證高度平衡,肯定是有一個很關鍵的條件,被稱為平衡因子BF。平衡因子的定義是二叉樹上的結點的左子樹的深度減去右子樹的深度的值。平衡因子只可能是-1,0,1,即二叉樹結點的平衡因子的絕對值不能大於1。

平衡二叉樹構建的基本思想是在構建二叉排序樹的過程中,每當插入一個新的節點,先檢查是否因插入而破壞了樹的平衡性,如果是,則找出最小不平衡子樹。在保持二叉排序樹特性的前提下,調整最小不平衡樹中各結點之間連結關係,進行相應的旋轉,使之變成新的平衡子樹。

AVL樹結點

template<class T>
struct AVLTreeNode
 {
	 AVLTreeNode(const T& data)
		 : _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
		 , _data(data), _bf(0)
	 {}
	 AVLTreeNode<T>* _pLeft; // 該節點的左孩子
	 AVLTreeNode<T>* _pRight; // 該節點的右孩子
	 AVLTreeNode<T>* _pParent; // 該節點的雙親
	 T _data;
	 int _bf; // 該節點的平衡因子
 };

旋轉分為四種:左旋、右旋、先左旋再右旋、先右旋再左旋。

在這裡插入圖片描述
經過平衡因子的計算與比較和數次旋轉,圖1的不平衡的二叉排序樹變成了圖2的高度平衡的二叉排序樹。

AVL樹的查詢效率非常高,時間複雜度為O(logN),但是如果要對AVL樹做一些結構修改的操作,效能非常低下。

比如:
插入時要維護其絕對平衡,旋轉的次數比較多,更差的是在刪除時,有可能一直要讓旋轉持續到根的位置。
因此如果需要一種查詢高效且有序的資料結構,而且資料的個數為靜態的(即不會改變),可以考慮AVL樹,但一個結構經常修改,就不太適合。

相關文章