1. 程式人生 > >使用C++實現AVL樹模板

使用C++實現AVL樹模板

今天練習編寫了一下AVL樹。參考了Weiss的資料結構與演算法C++描述版。上一個實現了一般的二叉搜尋樹,在使用的過程中可能會慢慢變得不平衡,這樣很可能會降低查詢、插入等等的效率,因此我們需要使用演算法來實現樹的平衡。AVL樹是一種比較經典的平衡二叉搜尋樹,它規定每個節點的左子樹和右子樹的高度差最多為1,預設空樹的高度為-1,這樣能保證樹的深度為O(logN),而且除了遍歷,賦值,基本上所有樹的操作都可以以時間O(logN)執行(當然插入和刪除後因為要重新平衡樹,可能時間會長於O(logN))。(Ps,上面文字中的log指log2)

AVL樹中,最重要的便是讓樹重新平衡,我們稱個過程為旋轉。旋轉包括四種,主要是由於插入位置的原因導致的。旋轉的過程可以看程式碼中的註釋部分(569行-639行),有詳細的解釋。

這次編寫的過程中,將C++模板類的定義了和函式實現進行了分開(但是仍然在標頭檔案中),遇到了比較多的問題。先看看程式碼,然後對裡面我遇到的問題進行總結。

先看看AVL樹的實現程式碼  AVLTree.h:

#ifndef AVL_TREE_H
#define AVL_TREE_H

#include<iostream>
using namespace std;


template <typename Comparable>
class AVLTree
{

public:
	typedef enum _order {PREORDER, INORDER, POSTORDER} ORDER; // 通過enum定義常量

public:
	AVLTree() :m_root(nullptr){}
	AVLTree(const AVLTree &rhs)
	{
		m_root = clone(rhs.m_root);
	}
	~AVLTree()
	{
		makeEmpty();
	}

	/**
	* 返回樹的高度。空樹的高度定義為-1
	*/
	int getHeight() const
	{
		return m_root.height;
	}

	/**
	* 找到樹中的最小值,通過呼叫private的findMin實現遞迴
	*/
	const Comparable & findMin() const
	{
		return findMin(m_root)->element;
	}

	/**
	* 找到樹中的最大值,通過呼叫private的findMax實現遞迴
	*/
	const Comparable & findMax() const
	{
		return findMax(m_root)->element;
	}

	/**
	* 當x找到時返回真,否則返回假
	* 呼叫了private的那個同名函式,這個是為了遞迴實現
	*(因為private中包含了一個AVLNode的指標t)
	*/
	bool contains(const Comparable &x) const
	{
		return contains(x, m_root);
	}

	/**
	* 判斷樹是否為空
	*/
	bool isEmpty() const
	{
		return  nullptr == m_root;
	}

	/**
	* 把樹遍歷一遍(順序可以自己選擇,預設為中序)
	*/
	void printTree(ORDER or = INORDER, ostream & out = cout) const
	{
		if (isEmpty())
			out << "Empty tree!" << endl;
		else
		{
			switch (or)
			{ 
				case PREORDER:
					preOrder(m_root, cout);
					break; 
				case INORDER:
					inOrder(m_root, cout);
					break;
				case POSTORDER:
					postOrder(m_root, cout);
					break;
				default:
					cerr << "列印樹的順序只能為PREORDER, INORDER, POSTORDER!" << endl;
					break;
			}
		}		
	}

	/**
	* 清空樹
	*/
	void makeEmpty()
	{
		makeEmpty(m_root);
	}

	/**
	* 把x插入樹中,如果重複了就忽略
	*/
	void insert(const Comparable &x)
	{
		insert(x, m_root);
	}

	/**
	* 把x從樹中刪除。如果x不在樹中就什麼都不做。
	*/
	void remove(const Comparable &x)
	{
		remove(x, m_root);
	}

	/**
	* 深拷貝
	*/
	const AVLTree & operator= (const AVLTree &rhs)
	{
		if (this != &rhs)
		{
			AVLNode *tmp = clone(rhs.m_root);
			makeEmpty();
			m_root = tmp;
		}
		return *this;
	}


private:
	struct AVLNode{
		Comparable element;
		AVLNode *left;
		AVLNode *right;
		int height;

