樹適合於表示某些領域的層次結構(比如Linux的文件目錄結構),使用樹進行查找比使用鏈表快的多,理想情況下樹的查找復雜度O(log(N))
,而鏈表為O(N)
,但理想情況指的是什么情況呢?一般指樹是完全平衡的時候。哪最壞的情況是什么呢?就是樹退化為鏈表的時,這時候查找的復雜度與鏈表相同。就失去了樹結構的意義。所以樹的平衡是非常重要的,這一節我們主要討論樹的平衡問題。
如果樹中任一節點的兩個子樹的高度差為0或者1,該二叉樹就是高度平衡的。 上圖中,A是平衡二叉搜索樹,B是不平衡的,C直接退化為鏈表了。
為保持樹的平衡,有兩種策略,一種是全局的,即當插入和刪除操作完畢后,對樹進行重建,全局調整樹為平衡樹;另一種是局部調整,即當插入或者刪除導致樹不平衡時就立即在局部范圍內調整,使樹保持平衡,這個是后面要討論的AVL樹。下面我們先討論一下全局調整的方法。
有序數組創建二叉查找樹
要想實現樹的平衡,最簡單的想法是我們可以設想一下將樹的所有節點從小到大排序后,將中間值作為根節點,左側的值作為左子樹,右側的所有值作為右子樹,每個子樹再按根節點的划分方法,以此類推,代碼表示如下:
// data是排序后的數組
template<class T>
void BST<T>::balance (T data[], int first, int last) {
if (first <= last) {
int middle = (first + last)/2; //父節點,這種方法相當於一層一層的構造下一層子節點的父節點
insert(data[middle]);
balance(data,first,middle-1); //左子樹再遞歸調用繼續構造
balance(data,middle+1,last); //右子樹再遞歸調用繼續構造
}
}
哪怎么得到有序數組呢?直接用排序算法排序?在二叉查找樹中,這種方法比較笨,可以利用二叉查找樹的性質,中序遍歷得到有序序列。可以先對樹做中序遍歷,得到排序數組,再用balance
進行平衡。
為什么二叉查找樹中序遍歷得到有序序列呢?這和二叉查找樹的定義有關,對於二叉查找樹中的一個節點,其左子樹的值小於該節點,其右子樹的值大於該節點。而中序遍歷是:左->中->右,這個順序,剛好是從小到大的順序。比如上圖中的A、B、C三顆二叉查找樹,只要是數據相同的二叉查找樹,不管怎么排列,中序遍歷的結果都是相同的
{10,15,20,23,25,30}
。
這種辦法是比較笨的辦法,代價比較大,等於是完全重新建立二叉查找樹,有沒有聰明一點的方法呢?下面DSW算法就是比較聰明的辦法。
DSW算法(Day–Stout–Warren algorithm)
主要思路:
- 先將任意的二叉查找樹轉化為類似於鏈表的樹,成為主鏈或主干(backbone or vine);
- 圍繞主鏈中第二個節點的父節點,反復將其旋轉,將這棵被拉伸的樹在一系列步驟中轉化為完全平衡的樹;
第一階段:右旋轉形成主鏈
其中涉及旋轉(左旋轉、右旋轉)的操作,我們先看一下右旋轉的邏輯,左旋轉與右旋轉對稱,偽代碼如下:
/************************************************************************
* 子節點Ch圍繞父節點Par的右旋轉
* Before After
* Gr Gr
* \ \
* Par Ch
* / \ / \
* Ch Z X Par
* / \ / \
* X Y Y Z
***********************************************************************/
rotateRight(Gr, Par, Ch)
if Par不是樹的根節點 //即Gr節點存在
將Ch轉作為Gr的右子節點(即,Gr作為Ch的父節點)
Ch的右子樹轉作為Par的左子樹
節點Ch將Par作為右子節點
接下來開始DSW算法的第一階段:創建主鏈:偽代碼如下:
// 創建主鏈,采用右旋轉,將所有的左子樹都旋轉到主鏈上,最后形成一條右子樹(單鏈形式)
createBackbone(root)
tmp = root;
while (tmp != 0)
if tmp有左子節點
圍繞tmp旋轉該子節點; //該左子節點將成為tmp的父節點
tmp設置為剛剛成為父節點的子節點;
else
將tmp設置為它的右子節點;
其過程如下圖所示:
可以看到,右旋的過程就是不斷把左子樹旋轉到主鏈的過程。
第二階段:左旋轉轉換為平衡樹
右旋轉形成主鏈后,下個階段需要左旋轉,我們看一下左旋轉,分析思路與右旋轉相同,下圖中節點D圍繞節點B左旋轉,
/************************************************************************
* 子節點Ch圍繞父節點Par的左旋轉
* Before After
* Gr Gr
* \ \
* Par(B) Ch(D)
* / \ / \
* A Ch(D) Par(B) E
* / \ / \
* C E A C
***********************************************************************/
rotateLeft(Gr, Par, Ch)
if Par不是樹的根節點 //即Gr節點存在
將Ch轉作為Gr的右子節點(即,Gr作為Ch的父節點)
Ch的左子樹轉作為Par的右子樹
節點Ch將Par作為左子節點
通過右旋轉形成主鏈后,開始第二階段:主鏈轉換為平衡樹:偽代碼如下:
// 需要注意的是,每次順着主鏈向下操作時,每隔兩個節點,都圍繞其父節點進行旋轉
createPerfectTree
n = 節點數;
m = 2^[log(n+1)]-1; //計算當前節點數n與最接近完全平衡二叉樹中節點數之間的差,多出的節點將單獨處理
從主鏈的頂部開始做n-m次旋轉; //從主鏈的頂部第二個節點開始,每隔一個節點進行左旋
while (m > 1) // 上面單獨處理的結束,開始下面的處理
m = m/2;
從主鏈的頂部開始做m次旋轉; //從主鏈的頂部第二個節點開始,每隔一個節點進行左旋
過程如下圖所示:
最開始,左旋轉2次,之后進入while循環。進入while循環后,第1輪左旋轉3次,第2輪左旋轉1次,然后得出平衡樹。最后還是要注意,是間隔1個節點圍繞其父節點進行旋轉(或者說是每次從主鏈根節點開始,偶數節點圍繞奇數節點左旋轉)。可以看到,左旋轉就是不斷將左右子樹進行平衡的過程。
DSW算法源代碼
#include<iostream>
#include<math.h>
#include<stdlib.h>
#include<list>
#include<stack>
#include<queue>
using namespace std;
//棧實現
template<class T>
class Stack : public stack<T> {
public:
T pop() {
T tmp = stack<T>::top();
stack<T>::pop();
return tmp;
}
};
//隊列實現
template<class T>
class Queue : public queue<T> {
public:
T dequeue() {
T tmp = queue<T>::front();
queue<T>::pop();
return tmp;
}
void enqueue(const T& el) {
queue<T>::push(el);
}
};
//樹節點類
template<class T>
class Node {
public:
Node():left(NULL),right(NULL){}
Node(const T& e,Node<T>* l=NULL,Node<T>*r=NULL):data(e),left(l),right(r){}
~Node(){}
T data;
Node* left;
Node* right;
};
//二叉查找樹的實現類
template<class T>
class BST {
public:
BST():root(NULL),count(0){}
BST(T* a, int len); //根據數組中的數據構造樹,調試測試用
~BST() {
clear();
}
bool isEmpty() const {
return NULL == root;
}
void clear() {
clear(root);
root = NULL;
}
uint count;
void insert(const T&); //插入
void inorder() {//深度遍歷之中序樹遍歷
inorder(root);
}
void breadthFirst(); //廣度優先遍歷
virtual void visit(Node<T>* p) {
cout << p->data << ' ';
}
protected:
Node<T>* root; //根節點
void clear(Node<T>*);
void inorder(Node<T>*);
};
//根據數組中的內容構造樹
template<class T>
BST<T>::BST(T* a, int len) {
root = NULL;
count = 0;
for (int i = 0; i < len; i++) {
insert(a[i]);
}
}
//清除節點p及其子節點
template<class T>
void BST<T>::clear(Node<T> *p) {
if (p != NULL) {
clear(p->left);
clear(p->right);
delete p;
}
count = 0;
}
//插入,非遞歸形式
template<class T>
void BST<T>::insert(const T& el) {
Node<T> *p = root, *prev = NULL;
while (p != NULL) { // find a place for inserting new node;
prev = p;
if (el < p->data)
p = p->left;
else p = p->right;
}
if (root == NULL) // tree is empty;
root = new Node<T>(el);
else if (el < prev->data)
prev->left = new Node<T>(el);
else prev->right = new Node<T>(el);
++count;
}
//廣度優先遍歷(從上到下,從左到右,一層一層的向下訪問)
template<class T>
void BST<T>::breadthFirst() {
Queue<Node<T>*> m_queue; //要理解這里為什么要用隊列,這個隊列的作用是把下一層的數據放到本層數據的后面
Node<T>* p = root;
if (p != NULL) {
m_queue.enqueue(p);
while (!m_queue.empty()) {
p = m_queue.dequeue();
visit(p);
if (p->left != NULL)
m_queue.enqueue(p->left);
if (p->right != NULL)
m_queue.enqueue(p->right);
}
}
}
//中序遍歷,遞歸實現
template<class T>
void BST<T>::inorder(Node<T> *p) {
if (p != NULL) {
inorder(p->left);
visit(p);
inorder(p->right);
}
}
template<class T>
class DswBST: public BST<T> {
public:
DswBST(T* a, int len); //根據數組中的數據構造樹,調試測試用
void dswBalance();
protected:
void createBackbone();
void creatPerfectTree();
void rotateRight(Node<T>* Gr, Node<T>* Par, Node<T>* Ch);
void rotateLeft(Node<T>* Gr, Node<T>* Par, Node<T>* Ch);
};
template<class T>
DswBST<T>::DswBST(T* a, int len) {
for (int i = 0; i < len; i++) {
this->insert(a[i]);
}
}
template<class T>
void DswBST<T>::dswBalance() {
createBackbone();
creatPerfectTree();
}
// 二叉查找樹轉化成主鏈的過程分析
/**********************************************************************************************
* 5 <-tmp 5 5 5 5
* \ \ \ \ \
* 10 10 10 10 10
* \ \ \ \ \
* 20 15 15 15 15
* / \ \ \ \ \
* 15 30 20 20 20 20
* / \ \ \ \ \
* 25 40 30 <-tmp 25 <-tmp 23 23
* / \ / \ / \ \ \
* 23 28 25 40 23 30 25 25
* / \ / \ \ \
* 23 28 28 40 30 <-tmp 28
* / \ \
* 28 40 30
* \
* 40 <-tmp
***********************************************************************************************/
template<class T>
void DswBST<T>::createBackbone() {
Node<T> *Gr = 0, *Par = this->root, *Ch = 0;
while(Par != 0) {
Ch = Par->left;
if(Ch != 0) {
rotateRight(Gr, Par, Ch);
Par = Ch;
} else {
Gr = Par;
Par = Par->right;
}
// 旋轉過程中,如果是繞根節點的右節點旋轉時要將根節點置為原根節點的右節點
if(Gr == 0)
this->root = Ch;
}
}
/************************************************************************
* 子節點Ch圍繞父節點Par的右旋轉
* Before After
* Gr Gr
* \ \
* Par Ch
* / \ / \
* Ch Z X Par
* / \ / \
* X Y Y Z
***********************************************************************/
template<class T>
void DswBST<T>::rotateRight(Node<T>* Gr, Node<T>* Par, Node<T>* Ch) {
if(Gr != 0)
Gr->right = Ch;
Par->left = Ch->right;
Ch->right = Par;
}
template<class T>
void DswBST<T>::rotateLeft(Node<T>* Gr, Node<T>* Par, Node<T>* Ch) {
if(Gr != 0)
Gr->right = Ch;
Par->right = Ch->left;
Ch->left = Par;
}
template<class T>
void DswBST<T>::creatPerfectTree() {
int n = this->count;
if (n < 3) {
return; //節點數目小於3不用平衡
}
int m = (1 << ((int)(log10(n+1)/log10(2)))) - 1;
Node<T> *Gr = 0;
Node<T> *Par = this->root;
Node<T> *Ch = this->root->right;
this->root = this->root->right; //修改root指針
// 第一階段: 左旋n-m次
for(int i = 0; i < n - m; i++) {
rotateLeft(Gr, Par, Ch);
Gr = Ch;
Par = Gr->right;
if (0 != Par) {
Ch = Par->right;
} else {
break;
}
}
// 第二階段,進入while循環
while( m > 1) {
m = m >> 1;
Node<T> *Gr = 0;
Node<T> *Par = this->root;
Node<T> *Ch = this->root->right;
this->root = this->root->right;
for(int i = 0; i < m; i++) {
rotateLeft(Gr, Par, Ch);
Gr = Ch;
Par = Gr->right;
if (0 != Par) {
Ch = Par->right;
} else {
break;
}
}
}
}
int main()
{
int a[] = { 5,10,20,15,30,25,40,23,28};
DswBST<int> tree(a, sizeof(a) / sizeof(a[0]));
tree.breadthFirst();
cout << endl;
tree.inorder();
cout << endl;
tree.dswBalance();
tree.breadthFirst();
cout << endl;
tree.inorder();
return 0;
}
DSW論文:One-Time Binary Search Tree Balancing:
The Day/Stout/Warren (DSW) Algorithm