1. 程式人生 > >資料結構中各種樹

資料結構中各種樹

資料結構中各種樹

作者:Poll的筆記 
部落格出處:http://www.cnblogs.com/maybe2030/ 

資料結構中有很多樹的結構,其中包括二叉樹、二叉搜尋樹、2-3樹、紅黑樹等等。本文中對資料結構中常見的幾種樹的概念和用途進行了彙總,不求嚴格精準,但求簡單易懂。

1. 二叉樹

二叉樹是資料結構中一種重要的資料結構,也是樹表家族最為基礎的結構。

二叉樹的定義:二叉樹的每個結點至多隻有二棵子樹(不存在度大於2的結點),二叉樹的子樹有左右之分,次序不能顛倒。二叉樹的第i層至多有2i-1個結點;深度為k的二叉樹至多有2k-1個結點;對任何一棵二叉樹T,如果其終端結點數為n0,度為2的結點數為n2,則n0=n2+1。

二叉樹的示例

2009050402

滿二叉樹和完全二叉樹:

滿二叉樹:除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點。也可以這樣理解,除葉子結點外的所有結點均有兩個子結點。節點數達到最大值,所有葉子結點必須在同一層上。

滿二叉樹的性質:

1) 一顆樹深度為h,最大層數為k,深度與最大層數相同,k=h;

2) 葉子數為2h;

3) 第k層的結點數是:2k-1;

4) 總結點數是:2k-1,且總節點數一定是奇數。

完全二叉樹:若設二叉樹的深度為h,除第 h 層外,其它各層 (1~(h-1)層) 的結點數都達到最大個數,第h層所有的結點都連續集中在最左邊,這就是完全二叉樹。

注:完全二叉樹是效率很高的資料結構,堆是一種完全二叉樹或者近似完全二叉樹,所以效率極高,像十分常用的排序演算法、Dijkstra演算法、Prim演算法等都要用堆才能優化,二叉排序樹的效率也要藉助平衡性來提高,而平衡性基於完全二叉樹。

141749056837546

二叉樹的性質

1) 在非空二叉樹中,第i層的結點總數不超過2i-1, i>=1;

2) 深度為h的二叉樹最多有2h-1個結點(h>=1),最少有h個結點;

3) 對於任意一棵二叉樹,如果其葉結點數為N0,而度數為2的結點總數為N2,則N0=N2+1;

4) 具有n個結點的完全二叉樹的深度為log2(n+1);

5)有N個結點的完全二叉樹各結點如果用順序方式儲存,則結點之間有如下關係:

若I為結點編號則 如果I>1,則其父結點的編號為I/2;

如果2I<=N,則其左兒子(即左子樹的根結點)的編號為2I;若2I>N,則無左兒子;

如果2I+1<=N,則其右兒子的結點編號為2I+1;若2I+1>N,則無右兒子。

6)給定N個節點,能構成h(N)種不同的二叉樹,其中h(N)為卡特蘭數的第N項,h(n)=C(2*n, n)/(n+1)。

7)設有i個枝點,I為所有枝點的道路長度總和,J為葉的道路長度總和J=I+2i。

2. 二叉查詢樹

二叉查詢樹定義:又稱為是二叉排序樹(Binary Sort Tree)或二叉搜尋樹。二叉排序樹或者是一棵空樹,或者是具有下列性質的二叉樹:

1) 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;

2) 若右子樹不空,則右子樹上所有結點的值均大於或等於它的根結點的值;

3) 左、右子樹也分別為二叉排序樹;

4) 沒有鍵值相等的節點。

二叉查詢樹的性質:對二叉查詢樹進行中序遍歷,即可得到有序的數列。

二叉查詢樹的時間複雜度:它和二分查詢一樣,插入和查詢的時間複雜度均為O(logn),但是在最壞的情況下仍然會有O(n)的時間複雜度。原因在於插入和刪除元素的時候,樹沒有保持平衡(比如,我們查詢上圖(b)中的“93”,我們需要進行n次查詢操作)。我們追求的是在最壞的情況下仍然有較好的時間複雜度,這就是平衡查詢樹設計的初衷。

二叉查詢樹的高度決定了二叉查詢樹的查詢效率。

二叉查詢樹的插入過程如下:

1) 若當前的二叉查詢樹為空,則插入的元素為根節點;

2) 若插入的元素值小於根節點值,則將元素插入到左子樹中;

3) 若插入的元素值不小於根節點值,則將元素插入到右子樹中。

