【algo&ds】【吐血整理】4.樹和二叉樹、徹底二叉樹、滿二叉樹、二叉查找樹、平衡二叉樹、堆、哈夫曼樹、B樹、字典樹、紅黑樹、跳錶、散列表

本博客內容耗時4天整理,若是須要轉載,請註明出處,謝謝。php

1.樹

1.1樹的定義

在計算機科學中,樹(英語:tree)是一種抽象數據類型(ADT)或是實做這種抽象數據類型的數據結構,用來模擬具備樹狀結構性質的數據集合。它是由n(n>0)個有限節點組成一個具備層次關係的集合。把它叫作「樹」是由於它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具備如下的特色:html

  • 每一個節點都只有有限個子節點或無子節點;
  • 沒有父節點的節點稱爲根節點;
  • 每個非根節點有且只有一個父節點;
  • 除了根節點外,每一個子節點能夠分爲多個不相交的子樹;
  • 樹裏面沒有環路(cycle)

Snipaste_2019-11-16_22-23-17.png

1.2常見術語

  1. 節點的度:一個節點含有的子樹的個數稱爲該節點的度;
  2. 樹的度:一棵樹中,最大的節點度稱爲樹的度;
  3. 葉節點終端節點:度爲零的節點;
  4. 非終端節點分支節點:度不爲零的節點;
  5. 父親節點父節點:若一個節點含有子節點,則這個節點稱爲其子節點的父節點;
  6. 孩子節點子節點:一個節點含有的子樹的根節點稱爲該節點的子節點;
  7. 兄弟節點:具備相同父節點的節點互稱爲兄弟節點;
  8. 節點的層次:從根開始定義起,根爲第1層,根的子節點爲第2層,以此類推;
  9. 深度:對於任意節點n,n的深度爲從根到n的惟一路徑長,根的深度爲0;
  10. 高度:對於任意節點n,n的高度爲從n到一片樹葉的最長路徑長,全部樹葉的高度爲0;
  11. 堂兄弟節點:父節點在同一層的節點互爲堂兄弟;
  12. 節點的祖先:從根到該節點所經分支上的全部節點;
  13. 子孫:以某節點爲根的子樹中任一節點都稱爲該節點的子孫。
  14. 森林:由m(m>=0)棵互不相交的樹的集合稱爲森林;

1.3樹的種類

  • 無序樹:樹中任意節點的子節點之間沒有順序關係,這種樹稱爲無序樹,也稱爲自由樹node

  • 有序樹:樹中任意節點的子節點之間有順序關係,這種樹稱爲有序樹;算法

    • 二叉樹編程

      :每一個節點最多含有兩個子樹的樹稱爲二叉樹;數組

      • 徹底二叉樹緩存

        :對於一顆二叉樹,假設其深度爲d(d>1)。除了第d層外,其它各層的節點數目均已達最大值,且第d層全部節點從左向右連續地緊密排列,這樣的二叉樹被稱爲徹底二叉樹;markdown

        • 滿二叉樹:全部葉節點都在最底層的徹底二叉樹;
      • 平衡二叉樹AVL樹):當且僅當任何節點的兩棵子樹的高度差不大於1的二叉樹;數據結構

      • 排序二叉樹(二叉查找樹(英語:Binary Search Tree)):也稱二叉搜索樹、有序二叉樹;數據結構和算法

    • 霍夫曼樹帶權路徑最短的二叉樹稱爲哈夫曼樹或最優二叉樹;

    • B樹:一種對讀寫操做進行優化的自平衡的二叉查找樹,可以保持數據有序,擁有多於兩個子樹。

1.4樹的存儲結構

1.兒子-兄弟表示法

Snipaste_2019-11-16_22-00-10.png

2.還有一種靜態寫法,數據域保留,指針域存放其全部子節點的地址(好比開一個數組,存放全部子節點的地址,有點相似於靜態鏈表)

struct node{
    typename data;//數據域
    vector child;//指針域
}Node[maxn];

其對應的建立節點函數以下:

int index = 0;
int newNode(int v){
    Node[index].data = v;
    Node[index].child.clear();
    return index++;
}

2.二叉樹

Snipaste_2019-11-16_22-18-35.png

2.1二叉樹的性質

性質1

在二叉樹的第i層上至多有2^(i-1)個結點(i>0)

由於一個節點度不大於2(即每一個結點只能有兩棵子樹),若是假設這棵二叉樹是一棵滿二叉樹,那麼每一個結點都有兩棵子樹,按每層來看的話,會發現它是首項爲1,公比爲2的等比數列,因此第i層最多有2^(i-1)個結點(i>0)

性質2

一棵深度爲k的二叉樹中,最多有2^k-1個結點

根據性質一,經過等比數列前N項和展開就能夠證實出來。

性質3

具備n個結點的徹底二叉樹的深度k爲[log2n]+1

log2n是以2爲底,markdown語法只會幾個經常使用的,將就着看。

性質4

對於一棵非空的二叉樹,若是葉子結點數爲n0,度爲2的結點數爲n2,
n0 = n2+1

整棵樹的結點數: n = n0+n1+n2 (n0葉子結點數,n1度爲1的結點數,n2度爲2的結點數)
根據節點度的定義子結點的的數量爲: n0×0+n1×1+n2×2
子節點的數量爲:n-1 (除根之外每一個結點都是子節點)

n-1 = n0×0+n1×1+n2×2 = n0+n1+n2-1

因此解得 n0 = n2+1

性質5

對於具備n個節點的徹底二叉樹,若是按照從上至下和從左至右的順序對二叉樹中全部結點進行從1的編號,則對於任意的序號爲i結點,有:

  • 若是i>1,則序號爲i的結點的雙親結點爲i/2,若是i==1,則序號爲i的結點是根節點,無雙親結點
  • 若是2*i<=n,則序號爲i的結點的左孩子節點的序號爲2*i,不然i結點無左孩子
  • 若是2*i+1<=n,則序號爲i的結點的右孩子節點的序號爲2*i+1,不然i結點無右孩子

注意:性質1,2,4全部二叉樹都通用,性質3,5只有徹底二叉樹適用

Snipaste_2019-11-16_22-25-26.png

2.2徹底二叉樹

對於一顆二叉樹,假設其深度爲d(d>1)。除了第d層外,其它各層的節點數目均已達最大值,且第d層全部節點從左向右連續地緊密排列,這樣的二叉樹被稱爲徹底二叉樹

Snipaste_2019-11-16_11-03-56.png

1.二叉樹的順序存儲結構

由於徹底二叉樹的這種特性,因此它也可使用數組這種連續存儲結構來實現。具體地說,若是對徹底二叉樹當中的任何一個節點(設編號爲x),其左孩子的編號必定是2x,而右孩子的編號必定是2x+1。也就是說,能夠經過創建一個大小爲2^k(k爲徹底二叉樹的最大高度)的數組來存放全部節點的信息,這樣作的好處是,能夠直接用數組的下標來獲取指定序號的節點數據,而且能夠直接計算獲得左右孩子的編號。

Snipaste_2019-11-16_22-27-26.png

除此以外,該數組中元素存放的順序剛好爲該徹底二叉樹的層序遍歷序列。而判斷某個節點是否爲葉子節點的標誌爲:**該節點(記下標爲root)的左子節點的編號root*2大於節點總個數n**

如上圖舉例,4爲葉子節點,它的左孩子節點爲2*4=8>節點總個數7

2不是葉子節點2*2=4<7

固然非徹底二叉樹(也就是通常的二叉樹)也能夠這樣使用順序存儲結構來實現,可是會形成空間浪費。原理就是把空的節點補全(數據域都爲空),使得一個通常二叉樹變成對應的徹底二叉樹。

Snipaste_2019-11-16_22-30-12.png

因此通常二叉樹更多地使用鏈表來實現。

2.二叉樹的鏈表存儲結構

通常來講,二叉樹採用鏈表來定義,和普通鏈表的區別是,因爲二叉樹每一個節點有兩條出邊,所以指針域變成了兩個——分別指向左子樹根節點地址和右子樹根節點地址。若是某個子樹不存在,則指向NULL。其定義方式以下:

struct node{
    typename data; //數據域
    node* lchild;  //指向左子樹根節點的地址
    node* rchild;  //指向右子樹根節點的地址
}

