《資料結構與演算法分析》學習筆記-第四章-樹

CrazyCatJack發表於2021-02-19


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 樹的遍歷和應用

  1. 先序遍歷: 對節點的處理工作是在它的所有兒子節點被處理之前進行的
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);
}
  1. 後序遍歷:對節點的處理工作是在它的所有兒子節點被處理之後進行的
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 表示式樹

表示式樹的樹葉是運算元,比如常量或變數,而其它節點為操作符。

  1. 中序遍歷inorder traversal(得到中綴表示式):遞迴的列印出左子樹,中間,右子樹
  2. 後序遍歷postorder traversal(得到字尾表示式):遞迴的列印出左子樹,右子樹,中間
  3. 先序遍歷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 實現

  1. 節點定義
struct AvlNode {
    ElementType Element;
    AvlTree Left;
    AvlTree Right;
    int Height;
}
typedef struct AvlNode *Position;
typedef struct AvlNode *AvlTree;
  1. Height
static int
Height(Position P)
{
    if (P == NULL) {
        return -1;
    } else {
        return P->Height;
    }
}
  1. 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;
}
  1. 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;
}
  1. 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;
}
  1. DoubleRotateWithLeft
AvlTree
DoubleRotateWithLeft(Position P)
{
    P->Left = SingleRotateWithRight(P->Left);
    return SingleRotateWithLeft(P);
}
  1. 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)項時,例程不是總去分裂節點,而是搜尋能夠接納新兒子的兄弟,此時能夠更好的利用空間

參考文獻

  1. Mark Allen Weiss.資料結構與演算法分析[M].America, 2007

本文作者: CrazyCatJack

本文連結: https://www.cnblogs.com/CrazyCatJack/p/13339994.html

版權宣告:本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

關注博主:如果您覺得該文章對您有幫助,可以點選文章右下角推薦一下,您的支援將成為我最大的動力!


相關文章