二叉查詢樹的刪除,分三種情況進行處理:

1) p為葉子節點,直接刪除該節點,再修改其父節點的指標(注意分是根節點和不是根節點),如圖a;

2) p為單支節點(即只有左子樹或右子樹)。讓p的子樹與p的父親節點相連,刪除p即可(注意分是根節點和不是根節點),如圖b;

3) p的左子樹和右子樹均不空。找到p的後繼y,因為y一定沒有左子樹,所以可以刪除y,並讓y的父親節點成為y的右子樹的父親節點,並用y的值代替p的值;或者方法二是找到p的前驅x,x一定沒有右子樹,所以可以刪除x,並讓x的父親節點成為y的左子樹的父親節點。如圖c。

2012032717571645

20120327175833582012032717584562

二叉樹相關實現原始碼:

插入操作:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

struct node

{

    int val;

    pnode lchild;

    pnode rchild;

};

 

pnode BT = NULL;

 

 

//遞迴方法插入節點

pnode insert(pnode root, int x)

{

    pnode p = (pnode)malloc(LEN);

    p->val = x;

    p->lchild = NULL;

    p->rchild = NULL;

    if(root == NULL){

        root = p;    

    }    

    else if(x < root->val){

        root->lchild = insert(root->lchild, x);    

    }

    else{

        root->rchild = insert(root->rchild, x);    

    }

    return root;

}

 

//非遞迴方法插入節點

void insert_BST(pnode q, int x)

{

    pnode p = (pnode)malloc(LEN);

    p->val = x;

    p->lchild = NULL;

    p->rchild = NULL;

    if(q == NULL){

        BT = p;

        return ;    

    }        

    while(q->lchild != p && q->rchild != p){

        if(x < q->val){

            if(q->lchild){

                q = q->lchild;    

            }    

            else{

                q->lchild = p;

            }        

        }    

        else{

            if(q->rchild){

                q = q->rchild;    

            }    

            else{

                q->rchild = p;    

            }

        }

    }

    return;

}

刪除操作:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

bool delete_BST(pnode p, int x) //返回一個標誌,表示是否找到被刪元素

{

    bool find = false;

    pnode q;

    p = BT;

    while(p && !find){  //尋找被刪元素

        if(x == p->val){  //找到被刪元素

            find = true;    

        }    

        else if(x < p->val){ //沿左子樹找

            q = p;

            p = p->lchild;    

        }

        else{   //沿右子樹找

            q = p;

            p = p->rchild;    

        }

    }

    if(p == NULL){   //沒找到

        cout << "沒有找到" << x << endl;    

    }

    

    if(p->lchild == NULL && p->rchild == NULL){  //p為葉子節點

        if(p == BT){  //p為根節點

            BT = NULL;    

        }

        else if(q->lchild == p){  

            q->lchild = NULL;

        }        

        else{

            q->rchild = NULL;    

        }

        free(p);  //釋放節點p

    }

    else if(p->lchild == NULL || p->rchild == NULL){ //p為單支子樹

        if(p == BT){  //p為根節點

            if(p->lchild == NULL){

                BT = p->rchild;    

            }    

            else{

                BT = p->lchild;    

            }

        }    

        else{

            if(q->lchild == p && p->lchild){ //p是q的左子樹且p有左子樹

                q->lchild = p->lchild;    //將p的左子樹連結到q的左指標上

            }    

            else if(q->lchild == p && p->rchild){

                q->lchild = p->rchild;    

            }

            else if(q->rchild == p && p->lchild){

                q->rchild = p->lchild;    

            }

            else{

                q->rchild = p->rchild;

            }

        }

        free(p);

    }

    else{ //p的左右子樹均不為空

        pnode t = p;

        pnode s = p->lchild;  //從p的左子節點開始

        while(s->rchild){  //找到p的前驅,即p左子樹中值最大的節點

            t = s;  

            s = s->rchild;    

        }

        p->val = s->val;   //把節點s的值賦給p

        if(t == p){

            p->lchild = s->lchild;    

        }    

        else{

            t->rchild = s->lchild;    

        }

        free(s);

    }

    return find;

}

查詢操作:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

pnode search_BST(pnode p, int x)

{

    bool solve = false;

    while(p && !solve){

        if(x == p->val){

            solve = true;    

        }    

        else if(x < p->val){

            p = p->lchild;    

        }

        else{

            p = p->rchild;    

        }

    }

    if(p == NULL){

        cout << "沒有找到" << x << endl;    

    }

    return p;

}

 

