二叉查找樹的平衡(DSW)

樹適合於表示某些領域的層次結構(比如Linux的文件目錄結構),使用樹進行查找比使用鏈表快的多,理想情況下樹的查找複雜度O(log(N)),而鏈表爲O(N),但理想情況指的是什麼情況呢?一般指樹是完全平衡的時候。哪最壞的情況是什麼呢?就是樹退化爲鏈表的時,這時候查找的複雜度與鏈表相同。就失去了樹結構的意義。所以樹的平衡是非常重要的,這一節我們主要討論樹的平衡問題。

image
如果樹中任一節點的兩個子樹的高度差爲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左旋轉,
image

/************************************************************************
 *  子節點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

可關注微信公衆號,一起交流學習!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章