演算法學習筆記(二)——二叉查詢樹和AVL樹
1.二叉查詢樹
二叉查詢樹是一顆二叉樹,其中每一個結點都含有一個可比較的鍵以及相關聯的值,並且每個結點的鍵都大於左子樹的任意結點的鍵而小於右子樹的任意結點的鍵
-
結點定義
template <typename T> class BSTNode { public: int key; //鍵 T val; //值 BSTNode *left, *right; int N; BSTNode(int k, T v, int n) : key(k), val(v), N(1), left(NULL), right(NULL){} };
-
查詢(遞迴實現)
由二叉樹查詢樹的結構可知,查詢每個結點所需要的比較次數為結點的深度加一。只要結點的關鍵字若等於查詢的關鍵字,則查詢成功;若小於結點的關鍵字則遞迴查詢左子樹,大於結點的關鍵字則遞迴查詢右子樹,若子樹為空,則查詢失敗。
程式碼實現:
template <typename T> T get(BSTNode *node, int key) { if (node->key == key) return node->val; else if (node->key > key) return get(node->left, key); //左子樹遞迴查詢 else return get(node->right, key); //右子樹遞迴查詢 }
二叉查詢樹的查詢在理論情況下,n個結點的完美二叉查詢樹的高度為\(log_2(n+1)-1\),因此一般的二叉查詢樹的查詢次數\(\lceil log_2(n+1)\rceil\),在最壞情況下,二叉查詢樹的每一層只有一個結點查詢次數為n,因此二叉查詢樹的時間複雜度在\(O(logn)\sim O(n)\)
-
插入(遞迴實現)
二叉查詢樹的插入操作與查詢操作基本相同,比較鍵值是否相等,相等則返回,表示已經存在,不相等則根據大小在左右子樹遞迴查詢,直到找到相等鍵值的結點,或者子結點為空時,插入空節點的位置
程式碼實現:
template <typename T> BSTNode *put(BSTNode *node, int key, T val) { if (node == NULL) return new BSTNode(key, val, 1); if (node->key == key) node->val = val; else if (node->key > key) node->left = put(node->left, key, val); else node->right = put(node->right, key, val); node->N = 1 + size(node->left) + size(node->right); return node; }
-
刪除(遞迴實現)
刪除的操作較為複雜,分為三種情況
-
葉子結點
刪除葉子結點並不會影響二叉查詢樹的結構,直接將指向待刪除結點的連結指向空即可
-
結點只有左子樹或者右子樹
只要將指向待刪除結點的連結指向左子樹或者右子樹
-
結點有左右子樹
刪除這種型別的結點有可能會破壞平衡二叉查詢樹的結構,在刪除這個結點之後我們需要處理兩個子樹,我們只需要找到該結點的後繼結點(右子樹的最小結點),將指向該結點的連結指向後繼結點即可
程式碼實現:
BSTNode *delKey(BSTNode *node, int key) { if (node == nullptr) return nullptr; if (key < node->key) { node->left = delKey(node->left, key); } else if (key > node->key) { node->right = delKey(node->right, key); } else { if (node->left == nullptr || node->right == nullptr) //葉子結點和只有一個子樹的結點 { BSTNode *temp = node; node = (node->left != nullptr) ? node->left : node->right; delete temp; } else //有左右子樹的結點 { BSTNode *temp = getMin(node->right); //查詢後繼結點 temp->right = delMin(node->right); //後繼結點的右子樹為刪除後繼結點後的右子樹 temp->left = node->left; } } node->N = size(node->left) + size(node->right) + 1; return node; }
在前兩種情況下,刪除結點的操作複雜度都是常數級,第三種情況查詢後繼結點為內部查詢操作,相當於兩個層次的查詢操作,因此總複雜度仍為\(O(logn)\sim O(n)\);
-
-
總結
二叉查詢樹的查詢,插入,刪除效能都與樹的高度相關,如果能將二叉查詢樹更加平衡一些,減少線性結構,能有效提高效能。
程式碼附錄
#include <iostream> #include <vector> #include <queue> using namespace std; template <typename T> class BSTree { private: class BSTNode //結點定義 { public: int key; T val; BSTNode *left, *right; int N; BSTNode(int k, T v, int n) : key(k), val(v), N(1), left(NULL), right(NULL) { } BSTNode(BSTNode *node) { key = node->key; val = node->val; left = node->left; right = node->right; N = node->N; } }; BSTNode *root; //根節點 int size(BSTNode *node) { if (node == NULL) return 0; else return root->N; } T get(BSTNode *node, int key) { if (node->key == key) return node->val; else if (node->key > key) return get(node->left, key); else return get(node->right, key); } BSTNode *put(BSTNode *node, int key, T val) { if (node == NULL) return new BSTNode(key, val, 1); if (node->key == key) node->val = val; else if (node->key > key) node->left = put(node->left, key, val); else node->right = put(node->right, key, val); node->N = 1 + size(node->left) + size(node->right); return node; } BSTNode *delMin(BSTNode *node) { if (node->left == NULL) return node->right; node->left = delMin(node->left); node->N = size(node->left) + size(node->right); return node; } BSTNode *getMin(BSTNode *node) { while (node->left != NULL) node = node->left; return node; } BSTNode *delKey(BSTNode *node, int key) { if (node == nullptr) return nullptr; if (key < node->key) { node->left = delKey(node->left, key); } else if (key > node->key) { node->right = delKey(node->right, key); } else { if (node->left == nullptr || node->right == nullptr) { BSTNode *temp = node; node = (node->left != nullptr) ? node->left : node->right; delete temp; } else { BSTNode *temp = getMin(node->right); temp->right = delMin(node->right); temp->left = node->left; } } node->N = size(node->left) + size(node->right) + 1; return node; } public: BSTree() { root = nullptr; } int size() { return size(root); } T get(int key) //查詢操作 { return get(root, key); } void put(int key, T val) //插入操作 { root = put(root, key, val); } void delMin() //刪除最小值 { root = delMin(root); } void delKey(int key) //刪除操作 { root = delKey(root, key); } void print() //輸出二叉查詢樹 { vector<int> BFS; queue<BSTNode *> BiQueue; BiQueue.push(root); while (!BiQueue.empty()) { BSTNode *temp = BiQueue.front(); BiQueue.pop(); BFS.push_back(temp->key); if (temp->left != NULL) BiQueue.push(temp->left); if (temp->right != NULL) BiQueue.push(temp->right); ; } for (auto &ch : BFS) { cout << ch << " "; } cout << endl; } }; int main() { BSTree<int> *bitree = new BSTree<int>(); for (int i = 0; i < 10; i++) { int t = rand() % 100; cout << t << endl; bitree->put(t, i); } bitree->print(); bitree->delKey(0); bitree->print(); }
2.平衡二叉查詢樹(AVL樹)
平衡二叉查詢樹,它能保持二叉樹的高度平衡,儘量降低二叉樹的高度,減少樹的平均查詢長度。
AVL樹的性質:
- 左子樹與右子樹高度之差的絕對值不超過1
- 樹的每個左子樹和右子樹都是AVL樹
- 每一個節點都有一個平衡因子(balance factor),任一節點的平衡因子是-1、0、1(每一個節點的平衡因子 = 右子樹高度 - 左子樹高度)
-
AVL樹的結點設計
AVL樹的結點與二叉查詢樹的結點相似,多了一個值來作為平衡因子判斷結點是否平衡
程式碼實現:
template <typename T> class AVLTreeNode { public: T key; int height; AVLTreeNode *left; AVLTreeNode *right; AVLTreeNode(T val, AVLTreeNode *l, AVLTreeNode *r) : key(val), height(0), left(l), right(r) {} };
-
AVL樹的自平衡操作——旋轉
-
右旋(LL)
將pivot變為根節點,root變為pivot的右子樹,pivot的右子樹變為root的左子樹
程式碼實現
AVLTreeNode *LL(AVLTreeNode *node) //左左 { AVLTreeNode *new_root = node->left; node->left = new_root->right; new_root->right = node; node->height = max(height(node->left), height(node->right)) + 1; new_root->height = max(height(new_root->left), height(new_root->right)) + 1; return new_root; }
-
左旋(RR)
與LL情況相反即可,將pivot變為根節點,root變為pivot的左子樹,pivot的左子樹變為root的右子樹
程式碼實現
AVLTreeNode *RR(AVLTreeNode *node) //右右 { AVLTreeNode *new_root = node->right; node->right = new_root->left; new_root->left = node; node->height = max(height(node->left), height(node->right)) + 1; new_root->height = max(height(new_root->left), height(new_root->right)) + 1; return new_root; }
-
左右旋轉(LR)
兩次旋轉,第一次是對root的左旋,第二次是對原來root的父母結點進行右旋
程式碼實現
AVLTreeNode *LR(AVLTreeNode *node) //左右 { node->left = LL(node->left); return LL(node); }
-
右左旋轉(RL)
兩次旋轉,第一次是對root的右旋,第二次是對原來root的父母結點進行左旋
程式碼實現
AVLTreeNode *RL(AVLTreeNode *node) //右左 { node->right = LL(node->right); return RR(node); }
-
-
插入操作
AVL本身也是二叉查詢樹,因此插入操作與二叉查詢樹類似,只需要加上旋轉的操作來調整高度。當給一個平衡的AVL樹進行插入一個新結點P時,從P到根結點的路徑上,每個子樹的高度都有可能+1,所以要對路徑上的每一個結點進行調整。
程式碼實現
AVLTreeNode *insert(AVLTreeNode *node, T key) { if (node == NULL) { AVLTreeNode *temp = new AVLTreeNode(key, NULL, NULL); node = temp; } else if (key < node->key) { node->left = insert(node->left, key); if (height(node->left) - height(node->right) == 2) { if (key < node->left->key) node = LL(node); else node = LR(node); } } else if (key > node->key) { node->right = insert(node->right, key); if (height(node->right) - height(node->left) == 2) { if (key > node->right->key) node = RR(node); else node = RL(node); } } node->height = max(height(node->right), height(node->left)) + 1; return node; }
-
刪除操作
同樣,AVL樹的刪除操作與二叉查詢樹的刪除操作相似,只需要加上旋轉的操作來調整高度。
程式碼實現
AVLTreeNode *remove(AVLTreeNode *node, AVLTreeNode *delnode) { if (node == nullptr || delnode == nullptr) return nullptr; if (delnode->key < node->key) { node->left = remove(node->left, delnode); if (height(node->right) - height(node->left) == 2) { if (height(node->right->left) > height(node->right->right)) node = RL(node); else node = RR(node); } } else if (delnode->key > node->key) { node->right = remove(node->right, delnode); if (height(node->left) - height(node->right) == 2) { if (height(node->left->right) > height(node->left->left)) node = LR(node); else node = LL(node); } } else { if (node->left == nullptr || node->right == nullptr) { AVLTreeNode *temp = node; node = (node->left != nullptr) ? node->left : node->right; delete temp; } else { if (height(node->left) > height(node->right)) { AVLTreeNode *max = getMax(node->left); node->key = max->key; node->left = remove(node->left, max); } else { AVLTreeNode *min = getMin(node->right); node->key = min->key; node->right = remove(node->right, min); } } } return node; }
-
總結
在AVL樹中,不管我們進行執行插入還是刪除操作,都要進行旋轉來調整平衡,而調整是非常耗時的,因此AVL適合用於插入刪除次數較少,查詢次數較多的情況。