3. 平衡二叉樹

我們知道,對於一般的二叉搜尋樹(Binary Search Tree),其期望高度(即為一棵平衡樹時)為log2n,其各操作的時間複雜度O(log2n)同時也由此而決定。但是,在某些極端的情況下(如在插入的序列是有序的時),二叉搜尋樹將退化成近似鏈或鏈,此時,其操作的時間複雜度將退化成線性的,即O(n)。我們可以通過隨機化建立二叉搜尋樹來儘量的避免這種情況,但是在進行了多次的操作之後,由於在刪除時,我們總是選擇將待刪除節點的後繼代替它本身,這樣就會造成總是右邊的節點數目減少,以至於樹向左偏沉。這同時也會造成樹的平衡性受到破壞,提高它的操作的時間複雜度。於是就有了我們下邊介紹的平衡二叉樹。

平衡二叉樹定義:平衡二叉樹(Balanced Binary Tree)又被稱為AVL樹(有別於AVL演算法),且具有以下性質:它是一 棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。平衡二叉樹的常用演算法有紅黑樹、AVL樹等。在平衡二叉搜尋樹中,我們可以看到,其高度一般都良好地維持在O(log2n),大大降低了操作的時間複雜度。

最小二叉平衡樹的節點的公式如下:

F(n)=F(n-1)+F(n-2)+1

這個類似於一個遞迴的數列,可以參考Fibonacci數列,1是根節點,F(n-1)是左子樹的節點數量,F(n-2)是右子樹的節點數量。

3.1 平衡查詢樹之AVL樹

有關AVL樹的具體實現,可以參考C小加的部落格《一步一步寫平衡二叉樹(AVL)》

AVL樹定義:AVL樹是最先發明的自平衡二叉查詢樹。AVL樹得名於它的發明者 G.M. Adelson-Velsky 和 E.M. Landis,他們在 1962 年的論文 “An algorithm for the organization of information” 中發表了它。在AVL中任何節點的兩個兒子子樹的高度最大差別為1,所以它也被稱為高度平衡樹,n個結點的AVL樹最大深度約1.44log2n。查詢、插入和刪除在平均和最壞情況下都是O(logn)。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。這個方案很好的解決了二叉查詢樹退化成連結串列的問題,把插入,查詢,刪除的時間複雜度最好情況和最壞情況都維持在O(logN)。但是頻繁旋轉會使插入和刪除犧牲掉O(logN)左右的時間,不過相對二叉查詢樹來說,時間上穩定了很多。

AVL樹的自平衡操作——旋轉:

AVL樹最關鍵的也是最難的一步操作就是旋轉。旋轉主要是為了實現AVL樹在實施了插入和刪除操作以後,樹重新回到平衡的方法。下面我們重點研究一下AVL樹的旋轉。

對於一個平衡的節點,由於任意節點最多有兩個兒子,因此高度不平衡時,此節點的兩顆子樹的高度差2.容易看出,這種不平衡出現在下面四種情況:

2012082016021366

1) 6節點的左子樹3節點高度比右子樹7節點大2,左子樹3節點的左子樹1節點高度大於右子樹4節點,這種情況成為左左。

2) 6節點的左子樹2節點高度比右子樹7節點大2,左子樹2節點的左子樹1節點高度小於右子樹4節點,這種情況成為左右。

3) 2節點的左子樹1節點高度比右子樹5節點小2,右子樹5節點的左子樹3節點高度大於右子樹6節點,這種情況成為右左。

4) 2節點的左子樹1節點高度比右子樹4節點小2,右子樹4節點的左子樹3節點高度小於右子樹6節點,這種情況成為右右。

從圖2中可以可以看出,1和4兩種情況是對稱的,這兩種情況的旋轉演算法是一致的,只需要經過一次旋轉就可以達到目標,我們稱之為單旋轉。2和3兩種情況也是對稱的,這兩種情況的旋轉演算法也是一致的,需要進行兩次旋轉,我們稱之為雙旋轉。

單旋轉

單旋轉是針對於左左和右右這兩種情況的解決方案,這兩種情況是對稱的,只要解決了左左這種情況,右右就很好辦了。圖3是左左情況的解決方案,節點k2不滿足平衡特性,因為它的左子樹k1比右子樹Z深2層,而且k1子樹中,更深的一層的是k1的左子樹X子樹,所以屬於左左情況。