對應的存儲結構圖示以下:

Snipaste_2019-11-16_14-41-54.png

因爲在二叉樹建樹前根節點不存在,所以其地址通常設置爲NULL

node* root = NULL;

新建節點的代碼實現以下:

node* newNode(int v){
    node* Node = new node;//申請一個node型變量的地址空間
    Node->data = v; //節點數據域
    Node->lchild = Node->rchild =NULL;
    return Node;
}

2.3二叉樹的四種遍歷方式

二叉樹的遍歷是指經過必定順序訪問二叉樹的全部結點。遍歷方法通常有四種:先序遍歷、中序遍歷、後序遍歷及層次遍歷。

其中,前三種通常使用深度優先搜索(DFS)實現,而層次遍歷通常用廣度優先搜索(BFS)實現。

把一顆二叉樹分紅三個部分,根節點、左子樹、右子樹,且對左子樹和右子樹一樣進行這樣的劃分,這樣對樹的遍歷就能夠分解爲對這三部分的遍歷。不管是這三種遍歷中的哪種,左子樹必定先於右子樹遍歷,且所謂的「先中後」都是指根結點root在遍歷中的位置,所以先序遍歷的訪問順序是根結點→左子樹→右子樹,中序遍歷的訪問順序是左子樹→根結點→右子樹,後序遍歷的訪問順序是左子樹→右子樹→根結點。

層次遍歷,是把二叉樹從第一層(也就是根節點)開始遍歷,每一層從左到右遍歷完,而後遍歷下一層,直到全部節點都訪問完。

1.先序遍歷

Snipaste_2019-11-16_22-40-27.png

遞歸實現

void PreorderTraversal( BinTree BT )
{
    if( BT ) {
        printf("%d ", BT->Data );
        PreorderTraversal( BT->Left );
        PreorderTraversal( BT->Right );
    }
}

非遞歸實現

void PreorderTraversal( BinTree BT ) {
    BinTree T = BT;
    Stack S = CreatStack( MaxSize );        /*建立並初始化堆棧S*/
    while( T || !IsEmpty(S) ) {
        while(T) {                          /*一直向左並將沿途結點壓入堆棧*/
            Push(S,T);
            printf("%5d", T->Data);         /*(訪問)打印結點*/
            T = T->Left;
        }
        if(!IsEmpty(S)) {
            T = Pop(S);                     /*結點彈出堆棧*/
            T = T->Right;                   /*轉向右子樹*/
        }
    }
}

因爲先序遍歷先訪問根節點,所以對一棵二叉樹的先序遍歷序列,序列的第一個必定是根節點

2.中序遍歷

Snipaste_2019-11-16_22-44-48.png

遞歸實現

void InorderTraversal( BinTree BT )
{
    if( BT ) {
        InorderTraversal( BT->Left );
        /* 此處假設對BT結點的訪問就是打印數據 */
        printf("%d ", BT->Data); /* 假設數據爲整型 */
        InorderTraversal( BT->Right );
    }
}

非遞歸實現思路

  • 遇到一個結點,就把它壓棧,並去遍歷它的左子樹;
  • 當左子樹遍歷結束後,從棧頂彈出這個結點並訪問它;
  • 而後按其右指針再去中序遍歷該結點的右子樹。
void InorderTraversal( BinTree BT ) {
    BinTree T = BT;
    Stack S = CreatStack( MaxSize );        /*建立並初始化堆棧S*/
    while( T || !IsEmpty(S) ) {
        while(T) {                          /*一直向左並將沿途結點壓入堆棧*/
            Push(S,T);
            T = T->Left;
        }
        if(!IsEmpty(S)) {
            T = Pop(S);                     /*結點彈出堆棧*/
            printf("%5d", T->Data);         /*(訪問)打印結點*/
            T = T->Right;                   /*轉向右子樹*/
        }
    }
}

因爲中序遍歷老是把根節點放在左子樹和右子樹中間,所以只要知道根節點,就能夠經過根節點在中序遍歷序列中的位置區分出左子樹和右子樹。

3.後序遍歷

Snipaste_2019-11-16_22-47-14.png

遞歸實現

void PostorderTraversal( BinTree BT )
{
    if( BT ) {
        PostorderTraversal( BT->Left );
        PostorderTraversal( BT->Right );
        printf("%d ", BT->Data);
    }
}

非遞歸實現

void PostOrderTraversal(Bintree BT) {
    Bintree T = BT;
    Bintree LT = NULL;
    Stack S = CreateStack(Maxsize);
    while (T || !IsEmpty(S)) {
        while (T) {
            Push(S, T);
            T = T->left;   //轉向左子樹
        }
        if (!IsEmpty(S)) {
            T = Pop(s);
            if ((T-> Right== NULL)||(T->Right == LT)) {    //判斷右節點爲空或者右節點已經輸出
                printf("%d", T->Data);
                LT = T; //記錄下上一個被輸出的
                     T = NULL;
            } else {
                Push(S, T);  //第二次入棧(至關於T沒有出棧)
                T = T->Right;  //轉向右子樹
            }
        }
    }
}

後序遍歷老是把根節點放在最後訪問,這和先序遍歷剛好相反,所以對於後序遍歷序列來講,序列的最後一個必定是根節點

4.層序遍歷

Snipaste_2019-11-16_23-18-06.png

void LevelorderTraversal ( BinTree BT )
{
    Queue Q;
    BinTree T;

    if ( !BT ) return; /* 如果空樹則直接返回 */

    Q = CreatQueue(); /* 建立空隊列Q */
    AddQ( Q, BT );
    while ( !IsEmpty(Q) ) {
        T = DeleteQ( Q );
        printf("%d ", T->Data); /* 訪問取出隊列的結點 */
        if ( T->Left )   AddQ( Q, T->Left );
        if ( T->Right )  AddQ( Q, T->Right );
    }
}

這裏的遍歷就是採用的廣度優先搜索的思想,能夠參考另一篇文章

注意點:隊列中的元素是node*型而不是node型。隊列中保存的值是原元素的一個副本,也就是說相似於值傳遞,而不是引用傳遞,因此經過存地址的方式,來達到聯動修改的目的。更多的說明請參考文章

2.4遍歷二叉樹的應用

1.輸出二叉樹中的葉子結點。

void PreOrderPrintLeaves( BinTree BT ) {
    if( BT ) {
        if ( !BT-Left && !BT->Right )
            printf(「%d」, BT->Data );
        PreOrderPrintLeaves ( BT->Left );
        PreOrderPrintLeaves ( BT->Right );
    }
}

2.求二叉樹的高度。

int PostOrderGetHeight( BinTree BT ) {
    int HL, HR, MaxH;
    if( BT ) {
        HL = PostOrderGetHeight(BT->Left); /*求左子樹的深度*/
        HR = PostOrderGetHeight(BT->Right); /*求右子樹的深度*/
        MaxH = (HL > HR)? HL : HR; /*取左右子樹較大的深度*/
        return ( MaxH + 1 ); /*返回樹的深度*/
    } else return 0; /* 空樹深度爲0 */
}

3.給出中序遍歷和先序遍歷的序列,構建完整二叉樹

  • 根據先序遍歷序列第一個結點肯定根結點;
  • 根據根結點在中序遍歷序列中分割出左右兩個子序列;
  • 對左子樹和右子樹分別遞歸使用相同的方法繼續分解。
BiTNode* CreatBiTree(char *pre,char *in,int n) {
    if(n<=0) return NULL;
    BiTree bt;
    bt=(BiTree)malloc(sizeof(BiTree));
    bt->data=pre[0];
    char *p=strchr(in,pre[0]);
    int len=p-in;
    bt->lchild=CreatBiTree(pre+1,in,len);
    bt->rchild=CreatBiTree(pre+len+1,p+1,n-len-1);
    return bt;
}

2.5總結

結論:中序序列能夠與先序序列、後序序列、層序序列中的任意一個來構建惟一的二叉樹,然後三者兩兩搭配或是三個一塊兒上都沒法構建惟一的二叉樹。緣由是先序、後序、層序均是提供根節點,做用是相同的,沒法惟一肯定一顆二叉樹,都必須由中序遍歷序列來區分出左右子樹。

3.二叉查找樹

二叉搜索樹(BST,Binary Search Tree),也稱二叉排序樹或二叉查找樹。