		AVLNode(const Comparable &theElement,
			AVLNode *lt,
			AVLNode *rt,
			int h = 0)
			: element(theElement), left(lt), right(rt), height(h) {}
	};

	AVLNode *m_root; // 根節點
	static const int ALLOW_IMBALANCE = 1; // 允許實施平衡的高度界限

	/**
	* 用於比較兩個數的大小(主要用於比較高度)
	*/
	int max (int a, int b)
	{
		return a >= b ? a : b; 
	}

	/**
	* 獲得節點高度,空樹的高度為-1
	*/
	inline int height(AVLNode *t) const
	{
		return nullptr == t ? -1 : t->height;
	}

	/**
	* 在樹t中插入元素x,如果重複則什麼也不做
	*/
	void insert(const Comparable &x, AVLNode * &t);

	/**
	* 在樹t中刪除元素x
	*/
	void remove(const Comparable &x, AVLNode * &t);
	

	/**
	* 查詢最小的元素, 通過遞迴的方法
	*/
	AVLNode * findMin(AVLNode *t) const;
	

	/**
	* 查詢最大的元素, 通過迴圈的方法
	*/
	AVLNode * findMax(AVLNode *t) const;

	/**
	* 通過遍歷的方法查詢x是否在樹(或子樹)t中
	*/
	bool contains(const Comparable &x, AVLNode * t) const;
	
	/**
	* 清空樹
	*/
	void makeEmpty(AVLNode * &t);
	
	/**
	* 按前序列印子樹
	*/

	void preOrder(AVLNode *t, ostream & out) const;
	/**
	* 按中序列印子樹
	*/

	void inOrder(AVLNode *t, ostream & out) const;
	/**
	* 按後序列印子樹
	*/
	void postOrder(AVLNode *t, ostream & out) const;

	/**
	* 複製子樹
	*/
	AVLNode * clone(AVLNode *t) const;

	/**
	* 平衡子樹
	*/
	void balance(AVLNode * &t);

	/**
	* 左旋(右子樹比左子樹高2,並且新插入的元素在右子樹的右邊)
	* 此時以右子樹(k1)為軸,它的根(k2)進行左旋
	* 可以理解為它的根在它的左邊,所以左旋(在左邊旋轉)
	*      K2                           K1
	*     /  \                         /  \
	*    X    k1         -----        K2   Z
	*         / \                    /  \   \
	*        Y   Z                  X    Y   Z'
	*             \
	*              Z'
	* Z'可能在Z的左邊,也可以在Z的右邊。例子中假設在右邊。
	**/
	void rotateWithRightChild(AVLNode * & k2);

	/**
	* 右旋(左子樹比右子樹高2,並且新插入的元素在左子樹的左邊)
	* 此時以左子樹(k1)為軸,它的根(k2)進行右旋
	* 可以理解為它的根在它的右邊,所以右旋(在右邊旋轉)
	*       k2                      k1
	*      /  \                    /  \
	*     k1   Z      -------     X    k2
	*    /  \                    /     / \
	*   X    Y                  X'     Y   Z 
	*  /
	* X'
	* X'可能在X的左邊,也可以在X的右邊。例子中假設在左邊。
	*/
	void rotateWithLeftChild(AVLNode * & k2);

	/**
	* 左右雙旋(左子樹K1比右子樹D高2,並且新插入的元素在左子樹K1的右邊K2)
	* 第一步:對左子樹k1進行一次左旋(軸為k2)
	* 第二步:對整個樹k3進行一次右旋(軸為k2)
	*       k3                k3              k2
	*      /  \              /  \            /  \
	*     k1   D    ----    k2   D  ----    k1   k3
	*    / \               / \             / \   / \
	*   A   k2            k1  C           A   B  C  D
	*       / \          / \
	*      B   C        A   B
	*/
	void doubleWithLeftChild(AVLNode * & k3);