avltree35

為使樹恢復平衡,我們把k2變成這棵樹的根節點,因為k2大於k1,把k2置於k1的右子樹上,而原本在k1右子樹的Y大於k1,小於k2,就把Y置於k2的左子樹上,這樣既滿足了二叉查詢樹的性質,又滿足了平衡二叉樹的性質。

這樣的操作只需要一部分指標改變,結果我們得到另外一顆二叉查詢樹,它是一棵AVL樹,因為X向上一移動了一層,Y還停留在原來的層面上,Z向下移動了一層。整棵樹的新高度和之前沒有在左子樹上插入的高度相同,插入操作使得X高度長高了。因此,由於這顆子樹高度沒有變化,所以通往根節點的路徑就不需要繼續旋轉了。

雙旋轉

對於左右和右左這兩種情況,單旋轉不能使它達到一個平衡狀態,要經過兩次旋轉。雙旋轉是針對於這兩種情況的解決方案,同樣的,這樣兩種情況也是對稱的,只要解決了左右這種情況,右左就很好辦了。圖4是左右情況的解決方案,節點k3不滿足平衡特性,因為它的左子樹k1比右子樹Z深2層,而且k1子樹中,更深的一層的是k1的右子樹k2子樹,所以屬於左右情況。

2012082016534455

為使樹恢復平衡,我們需要進行兩步,第一步,把k1作為根,進行一次右右旋轉,旋轉之後就變成了左左情況,所以第二步再進行一次左左旋轉,最後得到了一棵以k2為根的平衡二叉樹。

AVL樹實現原始碼:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

//AVL樹節點資訊

template<class T>

class TreeNode

{

    public:

        TreeNode():lson(NULL),rson(NULL),freq(1),hgt(0){}

        T data;//值

        int hgt;//高度

        unsigned int freq;//頻率

        TreeNode* lson;//指向左兒子的地址

        TreeNode* rson;//指向右兒子的地址

};

//AVL樹類的屬性和方法宣告

template<class T>

class AVLTree

{

    private:

        TreeNode<T>* root;//根節點

        void insertpri(TreeNode<T>* &node,T x);//插入

        TreeNode<T>* findpri(TreeNode<T>* node,T x);//查詢

        void insubtree(TreeNode<T>* node);//中序遍歷

        void Deletepri(TreeNode<T>* &node,T x);//刪除

        int height(TreeNode<T>* node);//求樹的高度

        void SingRotateLeft(TreeNode<T>* &k2);//左左情況下的旋轉

        void SingRotateRight(TreeNode<T>* &k2);//右右情況下的旋轉

        void DoubleRotateLR(TreeNode<T>* &k3);//左右情況下的旋轉

        void DoubleRotateRL(TreeNode<T>* &k3);//右左情況下的旋轉

        int Max(int cmpa,int cmpb);//求最大值

 

    public:

        AVLTree():root(NULL){}

        void insert(T x);//插入介面

        TreeNode<T>* find(T x);//查詢介面

        void Delete(T x);//刪除介面

        void traversal();//遍歷介面

 

};

//計算節點的高度

template<class T>

int AVLTree<T>::height(TreeNode<T>* node)

{

    if(node!=NULL)

        return node->hgt;

    return -1;

}

//求最大值

template<class T>

int AVLTree<T>::Max(int cmpa,int cmpb)

{

    return cmpa>cmpb?cmpa:cmpb;

}

//左左情況下的旋轉

template<class T>

void AVLTree<T>::SingRotateLeft(TreeNode<T>* &k2)

{

    TreeNode<T>* k1;

    k1=k2->lson;

    k2->lson=k1->rson;

    k1->rson=k2;

 

    k2->hgt=Max(height(k2->lson),height(k2->rson))+1;

    k1->hgt=Max(height(k1->lson),k2->hgt)+1;

}

//右右情況下的旋轉

template<class T>

void AVLTree<T>::SingRotateRight(TreeNode<T>* &k2)

{

    TreeNode<T>* k1;

    k1=k2->rson;

    k2->rson=k1->lson;

    k1->lson=k2;

 

    k2->hgt=Max(height(k2->lson),height(k2->rson))+1;

    k1->hgt=Max(height(k1->rson),k2->hgt)+1;

}

//左右情況的旋轉

template<class T>

void AVLTree<T>::DoubleRotateLR(TreeNode<T>* &k3)