二叉搜索樹:一棵二叉樹,能夠爲空;若是不爲空,知足如下性質:

  1. 非空左子樹的全部鍵值小於其根結點的鍵值。
  2. 非空右子樹的全部鍵值大於其根結點的鍵值。
  3. 左、右子樹都是二叉搜索樹。

Snipaste_2019-11-17_10-20-57.png

能夠理解爲二叉查找樹就是順序存儲結構的二分查找的另外一種存儲結構。

二叉查找樹的常見操做集

  • BinTree Find( ElementType X, BinTree BST )

    從二叉搜索樹BST中查找元素X,返回其所在結點的地址;

    算法分析

    查找從根結點開始,若是樹爲空,返回NULL
    若搜索樹非空,則根結點關鍵字和X進行比較,並進行不一樣處理:
    若X小於根結點鍵值,只需在左子樹中繼續搜索;
    若是X大於根結點的鍵值,在右子樹中進行繼續搜索;
    若二者比較結果是相等,搜索完成,返回指向此結點的指針。

    遞歸實現

    BinTree Find( ElementType X, BinTree BST ) {
      if( !BST ) return NULL; /*查找失敗*/
      if( X > BST->Data )
          return Find( X, BST->Right ); /*在右子樹中繼續查找*/
      else if( X < BST->Data )
          return Find( X, BST->Left ); /*在左子樹中繼續查找*/
      else /* X == BST->Data */
          return BST; /*查找成功,返回結點的找到結點的地址*/
    }

    迭代實現

    BinTree IterFind( ElementType X, BinTree BST ) {
      while( BST ) {
          if( X > BST->Data )
              BST = BST->Right; /*向右子樹中移動,繼續查找*/
          else if( X < BST->Data )
              BST = BST->Left; /*向左子樹中移動,繼續查找*/
          else /* X == BST->Data */
              return BST; /*查找成功,返回結點的找到結點的地址*/
      }
      return NULL; /*查找失敗*/
    }

    查找的效率取決於樹的高度,讓我聯想到了二叉樹的最大高度爲log2N(以2爲底,N的對數,N爲二叉樹的總節點個數)。

  • BinTree FindMin( BinTree BST )

    從二叉搜索樹BST中查找並返回最小元素所在結點的地址;

    算法分析

    最小元素必定是在樹的最左分枝的端結點上

    遞歸實現

    BinTree FindMin( BinTree BST ) {
      if( !BST ) return NULL; /*空的二叉搜索樹,返回NULL*/
      else if( !BST->Left )
          return BST; /*找到最左葉結點並返回*/
      else
          return FindMin( BST->Left ); /*沿左分支繼續查找*/
    }

    迭代實現

    BinTree FindMin( BinTree BST ) {
      if(BST)
          while( BST->Left ) BST = BST->Left;/*沿左分支繼續查找,直到最左葉結點*/
      return BST;
    }
  • BinTree FindMax( BinTree BST )

    從二叉搜索樹BST中查找並返回最大元素所在結點的地址。

    算法分析

    最大元素必定是在樹的最右分枝的端結點上

    遞歸實現

    BinTree FindMax( BinTree BST ) {
      if( !BST ) return NULL; /*空的二叉搜索樹,返回NULL*/
      else if( !BST->Right )
          return BST; /*找到最右葉結點並返回*/
      else
          return FindMin( BST->Right ); /*沿右分支繼續查找*/
    }

    迭代實現

    BinTree FindMax( BinTree BST ) {
      if(BST )
          while( BST->Right ) BST = BST->Right;/*沿右分支繼續查找,直到最右葉結點*/
      return BST;
    }
  • BinTree Insert( ElementType X, BinTree BST )

    算法分析

    關鍵是要找到元素應該插入的位置,能夠採用與Find相似的方法

    遞歸實現

    BinTree Insert( ElementType X, BinTree BST ) {
      if( !BST ) {
          /*若原樹爲空,生成並返回一個結點的二叉搜索樹*/
          BST = malloc(sizeof(struct TreeNode));
          BST->Data = X;
          BST->Left = BST->Right = NULL;
      } else /*開始找要插入元素的位置*/
          if( X < BST->Data )
              BST->Left = Insert( X, BST->Left);/*遞歸插入左子樹*/
          else if( X > BST->Data )
              BST->Right = Insert( X, BST->Right);/*遞歸插入右子樹*/
      /* else X已經存在,什麼都不作 */
      return BST;
    }

    迭代實現

    BinTree Insert( ElementType X, BinTree BST ) {
      while( BST ) {
          if( X > BST->Data )
              BST = BST->Right; /*向右子樹中移動,繼續查找*/
          else if( X < BST->Data )
              BST = BST->Left; /*向左子樹中移動,繼續查找*/
          else /* X == BST->Data */
              return BST; /*查找成功,返回結點的找到結點的地址*/
      }
        if( !BST ) {
          BST = malloc(sizeof(struct TreeNode));
          BST->Data = X;
          BST->Left = BST->Right = NULL;
      } 
      return BST; 
    }
  • BinTree Delete( ElementType X, BinTree BST )

    算法分析

    要考慮三種狀況

    • 要刪除的是葉結點:直接刪除,並再修改其父結點指針---置爲NULL
    • 要刪除的結點只有一個孩子結點: 將其父結點的指針指向要刪除結點的孩子結點
    • 要刪除的結點有左、右兩棵子樹: 用另外一結點替代被刪除結點:右子樹的最小元素 或者 左子樹的最大元素

    遞歸實現

    BinTree Delete( ElementType X, BinTree BST ) {
      Position Tmp;
      if( !BST ) printf("要刪除的元素未找到");
      else if( X < BST->Data )
          BST->Left = Delete( X, BST->Left); /* 左子樹遞歸刪除 */
      else if( X > BST->Data )
          BST->Right = Delete( X, BST->Right); /* 右子樹遞歸刪除 */
      else /*找到要刪除的結點 */
          if( BST->Left && BST->Right ) { /*被刪除結點有左右兩個子結點 */
              Tmp = FindMin( BST->Right );
              /*在右子樹中找最小的元素填充刪除結點*/
              BST->Data = Tmp->Data;
              BST->Right = Delete( BST->Data, BST->Right);
              /*在刪除結點的右子樹中刪除最小元素*/
          } else { /*被刪除結點有一個或無子結點*/
              Tmp = BST;
              if( !BST->Left ) /* 有右孩子或無子結點*/
                  BST = BST->Right;
              else if( !BST->Right ) /*有左孩子或無子結點*/
                  BST = BST->Left;
              free( Tmp );
          }
      return BST;
    }

    迭代實現

    Status Delete(BTreePtr T, ElemType e) {
        BTreePtr p, pp, minP, minPP, child;
        child = NULL;
        p = T;
        pp = NULL;
    
        while ( (p != NULL) && (p->data != e) ) {
            pp = p;
    
            if (e > p->data) {
                p = p->rchild;
            } else {
                p = p->lchild;
            }
        }
    
        if (p == NULL) return FALSE;
    
        //雙節點
        if ((p->lchild != NULL) && (p->rchild != NULL))
        {
            minPP = p;
            minP = p->rchild;
    
            while (minP->lchild != NULL) {
                minPP = minP;
                minP = minP->lchild;
            }
            p->data = minP->data;
            minPP->lchild = minP->rchild;
            free(minP);
    
            return TRUE;
        }
    
        //有一個節點
        if ((p->lchild != NULL) || (p->rchild != NULL)) { //應該將原有的pp同child鏈接在一塊兒
    
            if (p->lchild) {
                child = p->lchild;
            } else {
               child = p->rchild;
            }
            if(pp->data>p->data)
            {
                pp->lchild=child;
            } else
            {
                pp->rchild=child;
            }
            free(p);
            return TRUE;
        }
    
        //沒有節點
        if (pp->lchild == p) {//這裏面臨pp除p之外的節點爲null的狀況
            pp->lchild = child;
        } else {
            pp->rchild = child;
        }
    
        return TRUE;
    }

4.平衡二叉樹

Snipaste_2019-11-17_11-44-06.png

平衡因子(Balance Factor,簡稱BF): BF(T) = hL-hR,其中hL和hR分別爲T的左、右子樹的高度。