	/**
	* 右左雙旋(右子樹K1比左子樹A高2,並且新插入的元素在右子樹K1的左邊K2)
	* 第一步:對右子樹k1進行一次右旋(軸為k2)
	* 第二步:對整個樹k3進行一次左旋(軸為k2)
	*      k3              k3                 k2
	*     /  \            /  \               /  \
	*    A    k1   ----  A    k2     ----  k3    k1
	*        / \             /  \         /  \  /  \
	*       K2  D           B    k1      A    B C   D
	*      / \                  /  \
	*     B   C                C    D
	*/
	void doubleWithRightChild(AVLNode * & k3);

	/**
	* 更新節點的高度
	*/
	inline void updateHeight(AVLNode * & t)
	{
		t->height = max(height(t->left), height(t->right)) + 1;
	}

};


/**
* 複製子樹
*/
template <typename Comparable>
typename AVLTree<Comparable>::AVLNode * 
	AVLTree<Comparable>::clone(
		typename AVLTree<Comparable>::AVLNode *t) const
{
	if (t == nullptr)
		return nullptr;

	return new AVLNode(t->element, clone(t->left), clone(t->right));
}

/**
* 按前序列印子樹
*/
template <typename Comparable>
void AVLTree<Comparable>::preOrder(
	typename AVLTree<Comparable>::AVLNode*t, 
	ostream & out) const
{
	if (nullptr != t)
	{	
		out << t->element << endl;
		preOrder(t->left, out);
		preOrder(t->right, out);
	}
}



/**
* 按中序列印子樹
*/
template <typename Comparable>
void AVLTree<Comparable>::inOrder(
	typename AVLTree<Comparable>::AVLNode *t, 
	ostream & out) const
{
	if (nullptr != t)
	{
		inOrder(t->left, out);
		out << t->element << endl;
		inOrder(t->right, out);
	}
}

/**
* 按後序列印子樹
*/
template <typename Comparable>
void AVLTree<Comparable>::postOrder(
	typename AVLTree<Comparable>::AVLNode*t,
	ostream & out) const
{

	if (nullptr != t)
	{
		postOrder(t->left, out);
		postOrder(t->right, out);
		out << t->element << endl;
	}
}


/**
* 清空樹
*/
template <typename Comparable>
void AVLTree<Comparable>::makeEmpty(
	typename AVLTree<Comparable>::AVLNode * &t)
{
	if (t != nullptr)
	{
		makeEmpty(t->left);
		makeEmpty(t->right);
		delete t;
	}
	t = nullptr;
}

/**
* 查詢最小的元素, 通過遞迴的方法
*/
template <typename Comparable>
typename AVLTree<Comparable>::AVLNode * 
	AVLTree<Comparable>::findMin(
	typename AVLTree<Comparable>::AVLNode *t) const
{
	if (t == nullptr)
		return nullptr;
	if (t->left == nullptr)
		return t;
	return findMin(t->left);
}

/**
* 查詢最大的元素, 通過迴圈的方法
*/
template <typename Comparable>
typename AVLTree<Comparable>::AVLNode * 
	AVLTree<Comparable>::findMax(
	typename AVLTree<Comparable>::AVLNode *t) const
{
	if (t != nullptr)
		while (t->right != nullptr)
			t = t->right;
	return t;
}

/**
* 在樹t中刪除元素x,一定要主要保持樹的平衡
*/
template <typename Comparable>
void AVLTree<Comparable>::remove(
	const Comparable &x, 
	typename AVLTree<Comparable>::AVLNode * &t) 
{
	if (t == nullptr)
		return; // 沒有找要刪除的節點x

	if (x < t->element)
		remove(x, t->left);
	else if (t->element < x)
		remove(x, t->right);
	else if (t->left != nullptr &&
		t->right != nullptr)
	{
		t->element = findMin(t->right)->element;
		remove(t->element, t->right);
	}
	else
	{
		//typename AVLTree<Comparable>::AVLNode * oldNode = t;
		auto oldNode = t; // 這一句等於上面的那長長的語句,可以看出C++11中的auto還是非常有用 的
		t = (t->left != nullptr) ? t->left : t->right;
		delete oldNode;
	}

	balance(t);
}