{

    SingRotateRight(k3->lson);

    SingRotateLeft(k3);

}

//右左情況的旋轉

template<class T>

void AVLTree<T>::DoubleRotateRL(TreeNode<T>* &k3)

{

    SingRotateLeft(k3->rson);

    SingRotateRight(k3);

}

//插入

template<class T>

void AVLTree<T>::insertpri(TreeNode<T>* &node,T x)

{

    if(node==NULL)//如果節點為空,就在此節點處加入x資訊

    {

        node=new TreeNode<T>();

        node->data=x;

        return;

    }

    if(node->data>x)//如果x小於節點的值,就繼續在節點的左子樹中插入x

    {

        insertpri(node->lson,x);

        if(2==height(node->lson)-height(node->rson))

            if(x<node->lson->data)

                SingRotateLeft(node);

            else

                DoubleRotateLR(node);

    }

    else if(node->data<x)//如果x大於節點的值,就繼續在節點的右子樹中插入x

    {

        insertpri(node->rson,x);

        if(2==height(node->rson)-height(node->lson))//如果高度之差為2的話就失去了平衡,需要旋轉

            if(x>node->rson->data)

                SingRotateRight(node);

            else

                DoubleRotateRL(node);

    }

    else ++(node->freq);//如果相等,就把頻率加1

    node->hgt=Max(height(node->lson),height(node->rson));

}

//插入介面

template<class T>

void AVLTree<T>::insert(T x)

{

    insertpri(root,x);

}

//查詢

template<class T>

TreeNode<T>* AVLTree<T>::findpri(TreeNode<T>* node,T x)

{

    if(node==NULL)//如果節點為空說明沒找到,返回NULL

    {

        return NULL;

    }

    if(node->data>x)//如果x小於節點的值,就繼續在節點的左子樹中查詢x

    {

        return findpri(node->lson,x);

    }

    else if(node->data<x)//如果x大於節點的值,就繼續在節點的左子樹中查詢x

    {

        return findpri(node->rson,x);

    }

    else return node;//如果相等,就找到了此節點

}

//查詢介面

template<class T>

TreeNode<T>* AVLTree<T>::find(T x)

{

    return findpri(root,x);

}

//刪除

template<class T>

void AVLTree<T>::Deletepri(TreeNode<T>* &node,T x)

{

    if(node==NULL) return ;//沒有找到值是x的節點

    if(x < node->data)

    {

         Deletepri(node->lson,x);//如果x小於節點的值,就繼續在節點的左子樹中刪除x

         if(2==height(node->rson)-height(node->lson))

            if(node->rson->lson!=NULL&&(height(node->rson->lson)>height(node->rson->rson)) )

                DoubleRotateRL(node);

            else

                SingRotateRight(node);

    }

 

    else if(x > node->data)

    {

         Deletepri(node->rson,x);//如果x大於節點的值,就繼續在節點的右子樹中刪除x

         if(2==height(node->lson)-height(node->rson))

            if(node->lson->rson!=NULL&& (height(node->lson->rson)>height(node->lson->lson) ))

                DoubleRotateLR(node);

            else

                SingRotateLeft(node);

    }

 

    else//如果相等,此節點就是要刪除的節點

    {

        if(node->lson&&node->rson)//此節點有兩個兒子

        {

            TreeNode<T>* temp=node->rson;//temp指向節點的右兒子

            while(temp->lson!=NULL) temp=temp->lson;//找到右子樹中值最小的節點

            //把右子樹中最小節點的值賦值給本節點

            node->data=temp->data;

            node->freq=temp->freq;

            Deletepri(node->rson,temp->data);//刪除右子樹中最小值的節點

            if(2==height(node->lson)-height(node->rson))

            {

                if(node->lson->rson!=NULL&& (height(node->lson->rson)>height(node->lson->lson) ))

                    DoubleRotateLR(node);

                else

                    SingRotateLeft(node);

            }

        }

        else//此節點有1個或0個兒子

        {

            TreeNode<T>* temp=node;

            if(node->lson==NULL)//有右兒子或者沒有兒子

            node=node->rson;

            else if(node->rson==NULL)//有左兒子

            node=node->lson;

            delete(temp);

            temp=NULL;

        }

    }

    if(node==NULL) return;

    node->hgt=Max(height(node->lson),height(node->rson))+1;

    return;

}

//刪除介面

template<class T>

void AVLTree<T>::Delete(T x)

{

    Deletepri(root,x);

}