平衡二叉樹(Balanced Binary Tree)(AVL樹)空樹,或者任一結點左、右子樹高度差的絕對值不超過1,即|BF(T) |≤ 1

徹底二叉樹、滿二叉樹其實都是平衡二叉樹,可是非徹底二叉樹也有多是平衡二叉樹。

好比:

Snipaste_2019-11-17_11-45-45.png

平衡二叉樹的調整

Snipaste_2019-11-17_11-49-33.png

Snipaste_2019-11-17_11-50-29.png

Snipaste_2019-11-17_11-53-07.png

Snipaste_2019-11-17_11-54-39.png

typedef struct AVLNode *Position;
typedef Position AVLTree; /* AVL樹類型 */
struct AVLNode{
    ElementType Data; /* 結點數據 */
    AVLTree Left;     /* 指向左子樹 */
    AVLTree Right;    /* 指向右子樹 */
    int Height;       /* 樹高 */
};
 
int Max ( int a, int b )
{
    return a > b ? a : b;
}
 
AVLTree SingleLeftRotation ( AVLTree A )
{ /* 注意:A必須有一個左子結點B */
  /* 將A與B作左單旋,更新A與B的高度,返回新的根結點B */     
 
    AVLTree B = A->Left;
    A->Left = B->Right;
    B->Right = A;
    A->Height = Max( GetHeight(A->Left), GetHeight(A->Right) ) + 1;
    B->Height = Max( GetHeight(B->Left), A->Height ) + 1;
  
    return B;
}
 
AVLTree DoubleLeftRightRotation ( AVLTree A )
{ /* 注意:A必須有一個左子結點B,且B必須有一個右子結點C */
  /* 將A、B與C作兩次單旋,返回新的根結點C */
     
    /* 將B與C作右單旋,C被返回 */
    A->Left = SingleRightRotation(A->Left);
    /* 將A與C作左單旋,C被返回 */
    return SingleLeftRotation(A);
}
 
/*************************************/
/* 對稱的右單旋與右-左雙旋請本身實現 */
/*************************************/
 
AVLTree Insert( AVLTree T, ElementType X )
{ /* 將X插入AVL樹T中,而且返回調整後的AVL樹 */
    if ( !T ) { /* 若插入空樹,則新建包含一個結點的樹 */
        T = (AVLTree)malloc(sizeof(struct AVLNode));
        T->Data = X;
        T->Height = 0;
        T->Left = T->Right = NULL;
    } /* if (插入空樹) 結束 */
 
    else if ( X < T->Data ) {
        /* 插入T的左子樹 */
        T->Left = Insert( T->Left, X);
        /* 若是須要左旋 */
        if ( GetHeight(T->Left)-GetHeight(T->Right) == 2 )
            if ( X < T->Left->Data ) 
               T = SingleLeftRotation(T);      /* 左單旋 */
            else 
               T = DoubleLeftRightRotation(T); /* 左-右雙旋 */
    } /* else if (插入左子樹) 結束 */
     
    else if ( X > T->Data ) {
        /* 插入T的右子樹 */
        T->Right = Insert( T->Right, X );
        /* 若是須要右旋 */
        if ( GetHeight(T->Left)-GetHeight(T->Right) == -2 )
            if ( X > T->Right->Data ) 
               T = SingleRightRotation(T);     /* 右單旋 */
            else 
               T = DoubleRightLeftRotation(T); /* 右-左雙旋 */
    } /* else if (插入右子樹) 結束 */
 
    /* else X == T->Data,無須插入 */
 
    /* 別忘了更新樹高 */
    T->Height = Max( GetHeight(T->Left), GetHeight(T->Right) ) + 1;
     
    return T;
}

平衡二叉查找樹不只知足上面平衡二叉樹的定義,還知足二叉查找樹的特色。最早被髮明的平衡二叉查找樹是AVL 樹,它嚴格符合我剛講到的平衡二叉查找樹的定義,即任何節點的左右子樹高度相差不超過 1,是一種高度平衡的二叉查找樹。

5.堆

堆(英語:Heap)是計算機科學中的一種特別的樹狀數據結構。如果知足如下特性,便可稱爲堆:「給定堆中任意節點P和C,若P是C的母節點,那麼P的值會小於等於(或大於等於)C的值」。若母節點的值恆小於等於子節點的值,此堆稱爲最小堆(min heap);反之,若母節點的值恆大於等於子節點的值,此堆稱爲最大堆(max heap)。在堆中最頂端的那一個節點,稱做根節點(root node),根節點自己沒有母節點(parent node)。