/**
* 在樹t中插入元素x,如果重複則什麼也不做
*/
template <typename Comparable>
void AVLTree<Comparable>::insert(
	const Comparable &x, 
	typename AVLTree<Comparable>::AVLNode * &t) 
{
	if (nullptr == t)
		t = new AVLNode(x, nullptr, nullptr);
	else if (x < t->element)
	{
		insert(x, t->left); // 如果帶插入的值小於目前的節點,則插入在左子樹
		/*
		if (height(t->left) - height(t->right) == 2) // 插入後如果節點的左子樹比右子樹高2
		{
			if (x < t->left->element)
				rotateWithLeftChild(t); // 右旋
			else
				doubleWithLeftChild(t); // 左-右雙旋
		}
		*/

	}
	else if (t->element < x)
	{
		insert(x, t->right);// 如果帶插入的值大於目前的節點,則插入在右子樹
		/*
		if (height(t->right) - height(t->left) == 2)// 插入後如果節點的右子樹比左子樹高2
		{
			if (t->right->element < x)
				rotateWithRightChild(t); // 左旋
			else
				doubleWithRightChild(t); // 右-左雙旋
		}
		*/

	}
	else
		; // 表示在樹中找到了x,則什麼也不做

	balance(t); // 每次完成插入都檢查子樹是否平衡(並且預設更新節點t的高度)
}

/**
* 平衡子樹
*/
template <typename Comparable>
void AVLTree<Comparable>::balance(
	typename AVLTree<Comparable>::AVLNode * &t)
{
	if (nullptr == t)
		return;
	// 如果左子樹的高度與右子樹高度差大於實施平衡調整的限度
	if (height(t->left) - height(t->right) > AVLTree<Comparable>::ALLOW_IMBALANCE)
	{
		// 判斷是左子樹的左邊高還是右邊高,如果左子樹左邊高
		if (height(t->left->left) >= height(t->left->right))
		{
			rotateWithLeftChild(t); 
		}
		else
		{ 
			doubleWithLeftChild(t);
		}

	}
	// 如果右子樹的高度與左子樹高度差大於實施平衡調整的限度
	else if (height(t->right) - height(t->left) > AVLTree<Comparable>::ALLOW_IMBALANCE)
	{
		if (height(t->right->right) >= height(t->right->left))
		{
			rotateWithRightChild(t);
		}
		else
		{
			doubleWithRightChild(t);
		}
	}
	else
		;

	updateHeight(t);
}

/**
* 通過遍歷的方法查詢x是否在樹(或子樹)t中
*/
template <typename Comparable>
bool AVLTree<Comparable>::contains(
	const Comparable &x, 
	typename AVLTree<Comparable>::AVLNode * t) const
{
	if (t == nullptr) // 遍歷中未找到元素的中止條件
		return false;
	else if (x < t->element)
		return contains(x, t->left);
	else if (t->element < x)
		return contains(x, t->right);
	else // 如果 x 不大於 也 不小於t所指的節點中的元素,則x==t->element
		return true;
}

/**
* 右旋(左子樹比右子樹高2,並且新插入的元素在左子樹的左邊)
* 此時以左子樹(k1)為軸,它的根(k2)進行右旋
* 可以理解為它的根在它的右邊,所以右旋(在右邊旋轉)
*       k2                      k1
*      /  \                    /  \
*     k1   Z      -------     X    k2
*    /  \                    /     / \
*   X    Y                  X'     Y   Z
*  /
* X'
* X'可能在X的左邊,也可以在X的右邊。例子中假設在左邊。
*/
template <typename Comparable>
void AVLTree<Comparable>::rotateWithLeftChild(
	typename AVLTree<Comparable>::AVLNode * & k2)
{
	//typename AVLTree<Comparable>::AVLNode * k1 = k2->left; // 獲得k2的左節點
	auto k1 = k2->left; // 使用auto定義
	// 開始旋轉
	k2->left = k1->right; 
	k1->right = k2;
	//更新高度, 先更新k2可以,更新k1時減少一次height函式的呼叫
	k2->height = max(height(k2->left), height(k2->right)) + 1;//等價於updateHeight(k2)
	k1->height = max(height(k1->left), k2->height) + 1;

	k2 = k1;
	
}