//中序遍歷函式

template<class T>

void AVLTree<T>::insubtree(TreeNode<T>* node)

{

    if(node==NULL) return;

    insubtree(node->lson);//先遍歷左子樹

    cout<<node->data<<" ";//輸出根節點

    insubtree(node->rson);//再遍歷右子樹

}

//中序遍歷介面

template<class T>

void AVLTree<T>::traversal()

{

    insubtree(root);

}

 

3.2 平衡二叉樹之紅黑樹

紅黑樹的定義:紅黑樹是一種自平衡二叉查詢樹,是在電腦科學中用到的一種資料結構,典型的用途是實現關聯陣列。它是在1972年由魯道夫·貝爾發明的,稱之為”對稱二叉B樹”,它現代的名字是在 Leo J. Guibas 和 Robert Sedgewick 於1978年寫的一篇論文中獲得的。它是複雜的,但它的操作有著良好的最壞情況執行時間,並且在實踐中是高效的: 它可以在O(logn)時間內做查詢,插入和刪除,這裡的n是樹中元素的數目。

紅黑樹和AVL樹一樣都對插入時間、刪除時間和查詢時間提供了最好可能的最壞情況擔保。這不只是使它們在時間敏感的應用如實時應用(real time application)中有價值,而且使它們有在提供最壞情況擔保的其他資料結構中作為建造板塊的價值;例如,在計算幾何中使用的很多資料結構都可以基於紅黑樹。此外,紅黑樹還是2-3-4樹的一種等同,它們的思想是一樣的,只不過紅黑樹是2-3-4樹用二叉樹的形式表示的。

紅黑樹的性質:

紅黑樹是每個節點都帶有顏色屬性的二叉查詢樹,顏色為紅色或黑色。在二叉查詢樹強制的一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求:

性質1. 節點是紅色或黑色。

性質2. 根是黑色。

性質3. 所有葉子都是黑色(葉子是NIL節點)。

性質4. 每個紅色節點必須有兩個黑色的子節點。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點。)

性質5. 從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。

下面是一個具體的紅黑樹的圖例:

450px-Red-black_tree_example.svg

這些約束確保了紅黑樹的關鍵特性: 從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。結果是這個樹大致上是平衡的。因為操作比如插入、刪除和查詢某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同於普通的二叉查詢樹。

要知道為什麼這些性質確保了這個結果,注意到性質4導致了路徑不能有兩個毗連的紅色節點就足夠了。最短的可能路徑都是黑色節點,最長的可能路徑有交替的紅色和黑色節點。因為根據性質5所有最長的路徑都有相同數目的黑色節點,這就表明了沒有路徑能多於任何其他路徑的兩倍長。

以下內容整理自wiki百科之紅黑樹

紅黑樹的自平衡操作:

因為每一個紅黑樹也是一個特化的二叉查詢樹,因此紅黑樹上的只讀操作與普通二叉查詢樹上的只讀操作相同。然而,在紅黑樹上進行插入操作和刪除操作會導致不再符合紅黑樹的性質。恢復紅黑樹的性質需要少量(O(logn))的顏色變更(實際是非常快速的)和不超過三次樹旋轉(對於插入操作是兩次)。雖然插入和刪除很複雜,但操作時間仍可以保持為O(logn) 次。

我們首先以二叉查詢樹的方法增加節點並標記它為紅色。如果設為黑色,就會導致根到葉子的路徑上有一條路上,多一個額外的黑節點,這個是很難調整的(違背性質5)。但是設為紅色節點後,可能會導致出現兩個連續紅色節點的衝突,那麼可以通過顏色調換(color flips)和樹旋轉來調整。下面要進行什麼操作取決於其他臨近節點的顏色。同人類的家族樹中一樣,我們將使用術語叔父節點來指一個節點的父節點的兄弟節點。注意:

  • 性質1和性質3總是保持著。
  • 性質4只在增加紅色節點、重繪黑色節點為紅色,或做旋轉時受到威脅。
  • 性質5只在增加黑色節點、重繪紅色節點為黑色,或做旋轉時受到威脅。

插入操作:

假設,將要插入的節點標為N,N的父節點標為P,N的祖父節點標為G,N的叔父節點標為U。在圖中展示的任何顏色要麼是由它所處情形這些所作的假定,要麼是假定所暗含的。

情形1: 該樹為空樹,直接插入根結點的位置,違反性質1,把節點顏色有紅改為黑即可。

