數據結構(二)——查找算法、樹

查找算法

  • 二分查找
    略 mid=(lo + hi)/2;

  • 數組

  • 普通鏈表

  • 內核鏈表

  • 企業鏈表

    • 實現例子:
        typedef struct LISTNODE//結點定義
				{
				    struct LISTNODE *next;
				}ListNode;
        typedef struct Person//需要把結點放在最前面(注意,這裏不是指針,如果要使用指針,需要改一下定義)
				{
				    ListNode node;
				    char name[20];
				    int age;
				}Person;
        //向指定位置插入結點
				int LinkedList::List_Insert(ListNode *data,int pos)//不必釋放空間
				{
				    if(nullptr == data)
				        return -1;
				    if((pos<0)||(pos>length))
				        pos = length;
				    ListNode* pCurrent = &head;
				    while(pos--)
				        pCurrent = pCurrent->next;

				    data->next = pCurrent->next;
				    pCurrent->next = data;

				    length++;

				    return 0;
				}
				//調用方法
				myList->List_Insert((ListNode*)(&p[1]),1);//需要強制轉換
    • 後進先出

    • 後綴表達式實現

      • 構造後綴表達式:

        • 循環進行,直至表達式爲空,如果是數字則直接輸出;

        • ‘(‘直接入棧,’)‘則將’(‘之前的符號全部輸出,並彈出’(’;

        • 如果是其它符號,若棧爲空則直接入棧,否則如果棧頂元素的優先級大於當前符號則彈出棧頂元素,直至棧頂元素優先級小於該元素或棧爲空,這時候將該符號入棧

      • 運算後綴表達式:

        • 循環進行,直至表達式爲空,如果是數字則進棧

        • 如果是符號,(這裏我們假設是’+'這類符號)則從棧中彈出一個元素,把它放在符號的右邊,再從棧中彈出一個元素,把它放在符號的左邊,然後運算,之後把結果入棧

    • 二叉樹遍歷非遞歸實現

      • 這裏我們需要新建一個結構體,它包含指向二叉樹的結點的指針,以及一個標誌位,初始化時標誌位爲false

      • 取走二叉樹的頭結點,把它入棧

      • 從二叉樹中取出一個元素,如果是null,則繼續取,取出一個結點後,如果標誌位爲false,則將標誌位置爲true,這裏我們要注意,棧是後進先出的,所以如果我們要的是先序遍歷的結果,則應該新建兩個結構體對象,一個包含左結點,一個包含右結點,先把右的入棧,再把左的入棧,然後在把取出來的這個入棧;如果標誌位爲true,則輸出該結點中包含的結點信息(模擬一下就懂了,不難的)

      • 當棧爲空時,則退出

    • 隊列

      • 先進先出
    • hash(散列表)

      • 散列函數

      • 衝突處理

        • 拉鍊法
          方法是將大小爲M的數組中的每個元素指向一條鏈表,鏈表中的每個結點都存儲了散列值爲該元素的索引的鍵值對。散列最主要的目的在於均勻地將鍵散佈開來,因此在計算散列後鍵的順序信息就丟失了。如果你需要快速找到最大或者最小的鍵,或是查找每個範圍內的鍵,散列表都不是合適的選擇。基於拉鍊表的散列表的實現簡單。在鍵的順序並不重要的應用中,它可能是最快的(也是使用最廣泛的)符號表實現。

        • 線性探測法
          用大小爲M的數組保存N個鍵值對,其中M>N。我們需要依靠數組中的空位解決碰撞衝突。基於這種策略的所有方法被統稱爲開放地址散列表。開放地址散列表中最簡單的方法叫做線性探測法:當碰撞發生時(當一個鍵的散列值已經被另一個不同的鍵佔用),我們直接檢查散列表中的下一個位置(將索引值加1)。這樣的線性探測可能會產生三種結果:
          (1)命中,該位置的鍵和被查找的鍵相同;
          (2)未命中,鍵爲空(該位置沒有鍵);
          (3)繼續查找,該位置的鍵和被查找的鍵不同。
          我們用散列函數找到鍵在數組中的索引,檢查其中的鍵和被查找的鍵是否相同。如果不同則繼續查找(將索引增大,到達數組結尾時返回數組開頭),直到找到該鍵或者遇到一個空元素。

        • 刪除操作:
          如何從基於線性探測的散列表中刪除一個鍵?仔細想一想,你會發現直接將該鍵所在的位置設爲null是不行的的,因爲這會使得在此位置之後的元素無法被查找。(後面的null之前的元素全部要重新插入)爲了保證性能,我們會動態調整數組的大小來保證使用率在1/8到1/2之間,另外,短小的鍵簇才能保證較高的效率。

搞清楚樹的基本概念

  • 度:指的是一個節點擁有子節點的個數。如二叉樹的節點的最大度爲2。

  • 深度:數的層數,根節點爲第一層,依次類推。

  • 葉子節點:度爲0的節點,即沒有子節點的節點。

  • 樹:樹中的每一個節點,可以有n(後續節點)個子節點,但是每個節點只有一個前驅節點。

  • 二叉樹:除了葉子節點外,每個節點只有兩個分支,左子樹和右子樹,每個節點的最大度數爲2;

  • 滿二叉樹:除了葉結點外每一個結點都有左右子葉且葉結點都處在最底層的二叉樹;

  • 完全二叉樹:只有最下面的兩層結點度小於2,並且最下面一層的結點都集中在該層最左邊的若干位置的二叉樹。也就是說,在滿叉樹的基礎上,我在最底層從右往左刪去若干節點,得到的都是完全二叉樹。所以說,滿二叉樹一定是完全二叉樹,但是完全二叉樹不一定是滿二叉樹

  • 平衡二叉樹:樹的左右子樹的高度差不超過1的數,空樹也是平衡二叉樹的一種。平衡二叉樹,又稱AVL樹。它或者是一棵空樹,或者是具有下列性質的二叉樹:它的左子樹和右子樹都是平衡二叉樹,且左子樹和右子樹的高度之差之差的絕對值不超過1.

  • 哈夫曼樹:帶權路徑長度達到最小的二叉樹,也叫做最優二叉樹。不關心樹的結構,只要求帶權值的路徑達到最小值,哈夫曼樹可能是完全二叉樹也可能是滿二叉樹。

  • 單詞查找樹(Trie樹,又稱字典樹):是一種樹形結構,是一種哈希樹的變種,是一種用於快速檢索的多叉樹結構。典型應用是用於統計和排序大量的字符串(但不侷限於字符串),所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:最大限度地減少無謂的字符串比較,查詢效率比哈希表高。Tries樹的核心思想是空間換時間。利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的。

  • 關於B樹

    • 二叉搜索樹:二叉樹,每個結點只存儲一個關鍵字,等於則命中,小於走左結點,大於走右結點;二叉排序樹(Binary Sort Tree),又稱二叉查找樹(Binary Search Tree),亦稱二叉搜索樹(使用的是二分查找的思想)

    • 關於B樹的,可以看一下這則漫畫:https://www.sohu.com/a/154640931_478315

    • B(B-)樹:多路搜索樹,每個結點存儲M/2到M個關鍵字,非葉子結點存儲指向關鍵字範圍的子結點;所有關鍵字在整顆樹中出現,且只出現一次,非葉子結點可以命中;

    • B+樹:在B-樹基礎上,爲葉子結點增加鏈表指針,所有關鍵字都在葉子結點中出現,非葉子結點作爲葉子結點的索引;B+樹總是到葉子結點才命中;

    • B*樹:在B+樹基礎上,爲非葉子結點也增加鏈表指針,將結點的最低利用率從1/2提高到2/3;

    • B樹和B+樹都是排序樹,隨機檢索性能很好,順序檢索效率差。

B樹

  • 關於B樹的應用(更常用於文件系統的索引及某一些數據庫)

    • 數據庫索引爲什麼使用樹結構存儲呢?
      A:樹的查詢效率高,而且可以保持有序

    • 竟然如此,爲什麼索引沒有使用二叉查找樹來實現呢?
      A:其實從算法邏輯上來講,二叉查找樹的查找速度和比較次數都是最優的,但是我們不得不考慮一個現實問題:磁盤IO。
      數據庫索引是存儲在磁盤上的,當數據量比較大的時候,索引的大小可能有幾個G甚至更多。
      當我們利用索引查詢的時候,能把整個索引全部加載到內存嗎?顯然不可能。能再的只有逐一加載每一個磁盤頁,這裏的磁盤頁對應着索引樹的結點。

      • 如果使用二叉樹,最壞的情況下,磁盤IO次數等於索引樹的高度。
      • 爲了減少磁盤IO次數,我們就需要把原本“瘦高”的樹變得“矮胖”。這就是B-樹的特徵之一。
      • B樹是一種多路平衡搜索樹,它的每一個節點最多包含k個孩子,k被稱爲B樹的階。k的大小取決於磁盤頁的大小。
  • 下面來具體介紹一下B-樹(Balance Tree),一個m階的B樹具有如下幾個特徵:(葉節點後面還有東西,算深度時得注意+1)
    1.根結點至少有兩個子女。

    2.每個中間節點都包含k-1個元素和k個孩子,其中 m/2 <= k <= m

    3.每一個葉子節點都包含k-1個元素,其中 m/2 <= k <= m

    4.所有的葉子結點都位於同一層。

    5.每個節點中的元素從小到大排列,節點當中k-1個元素正好是k個孩子包含的元素的值域分劃。

B+樹(要多餘B樹做比較)

  • 關於B+樹的應用(大部分數據庫的索引)

    • 什麼是B+樹?(注意看圖)
      A:1.有k個子樹的中間節點包含有k個元素(B樹中是k-1個元素),每個元素不保存數據,只用來索引,所有數據都保存在葉子節點。

      2.所有的葉子結點中包含了全部元素的信息,及指向含這些元素記錄的指針,且葉子結點本身依關鍵字的大小自小而大順序鏈接。

      3.所有的中間節點元素都同時存在於子節點,在子節點元素中是最大(或最小)元素。

  • 什麼是衛星數據?
    A:所謂衛星數據,指的是索引元素所指向的數據記錄,比如數據庫中的某一行。在B-樹中,無論中間節點還是葉子節點都帶有衛星數據。

  • 數據庫索引爲什麼使用B+樹而不是B樹?
    A:1.單一節點存儲更多的元素,使得查詢的IO次數更少。(B+樹中,只有葉子節點有衛星數據,其餘節點僅僅是索引,沒有任何數據關聯,因此磁盤頁可以容納更多的節點元素)

    2.所有查詢都要查找到葉子節點,查詢性能穩定。(B+樹的衛星數據只存在於葉節點)

    3.所有葉子節點形成有序鏈表,便於範圍查詢。 (B+樹的葉子節點裏面的衛星數據是有序的,而且葉子節點與葉子節點之間有鏈表指針)

紅黑樹與AVL樹

  • 紅黑樹

    • 二叉查找樹存在着它的缺陷,有時候二叉查找樹的性能會變成線性,如何解決二叉查找樹多次插入新節點而導致的不平衡呢?我們的主角“紅黑樹”應運而生。

    • 複雜度:O(logN)

    • 紅黑樹(Red Black Tree)是一種自平衡的二叉查找樹。除了符合二叉查找樹的基本特性外,它還具有下列的附加特性:
      1.節點是紅色或黑色。

      2.根節點是黑色。

      3.每個葉子節點都是黑色的空節點(NIL節點)。

      4.每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)

      5.從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

    • 紅黑樹從根到葉子的最長路徑不會超過最短路徑的2倍(性能上不如AVL樹,但調節比AVL簡單)

    • 紅黑樹的調整:(不看圖賊難理解,這是重點……)

      • 變色
        與其父節點交換顏色,即其父節點變爲紅色,而當前節點變爲黑色,注意根節點是黑色,就不要把它換爲紅色,實現不行就得用旋轉

      • 旋轉

        • 左旋轉
          左旋是將某個節點旋轉爲其右孩子的左孩子(最好看圖,旋轉後根節點(相對這裏的)爲黑(變色))
        • 右旋轉
          右旋是節點旋轉爲其左孩子的右孩子(最好看圖,旋轉後根節點(相對這裏的)爲黑(變色))
    • 應用

      • set

      • map

  • AVL樹(平衡二叉樹)

    • 把插入,查找,刪除的時間複雜度最好情況和最壞情況都維持在O(logN)。但是頻繁旋轉會使插入和刪除犧牲掉O(logN)左右的時間,不過相對二叉查找樹來說,時間上穩定了很多

    • 特性:

      • 具有二叉查找樹的全部特性。
      • 每個節點的左子樹和右子樹的高度差至多等於1。
    • AVL樹的調整:(多看圖、多練)(重點啊~)
      -單旋轉(左-左型、右-右型)(這裏的左右看的是失衡節點的父節點的位置)
      主要看失衡節點向上的三個結點,進行左旋轉,第二個結點將成爲第一、三結點的父節點

      -雙旋轉(左-右型、右-左型)
      先對失衡節點向上的兩個結點進行調整(這兩個結點的父、子關係交換),把它變成左-左型或右-右型

    • AVL樹的刪除操作:

      • 同插入操作一樣,刪除結點時也有可能破壞平衡性,這就要求我們刪除的時候要進行平衡性調整。刪除分爲以下幾種情況:
        • 首先在整個二叉樹中搜索要刪除的結點,如果沒搜索到直接返回不作處理,否則執行以下操作:
          1.要刪除的節點是當前根節點T。如果左右子樹都非空。在高度較大的子樹中實施刪除操作。分兩種情況:
          (1)、左子樹高度大於右子樹高度,將左子樹中最大的那個元素賦給當前根節點,然後刪除左子樹中元素值最大的那個節點。

          (2)、左子樹高度小於右子樹高度,將右子樹中最小的那個元素賦給當前根節點,然後刪除右子樹中元素值最小的那個節點。

          2.如果左右子樹中有一個爲空,那麼直接用那個非空子樹或者是NULL替換當前根節點即可。

          3.要刪除的節點元素值小於當前根節點T值,在左子樹中進行刪除。

          4.要刪除的節點元素值大於當前根節點T值,在右子樹中進行刪除。

線索二叉樹定義:

  • 通過考察各種二叉鏈表,不管二叉樹的形態如何,空鏈域的個數總是多過非空鏈域的個數。準確的說,n各結點的二叉鏈表共有2n個鏈域,非空鏈域爲n-1個,但其中的空鏈域卻有n+1個。因此,提出了一種方法,利用原來的空鏈域存放指針,指向樹中其他結點。這種指針稱爲線索。

  • 二叉樹的遍歷本質上是將一個複雜的非線性結構轉換爲線性結構,使每個結點都有了唯一前驅和後繼(第一個結點無前驅,最後一個結點無後繼)。對於二叉樹的一個結點,查找其左右子女是方便的,其前驅後繼只有在遍歷中得到。爲了容易找到前驅和後繼,有兩種方法。一是在結點結構中增加向前和向後的指針fwd和bkd,這種方法增加了存儲開銷,不可取;二是利用二叉樹的空鏈指針。現將二叉樹的結點結構重新定義如下:
    lchild ltag data rtag rchild

  • 其中:
    ltag=0 時lchild指向左子女;
    ltag=1 時lchild指向前驅;
    rtag=0 時rchild指向右子女;
    rtag=1 時rchild指向後繼;

樹的一些其它概念

  • 對任何非空二叉樹T,若n0 表示葉結點的個數、n2 表示度爲2 的非葉結點的個數,那麼兩者滿足關係n0 = n2 + 1。

證明:首先,假設該二叉樹有N 個節點,那麼它會有多少條邊呢?答案是N - 1,這是因爲除了根節點,其餘的每個節點都有且只有一個父節點,那麼這N 個節點恰好爲樹貢獻了N - 1 條邊。這是從下往上的思考,而從上往下(從樹根到葉節點)的思考,容易得到每個節點的度數和 0n0 + 1n1 + 2n2 即爲邊的個數。
因此,我們有等式 N - 1 = n1 + 2
n2,把N 用n0 + n1 + n2 替換,得到n0 + n1 + n2 - 1 = n1 + 2*n2,於是有n0 = n2 + 1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章