/**
* 左旋(右子樹比左子樹高2,並且新插入的元素在右子樹的右邊)
* 此時以右子樹(k1)為軸,它的根(k2)進行左旋
* 可以理解為它的根在它的左邊,所以左旋(在左邊旋轉)
*      K2                           K1
*     /  \                         /  \
*    X    k1         -----        K2   Z
*         / \                    /  \   \
*        Y   Z                  X    Y   Z'
*             \
*              Z'
* Z'可能在Z的左邊,也可以在Z的右邊。例子中假設在右邊。
**/
template <typename Comparable>
void AVLTree<Comparable>::rotateWithRightChild(
	typename AVLTree<Comparable>::AVLNode * & k2)
{
	//typename AVLTree<Comparable>::AVLNode * k1 = k2->right;
	auto k1 = k2->right; // 使用auto定義
	// 開始旋轉
	k2->right = k1->left;
	k1->left = k2;
	//更新高度, 先更新k2可以減少一次height函式的呼叫
	k2->height = max(height(k2->left), height(k2->right)) + 1;
	k1->height = max(k2->height, height(k1->right)) + 1; 

	k2 = k1;
}

/**
* 左右雙旋(左子樹K1比右子樹D高2,並且新插入的元素在左子樹K1的右邊K2)
* 第一步:對左子樹k1進行一次左旋(軸為k2)
* 第二步:對整個樹k3進行一次右旋(軸為k2)
*       k3                k3              k2
*      /  \              /  \            /  \
*     k1   D    ----    k2   D  ----    k1   k3
*    / \               / \             / \   / \
*   A   k2            k1  C           A   B  C  D
*       / \          / \
*      B   C        A   B
* 注:一般來說,只會有B或C一個存在,就會進行樹的平衡調整
*/
template <typename Comparable>
void AVLTree<Comparable>::doubleWithLeftChild(
	typename AVLTree<Comparable>::AVLNode * & k3)
{
	rotateWithRightChild(k3->left);
	rotateWithLeftChild(k3);
}


/**
* 右左雙旋(右子樹K1比左子樹A高2,並且新插入的元素在右子樹K1的左邊K2)
* 第一步:對右子樹k1進行一次右旋(軸為k2)
* 第二步:對整個樹k3進行一次左旋(軸為k2)
*      k3              k3                 k2
*     /  \            /  \               /  \
*    A    k1   ----  A    k2     ----  k3    k1
*        / \             /  \         /  \  /  \
*       K2  D           B    k1      A    B C   D
*      / \                  /  \
*     B   C                C    D
*注:一般來說,只會有B或C一個存在,就會進行樹的平衡調整
*/
template <typename Comparable>
void AVLTree<Comparable>::doubleWithRightChild(
	typename AVLTree<Comparable>::AVLNode * & k3)
{
	rotateWithLeftChild(k3->right);
	rotateWithRightChild(k3);
}
#endif  //AVL_TREE_H

我就按程式碼行序來總結:

1、定義了一個private clone函式(217行,296行),供拷貝建構函式(17行)和賦值操作符函式呼叫(122行)。

2、分別定義了public函式和對應的private函式,從而能夠使用遞迴的方式實現一些功能,比如findMax和findMin。

3、樹的深度優先遍歷分為三種,前序遍歷,中序遍歷和後序遍歷,因此在列印樹的函式中,可以通過引數指定是通過什麼方法進行遍歷。為了更加清楚的表現出遍歷的形式,使用enum定義了一些常量(13行),供printTree(71行使用)。由於printTree有兩個引數,而且都有預設的引數。由於有預設引數的形參應該放在形參列表的末尾,而且我覺得經常使用的引數時調整列印的順序,第一個引數是接收列印順序,第二個引數時接收列印到的輸出流。然後通過switch語句選擇列印的方式,呼叫不同的私有函式。