情形2: 插入節點N的父節點P為黑色,不違反任何性質,無需做任何修改。在這種情形下,樹仍是有效的。性質5也未受到威脅,儘管新節點N有兩個黑色葉子子節點;但由於新節點N是紅色,通過它的每個子節點的路徑就都有同通過它所取代的黑色的葉子的路徑同樣數目的黑色節點,所以依然滿足這個性質。

注: 情形1很簡單,情形2中P為黑色,一切安然無事,但P為紅就不一樣了,下邊是P為紅的各種情況,也是真正難懂的地方。

情形3: 如果父節點P和叔父節點U二者都是紅色,(此時新插入節點N做為P的左子節點或右子節點都屬於情形3,這裡右圖僅顯示N做為P左子的情形)則我們可以將它們兩個重繪為黑色並重繪祖父節點G為紅色(用來保持性質4)。現在我們的新節點N有了一個黑色的父節點P。因為通過父節點P或叔父節點U的任何路徑都必定通過祖父節點G,在這些路徑上的黑節點數目沒有改變。但是,紅色的祖父節點G的父節點也有可能是紅色的,這就違反了性質4。為了解決這個問題,我們在祖父節點G上遞迴地進行上述情形的整個過程(把G當成是新加入的節點進行各種情形的檢查)。比如,G為根節點,那我們就直接將G變為黑色(情形1);如果G不是根節點,而它的父節點為黑色,那符合所有的性質,直接插入即可(情形2);如果G不是根節點,而它的父節點為紅色,則遞迴上述過程(情形3)。

2011120116425251

情形4: 父節點P是紅色而叔父節點U是黑色或缺少,新節點N是其父節點的左子節點,而父節點P又是其父節點G的左子節點。在這種情形下,我們進行鍼對祖父節點G的一次右旋轉; 在旋轉產生的樹中,以前的父節點P現在是新節點N和以前的祖父節點G的父節點。我們知道以前的祖父節點G是黑色,否則父節點P就不可能是紅色(如果P和G都是紅色就違反了性質4,所以G必須是黑色)。我們切換以前的父節點P和祖父節點G的顏色,結果的樹滿足性質4。性質5也仍然保持滿足,因為通過這三個節點中任何一個的所有路徑以前都通過祖父節點G,現在它們都通過以前的父節點P。在各自的情形下,這都是三個節點中唯一的黑色節點。

Red-black_tree_insert_case_5

情形5: 父節點P是紅色而叔父節點U是黑色或缺少,並且新節點N是其父節點P的右子節點而父節點P又是其父節點的左子節點。在這種情形下,我們進行一次左旋轉調換新節點和其父節點的角色; 接著,我們按情形4處理以前的父節點P以解決仍然失效的性質4。注意這個改變會導致某些路徑通過它們以前不通過的新節點N(比如圖中1號葉子節點)或不通過節點P(比如圖中3號葉子節點),但由於這兩個節點都是紅色的,所以性質5仍有效。

Red-black_tree_insert_case_5

注: 插入實際上是原地演算法,因為上述所有呼叫都使用了尾部遞迴。

刪除操作:

如果需要刪除的節點有兩個兒子,那麼問題可以被轉化成刪除另一個只有一個兒子的節點的問題。對於二叉查詢樹,在刪除帶有兩個非葉子兒子的節點的時候,我們找到要麼在它的左子樹中的最大元素、要麼在它的右子樹中的最小元素,並把它的值轉移到要刪除的節點中。我們接著刪除我們從中複製出值的那個節點,它必定有少於兩個非葉子的兒子。因為只是複製了一個值,不違反任何性質,這就把問題簡化為如何刪除最多有一個兒子的節點的問題。它不關心這個節點是最初要刪除的節點還是我們從中複製出值的那個節點。

我們只需要討論刪除只有一個兒子的節點(如果它兩個兒子都為空,即均為葉子,我們任意將其中一個看作它的兒子)。如果我們刪除一個紅色節點(此時該節點的兒子將都為葉子節點),它的父親和兒子一定是黑色的。所以我們可以簡單的用它的黑色兒子替換它,並不會破壞性質3和性質4。通過被刪除節點的所有路徑只是少了一個紅色節點,這樣可以繼續保證性質5。另一種簡單情況是在被刪除節點是黑色而它的兒子是紅色的時候。如果只是去除這個黑色節點,用它的紅色兒子頂替上來的話,會破壞性質5,但是如果我們重繪它的兒子為黑色,則曾經通過它的所有路徑將通過它的黑色兒子,這樣可以繼續保持性質5。

