《數據結構與算法分析》學習筆記-第四章-樹


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指針html

typedef struct TreeNode *PtrToNode;
typedef PtrToNode Tree;
struct TreeNode
{
    ElementType Element;
    Tree Left;
    Tree Right;
}

4.2.2 表達式樹

表達式樹的樹葉是操做數,好比常量或變量,而其它節點爲操做符。git

  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.htmlpost

版權聲明:本博客全部文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!測試

關注博主:若是您以爲該文章對您有幫助,能夠點擊文章右下角推薦一下,您的支持將成爲我最大的動力!線程

相關文章
相關標籤/搜索