在上一篇非“遞歸學習樹結構(四)--BST(二叉排序)樹”中我們介紹了怎麼樣創建一顆二叉排序樹,以及如何刪除二叉排序樹中的一個結點。但是考慮到BST的效率,着實讓人糾結,回想一下上一篇中的最後一張圖,是不是有一種DT的感覺,BST直接退化成了一個鏈表(但是與鏈表還是有本質上的不同),查找的時間複雜度回到瞭解放前(O(n))。這種結果我們是不願意看到的,我們就要分析,是什麼原因導致了這種結果?是因爲樹嚴重失衡,所以,我們在建樹和刪除樹中節點的過程中要時刻保證樹是平衡的,所謂的平衡,就是:“對於樹中的任一節點N,其左子節點L與其右子節點R的高度差的絕對值小於等於1”。可見,對於AVL樹的結點,需要添加一個height值。
頭文件的定義如下:
/*AVL.h*/
#ifndef __AVL__H_
#define __AVL__H_
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int RESULT;
#ifndef SUCCESS
#define SUCCESS 0
#endif
#ifndef FAILURE
#define FAILURE -1
#endif
typedef int ValueType;
typedef struct _Node
{
struct _Node* plc; /*左字結點指針*/
struct _Node* prc; /*右子結點指針*/
int height; /*當前結點的高度*/
int key; /*key字段*/
int cnt; /*計數*/
ValueType val; /*val字段*/
}Node;
/*建樹 插入節點*/
RESULT insert(Node** root, int key, int val);
/*刪除指定key的元素*/
RESULT delete(Node** root, const int key);
/* 堆棧所存儲的值的數據類型 */
#define ELEM_TYPE Node
#endif//__AVL__H_
AVL樹是通過什麼來調整數的平衡(調平)的呢?通過節點的旋轉,何謂旋轉?如下圖所示
上(A)圖是一個最簡單的旋轉,10本來是根節點,其左子節點的高度值是-1,右子節點的高度值是1,兩者差的絕對值是2,因此需要調平,變成了(B)。
下面貼上兩張動態圖片:
相信的旋轉情況,請參見http://blog.csdn.net/collonn/article/details/20128205文章中的圖示,情況考慮的很周全,有八種情況,但是我們實際編碼要考慮的就四種情況,那就是:左旋轉(L),右旋轉(R),先左後右旋轉(LR),先右後左旋轉(RL)。
旋轉看上去很複雜,好像整棵樹都變化很大,實際上變化的只是失衡結點相關兩的幾個結點之間的重新拼接的過程,其實,旋轉操作並不困難,困難的是
1.如何判斷樹是否失衡,(當前結點的左右子樹高度差是否達到2)
2.如何找到失衡點,(將插入判斷過程中入棧的結點一一出棧,如果當前出棧的結點的左右子樹高度差等於2,則失衡)
3.如何判斷失衡點應該進行什麼樣的旋轉操作(根據失衡點左右子節點高度值的關係來判斷。)
具體的還是來看代碼吧,插入和刪除都有很詳細的註釋說明
/*BST.c*/
#include"AVL.h"
#include "stack.h"
/*創建節點*/
Node* createnode(int key,int val)
{
Node* node = (Node*)malloc(sizeof(Node));
node->height = 0;
node->key = key;
node->val = val;
node->cnt = 1;
node->plc = NULL;
node->prc = NULL;
return node;
}
/*返回當前結點的height*/
static int Getheight(Node* p)
{
return (p == NULL) ? -1 : p->height;
}
/*返回高度值較大的值*/
static int Max(int h1, int h2)
{
return h1 > h2 ? h1 : h2;
}
/*左旋轉*/
Node* Left_Rotate(Node* p1)
{
Node* p2 = p1->plc;
p1->plc = p2->prc;
p2->prc = p1;
p1->height = Max(Getheight(p1->plc),Getheight(p1->prc)) + 1;
p2->height = Max(Getheight(p2->plc),Getheight(p2->prc)) + 1;
return p2;
}
/*右旋轉*/
Node* Right_Rotate(Node* p1)
{
Node* p2 = p1->prc;
p1->prc = p2->plc;
p2->plc = p1;
p1->height = Max(Getheight(p1->plc),Getheight(p1->prc)) + 1;
p2->height = Max(Getheight(p2->plc),Getheight(p2->prc)) + 1;
return p2;
}
/*先左,再右旋轉*/
Node* LR_Rotate(Node* p)
{
p->plc = Right_Rotate(p->plc);
return Left_Rotate(p);
}
/*先右,再左旋轉*/
Node* RL_Rotate(Node* p)
{
p->prc = Left_Rotate(p->prc);
return Right_Rotate(p);
}
/*結點調整函數,從樹中刪除節點時使用此函數主要是判斷當前結點是否失衡,
屬於哪種失衡,應該用哪種旋轉解決*/
Node* Rotate( Node* node )
{
if (Getheight(node->plc) - Getheight(node->prc) == 2) {
if (Getheight(node->plc->plc) >= Getheight(node->plc->prc)) {
node = Left_Rotate(node);
}
else {
node = LR_Rotate(node);
}
}
if (Getheight(node->prc) - Getheight(node->plc) == 2) {
if (Getheight(node->prc->prc) >= Getheight(node->prc->plc)) {
node = Right_Rotate(node);
}
else {
node = RL_Rotate(node);
}
}
return node;
}
RESULT _insert_(Node* tmp, Node* node, Stack* stack)
{
/*插入過程的所“走過”的結點入棧:*/
while (tmp != NULL) {
(*stack).push(stack, tmp);
if (node->val < tmp->val) {
if (tmp->plc == NULL) {
tmp->plc = node;
break;
}
else {
tmp = tmp->plc;
}
}
else if (node->val > tmp->val) {
if (tmp->prc == NULL) {
tmp->prc = node;
break;
}
else {
tmp = tmp->prc;
}
}
else {
tmp->cnt++;
return SUCCESS;
}
}
return SUCCESS;
}
/*插入節點*/
RESULT insert(Node** root, int key, ValueType val)
{
Node* node = createnode(key,val);
if (NULL == node)
{
perror("malloc failure!!!\n");
return FAILURE;
}
if (*root == NULL) {
*root = node;
return SUCCESS;
}
Stack stack;
registstack(&stack);
stack.init_size(&stack,1000);
Node* tmp = *root;
/*插入節點過程*/
if (SUCCESS != _insert_(tmp,node,&stack))
{
return FAILURE;
}
/*這裏爲什麼要提前彈出一次操作呢?因爲最後入棧的是插入節點的直接父節點,
如果插入節點後失衡,說明在插入之前就已經失衡了,這種情況是不存在的,這裏要清楚再某個結點失衡這一概念
因爲樹一直在自動調整平衡,在插入操作開始之時,樹肯定是個平衡樹,
如果插入節點後引起失衡,那麼肯定是在插入節點的父節點的父節點開始失衡的,
所以,插入節點的父節點就不需要進行判斷是否失衡了,從插入節點的爺爺節點開始判斷就行了*/
tmp = stack.top(&stack);/*保存插入節點的直接父節點*/
stack.pop(&stack);
int ori_height = tmp->height;/*保存插入節點的父節點的原始高度值*/
/*父節點的高度值是否有必要改變,如果一開始父節點有個子節點,
則高度值不變,如果父節點原來也是個葉子節點,此時高度值變爲1*/
tmp->height = Max(Getheight(tmp->plc), Getheight(tmp->prc)) + 1;
/*如果插入節點的原始高度不是0,說明插入節點的父節點的高度值不會改變,
也不會一起其他節點高度值改變,所以不可能造成失衡,直接返回原來的根節點即可
但是如果插入節點的高度值是0,說明插入節點的父節點原來是個葉子節點,
插入新節點之後成爲父節點,其高度值改變,其他節點的高度也也會跟着改變,有可能造成失衡*/
if (ori_height == 0)/*如果有可能造成失衡*/
{
/*出棧過程,判斷插入了該節點以後,是否引起失衡*/
while (!stack.is_empty(&stack)) {
Node* n = stack.top(&stack);
if (Getheight(n->plc) - Getheight(n->prc) == 2) /*判斷當前結點的左子樹高度是否比右子樹高度高出2*/
{
/*在判斷插入的元素是插入到了左子樹還是右子樹,如果是插入左子樹就是LL(左左)旋轉*/
if (val < n->plc->val) {
/*左旋*/
n = Left_Rotate(n);
}
else { /*否則就是LR(先左後右旋轉)*/
/*先左後右旋*/
n = LR_Rotate(n);
}
}
else if (Getheight(n->prc) - Getheight(n->plc) == 2)/*判斷當前結點的右子樹高度比左子樹高度高2*/
{
/*判斷是不是RR(右右)旋轉*/
if (val > n->prc->val) {
/*右旋*/
n = Right_Rotate(n);
}
else {/*判斷是不是RL(先右後左旋轉)*/
/*先右後左旋*/
n = RL_Rotate(n);
}
}
n->height = Max(Getheight(n->plc), Getheight(n->prc)) + 1;
tmp = n;
stack.pop(&stack);
}
/*tmp最後保存的是根節點,根節點可能不變,如果在根節點出現旋轉,就有可能會改變根節點,
因此將tmp最後保存(必然是根節點,不管是舊的還是新生成的)的結點當做根節點賦值給root*/
*root = tmp;
}
return SUCCESS;
}
/*處理要刪除節點的右子樹*/
void handlerightbranch( Node*tmp )
{
Stack stack; /*用來保存尋找要刪除節點過車中的結點*/
registstack(&stack);
stack.init_size(&stack, 1000);
Node* r = tmp->prc;
while (r->plc != NULL) {
stack.push(&stack, r);
r = r->plc;
}
tmp->val = r->val;/*右子樹的最左子節點的val替換掉要刪除節點的val*/
tmp->key = r->key;
if (stack.is_empty(&stack))/*如果棧爲空,說明待刪除節點右子樹的最左節點就是待刪除結點的右子節點,
此時只需要把待刪除節點右子節點指向原右子節點的右子節點*/ {
tmp->prc = r->prc;
free(r);
r = NULL;
}
else {
Node* sn = stack.top(&stack);
sn->plc = r->prc;
free(r);
r = NULL;
stack.pop(&stack);
Node* n = sn;/*用作判斷棧頂結點是其下一個結點(棧頂結點的父節點)的左子節點還是右子節點*/
Node* newnode = Rotate(sn);/*獲取原來以sn爲根節點的子樹調平後的新的根節點(也有可能不需要進行調平)*/
/*對入棧的每一個一點依次進行調平處理,最後newnode保存的是一個新的平衡的右子樹*/
while (!stack.is_empty(&stack)) {
Node* t = stack.top(&stack);
if (t->plc == n) {
t->plc = newnode;
}
else {
t->prc = newnode;
}
n = t;
newnode = Rotate(t);
stack.pop(&stack);
}
tmp->prc = newnode;
}
}
/*在AVL樹中,如果一個結點的左子節點是NULL,
那麼它的右子節點必然是個葉子節點,
否則就違背了AVL樹的平衡*/
/*刪除指定key的元素*/
RESULT delete(Node** root,const int key)
{
if (*root == NULL)
return SUCCESS;
Stack stack; /*用來保存尋找要刪除節點過車中的結點*/
registstack(&stack);
stack.init_size(&stack, 1000);
stack.push(&stack,*root);
Node* tmp = *root;
while (tmp != NULL) {
if (key < tmp->key) {
tmp = tmp->plc;
stack.push(&stack, tmp);
}
else if (key > tmp->key) {
tmp = tmp->prc;
stack.push(&stack, tmp);
}
else {
break;
}
}
stack.pop(&stack);
/*如果要刪除的元素不在樹中*/
if (NULL == tmp) {
printf("don't find the node whit this key!!!\n");
return SUCCESS;
}
/*判斷一下要刪除的結點是不是葉子節點*/
if (tmp->plc == NULL && tmp->prc == NULL) {
Node* sn = stack.top(&stack);
if (sn == NULL)/*如果棧爲空,說明要刪除的是root節點*/ {
*root = NULL;
return SUCCESS;
}
else {
if (sn->plc == tmp) {
sn->plc = NULL;
}
else {
sn->prc = NULL;
}
free(tmp);
tmp = NULL;
sn->height = Max(Getheight(sn->plc), Getheight(sn->prc)) + 1;
stack.pop(&stack);
stack.push(&stack, sn);/*刪除的葉子節點的父節點更新了高度之後重新入棧*/
}
}
else if (tmp->prc == NULL && tmp->plc != NULL)/*如果右子節點是NULL,直接用左子節點替換掉要刪除的結點,然後判斷樹是否平衡*/ {
Node* sn = stack.top(&stack); /*要刪除節點的父節點*/
if (sn->plc == tmp) {
sn->plc = tmp->plc;
}
else {
sn->prc = tmp->plc;
}
free(tmp);
tmp = NULL;
sn->height = Max(Getheight(sn->plc), Getheight(sn->prc)) + 1;
stack.pop(&stack);
stack.push(&stack, sn);/*替換節點的父節點更新了高度之後重新入棧*/
}
else/*如果右子節點不是NULL,這種情況比較複雜,因爲要用右子節點的最左子節點來替換掉要刪除的結點,
刪除了右子樹的最左子節點之後,還要對右子樹進行調平*/ {
/**/
handlerightbranch(tmp);
stack.push(&stack,tmp);
}
Node* n = stack.top(&stack);
Node* newnode = Rotate(stack.top(&stack));
stack.pop(&stack);
while (!stack.is_empty(&stack)) {
Node* t = stack.top(&stack);
if (t->plc == n) {
t->plc = newnode;
}
else {
t->prc = newnode;
}
n = t;
newnode = Rotate(t);
stack.pop(&stack);
}
*root = newnode;
/*經過以上操作,以要刪除節點爲根節點的子樹是平衡的,但是可能會引起樹整體上的不平衡,
因此需要判斷從替換節點開始,向上逐一是否有失衡,有失衡就需要調平,一直到根節點爲止*/
return SUCCESS;
}
測試程式的代碼如下:
/*test.c*/
#include "AVL.h"
int main()
{
Node* root = NULL;
int elemarry[] = { 16, 12, 18, 10, 14, 17, 20, 9,11,13, 15, 8 };
for (int i = 0; i < sizeof(elemarry)/sizeof(int); i++)
{
if (SUCCESS != insert(&root, elemarry[i], elemarry[i]))
{
perror("insert failure!!!\n");
return FAILURE;
}
}
/*delete(&root, 11);*/
delete(&root, 22);
return 0;
}
以上代碼中用到的棧結構的實現代碼在此代碼
可能看到那麼長的代碼,很多人就覺得鬱悶,這裏面也許是LZ本人能力有限,代碼寫的有些冗餘,沒有細緻的去修改,只是儘量用非遞歸的方式去說明插入和刪除的過程。我們知道,其實所有的遞歸都可以用非遞歸的迭代方式實現,之所以如此,是因爲遞歸的本質也是一個迭代的過程,只不過這個過程是系統來操縱的,我們不用關心數據棧的問題,所以代碼看起來很簡潔很優美,而非遞歸實現其實就是我們自己創建一個棧,對棧進行入棧出棧的操作來實現了遞歸的過程,這樣讓我們更容易跟蹤程序的執行過程,理解起來也比較方便。
插入過程其實可以分爲兩部分,第一部分是跟BST樹的插入一樣一樣的,插入過程涉及到的點都一一入棧,第二部分就是插入新節點後,判斷哪些入棧的結點是否有失衡,如果失衡,就要進行調平,依次出棧進行判斷調平。
刪除過程基本上也是分兩步來走的,就是入棧和出棧。
入棧是在尋找要刪除的結點時發生,
出棧是刪除了節點以後,依次出棧判斷是否有失衡。
在入棧和出棧中間,我們有一個刪除要刪除節點的過程,這個過程我們分了三種情況,第一是要刪除的結點是葉子節點(直接刪),
第二是要刪除的結點右子節點是NULL,左子節點不是NULL(左子節點頂上去),第三種情況是右子節點不是NULL,前兩種情況好處理,
第三種情況其實相當於執行一遍“對要刪除節點的右子樹執行刪除其最左子節點“的刪除操作,刪除的最左子節點其實是用來去替換真正要刪除的結點,這個過程在BST的刪除中也有。這個過程也有入棧和出棧的過程,因爲也要判斷刪除了右子樹的最左子節點是否引發右子樹由失衡,如果失衡,要進行調整。
好了,AVL的非遞歸實現基本上完成了,如果實在覺得上面的代碼又長又亂,沒有胃口讀下去,那就找一找AVL樹的遞歸實現過程吧,起碼代碼直觀上感覺很清晰。
以上代碼僅供學習原理使用,代碼並不嚴謹,並有存在Bug的可能。如果您讀完了以上代碼,發現了BUG,請不吝指出,謝謝。