目錄
4.1 預備知識
- 對於大量的輸入資料,連結串列的執行緒訪問時間太慢,不宜使用。二叉查詢樹大部分操作的執行時間平均為O(logN)。
- 樹可以用幾種方式定義,定義樹的一種自然的方式是遞迴的方法。一棵樹是一些節點的集合。這個集合可以是空集。若非空,則一棵樹由稱作根節點r以及0個或多個費控的子樹T1, T2, ..., Tk組成。這些子樹中的每一棵的根都被來自根r的一條有向邊所連線
- 每一棵子樹的根叫做根r的兒子,而r是每一棵子樹的父親。
- 一棵樹是==N個節點和N-1條邊的集合,其中的一個節點叫做根。存在N-1條邊的結論是由:每條邊都將某個節點連線到它的父親,而除去根節點的每一個節點都有一個父親。
- 每個節點可以有任意多個兒子(可以是0個)。沒有兒子的節點叫做樹葉(leaf)。具有相同父親的節點叫做兄弟(sibling)。用類似的方法可以定義祖父和孫子的關係
- 對任意節點ni,ni的深度(depth)為從根到ni的唯一路徑的長。因此,根的深度是0。ni的高(height)是從ni到一片樹葉的最長路徑的長。因此所有樹葉的高是0。一棵樹的高等於它的根的高。一棵樹的深度等於它最深樹葉的深度,等於這棵樹的高。
- 如果存在從n1到n2的一條路徑,那麼n1是n2的一位祖先,而n2是n1的一個後裔。如果n1!=n2,那麼n1是n2的一個真祖先,而n2是n1的一個真後裔
4.1.1 樹的實現
typedef struct TreeNode *PtrToNode;
struct TreeNode
{
ElementType Element;
PtrToNode FirstChild;
PtrToNode NextSibling;
}
4.1.2 樹的遍歷和應用
- 先序遍歷: 對節點的處理工作是在它的所有兒子節點被處理之前進行的
static void
ListDir(DirectoryOrFile D, int Depth)
{
if (D is a legitimate entry)
{
PrintName(D, Depth);
if (D is a directory)
{
for each child, C, of D
ListDir(C, Depth+1);
}
}
}
void
ListDirectory(DirectoryOrFile D)
{
ListDir(D, 0);
}
- 後序遍歷:對節點的處理工作是在它的所有兒子節點被處理之後進行的
static void
SizeDirectory (DirectoryOrFile D)
{
int TotalSize;
TotalSize = 0;
if (D is a legitimate entry)
{
TotalSize = FileSize(D);
if (D is a directory)
for each child, C, of D
TotalSize += SizeDirectory(C)
}
return TotalSize;
}
4.2 二叉樹
- 二叉樹是一棵樹,其中每個節點都不能有多餘兩個的兒子
- 二叉樹的一個性質是平均二叉樹的深度要比N小得多,平均深度為O(N的平方根)
- 二叉查詢樹深度的平均值為O(logN),但是極端情況下這個深度是可以大到N-1的
4.2.1 實現
具有N個節點的每一棵二叉樹,都將需要N+1個NULL指標
typedef struct TreeNode *PtrToNode;
typedef PtrToNode Tree;
struct TreeNode
{
ElementType Element;
Tree Left;
Tree Right;
}
4.2.2 表示式樹
表示式樹的樹葉是運算元,比如常量或變數,而其它節點為操作符。
- 中序遍歷inorder traversal(得到中綴表示式):遞迴的列印出左子樹,中間,右子樹
- 後序遍歷postorder traversal(得到字尾表示式):遞迴的列印出左子樹,右子樹,中間
- 先序遍歷preorder traversal(得到字首表示式):遞迴的列印出中間,左子樹,右子樹
構造一棵表示式樹
void suffixExpression(char *inputStr)
{
int cnt, cnt2;
Stack s = CreateStack();
for (cnt = 0; inputStr[cnt] != '\0'; cnt++) {
if ((inputStr[cnt] >= '0') && (inputStr[cnt] <= '9')) {
PtrToTreeHead numTree = CreateTreeNode();
numTree->Element = inputStr[cnt];
printf("Push %c\n", numTree->Element);
Push(numTree, s);
}
for (cnt2 = 0; cnt2 < OPERATOR_TYPE; cnt2++) {
if (inputStr[cnt] == Operator[cnt2]) {
PtrToTreeHead operatorTree = CreateTreeNode();
operatorTree->Element = inputStr[cnt];
PtrToTreeHead num2Tree = Top(s);
Pop(s);
PtrToTreeHead num1Tree = Top(s);;
Pop(s);
operatorTree->LeftChild = num1Tree;
operatorTree->RightChild = num2Tree;
Push(operatorTree, s);
printf("operator=%c, num1=%c, num2=%c\n", operatorTree->Element, num1Tree->Element, num2Tree->Element);
}
}
}
PtrToTreeHead printTree = Top(s);
PrintTree(printTree);
DrstroyTree(printTree);
DistroyStack(s);
}
4.3 查詢樹ADT-二叉查詢樹
- 使二叉樹成為二叉查詢樹的性質是:對於樹中的每個節點X,它的左子樹中的所有關鍵字值都小於X的關鍵字值,而它的右子樹中所有關鍵字值大於X的關鍵字值。這意味著該樹所有的元素可以用某種統一的方式排序。
- 由於樹的遞迴定義,通常是遞迴的編寫這些操作的例程。因為二叉查詢樹的平均深度是O(logN),所以一般不必擔心棧空間被耗盡。
- 二叉查詢樹節點定義
struct TreeNode;
typedef struct TreeNode *Position;
typedef Position SearchTree;
struct TreeNode {
ElementType Element;
Search Left;
Srarch Right
}
- MakeEmpty
SearchTree
MakeEmpty(SearchTree treeHead)
{
if (treeHead == NULL) {
return NULL;
}
if (treeHead->Left != NULL) {
MakeEmpty(treeHead->Left);
}
if (treeHead->Right != NULL) {
MakeEmpty(treeHead->Right);
}
if (treeHead != NULL) {
free(treeHead);
}
}
- Find: 注意測試的順序。首先判斷是否為空樹,其次最不可能的情況應該安排在最後進行。這裡使用尾遞迴,可以用一次賦值和一個goto語句代替。尾遞迴在這裡的使用是合理的,因為演算法表示式的簡明性是以速度的降低為代價的。而這裡使用的棧空間的量也只不過是O(logN)而已。
SearchTree
Find(SearchTree treeHead, ElementType Element)
{
if (treeHead == NULL) {
return NULL;
}
if (Element < treeHead->Element) {
return Find(treeHead->Left, Element);
} else if (Element > treeHead->Element) {
return Find(treeHead->Right, Element);
} else {
return treeHead;
}
}
- FindMin遞迴實現
SearchTree
FindMin(SearchTree treeHead)
{
if (treeHead == NULL) {
return NULL;
}
if (treeHead->Left != NULL) {
return FindMin(treeHead->Left);
} else {
return treeHead;
}
}
- FindMin非遞迴實現
SearchTree
FindMin(SearchTree treeHead)
{
if (treeHead == NULL) {
return NULL;
}
SearchTree tmp = treeHead;
while (tmp->Left != NULL) {
tmp = tmp->Left;
}
return tmp;
}
- FindMax遞迴實現
SearchTree
FindMax(SearchTree treeHead)
{
if (treeHead == NULL) {
return NULL;
}
if (treeHead->Right != NULL) {
return FindMin(treeHead->Right);
} else {
return treeHead;
}
}
- FindMax非遞迴實現
SearchTree
FindMax(SearchTree treeHead)
{
if (treeHead == NULL) {
return NULL;
}
SearchTree tmp = treeHead;
while (tmp->Right != NULL) {
tmp = tmp->Right;
}
return tmp;
}
- Insert: 重複元素不重複插入,儲存在某個輔助資料結構中即可,例如表
SearchTree
Insert(SearchTree treeHead, ElementType element)
{
if (treeHead == NULL) {
treeHead = (SearchTree)malloc(sizeof(struct TreeNode));
if (treeHead == NULL) {
return NULL;
}
memset(treeHead, 0, sizeof(struct TreeNode));
treeHead->Element = element;
treeHead->Left = treeHead->Right = NULL;
} else if (element < treeHead->Element) {
treeHead->Left = Insert(treeHead->Left, element);
} else if (element > treeHead->Element) {
treeHead->Right = Insert(treeHead->Right, element);
}
return treeHead;
}
- Delete: 如果節點是一片樹葉,那麼他可以立即被刪除。如果節點有一個兒子,則該節點可以在其父節點調整指標繞過該節點指向它的兒子的時候(原父節點的孫子節點)該節點可以被刪除。所刪除的節點不再引用,只有在指向它的指標已被省去的情況下才能夠被去掉。
SearchTree
Delete(SearchTree T, ElementType element)
{
SearchTree tmp;
if (T == NULL) {
printf("Couldn't find element\n");
return NULL;
} else if (element < T->element) {
T->Left = Delete(T->Left, element);
} else if (element > T->element) {
T->Right = Delete(T->Right, element);
} else if (T->Left && T->Right) {
tmp = T;
T->Element = tmp->Element;
T->Right = Delete(T->Right, element);
} else {
tmp = T;
if (T->Left) {
T = T->Left;
} else if (T->Right) {
T = T->Right;
}
free(tmp);
}
}
- 懶惰刪除:如果刪除的次數不多,則通常使用的策略是懶惰刪除。即當一個元素要被刪除時,它仍留在樹中,而是隻做了個被刪除的記號。這種做法特別是在有重複關鍵字時很流行,因為此時記錄出現頻率數的域可以減一。如果樹中的實際節點數和“被刪除”的節點數相同,那麼樹的深度預計只上升一個小的常數。因此,存在一個與懶惰刪除相關的非常小的時間損耗。再有,如果被刪除的關鍵字是重新插入的,那麼分配一個新單元的開銷就避免了。
4.3.6 平均情形分析
- 除MakeEmpty外,我們期望上一節所有的操作都花費O(logN)時間,所有的操作都是O(d),其中d是包含所訪問的關鍵字的結點的深度。本節證明,假設所有的樹出現的機會均等。則樹的所有結點的平均深度為O(logN)。
- 一棵樹的所有節點的深度的和為內部路徑長。
- 令D(N)是具有N個節點的某棵樹T的內部路徑長。D(1) = 0。一棵N節點樹是由一棵i節點左子樹,和一棵(N-i-1)節點右子樹,以及深度為零的一個根節點組成。其中0<=i<N。D(i)為根的左子樹的內部路徑長。但是在原樹中,所有這些節點都要加深一度。同理右子樹。因此得到:D(N)=D(i)+D(N-i-1)+N-1。如果所有子樹的大小都等可能的出現,這對於二叉查詢樹是成立的,但是對於二叉樹則不成立。那麼D(i)和D(N-i-1)的平均值都是(1/N)D(j)(j=0 -> j=N-1)的和。於是D(N) = 2/N * (D(j) (j=0 -> j=N-1))+ N - 1。> D(N) = O(NlogN)。因此任意節點的期望深度為O(logN)
- 我們並不清楚是否所有的二叉查詢樹都是等可能出現的,上面描述的刪除演算法有助於使得左子樹比右子樹深,因為我們總是用右子樹的一個節點來代替刪除的節點,這種策略的準確效果仍然是未知的。
- 在沒有刪除或者使用使用懶惰刪除的i情況下,可以證明所有的二叉查詢樹都是等可能出現的。上述操作的平均執行時間都是O(logN)
- 樹的平衡:任何節點的深度均不得過深。許多演算法實現了平衡樹,更加複雜,更新平均時間更長,但是防止了處理起來很麻煩的一些簡單清醒。例如AVL樹
- 比較新的方法是放棄平衡條件,允許樹有任意的深度,但是每次操作之後要使用一個調整規則進行調整。使得後面的操作效率更高。這種型別的資料結構一般屬於自調整類結構。在二叉查詢樹的情況下,對於任意單個運算,我們不再保證O(logN)的時間界。但是可以證明任意連續M次操作在最壞的情形下,花費時間為O(MlogN)。因此這足以防止令人棘手的最壞情形。
4.4 AVL樹
- 一棵AVL樹是其每個節點的左子樹和右子樹的高度最多差一的二叉查詢樹(空樹的高度定為-1)。每一個節點在其節點結構中保留高度資訊。
- 一個AVL樹的高度最多為1.44log(N+2) - 1.328,實際上的高度只比logN稍微多一些
- 在高度為h的AVL樹中,最少節點數S(h) = S(h-1) + S(h-2) + 1。對於h=0, S(h) = 1; h=1, S(h)=2。且函式S(h)與斐波那契數列相關。
- 除去可能的插入外(假設懶惰刪除),所有樹操作都可以以時間O(logN)執行。
- 當進行插入操作時,需要更新通向根節點路徑上那些節點的所有平衡資訊,而插入操作的困難在於,插入一個節點可能破壞AVL樹的特性。如果發生這種情況,那麼就要把性質恢復以後才認為這一步插入完成。事實上,可以通過對樹進行簡單的修正來做到,也就是旋轉
4.4.1 單旋轉
- 樹的其餘部分必須知曉旋轉節點的變化。順著新插入的節點,向根部回溯,檢查路徑上的某個節點A是否不符合AVL性質(左右子樹高度差大於1).如果不符合,則該節點的向深處的下一個節點B進行旋轉。並且如果B是右子樹,則B的左子樹成為A的右子樹;如果B是左子樹,則B的右子樹成為A的左子樹。
- 抽象的形容是:把樹形象的看成是柔軟靈活的,抓住節點B,使勁搖動它,B成為新的跟,A成為B的子樹
4.4.2 雙旋轉
相當於兩次單旋轉。
4.4.3 實現
- 節點定義
struct AvlNode {
ElementType Element;
AvlTree Left;
AvlTree Right;
int Height;
}
typedef struct AvlNode *Position;
typedef struct AvlNode *AvlTree;
- Height
static int
Height(Position P)
{
if (P == NULL) {
return -1;
} else {
return P->Height;
}
}
- Insert
AvlTree
Insert(AvlTree T, ElementType X)
{
if (T == NULL) {
T = (struct AvlNode)malloc(sizeof(struct AvlNode));
if (T == NULL) {
return NULL;
}
memset(T, 0, sizeof(struct AvlNode));
T->Element = X;
T->Height = 0;
T->Left = T->Right = NULL;
} else if (X < T->Element) {
T->Left = Insert(T->Left, X);
if (Height(T->Left) - Height(T->Right) == 2) {
if (X < T->Left->Element) {
T = SingleRotateWithLeft(T);
} else {
T = DoubleRotateWithLeft(T);
}
}
} else if (X > T->Element) {
T->Right = Insert(T->Right, X);
if (Height(T->Right) - Height(T->Left) == 2) {
if (X > T->Right->Element) {
T = SingleRotateRight(T);
} else {
T = DoubleRotateRight(T);
}
}
}
T->Height = MAX(Height(T->Left), Height(T->Right)) + 1;
return T;
}
- SingleRotateWithLeft
AvlTree
SingleRotateWithLeft(Position P)
{
Position P1 = NULL;
P1 = P->Left;
P->Left = P1->Right;
P1->Right = P;
P->Height = Max(Height(P->Left), Height(P->Right)) + 1;
P1->Height = Max(Height(P1->Left), Height(P1->Right)) + 1;
return P1;
}
- SingleRotateWithRight
AvlTree
SingleRotateWithRight(Position P)
{
Position P1 = NULL;
P1 = P->Right;
P->Right = P1->Left;
P1->Left = P;
P->Height = Max(Height(P->Left) + Height(P->Right)) + 1;
P1->Height = Max(Height(P1->Left) + Height(P1->Right)) + 1;
return P1;
}
- DoubleRotateWithLeft
AvlTree
DoubleRotateWithLeft(Position P)
{
P->Left = SingleRotateWithRight(P->Left);
return SingleRotateWithLeft(P);
}
- DoubleRotateWithRight
AvlTree
DoubleRotateWithRight(Position P)
{
P->Right = SingleRotateWithLeft(P->Right);
return SingleRotateWithRight(P);
}
4.5 伸展樹
- 當一個節點被訪問後,它就要經過一系列AVL樹的旋轉被放到根上。注意,如果一個節點很深,那麼其路徑上就存在許多的節點也相對較深,通過重新構造可以使對這些節點的進一步訪問所花費的時間變少。因此,如果節點過深,我們還要求重新構造應具有平衡這棵樹(到某種程度)的作用。實際使用中,當一個節點被訪問時,它就很可能不久再被訪問到。且較為頻繁。
- 不要求保留高度或平衡資訊,因此節省空間並簡化程式碼
4.6 樹的遍歷
- 三種遍歷方式
- 首先處理NULL的情形,然後才是其餘工作
- 程式越緊湊,一些愚蠢的錯誤出現的可能就越小
- 層序遍歷不是用遞迴實現的,而是用佇列實現的,不使用遞迴所默示的棧。所有深度為D的節點要在深度為D+1的節點之前進行處理
4.7 B-樹
- 階:一個節點子節點(子樹)數目的最大值
- 樹的根其兒子數在2和M之間
- 除根外,所有非樹葉節點的兒子數在[M/2]到M之間
- 所有樹葉都在相同的深度上
- 所有的資料都儲存在樹葉上,每一個內部節點皆含有指向該節點各兒子的指標P1,P2,...,PM和分別代表在子樹P2, P3, ..., PM中發現的最小關鍵字的值K1, K2, ..., KM-1。有些指標是NULL,而其對應的Ki是未定義的。對於==每一個節點,其子樹P1中的所有關鍵字都小於子樹P2的關鍵字
- 樹葉包含實際資料,這些資料是關鍵字或者是指向含有這些關鍵字的記錄的指標
- B樹深度最多是log(M/2)N。插入和刪除可能需要O(M)的工作量來調整該節點上的所有資訊。對於每個插入和刪除,最壞情形的執行時間為O(Mlog(M)N) = O((M/logM) logN).查詢一次只花費O(logN)時間
- M最好(合法的)選擇是M=3或M=4,當M再增大時插入和刪除的時間就會增加
- 如果使用M階B樹,那麼磁碟訪問的次數是O(log(M)N),每次磁碟訪問花費O(logM)來確定分支的方向,該操作比一般都儲存器的區塊所花的時間少得多,因此認為是無足輕重的。
- 當一棵B樹得到它的第(M+1)項時,例程不是總去分裂節點,而是搜尋能夠接納新兒子的兄弟,此時能夠更好的利用空間
參考文獻
- Mark Allen Weiss.資料結構與演算法分析[M].America, 2007
本文作者: CrazyCatJack
本文連結: https://www.cnblogs.com/CrazyCatJack/p/13339994.html
版權宣告:本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!
關注博主:如果您覺得該文章對您有幫助,可以點選文章右下角推薦一下,您的支援將成為我最大的動力!