1 引言
二叉樹是數據結構中的重點與難點,也是應用較為廣泛的一類數據結構。二叉樹的基礎知識在之前的數據結構與算法——二叉樹基礎中已經詳細介紹。本篇文章將着重介紹兩類二叉樹,二叉搜索樹和平衡二叉樹。
2 二叉搜索樹
2.1 定義
二叉搜索樹又稱二叉查找樹,亦稱為二叉排序樹。設x為二叉查找樹中的一個節點,x節點包含關鍵字key,節點x的key值記為key[x]。如果y是x的左子樹中的一個節點,則key[y] <= key[x];如果y是x的右子樹的一個節點,則key[y] >= key[x]。
2.2 性質
(1)若左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
(2)若右子樹不空,則右子樹上所有節點的值均大於它的根節點的值;
(3)左、右子樹也分別為二叉搜索樹;
例如:圖2.2.1所示的二叉樹為一棵二叉搜索樹。
例如:圖2.2.2所示不是一棵二叉搜索樹,因為節點40的左孩子節點值為44,不滿足二叉搜索樹的定義。
2.3 節點結構
二叉樹的節點結構通常包含三部分,其中有:左孩子的指針,右孩子指針以及數據域。節點的圖示如下:
代碼定義:
struct BSTNode { int key; //節點數據 struct BSTNode *lchild, *rchild; /* 左右孩子指針 */ };
2.4 創建二叉搜索樹
現有序列:A = {61, 87, 59, 47, 35, 73, 51, 98, 37, 93}。根據此序列構造二叉搜索樹過程如下:
(1)i = 0,A[0] = 61,節點61作為根節點;
(2)i = 1,A[1] = 87,87 > 61,且節點61右孩子為空,故81為61節點的右孩子;
(3)i = 2,A[2] = 59,59 < 61,且節點61左孩子為空,故59為61節點的左孩子;
(4)i = 3,A[3] = 47,47 < 59,且節點59左孩子為空,故47為59節點的左孩子;
(5)i = 4,A[4] = 35,35 < 47,且節點47左孩子為空,故35為47節點的左孩子;
(6)i = 5,A[5] = 73,73 < 87,且節點87左孩子為空,故73為87節點的左孩子;
(7)i = 6,A[6] = 51,47 < 51,且節點47右孩子為空,故51為47節點的右孩子;
(8)i = 7,A[7] = 98,98 < 87,且節點87右孩子為空,故98為87節點的右孩子;
(9)i = 8,A[8] = 93,93 < 98,且節點98左孩子為空,故93為98節點的左孩子;創建完畢后如圖2.4中的二叉搜索樹:
2.5 查找
查找流程:
(1)如果樹是空的,則查找結束,無匹配。
(2)如果被查找的值和節點的值相等,查找成功。
(3)如果被查找的值小於節點的值,遞歸查找左子樹,
(4)如果被查找的值大於節點的值,遞歸查找右子樹,
查找代碼:
bool searchBST(BSTNode* T, int key, BSTNode* f, BSTNode **p) { if (!T) /* 查找不成功 */ { *p = f; return false; } else if (key == T->key) /* 查找成功 */ { *p = T; return true; } else if (key < T->key) return searchBST(T->lchild, key, T, p); /* 在左子樹中繼續查找 */ else return searchBST(T->rchild, key, T, p); /* 在右子樹中繼續查找 */ }
使用二叉搜索樹可以提高查找效率,其平均時間復雜度為O(log2n)。
2.6 插入
插入流程:
(1)先檢測該元素是否在樹中已經存在。如果已經存在,則不進行插入;
(2)若元素不存在,則進行查找過程,並將元素插入在查找結束的位置。
圖解過程:
代碼實現:
void insertBST(BSTNode **T,int key) //此處使用二重指針是因為要修改指針的指針 { BSTNode *s; if(*T==NULL) //到達查找結束位置,再次位置插入元素 { s = (BSTNode*)malloc(sizeof(BSTNode)); s->key = key; s->lchild = NULL; s->rchild = NULL; *T=s; } else if(key<(*T)->key)//要插入的值大於當前節點,往左子樹搜 { insertBST(&((*T)->lchild),key); } else if(key>(*T)->key)//大於當前節點,往右子樹搜 { insertBST(&((*T)->rchild),key); } }
2.7 刪除
1) 刪除節點為葉子節點
刪除葉子節點的方式最為簡單,只需查找到該節點,直接刪除即可。例如刪除圖2.4中的葉子節點37、節點51、節點60、節點73和節點93的方式是相同的。
2) 刪除的節點只有左子樹
刪除的節點若只有左子樹,將節點的左子樹替代該節點位置。例如:刪除圖2.4中的98節點:
3)刪除的節點只有右子樹
刪除的節點若只有右子樹,將節點的右子樹替代該節點位置。這種情況與刪除左子樹處理方式類似,不再贅述。
4)刪除的節點既有左子樹又有右子樹。
若刪除的節點既有左子樹又有右子樹,這種節點刪除過程相對復雜。其流程如下:
(1)遍歷待刪除節點的左子樹,找到其左子樹中的最大節點,即刪除節點的前驅節點;
(2)將最大節點代替被刪除節點;
(3)刪除左子樹中的最大節點;
(4)左子樹中待刪除最大節點一定為葉子節點或者僅有左子樹。按照之前情形刪除即可。
注:同樣可以使用刪除節點的右子樹中最小節點,即后繼節點代替刪除節點,此流程與使用前驅節點類似。
刪除代碼:
/* 從二叉排序樹中刪除節點p,並重接它的左或右子樹。 */ bool deleteBSTNode(BSTNode* p) { BSTNode* q,s; if((*p)->rchild==NULL) //右子樹空則只需重接它的左子樹(待刪節點是葉子也走此分支) { q=*p; *p=(*p)->lchild; free(q); } else if((*p)->lchild==NULL) //左子樹為空,只需重接它的右子樹 { q=*p; *p=(*p)->rchild; free(q); } else //左右子樹均不空 { q=*p; s=(*p)->lchild; while(s->rchild) // 轉到左子樹,然后向右到盡頭(找待刪節點的前驅) */ { q=s; s=s->rchild; } (*p)->key=s->key; //s指向被刪節點的直接前驅(將被刪節點前驅的值取代被刪節點的值) if(q!=*p) q->rchild=s->lchild; //重接q的右子樹 else q->lchild=s->lchild; //重接q的左子樹 free(s); } return TRUE; }
3 平衡二叉樹
3.1 定義
二叉搜索樹一定程度上可以提高搜索效率,但是當原序列有序,例如序列A = {1,2,3,4,5,6},構造二叉搜索樹如圖3.1。依據此序列構造的二叉搜索樹為右斜樹,同時二叉樹退化成單鏈表,搜索效率降低為O(n)。
在此二叉搜索樹中查找元素6需要查找6次。二叉搜索樹的查找效率取決於樹的高度,因此保持樹的高度最小,即可保證樹的查找效率。同樣的序列A,改為圖3.2方式存儲,查找元素6時只需比較3次,查找效率提升一倍。
可以看出當節點數目一定,保持樹的左右兩端保持平衡,樹的查找效率最高。這種左右子樹的高度相差不超過1的樹為平衡二叉樹。
3.2 平衡因子
定義:某節點的左子樹與右子樹的高度(深度)差即為該節點的平衡因子(BF,Balance Factor),平衡二叉樹中不存在平衡因子大於1的節點。在一棵平衡二叉樹中,節點的平衡因子只能取-1、1或者0。
3.3 節點結構
定義平衡二叉樹的節點結構:
typedef struct AVLNode *Tree; typedef int ElementType; struct AVLNode { int depth; //深度,這里計算每個結點的深度,通過深度的比較可得出是否平衡 Tree parent; //該結點的父節點,方便操作 ElementType val; //結點值 Tree lchild; Tree rchild; AVLNode(int val=0) //默認構造函數 { parent=NULL; depth=0; lchild=rchild=NULL; this->val=val; } };
對於給定結點數為n的AVL樹,最大高度為O(log2n)。
3.4 左旋與右旋
1) 左旋
如圖3.4.1所示的平衡二叉樹
如在此平衡二叉樹插入節點62,樹結構變為:
可以得出40節點的左子樹高度為1,右子樹高度為3,此時平衡因子為-2,樹失去平衡。為保證樹的平衡,此時需要對節點40做出旋轉,因為右子樹高度高於左子樹,對節點進行左旋操作,流程如下:
(1)節點的右孩子替代此節點位置
(2)右孩子的左子樹變為該節點的右子樹
(3)節點本身變為右孩子的左子樹
圖解過程:
2)右旋
右旋操作與左旋類似,操作流程為:
(1)節點的左孩子代表此節點
(2)節點的左孩子的右子樹變為節點的左子樹
(3)將此節點作為左孩子節點的右子樹。
圖解過程:
3.5 插入
假設一顆 AVL 樹的某個節點為A,有四種操作會使 A 的左右子樹高度差大於 1,從而破壞了原有 AVL 樹的平衡性。平衡二叉樹插入節點的情況分為以下四種:
1) A的左孩子的左子樹插入節點(LL)
例如:圖3.5.1所示的平衡二叉樹:
節點A的左孩子為B,B的左子樹為D,無論在節點D的左子樹或者右子樹中插入F均會導致節點A失衡。因此需要對節點A進行旋轉操作。A的平衡因子為2,值為正,因此對A進行右旋操作。
圖解過程:
代碼實現:
//LL型調整函數 //返回:新父節點 Tree LL_rotate(Tree node) { //node為離操作結點最近的失衡的結點 Tree parent=NULL,son; //獲取失衡結點的父節點 parent=node->parent; //獲取失衡結點的左孩子 son=node->lchild; }