需要進一步討論的是在要刪除的節點和它的兒子二者都是黑色的時候,這是一種複雜的情況。我們首先把要刪除的節點替換為它的兒子。出於方便,稱呼這個兒子為N(在新的位置上),稱呼它的兄弟(它父親的另一個兒子)為S。在下面的示意圖中,我們還是使用P稱呼N的父親,SL稱呼S的左兒子,SR稱呼S的右兒子。

如果N和它初始的父親是黑色,則刪除它的父親導致通過N的路徑都比不通過它的路徑少了一個黑色節點。因為這違反了性質5,樹需要被重新平衡。有幾種情形需要考慮:

情形1: N是新的根。在這種情形下,我們就做完了。我們從所有路徑去除了一個黑色節點,而新根是黑色的,所以性質都保持著。

注意: 在情形2、5和6下,我們假定N是它父親的左兒子。如果它是右兒子,則在這些情形下的左和右應當對調。

情形2: S是紅色。在這種情形下我們在N的父親上做左旋轉,把紅色兄弟轉換成N的祖父,我們接著對調N的父親和祖父的顏色。完成這兩個操作後,儘管所有路徑上黑色節點的數目沒有改變,但現在N有了一個黑色的兄弟和一個紅色的父親(它的新兄弟是黑色因為它是紅色S的一個兒子),所以我們可以接下去按情形4情形5情形6來處理。

Red-black_tree_insert_case_5

情形3: N的父親、S和S的兒子都是黑色的。在這種情形下,我們簡單的重繪S為紅色。結果是通過S的所有路徑,它們就是以前通過N的那些路徑,都少了一個黑色節點。因為刪除N的初始的父親使通過N的所有路徑少了一個黑色節點,這使事情都平衡了起來。但是,通過P的所有路徑現在比不通過P的路徑少了一個黑色節點,所以仍然違反性質5。要修正這個問題,我們要從情形1開始,在P上做重新平衡處理。

Red-black_tree_insert_case_5

情形4: S和S的兒子都是黑色,但是N的父親是紅色。在這種情形下,我們簡單的交換N的兄弟和父親的顏色。這不影響不通過N的路徑的黑色節點的數目,但是它在通過N的路徑上對黑色節點數目增加了一,添補了在這些路徑上刪除的黑色節點。

Red-black_tree_insert_case_5

情形5: S是黑色,S的左兒子是紅色,S的右兒子是黑色,而N是它父親的左兒子。在這種情形下我們在S上做右旋轉,這樣S的左兒子成為S的父親和N的新兄弟。我們接著交換S和它的新父親的顏色。所有路徑仍有同樣數目的黑色節點,但是現在N有了一個黑色兄弟,他的右兒子是紅色的,所以我們進入了情形6。N和它的父親都不受這個變換的影響。

Red-black_tree_insert_case_5

 

情形6: S是黑色,S的右兒子是紅色,而N是它父親的左兒子。在這種情形下我們在N的父親上做左旋轉,這樣S成為N的父親(P)和S的右兒子的父親。我們接著交換N的父親和S的顏色,並使S的右兒子為黑色。子樹在它的根上的仍是同樣的顏色,所以性質3沒有被違反。但是,N現在增加了一個黑色祖先: 要麼N的父親變成黑色,要麼它是黑色而S被增加為一個黑色祖父。所以,通過N的路徑都增加了一個黑色節點。

此時,如果一個路徑不通過N,則有兩種可能性:

  • 它通過N的新兄弟。那麼它以前和現在都必定通過S和N的父親,而它們只是交換了顏色。所以路徑保持了同樣數目的黑色節點。
  • 它通過N的新叔父,S的右兒子。那麼它以前通過S、S的父親和S的右兒子,但是現在只通過S,它被假定為它以前的父親的顏色,和S的右兒子,它被從紅色改變為黑色。合成效果是這個路徑通過了同樣數目的黑色節點。

在任何情況下,在這些路徑上的黑色節點數目都沒有改變。所以我們恢復了性質4。在示意圖中的白色節點可以是紅色或黑色,但是在變換前後都必須指定相同的顏色。

Red-black_tree_insert_case_5

紅黑樹實現原始碼:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

#define BLACK 1

#define RED 0

 

using namespace std;

 

class bst {

private:

 

<