堆始於J. W. J. Williams在1964年發表的堆排序(heap sort),當時他提出了二叉堆樹做爲此算法的數據結構。堆在戴克斯特拉算法(英語:Dijkstra's algorithm)中亦爲重要的關鍵。

在隊列中,調度程序反覆提取隊列中第一個做業並運行,由於實際狀況中某些時間較短的任務將等待很長時間才能結束,或者某些不短小,但具備重要性的做業,一樣應當具備優先權。堆即爲解決此類問題設計的一種數據結構。

優先隊列(Priority Queue):特殊的「隊列」,取出元素的順序是依照元素的優先權(關鍵字)大小,而不是元素進入隊列的前後順序。

Snipaste_2019-11-17_22-01-46.png

堆的兩個特性

  • 結構性:用數組表示的徹底二叉樹;
  • 有序性:任一結點的關鍵字是其子樹全部結點的最大值(或最小值)
    • 「最大堆(MaxHeap)」,也稱「大頂堆」:最大值
    • 「最小堆(MinHeap)」,也稱「小頂堆」 :最小值

Snipaste_2019-11-17_22-03-24.png

注意:從根結點到任意結點路徑上結點序列的有序性!

5.1堆的抽象數據類型描述

類型名稱:最大堆(MaxHeap)

數據對象集:徹底二叉樹,每一個結點的元素值不小於其子結點的元素值

操做集:最大堆H  MaxHeap,元素item  ElementType,主要操做有:
•MaxHeap Create( int MaxSize ):建立一個空的最大堆。
•Boolean IsFull( MaxHeap H ):判斷最大堆H是否已滿。
•Insert( MaxHeap H, ElementType item ):將元素item插入最大堆H。
•Boolean IsEmpty( MaxHeap H ):判斷最大堆H是否爲空。
•ElementType DeleteMax( MaxHeap H ):返回H中最大元素(高優先級)。

5.2最大堆的操做

1.結構體定義

typedef struct HeapStruct *MaxHeap;
struct HeapStruct {
    ElementType *Elements; /* 存儲堆元素的數組 */
    int Size; /* 堆的當前元素個數 */
    int Capacity; /* 堆的最大容量 */
};

2.最大堆的建立

MaxHeap Create( int MaxSize ) {
    /* 建立容量爲MaxSize的空的最大堆 */
    MaxHeap H = malloc( sizeof( struct HeapStruct ) );
    H->Elements = malloc( (MaxSize+1) * sizeof(ElementType));
    H->Size = 0;
    H->Capacity = MaxSize;
    H->Elements[0] = MaxData;
    /* 定義「哨兵」爲大於堆中全部可能元素的值,便於之後更快操做 */
    return H;
}

3.最大堆的插入

將新增結點插入到從其父結點到根結點的有序序列中

void Insert( MaxHeap H, ElementType item ) {
    /* 將元素item 插入最大堆H,其中H->Elements[0]已經定義爲哨兵 */
    int i;
    if ( IsFull(H) ) {
        printf("最大堆已滿");
        return;
    }
    i = ++H->Size; /* i指向插入後堆中的最後一個元素的位置 */
    for ( ; H->Elements[i/2] < item; i/=2 )/*H->Element[ 0 ] 是哨兵元素,它不小於堆中的最大元素,控制循環結束。*/
        H->Elements[i] = H->Elements[i/2]; /* 向下過濾結點 */
    H->Elements[i] = item; /* 將item 插入 */
}

T (N) = O ( log N )

4.最大堆的刪除

ElementType DeleteMax( MaxHeap H ) {
    /* 從最大堆H中取出鍵值爲最大的元素,並刪除一個結點 */
    int Parent, Child;
    ElementType MaxItem, temp;
    if ( IsEmpty(H) ) {
        printf("最大堆已爲空");
        return;
    }
    MaxItem = H->Elements[1]; /* 取出根結點最大值 */
    /* 用最大堆中最後一個元素從根結點開始向上過濾下層結點 */
    temp = H->Elements[H->Size--];
    for( Parent=1; Parent*2<=H->Size; Parent=Child ) {
        Child = Parent * 2;
        if( (Child!= H->Size) &&
                (H->Elements[Child] < H->Elements[Child+1]) )
            Child++; /* Child指向左右子結點的較大者 */
        if( temp >= H->Elements[Child] ) break;
        else /* 移動temp元素到下一層 */
            H->Elements[Parent] = H->Elements[Child];
    }
    H->Elements[Parent] = temp;
    return MaxItem;
}

5.最大堆的創建

創建最大堆:將已經存在的N個元素按最大堆的要求存放在一個一維數組中

方法1:經過插入操做,將N個元素一個個相繼插入到一個初始爲空的堆中去,其時間代價最大爲O(N logN)。

方法2:在線性時間複雜度下創建最大堆。
(1)將N個元素按輸入順序存入,先知足徹底二叉樹的結構特性
(2)調整各結點位置,以知足最大堆的有序特性。

/*----------- 建造最大堆 -----------*/
void PercDown( MaxHeap H, int p )
{ /* 下濾:將H中以H->Data[p]爲根的子堆調整爲最大堆 */
    int Parent, Child;
    ElementType X;
 
    X = H->Data[p]; /* 取出根結點存放的值 */
    for( Parent=p; Parent*2<=H->Size; Parent=Child ) {
        Child = Parent * 2;
        if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
            Child++;  /* Child指向左右子結點的較大者 */
        if( X >= H->Data[Child] ) break; /* 找到了合適位置 */
        else  /* 下濾X */
            H->Data[Parent] = H->Data[Child];
    }
    H->Data[Parent] = X;
}
 
void BuildHeap( MaxHeap H )
{ /* 調整H->Data[]中的元素,使知足最大堆的有序性  */
  /* 這裏假設全部H->Size個元素已經存在H->Data[]中 */
 
    int i;
 
    /* 從最後一個結點的父節點開始,到根結點1 */
    for( i = H->Size/2; i>0; i-- )
        PercDown( H, i );
}

6.哈夫曼樹&哈夫曼編碼

Snipaste_2019-11-17_22-19-21.png

Snipaste_2019-11-17_22-19-56.png

Snipaste_2019-11-17_22-22-40.png

6.1哈夫曼樹的定義

帶權路徑長度(WPL):設二叉樹有n個葉子結點,每一個葉子結點帶有權值 wk,從根結點到每一個葉子結點的長度爲 lk,則每一個葉子結點的帶權路徑長度之和就是:

clip_image002.png

最優二叉樹或哈夫曼樹: WPL最小的二叉樹

6.2構造哈夫曼樹

typedef struct TreeNode *HuffmanTree;
struct TreeNode {
    int Weight;
    HuffmanTree Left, Right;
}
HuffmanTree Huffman( MinHeap H ) {
    /* 假設H->Size個權值已經存在H->Elements[]->Weight裏 */
    int i;
    HuffmanTree T;
    BuildMinHeap(H); /*將H->Elements[]按權值調整爲最小堆*/
    for (i = 1; i < H->Size; i++) { /*作H->Size-1次合併*/
        T = malloc( sizeof( struct TreeNode) ); /*創建新結點*/
        T->Left = DeleteMin(H);
        /*從最小堆中刪除一個結點,做爲新T的左子結點*/
        T->Right = DeleteMin(H);
        /*從最小堆中刪除一個結點,做爲新T的右子結點*/
        T->Weight = T->Left->Weight+T->Right->Weight;
        /*計算新權值*/
        Insert( H, T ); /*將新T插入最小堆*/
    }
    T = DeleteMin(H);
    return T;
}

6.3哈夫曼樹的特色

  • 沒有度爲1的結點;
  • 哈夫曼樹的任意非葉節點的左右子樹交換後還是哈夫曼樹;
  • n個葉子結點的哈夫曼樹共有2n-1個結點;

對同一組權值{w1 ,w2 , …… , wn},是否存在不一樣構的兩棵哈夫曼樹呢?
對一組權值{ 1, 2 , 3, 3 },不一樣構的兩棵哈夫曼樹:

Snipaste_2019-11-17_22-34-58.png

6.4哈夫曼編碼

給定一段字符串,如何對字符進行編碼,可使得該字符串的編碼存儲空間最少?
[例] 假設有一段文本,包含58個字符,並由如下7個字符構成:a,e,i,s,t,空格(sp),換行(nl);這7個字符出現的次數不一樣。如何對這7個字符進行編碼,使得總編碼空間最少?
【分析】
(1)用等長ASCII編碼:58 ×8 = 464位;
(2)用等長3位編碼:58 ×3 = 174位;
(3)不等長編碼:出現頻率高的字符用的編碼短些,出現頻率低的字符則能夠編碼長些?

Snipaste_2019-11-17_22-38-27.png

Snipaste_2019-11-17_22-37-38.png

7.B樹

B樹的應用能夠參考另一篇文章

8.字典樹Trie

undefined

Trie 樹,也叫「字典樹」。顧名思義,它是一個樹形結構。它是一種專門處理字符串匹配的數據結構,用來解決在一組字符串集合中快速查找某個字符串的問題。它的一個經典應用場景就是輸入框的自動提示。

舉個例子來講明一下,咱們有 6 個字符串,它們分別是:how,hi,her,hello,so,see。咱們但願在裏面屢次查找某個字符串是否存在。若是每次查找,都是拿要查找的字符串跟這 6 個字符串依次進行字符串匹配,那效率就比較低。咱們就能夠先對這 6 個字符串作一下預處理,組織成 Trie 樹的結構,以後每次查找,都是在 Trie 樹中進行匹配查找。Trie 樹的本質,就是利用字符串之間的公共前綴,將重複的前綴合並在一塊兒。最後構造出來的就是下面這個圖中的樣子。

undefined

根節點不包含任何信息。每一個節點表示一個字符串中的字符,從根節點到紅色節點的一條路徑表示一個字符串(注意:紅色節點並不都是葉子節點)。

Trie樹的構造過程以下:

undefined

undefined

當咱們在 Trie 樹中查找一個字符串的時候,好比查找字符串「her」,那咱們將要查找的字符串分割成單個的字符 h,e,r,而後從 Trie 樹的根節點開始匹配。如圖所示,綠色的路徑就是在 Trie 樹中匹配的路徑。

undefined

若是咱們要查找的是字符串「he」呢?咱們還用上面一樣的方法,從根節點開始,沿着某條路徑來匹配,如圖所示,綠色的路徑,是字符串「he」匹配的路徑。可是,路徑的最後一個節點「e」並非紅色的。也就是說,「he」是某個字符串的前綴子串,但並不能徹底匹配任何字符串。

undefined

8.1字典樹的存儲結構

對於前面的trie樹的邏輯存儲結構,能夠理解爲下面這幅圖

undefined

8.2字典樹的代碼實現及經常使用操做

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

typedef int Status;

typedef struct Node {
    char data;
    struct Node *children[26];
    Status end;
} Trie, *TriePtr;

void Init(TriePtr *T)
{
    (*T) = (TriePtr)malloc(sizeof(Trie));
    (*T)->data = '/';
    (*T)->end = FALSE;
}

void Insert(TriePtr T, char *str) {

    int index;
    char c;

    while(c = *str++)
    {
        index = c - 'a';
        if (T->children[index] == NULL)
        {
            TriePtr Node;
            Node = (TriePtr)malloc(sizeof(Trie));
            Node->data = c;
            Node->end = FALSE;
            T->children[index] = Node;
        }

        T = T->children[index];
    }

    T->end = TRUE;
}


Status Search(TriePtr T, char *str) {

    int index;
    char c;

    while(c = *str++)
    {
        index = c - 'a';
        if (T->children[index] == NULL)
        {
            return FALSE;
        }

        T = T->children[index];
    }

    if (T->end) {
        return TRUE;
    } else {
        return FALSE;
    }
}


int main(int argc, char const *argv[])
{
    TriePtr T;
    Init(&T);
    char *str = "hello";
    char *str2 = "hi";

    Insert(T, str);

    printf("str is search %d\n", Search(T, str));
    printf("str2 is search %d\n", Search(T, str2));
    return 0;
}

8.3複雜度分析

時間複雜度

若是要在一組字符串中,頻繁地查詢某些字符串,用 Trie 樹會很是高效。構建 Trie 樹的過程,須要掃描全部的字符串,時間複雜度是 O(n)(n 表示全部字符串的長度和)。可是一旦構建成功以後,後續的查詢操做會很是高效。每次查詢時,若是要查詢的字符串長度是 k,那咱們只須要比對大約 k 個節點,就能完成查詢操做。跟本來那組字符串的長度和個數沒有任何關係。因此說,構建好 Trie 樹後,在其中查找字符串的時間複雜度是 O(k),k 表示要查找的字符串的長度。

空間複雜度

Trie 樹是很是耗內存的,用的是一種空間換時間的思路。

剛剛咱們在講 Trie 樹的實現的時候,講到用數組來存儲一個節點的子節點的指針。若是字符串中包含從 a 到 z 這 26 個字符,那每一個節點都要存儲一個長度爲 26 的數組,而且每一個數組存儲一個 8 字節指針(或者是 4 字節,這個大小跟 CPU、操做系統、編譯器等有關)。並且,即使一個節點只有不多的子節點,遠小於 26 個,好比 三、4 個,咱們也要維護一個長度爲 26 的數組。Trie 樹的本質是避免重複存儲一組字符串的相同前綴子串,可是如今每一個字符(對應一個節點)的存儲遠遠大於 1 個字節。按照咱們上面舉的例子,數組長度爲 26,每一個元素是 8 字節,那每一個節點就會額外須要 26*8=208 個字節。並且這仍是隻包含 26 個字符的狀況。若是字符串中不只包含小寫字母,還包含大寫字母、數字、甚至是中文,那須要的存儲空間就更多了。因此在某些狀況下,Trie 樹不必定會節省存儲空間。在重複的前綴並很少的狀況下,Trie 樹不但不能節省內存,還有可能會浪費更多的內存。

8.4Trie樹和紅黑樹、散列表的比較

在剛剛講的這個場景,在一組字符串中查找字符串,Trie 樹實際上表現得並很差。它對要處理的字符串有及其嚴苛的要求。

  • 第一,字符串中包含的字符集不能太大。咱們前面講到,若是字符集太大,那存儲空間可能就會浪費不少。即使能夠優化,但也要付出犧牲查詢、插入效率的代價。
  • 第二,要求字符串的前綴重合比較多,否則空間消耗會變大不少。
  • 第三,若是要用 Trie 樹解決問題,那咱們就要本身從零開始實現一個 Trie 樹,還要保證沒有 bug,這個在工程上是將簡單問題複雜化,除非必須,通常不建議這樣作。
  • 第四,咱們知道,經過指針串起來的數據塊是不連續的,而 Trie 樹中用到了指針,因此,對緩存並不友好,性能上會打個折扣。

綜合這幾點,針對在一組字符串中查找字符串的問題,咱們在工程中,更傾向於用散列表或者紅黑樹。由於這兩種數據結構,咱們都不須要本身去實現,直接利用編程語言中提供的現成類庫就好了。

9.紅黑樹

此章節內容都是參考極客時間專欄-《數據結構與算法之美》

9.1紅黑樹的定義

平衡二叉查找樹其實有不少,好比,Splay Tree(伸展樹)、Treap(樹堆)等,可是咱們提到平衡二叉查找樹,聽到的基本都是紅黑樹。它的出鏡率甚至要高於「平衡二叉查找樹」這幾個字,有時候,咱們甚至默認平衡二叉查找樹就是紅黑樹。

紅黑樹的英文是「Red-Black Tree」,簡稱 R-B Tree。它是一種不嚴格的平衡二叉查找樹。顧名思義,紅黑樹中的節點,一類被標記爲黑色,一類被標記爲紅色。除此以外,一棵紅黑樹還須要知足這樣幾個要求:

  • 根節點是黑色的;
  • 每一個葉子節點都是黑色的空節點(NIL),也就是說,葉子節點不存儲數據;
  • 任何相鄰的節點都不能同時爲紅色,也就是說,紅色節點是被黑色節點隔開的;
  • 每一個節點,從該節點到達其可達葉子節點的全部路徑,都包含相同數目的黑色節點;

這裏的第二點要求「葉子節點都是黑色的空節點」,稍微有些奇怪,它主要是爲了簡化紅黑樹的代碼實現而設置的。它的結構圖案例以下,圖中去掉了黑色的、空的葉子節點

undefined

爲何說紅黑樹是「近似平衡」的?

平衡二叉查找樹的初衷,是爲了解決二叉查找樹由於動態更新致使的性能退化問題。因此,「平衡」的意思能夠等價爲性能不退化。「近似平衡」就等價爲性能不會退化的太嚴重。

二叉查找樹不少操做的性能都跟樹的高度成正比。一棵極其平衡的二叉樹(滿二叉樹或徹底二叉樹)的高度大約是 log2n,因此若是要證實紅黑樹是近似平衡的,咱們只須要分析,紅黑樹的高度是否比較穩定地趨近 log2n 。

接下來一步一步推導紅黑樹的高度。

首先,咱們來看,若是咱們將紅色節點從紅黑樹中去掉,那單純包含黑色節點的紅黑樹的高度是多少呢?

紅色節點刪除以後,有些節點就沒有父節點了,它們會直接拿這些節點的祖父節點(父節點的父節點)做爲父節點。因此,以前的二叉樹就變成了四叉樹。

undefined

前面紅黑樹的定義裏有這麼一條:從任意節點到可達的葉子節點的每一個路徑包含相同數目的黑色節點。咱們從四叉樹中取出某些節點,放到葉節點位置,四叉樹就變成了徹底二叉樹。因此,僅包含黑色節點的四叉樹的高度,比包含相同節點個數的徹底二叉樹的高度還要小,因此去掉紅色節點的「黑樹」的高度也不會超過 log2n。

如今把紅色節點加回去,高度會變成多少呢?

在紅黑樹中,紅色節點不能相鄰,也就是說,有一個紅色節點就要至少有一個黑色節點,將它跟其餘紅色節點隔開。紅黑樹中包含最多黑色節點的路徑不會超過 log2n,因此加入紅色節點以後,最長路徑不會超過 2log2n,也就是說,紅黑樹的高度近似 2log2n。因此,紅黑樹的高度只比高度平衡的 AVL 樹的高度(log2n)僅僅大了一倍,在性能上,降低得並很少。這樣推導出來的結果不夠精確,實際上紅黑樹的性能更好。

爲何在工程中你們都喜歡用紅黑樹這種平衡二叉查找樹?

前面提到 Treap、Splay Tree,絕大部分狀況下,它們操做的效率都很高,可是也沒法避免極端狀況下時間複雜度的退化。儘管這種狀況出現的機率不大,可是對於單次操做時間很是敏感的場景來講,它們並不適用。AVL 樹是一種高度平衡的二叉樹,因此查找的效率很是高,可是,有利就有弊,AVL 樹爲了維持這種高度的平衡,就要付出更多的代價。每次插入、刪除都要作調整,就比較複雜、耗時。因此,對於有頻繁的插入、刪除操做的數據集合,使用 AVL 樹的代價就有點高了。紅黑樹只是作到了近似平衡,並非嚴格的平衡,因此在維護平衡的成本上,要比 AVL 樹要低。因此,紅黑樹的插入、刪除、查找各類操做性能都比較穩定。對於工程應用來講,要面對各類異常狀況,爲了支撐這種工業級的應用,咱們更傾向於這種性能穩定的平衡二叉查找樹。

9.2紅黑樹的實現

紅黑樹的創建過程會頻繁的破壞它的定義中的第三四點,而咱們要作的就是把破壞的點恢復過來。在這以前,須要掌握兩個很是重要的操做,左旋和右旋,能夠參考以下的圖片來理解。

undefined

1.插入操做的平衡調整

紅黑樹規定,插入的節點必須是紅色的。並且,二叉查找樹中新插入的節點都是放在葉子節點上。因此,關於插入操做的平衡調整,有這樣兩種特殊狀況,可是也都很是好處理。

  • 若是插入節點的父節點是黑色的,那咱們什麼都不用作,它仍然知足紅黑樹的定義。

  • 若是插入的節點是根節點,那咱們直接改變它的顏色,把它變成黑色就能夠了。

除此以外,其餘狀況都會違背紅黑樹的定義,因而咱們就須要進行調整,調整的過程包含兩種基礎的操做:左右旋轉改變顏色

紅黑樹的平衡調整過程是一個迭代的過程。咱們把正在處理的節點叫做關注節點。關注節點會隨着不停地迭代處理,而不斷髮生變化。最開始的關注節點就是新插入的節點。新節點插入以後,若是紅黑樹的平衡被打破,那通常會有下面三種狀況。咱們只須要根據每種狀況的特色,不停地調整,就可讓紅黑樹繼續符合定義,也就是繼續保持平衡。咱們下面依次來看每種狀況的調整過程。提醒你注意下,爲了簡化描述,我把父節點的兄弟節點叫做叔叔節點,父節點的父節點叫做祖父節點。

1.若是關注節點是 a,它的叔叔節點 d 是紅色
  • 將關注節點 a 的父節點 b、叔叔節點 d 的顏色都設置成黑色;
  • 將關注節點 a 的祖父節點 c 的顏色設置成紅色;
  • 關注節點變成 a 的祖父節點 c;
  • 跳到 CASE 2 或者 CASE 3。

undefined

2.若是關注節點是 a,它的叔叔節點 d 是黑色,關注節點 a 是其父節點 b 的右子節點
  • 關注節點變成節點 a 的父節點 b;
  • 圍繞新的關注節點b 左旋;
  • 跳到 CASE 3。

undefined

3.若是關注節點是 a,它的叔叔節點 d 是黑色,關注節點 a 是其父節點 b 的左子節點
  • 圍繞關注節點 a 的祖父節點 c 右旋;
  • 將關注節點 a 的父節點 b、兄弟節點 c 的顏色互換。
  • 調整結束。

undefined

2.刪除操做的平衡調整

刪除操做的平衡調整分爲兩步,第一步是針對刪除節點初步調整。初步調整隻是保證整棵紅黑樹在一個節點刪除以後,仍然知足最後一條定義的要求,也就是說,每一個節點,從該節點到達其可達葉子節點的全部路徑,都包含相同數目的黑色節點;第二步是針對關注節點進行二次調整,讓它知足紅黑樹的第三條定義,即不存在相鄰的兩個紅色節點。

1. 針對刪除節點初步調整

這裏須要注意一下,紅黑樹的定義中「只包含紅色節點和黑色節點」,通過初步調整以後,爲了保證知足紅黑樹定義的最後一條要求,有些節點會被標記成兩種顏色,「紅 - 黑」或者「黑 - 黑」。若是一個節點被標記爲了「黑 - 黑」,那在計算黑色節點個數的時候,要算成兩個黑色節點。在下面的講解中,若是一個節點既能夠是紅色,也能夠是黑色,在畫圖的時候,我會用一半紅色一半黑色來表示。若是一個節點是「紅 - 黑」或者「黑 - 黑」,我會用左上角的一個小黑點來表示額外的黑色。

CASE 1:若是要刪除的節點是 a,它只有一個子節點 b
  • 刪除節點 a,而且把節點 b 替換到節點 a 的位置,這一部分操做跟普通的二叉查找樹的刪除操做同樣;
  • 節點 a 只能是黑色,節點 b 也只能是紅色,其餘狀況均不符合紅黑樹的定義。這種狀況下,咱們把節點 b 改成黑色;
  • 調整結束,不須要進行二次調整。

undefined

CASE 2:若是要刪除的節點 a 有兩個非空子節點,而且它的後繼節點就是節點 a 的右子節點 c
  • 若是節點 a 的後繼節點就是右子節點 c,那右子節點 c 確定沒有左子樹。咱們把節點 a 刪除,而且將節點 c 替換到節點 a 的位置。這一部分操做跟普通的二叉查找樹的刪除操做無異;
  • 而後把節點 c 的顏色設置爲跟節點 a 相同的顏色;
  • 若是節點 c 是黑色,爲了避免違反紅黑樹的最後一條定義,咱們給節點 c 的右子節點 d 多加一個黑色,這個時候節點 d 就成了「紅 - 黑」或者「黑 - 黑」;
  • 這個時候,關注節點變成了節點 d,第二步的調整操做就會針對關注節點來作。

undefined

CASE 3:若是要刪除的是節點 a,它有兩個非空子節點,而且節點 a 的後繼節點不是右子節點
  • 找到後繼節點 d,並將它刪除,刪除後繼節點 d 的過程參照 CASE 1;
  • 將節點 a 替換成後繼節點 d;
  • 把節點 d 的顏色設置爲跟節點 a 相同的顏色;
  • 若是節點 d 是黑色,爲了避免違反紅黑樹的最後一條定義,咱們給節點 d 的右子節點 c 多加一個黑色,這個時候節點 c 就成了「紅 - 黑」或者「黑 - 黑」;
  • 這個時候,關注節點變成了節點 c,第二步的調整操做就會針對關注節點來作。

undefined

2.針對關注節點進行二次調整

通過初步調整以後,關注節點變成了「紅 - 黑」或者「黑 - 黑」節點。針對這個關注節點,咱們再分四種狀況來進行二次調整。二次調整是爲了讓紅黑樹中不存在相鄰的紅色節點。

CASE 1:若是關注節點是 a,它的兄弟節點 c 是紅色的
  • 圍繞關注節點 a 的父節點 b 左旋;
  • 關注節點 a 的父節點 b 和祖父節點 c 交換顏色;
  • 關注節點不變;
  • 繼續從四種狀況中選擇適合的規則來調整。

undefined

CASE 2:若是關注節點是 a,它的兄弟節點 c 是黑色的,而且節點 c 的左右子節點 d、e 都是黑色的
  • 將關注節點 a 的兄弟節點 c 的顏色變成紅色;
  • 從關注節點 a 中去掉一個黑色,這個時候節點 a 就是單純的紅色或者黑色;
  • 給關注節點 a 的父節點 b 添加一個黑色,這個時候節點 b 就變成了「紅 - 黑」或者「黑 - 黑」;
  • 關注節點從 a 變成其父節點 b;
  • 繼續從四種狀況中選擇符合的規則來調整。

undefined

CASE 3:若是關注節點是 a,它的兄弟節點 c 是黑色,c 的左子節點 d 是紅色,c 的右子節點 e 是黑色
  • 圍繞關注節點 a 的兄弟節點 c 右旋;
  • 節點 c 和節點 d 交換顏色;
  • 關注節點不變;
  • 跳轉到 CASE 4,繼續調整。

undefined

CASE 4:若是關注節點 a 的兄弟節點 c 是黑色的,而且 c 的右子節點是紅色的
  • 圍繞關注節點 a 的父節點 b 左旋;
  • 將關注節點 a 的兄弟節點 c 的顏色,跟關注節點 a 的父節點 b 設置成相同的顏色;
  • 將關注節點 a 的父節點 b 的顏色設置爲黑色;
  • 從關注節點 a 中去掉一個黑色,節點 a 就變成了單純的紅色或者黑色;
  • 將關注節點 a 的叔叔節點 e 設置爲黑色;
  • 調整結束。

undefined

9.3爲何紅黑樹的定義中,要求葉子節點是黑色的空節點?

之因此有這麼奇怪的要求,其實就是爲了實現起來方便。只要知足這一條要求,那在任什麼時候刻,紅黑樹的平衡操做均可以歸結爲咱們剛剛講的那幾種狀況。

仍是有點很差理解,我經過一個例子來解釋一下。假設紅黑樹的定義中不包含剛剛提到的那一條「葉子節點必須是黑色的空節點」,咱們往一棵紅黑樹中插入一個數據,新插入節點的父節點也是紅色的,兩個紅色的節點相鄰,這個時候,紅黑樹的定義就被破壞了。那咱們應該如何調整呢?img

你會發現,這個時候,咱們前面講的插入時,三種狀況下的平衡調整規則,沒有一種是適用的。可是,若是咱們把黑色的空節點都給它加上,變成下面這樣,你會發現,它知足 CASE 2 了。

img

你可能會說,你能夠調整一下平衡調整規則啊。好比把 CASE 2 改成「若是關注節點 a 的叔叔節點 b 是黑色或者不存在,a 是父節點的右子節點,就進行某某操做」。固然能夠,可是這樣的話規則就沒有原來簡潔了。

你可能還會說,這樣給紅黑樹添加黑色的空的葉子節點,會不會比較浪費存儲空間呢?答案是不會的。雖然咱們在講解或者畫圖的時候,每一個黑色的、空的葉子節點都是獨立畫出來的。實際上,在具體實現的時候,咱們只須要像下面這樣,共用一個黑色的、空的葉子節點就好了。

img

10.跳錶

咱們都知道二分查找算法,有它的侷限性,具體能夠參考文章。二分查找算法的侷限性,主要體如今它須要連續存儲結構,才能根據隨機訪問特性快速的實現二分,而且數組的內容必須是有序的

那麼,若是數據存儲在鏈表中,就真的無法用二分查找算法了嗎?

實際上,咱們只須要對鏈表稍加改造,就能夠支持相似「二分」的查找算法。咱們把改造以後的數據結構叫做跳錶(Skip list)。它確實是一種各方面性能都比較優秀的動態數據結構,能夠支持快速的插入、刪除、查找操做,寫起來也不復雜,甚至能夠替代紅黑樹(Red-black tree)。

10.1跳錶的定義

對於一個單鏈表來說,即使鏈表中存儲的數據是有序的,若是咱們要想在其中查找某個數據,也只能從頭至尾遍歷鏈表。這樣查找效率就會很低,時間複雜度會很高,是 O(n)。

img

每兩個結點提取一個結點到上一級,咱們把抽出來的那一級叫做索引或索引層。

img

若是咱們如今要查找某個結點,好比 16。咱們能夠先在索引層遍歷,當遍歷到索引層中值爲 13 的結點時,咱們發現下一個結點是 17,那要查找的結點 16 確定就在這兩個結點之間。而後咱們經過索引層結點的 down 指針,降低到原始鏈表這一層,繼續遍歷。這個時候,咱們只須要再遍歷 2 個結點,就能夠找到值等於 16 的這個結點了。這樣,原來若是要查找 16,須要遍歷 10 個結點,如今只須要遍歷 7 個結點。

能夠發現,加了一層索引以後,查找一個結點須要遍歷的結點個數減小了,也就是說查找效率提升了,通常的,咱們能夠針對數據的規模來添加多層索引。前面講的這種鏈表加多級索引的結構,就是跳錶。

10.2複雜度分析

時間複雜度分析

若是鏈表裏有 n 個結點,會有多少級索引呢?按照咱們剛纔講的,每兩個結點會抽出一個結點做爲上一級索引的結點,那第一級索引的結點個數大約就是 n/2,第二級索引的結點個數大約就是 n/4,第三級索引的結點個數大約就是 n/8,依次類推,也就是說,第 k 級索引的結點個數是第 k-1 級索引的結點個數的 1/2,那第 k級索引結點的個數就是 n/(2^k)。假設索引有 h 級,最高級的索引有 2 個結點。經過上面的公式,咱們能夠獲得 n/(2^h)=2,從而求得 h=log2n-1。若是包含原始鏈表這一層,整個跳錶的高度就是 log2n。咱們在跳錶中查詢某個數據的時候,若是每一層都要遍歷 m 個結點,那在跳錶中查詢一個數據的時間複雜度就是 O(m*logn)。那這個 m 的值是多少呢?按照前面這種索引結構,咱們每一級索引都最多隻須要遍歷 3 個結點,也就是說 m=3,爲何是 3 呢?我來解釋一下。假設咱們要查找的數據是 x,在第 k 級索引中,咱們遍歷到 y 結點以後,發現 x 大於 y,小於後面的結點 z,因此咱們經過 y 的 down 指針,從第 k 級索引降低到第 k-1 級索引。在第 k-1 級索引中,y 和 z 之間只有 3 個結點(包含 y 和 z),因此,咱們在 K-1 級索引中最多隻須要遍歷 3 個結點,依次類推,每一級索引都最多隻須要遍歷 3 個結點。

因此在跳錶中查詢任意數據的時間複雜度就是 O(logn)。這個查找的時間複雜度跟二分查找是同樣的。換句話說,咱們實際上是基於單鏈表實現了二分查找,是否是很神奇!!!

可是它也不是十分完美,由於它的算法本質是經過空間來換時間,索引越多,空間複雜度越高。

空間複雜度分析

假設原始鏈表大小爲 n,那第一級索引大約有 n/2 個結點,第二級索引大約有 n/4 個結點,以此類推,每上升一級就減小一半,直到剩下 2 個結點。若是咱們把每層索引的結點數寫出來,就是一個等比數列。

這幾級索引的結點總和就是 n/2+n/4+n/8…+8+4+2=n-2。因此,跳錶的空間複雜度是 O(n)。也就是說,若是將包含 n 個結點的單鏈表構形成跳錶,咱們須要額外再用接近 n 個結點的存儲空間。

固然,這裏的分析都是針對每兩個結點抽一個結點到上級索引,這個間隔的結點個數並非固定死的,若是把結點間隔擴大,那麼空間複雜度也會相應的下降,可是時間複雜度確定也會相應的提升。因此咱們能夠根據數據規模,使用場景須要來平衡時間和空間的效率,使得某一個特定場景擁有最大的時間空間效益。

11.散列表

Hash算法,你們應該都瞭解過,本章節不展開介紹hash算法的種類或者實現,只單純講一下散列算法。散列表就是一種算法思想,它的本質就是利用散列表,把O(n)級別的時間複雜度操做直接編程常數級別的,是否是一聽就以爲很神奇。

來看一道以前刷pat遇到的題目吧,題目比較簡單,就是用到的hash散列思想。

問題:給出N個正整數,再給出M個正整數,問這M個數中的每一個數分別是否在N個數中出現過,其中N,M≤10^5,且全部正整數均不超過10^5.例如N=5, M=3,N個正整數爲{8,3,7,6,2},欲查詢的M個正整數爲{7,4,2},因而後者中只有7和2在N個正整數中出現過,而4是沒有出現過的。
對這個問題,最直觀的思路是:對每一個欲查詢的正整數x,遍歷全部N個數,看是否有一個數與x相等。這種作法的時間複雜度爲O(NM),當N和M都很大(10^5級別)時,顯然是沒法承受的,沒錯我一開始就只想到了這一種解題思路,很簡單暴力,可是也很無腦耗時。

那麼,通常來說耗時長的算法,都是能夠經過提升空間複雜度來減少時間複雜度的,也就是很重要很重要的空間換時間的思想

散列表就是這種思想的最好體現,如下就是該問題的C語言實現。

#include<cstdio>
const int maxn = 10010;
bool hashTable[maxn] = {false};
int main(){
    int n,m,x;
    scanf("%d%d",&n,&m);
    for(int i =0;i<n;i++){
        scanf("%d",&x);
        hashTable[x] = true;
    }
    for(int i=0;i<m;i++){
        scanf("%d",&x);
        if(hashTable[x]==true){
            printf("YES\n");
        }else{
            printf("NO\n");
        }
    }
    return 0;
}

Output

3 2
1 2 3
1
YES
2
YES

更多的關於散列函數以及解決散列衝突的內容請參考文章

本文將持續更新,後期將持續整理並添加各數據結構和算法(僅限於本文中已經羅列出來的數據結構和算法,其它的內容,請查看個人algo專欄,我將持續更新並完善這個專欄,歡迎各位好心人能多提供幫助和資料)的使用場景和縱向對比,敬請關注。

本人還只是個小菜鳥,本博客收集了不少網上已有的內容,主要包括了極客時間《數據結構與算法之美》——王爭(本人已付費購買該專欄,支持付費),以及中國大學MOOC陳越老師、何欽銘老師開設的《數據結構》課程(能夠免費報名參加,牆裂推薦)還有算法筆記(胡凡、曾磊著)(主要是爲了刷pat)的內容,若是文中有描述不當的地方請在留言區指出(十分感謝),若是某些內容引用出處我忘記貼上或者冒犯了你的權益,請告知一下,很抱歉,我將立馬刪除並從新整理。

若是有好的參考資料或者思考也歡迎你們提供,十分感激。

參考資料

相關文章
相關標籤/搜索