在應用程式部分,就能使用t.printTree(AVLTree<int>::INORDER);來清晰明白的呼叫需要使用的列印方法(t是一個AVLTree<int>型別的物件)。

4、135行定義AVLNode中,儲存了節點的高度資訊(這是一般的二叉搜尋樹沒有的)

5、在二叉搜尋樹的程式中,定義remove函式和insert函式為const,這是因為一般的二叉搜尋樹其實只是儲存了一個指向根節點的指標,const可以保證指標的值不變,但是所指的根節點裡面的內容(注意:指標的內容沒變)如果修改了,也是可以通過編譯的。然而在AVL樹中,我們可能會因為調整數的平衡使得根節點所指的向的節點,比如本來指向A節點,由於調整樹,可能樹根就變成B節點了,這時候A和B的地址是不一樣的,所以AVLTree物件中的成員m_root的值會變化,因此remove函式和insert函式不能再為const了。

6、149行定義了一個靜態常量。由於靜態常量需要在一開始就賦值,所以在類定義的時候就賦值了。

7、224~280行是如何旋轉的方法,函式的定義在(569行-639行)。裡面寫了我是如何理解左旋右旋的,並且附上了過程圖。

8、我以前定義模板類的時候都把成員函式的實現寫在類定義裡面。這次我把成員函式的實現分離出來了,遇到了不少問題。最主要的就是在135行定義了一個AVLNode的struct,如果在分離形勢下要使用這個結構體型別的物件,應該顯式的宣告其為AVLNode<T> 中的型別typename AVLTree<Comparable>::AVLNode。

9、在類的成員函式實現的過程中,可以使用auto免去寫長長的型別的痛苦:比如427~428行。

10、562行也可以像註釋那樣寫,但是563行直接寫可以免去呼叫一次height函式

然後是我的測試程式碼:testDemo.cpp

#include<iostream>
#include<random>
#include<ctime>
using namespace std;

#include"AVLTree.h"

int main()
{
	AVLTree<int> t; // 建立一個AVL叉搜尋樹

	int a[16] = { 1, 2, 3, 4, 5, 6,
		7, 8, 9, 10, 11, 12,
		13, 14, 15, 16 };

	cout << "==== 測試插入(先插入1~8,插入15~9):" << endl;
	for (size_t i = 0; i < 8; ++i)
	{
		t.insert(a[i]);
	}

	for (size_t i = 15; i >= 8; --i)
	{
		t.insert(a[i]);
	}
	
	cout << "==== 測試中序列印:" << endl;
	t.printTree(AVLTree<int>::INORDER);
	cout << "==== 測試前序列印:" << endl;
	t.printTree(AVLTree<int>::PREORDER);

	
	cout << "==== 測設刪除:" << endl;
	for (size_t i = 0; i < 16; i+=2)
	{
		t.remove(i);
	}
	t.printTree();
	
	cout << "==== 測試拷貝建構函式:" << endl;
	AVLTree<int> t2(t);
	t2.printTree();
	
	cout << "==== 測試賦值操作:" << endl;
	AVLTree<int> t3;
	t3 = t;
	t.printTree();
	
	cout << "==== 測試最大最小值:" << endl;
	cout << "最大值:" << t.findMax() << endl;
	cout << "最小值:" << t.findMin() << endl;
	
	return 0;

}

執行結果如圖:


通過中序,我們可以看出這個樹是一棵二叉搜尋樹,然後通過中序和前序,我畫出了樹的形狀


可以看出,這棵樹是平衡的,也就是說這個AVL樹的旋轉演算法的實現目前來看沒有問題

以下是後面的結果


感覺這些資料結構,想起來比較簡單,但是使用語言實現的時候,會碰到很多難以想到的問題。因此實現過程還是非常重要的,紙上來得終覺淺,絕知此事要躬行!共勉!