使用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樹的旋轉演算法的實現目前來看沒有問題
以下是後面的結果
感覺這些資料結構,想起來比較簡單,但是使用語言實現的時候,會碰到很多難以想到的問題。因此實現過程還是非常重要的,紙上來得終覺淺,絕知此事要躬行!共勉!