【數據結構】查找


  • 平均查找長度(ASL, Average Search Length):在查找過程中,一次查找的長度是指需要比較的關鍵字次數,而平均查找長度則是所有查找過程中進行關鍵字比較次數的平均值,(即 ASL=\(\sum\)查找概率*比較次數)(一般為等概率1/n)
  • 靜態查找表:查找表的操作無需動態地修改查找表,如 順序查找、折半查找、散列查找等。
  • 動態查找表:需要動態地插入或刪除的查找表,如 二叉排序樹、二叉平衡樹、B樹、散列查找等。

線性結構

順序查找

  • 適用條件:
    適用於線性表

  • 基本思想:
    從線性表的一段開始,逐個檢查關鍵字是否滿足給定的條件。若查找到某個元素的關鍵字滿足給定的條件,則查找成功,返回該元素在線性表中的位置;若已經查找到表的另一端,但還沒有查找到符合給定條件的元素,則返回查找失敗的信息。

  • 具體實現:

typedef struct {    //查找表的數據結構
    ElemType *data;     //元素空間基址,建表時按實際長度分配,0號單元留空
    int length;     //表的長度
}List;      //Sequential Search Table

//在順序表L中順序查找關鍵字為key的元素。若找到則返回該元素在表中的位置
int Search_Seq(List L, ElemType key) {
    L.data[0] = key;        //“哨兵”
    for(i = L.length; L.data[i] != key; --i);       //從后往前找
    return i;       //如果表中不存在key,則會找到第0號(哨兵),返回0
}

引入哨兵的目的是使得函數內的循環不用每次都判斷數組是否會越界,因為滿足i==0時,函數一定會跳出。可以避免很多不必要的判斷語句,從而提高程序效率。

缺點是當n較大時,平均查找長度較大,效率低;優點是對數據元素的存儲沒有需求,順序存儲或鏈式存儲皆可;對表中記錄的有序性也沒有要求,無論記錄是否按關鍵碼有序,均可應用。

PS:線性的鏈表只能進行順序查找

  • 性能分析:
    • 平均查找長度
      \(ASL_{成功}\)=\({n+1} \over {2}\)
      \(ASL_{失敗}=n+1\) (因為0位置處還有一個哨兵需要比較,所以+1)
    • 時間復雜度O(n)
    • 優點:是對數據元素的存儲沒有需求,順序存儲或鏈式存儲皆可;對表中記錄的有序性也沒有要求,無論記錄是否按關鍵碼有序,均可應用。
    • 缺點:是當n較大時,平均查找長度較大,效率低;
  • 特點:
    • 順序查找下給出的查找序列可以有序,也可以無序
    • 查找算法簡單,但時間效率太低時間復雜度為O(n)

折半查找

  • 適用條件:
    僅適用於有序的順序表

  • 基本思想:
    首先將給定值key與表中中間位置元素的關鍵字比較,若相等,則查找成功,返回該元素存儲位置,若不等,則所需查找的元素只能在中間元素以外的左半部分或右半部分,然后再縮小的范圍內繼續進行同樣的查找,如此重復,直到找到為止;或確定表中沒有所需要查找的元素,則查找不成功,返回查找失敗的信息。

  • 折半查找判定樹:(不僅要畫出圓結點,還要畫出方結點,否則查找失敗不好計算)

    折半查找判定樹實際上是一棵二叉排序樹,它的中序序列是一個有序序列。(方形結點是虛構的,代表失敗,並不計入比較的次數之中)
    • 計算ASL:如 11個元素的有序表,即 一個繩子切11刀(11個成功),剩下12段(12個失敗)
      每個查找成功(圓)的元素查找概率相等的情況下,即 每個圓結點概率為1/11(11個存在的成功的結點)(也可以理解成n2,即 雙分支結點個數(其實也沒有度為1的結點)),比較次數為層數;
      每個查找失敗(方)的元素查找概率相等的情況下,即 每個方結點概率為1/12(11+1=12個不存在的失敗的結點)(也可以理解成n0,即 葉子結點個數),比較次數該方結點的父結點(圓結點)的層數

      注意:方形虛構的查找失敗結點,不算入比較次數中。

    • 每個根結點=(其最左結點+最右結點)/2 (向上或向下取整,要統一)

      注意:沒有左結點或右結點,就取自己代替。如上圖結點1沒有左結點,1=(1+2)/2

    • 折半查找法在查找不成功時和給定值進行關鍵字的比較次數最多樹的高度,即 \(⌊log_2n⌋+1\)
  • 具體實現:

//在有序表L中查找關鍵字為key的元素,若存在則返回其位置,不存在則返回-1
int Binary_Search(SeqList L, ElemType key) {
    int low = 0, high = L.length-1, mid;
    while(low <= high) {
        mid = (low + high) / 2;         //mid取中間值
        if(key == L.data[mid]) {        //key剛好等於中間值
            return mid;
        }else if(key < L.data[mid]) {   //key小於中間值
            high = mid - 1;             //在左半邊部分查找
        }else {                         //key大於中間值
            low = mid + 1;              //在右半邊部分查找
        }
        return -1;      //當循環結束(low >high)說明查找失敗了
    }
}
  • 性能分析:
    • 平均查找長度
      \(ASL_{成功}\)=\({n+1} \over {n}log_2(n+1)-1\)=\(log_2(n+1)-1\)
    • 時間復雜度O(logn)
    • 優點:折半查找的時間復雜度為O(logn),遠遠優於順序查找的O(n)
    • 缺點:雖然二分查找的效率高,但是要求表關鍵字有序
  • 特點:
    • 折半查找只適用順序存儲結構,鏈表上無法實現二分查找
    • 折半查找適用於那種一經建立就很少改動但又經常需要查找的線性表

分塊查找

分塊查找又稱索引順序查找。(因為用了索引表,而索引表中又是順序查找,所以叫索引順序查找)

  • 適用條件:
    適用於順序存儲結構和鏈式存儲結構(存放的數據量過大
  • 基本思想:
    將查找表分為若干個子塊。塊內的元素可以無序,但塊之間是有序的(即 第一個塊中的最大關鍵字小於第二個塊中的所有記錄的關鍵字,以此類推)。再建立一個索引表,索引表中的每個元素含有各塊的最大關鍵字和各塊中的第一個元素的地址,索引表按關鍵字有序排列

  • 查找過程分為兩步:
    • 第一步是在索引表確定待查記錄所在的,可以順序查找折半查找索引表(因為塊之間有序);
    • 第二步是在塊內順序查找(因為塊內可以無序)(當然,若塊內有序,也可以折半查找)。
  • 具體實現:

//未完待續
  • 性能分析:
    • 平均查找長度
      ASL = \(L_I\)+\(L_S\) (Index和Sequence,索引查找和塊內查找(塊內順序查找)的平均長度之和)
      長度為n的查找表均勻地分為b塊每塊s個記錄
      • 索引表中(塊間)采用順序查找,塊內采用順序查找:
        \(ASL_{成功}\)=\(L_I\)+\(L_S\)=(b+1)/2+(s+1)/2=\({s^2+2s+n} \over {2s}\)

        注意:此時,若s=\(\sqrt{n}\),則平均查找長度取最小值\(\sqrt{n}+1\)

      • 索引表中(塊間)采用折半查找,塊內采用順序查找:
        \(ASL_{成功}\)=\(L_I\)+\(L_S\)=\(log_2(b+1)+{s+1} \over {2}\)
      • 為使查找效率最高每個索引塊的大小應是√n。(即 索引項*索引塊=表長,等分了索引項和索引塊)

  • 特點:
    • 分塊查找下給定的序列應該是分塊有序的,適用於順序存儲結構和鏈式存儲結構(如果索引表不強制要求用折半查找的話)
    • 分塊查找下其平均查找長度介於順序查找和折半查找之間
    • 其平均查找長度有索引表主表兩部分組成

樹形結構

二叉排序樹與二叉平衡樹詳情請看《數據結構---樹》

二叉排序樹

二叉排序樹、平衡二叉樹等雖然屬於樹的內容,但這里更着重與查找的關系,因此放到這一章,二叉排序樹是一種動態的查找表(區分靜態還是動態是看查找過程中是否可以插入刪除元素)

  • 基本概念:
    二叉排序樹的特性如下:左子樹所有關鍵字均小於根關鍵字,而右子樹所有關鍵字均大於根關鍵字,而左右子樹也都是一棵二叉排序樹(即左小右大)

    PS:通過中序遍歷即可得出有序的序列

  • 刪除:
    二叉排序樹中刪除一個關鍵字的時候,需要格外注意,為了保持這種二叉排序樹的特性需要考慮以下三種情況:
    1. 待刪除節點為葉子節點,刪除即可
    2. 待刪除節點只有一側有子樹,將該節點刪除,將子樹直接接在待刪除節點的雙親節點之上即可(原來是左還是左,原來是右還是右)
    3. 待刪除節點左右子樹都存在,沿着待刪除節點的左子樹根節點的右指針一直往右走,找到其最右邊一個節點(即 直接前驅)(其實就是左子樹中最大的一個節點,比待刪除節點小,比左子樹中其他節點都大),將這個節點的記錄覆蓋到原來待刪除的節點中去,因為這個節點可能還帶有子樹(最多只能帶一個),那么刪除這個節點時,就參考上面(1)(2)兩種方法(這里也可以在右子樹中找右子樹中最小的那個,原理幾乎是一樣的,讀者可以自己去思考,並不難)
  • 相關結論:
    • 二叉排序樹進行中序遍歷時,可以得到按從小到大排列的關鍵字序列
    • 若從根結點到某個葉子結點有一條路徑,則路徑左邊的結點的關鍵字不一定小於路徑上的結點的關鍵字的值
    • 在對二叉排序樹進行插入操作時,每次都是從根結點出發查找插入位置,並把新結點作為葉子結點插入到合適的位置
    • 對於有n個關鍵碼的集合,其關鍵碼有n!種不同的排列,可構成的不同二叉排序樹有\({{1}\over{n+1}}C_{2n}^{n}\)(卡特蘭數)
    • 對二叉排序樹進行對關鍵字的查找時,其平均查找長度和二叉排序樹的形態有關,在最壞情況下,n個結點的二叉排序樹是一棵深度為n的單支樹(樹的高度達到最大),其ASL和單鏈表上的順序查找相同,為(n+1)/2最好情況下,n個結點的二叉樹會得到一棵形態與折半查找的判定樹相似的二叉樹(樹的高度達到最小),此時其ASL約為\(log_2n\)
    • 對於經常需要進行插入、刪除、查找相關操作的表,建議使用二叉排序樹
    • 二叉排序樹的插入必為一個新的葉子結點

二叉平衡樹

為避免樹的高度增長過快,降低二叉排序樹的性能,規定左右子樹高度差不超過1。
平衡二叉樹(Balanced Binary Tree)(簡稱平衡樹,AVL)也是二叉排序樹的一種,其特點在於,左右子樹的高度之差的絕對值不超過1,左右子樹高度之差被稱為平衡因子,每次插入一個新的值的時候,都要檢查二叉樹的平衡,也就是平衡調整

  • 平衡調整技巧
    一般情況下,只有新插入結點的祖先結點的平衡因子受影響,即 以這些祖先結點為根的子樹有可能失衡。下層的祖先結點恢復平衡將使上層的祖先結點恢復平衡,因此每次調整的使用應該先調整最下面的失衡子樹。因為平衡因子為0的祖先不可能失衡,所以從新插入的結點開始向上,遇到地第一個其平衡因子不等於0的祖先結點,為第一個可能失衡的結點,如果失衡則應調整以該結點為根的子樹。

  • 結論
    \(N_h\)表示的是高度為h的AVL樹的最少結點數,則\(N_0\)=0(空樹),\(N_1\)=1(僅包含根結點),現有結論\(N_h=N_{h-1}+N_{h-2}+1\)(h>1)

    例如:
    n0=0
    n1=1
    n2=2
    n3=4
    n4=7
    n5=12
    n6=20

B樹、B+樹

B樹和B+樹都是平衡的多叉樹,都可用於文件索引結構
B樹和B+樹都能有效的支持隨機查找,而B+樹還支持順序查找。

舉個簡單的例子, 比如我們要從123456中找出5這個數字:
順序查詢:先用5跟1比較,然后再跟2比較,然后再跟3比較,按數據順序一直比下去,直到找到;
隨機查詢:從數據中隨機抽出一個數字跟5比較,比如第一次隨機抽到了4跟5比較,然后再隨機抽一個3跟5比較,不斷的隨機抽然后比較,最終找到結果;
直接查詢:這個概念較多,不過一般指的是數據分類后或者建立索引后,根據數據結構組成,有效率的去查詢對比!

B樹

前面所說的查找算法都是在內存中進行的,適用於組織在內存中較小的文件。而對於存放在外存上的較大的文件(如,操作系統中的文件目錄存儲,數據庫中的文件索引結構的存儲,等等)都不可能是在內存中建立查找結構而只能是在磁盤中建立這個查找結構。在磁盤中組織查找結構,從任何一個結點指向其他結點都有可能讀取一次磁盤數據,再將這些數據寫入內存進行比較,而頻繁的進行磁盤I/O操作,其效率是很低的。所以,所有的二叉樹的查找結構在磁盤中都是低效的。

  • 基本概念:
    基於此,1970年R.Bayer和E.McCreight提出了一種稱為B樹多路平衡查找樹,適用於在磁盤等直接存儲設備上組織動態的索引表,在系統運行過程中插入或刪除記錄時,索引結構本身也可能發生變化,以保持較好的查詢性能。
    B樹是多路查找樹,是二叉排序樹的拓展
    簡單來說,B-樹就是平衡二叉樹的擴展,是一棵m叉樹,B-樹的階就是其最大分支數,其非根節點至少有⌈m/2⌉個分支,而根節點至少有2個分支,每個節點的關鍵字按順序從小到大排列。

  • 基本性質:

    m階B樹=m叉樹,而不是度為m的樹。這兩點要分開,m叉樹中可以不含有m棵子樹,而度為m的樹必須至少有一個結點有m棵子樹,因為樹的度是樹中結點的最大度數,這個結點必須存在。

    1. 樹中每個結點至多有m棵子樹(即至多含有m-1個關鍵字)(兩棵子樹指針夾着一個關鍵字
    2. 根結點不是終端結點,則至少含有兩棵子樹(即至少含有一個關鍵字
    3. 除根結點外的所有非葉結點至少有⌈m/2⌉子樹(即至少含有⌈m/2⌉-1關鍵字)(注意是向上取整,結點中包含越多關鍵字越好
    4. 關鍵字個數n的取值范圍(⌈m/2⌉-1≤n≤m-1)(即根結點1≤n≤m-1
    5. 所有葉子節點均在同一層、葉子節點除了包含了關鍵字和關鍵字記錄的指針外也有指向其子節點的指針只不過其指針地址都為 null;
    • 非根結點中關鍵字的個數范圍(⌈m/2⌉-1 ~ m-1)
  • 高度(磁盤存取次數):

    B樹中大部分操作所需的磁盤存儲次數與B樹的高度成正比。

    注意:B樹的高度不包括最后的不帶任何信息的葉結點所處的那一層。

    若n≥1,則對任意一棵包含n個關鍵字高度為h階數為m的B樹:

    • (最小高度)因為B樹中每個結點最多有m棵子樹,m-1個關鍵字,所以在一棵高度為h的m階B數中關鍵字的個數應滿足\(n≤(m-1)(1+m+m^2+...+m^{h-1})=m^h-1\),因此有\[h≥log_m(n+1)\]

    • (最大高度)若但每個結點中的關鍵字個數達到最少,則容納同樣多關鍵字的B樹的高度達到最大。由B樹的定義:第一層至少1個結點;第二層至少2個結點;除根結點外的每個非終端結點至少有⌈m/2⌉棵子樹,則第三層至少有2⌈m/2⌉個結點……第h+1層至少有\(2(⌈m/2⌉)^{h-1}\)個結點,注意到第h+1層是不包含任何信息的葉結點。對於關鍵字個數為n的B樹,葉結點(即 查找不成功的結點為n+1),由此有\(n+1≥2(⌈m/2⌉)^{h-1}\),即\[h≤log_{⌈m/2⌉}((n+1)/2)+1\]

    • 例如,一棵3階B樹共有8個關鍵字,則其高度范圍為2≤h≤3.17

  • 查找:
    B樹的查找操作和二叉排序樹的查找操作非常類似。
    1. 在B樹中找結點;(在磁盤上進行查找)
    2. 在結點內找關鍵字(在內存中進行查找)
      1. 先讓待查找關鍵字key和結點中的關鍵字比較,如果等於其中的關鍵字,則查找成功。
      2. 如果和所有關鍵字都不相等,則看key處在哪個范圍內,飯后取對應的指針所指向的子樹中查找。
  • 插入:

    在二叉排序樹中,僅需查找到需插入的終端結點的位置。但是,在B樹中找到插入的位置后,並不能簡單地將其添加到終端結點位置,因為此時可能會導致整棵樹不再滿足B樹中定義的要求(結點至多有m-1個關鍵字,非根節點關鍵字個數至少是⌈m/2⌉-1)一旦達到m個關鍵字就需要進行分裂。

    1. 定位
    2. 插入:可能導致關鍵字個數超過上限,分裂。
    3. 分裂:取這個關鍵字數組中的中間關鍵字(⌈n/2⌉)作為新的根結點的關鍵字向上進位到父結點中,然后其他關鍵字形成兩個結點作為新結點的左右孩子
  • 刪除:
    • 刪除的關鍵字在終端結點上:
      • 結點內關鍵字數量>⌈m/2⌉-1,此時參數不會破壞定義,直接刪除
      • 結點內關鍵字數量=⌈m/2⌉-1
        • 其左右兄弟結點中關鍵字數量>⌈m/2⌉-1(兄弟夠借),去兄弟中借(需要調整該結點、左(右)兄弟結點及其雙親結點(父子換位法),以達到新平衡
        • 其左右兄弟結點中關鍵字數量均不大於⌈m/2⌉-1(兄弟不夠借),則需要進行結點合並(從上一層的結點取關鍵字與下一層的結點合並)
    • 刪除的關鍵字在非終端結點上:需要先轉換成在終端結點上,再按照再終端結點上的情況來分別考慮對應的方法。
      相鄰關鍵字:對於不在終端結點上的關鍵字,它的相鄰關鍵字是其左子樹中值最大或者右子樹中值最小的關鍵字。
      1. 存在關鍵字數量>⌈m/2⌉-1結點的左子樹或者右子樹,在對應子樹上找到該關鍵字的相鄰關鍵字,然后將相鄰關鍵字替換待刪除的關鍵字
      2. 左右子樹的關鍵字數量均=⌈m/2⌉-1,則將這兩個左右子樹結點合並,然后刪除待刪除關鍵字。
  • 總結:
    1. 首先確定結點中關鍵字的個數范圍(⌈m/2⌉-1 ~ m-1)
    2. 插入時,若超過范圍,則將中間關鍵字向上進位到父節點中
    3. 刪除時,若低於范圍,若為分支結點,則父債子償,將后繼(前驅)結點兒子頂替父親(若兒子無力償還(頂替)(已到達最低關鍵字個數限制),則合並(與兄弟合並一起償還));若為葉子結點,則子債父償,父結點降位頂替兒子(就如同刪除了父親,后續操作繼續進行父債子償
    • 其實就是刪除之后拿前驅(后繼)一個個往前面補,直到(結點關鍵字個數不足以彌補,彌補過后結點中關鍵字個數不在范圍之中)補不了合並,每個結點關鍵字都在個數范圍之中
  • 插入步驟:
    • 當超過范圍,中間關鍵字作為父親擔起責任。
  • 刪除步驟:
    • 子債父償
    • 父債子償
    • 子償還不了,與兄弟合並,一起償還
    • 一起仍然償還不了,向上找父親的父親(爺爺)
    • 上一層繼續子債父償(父親的債爺爺償)
    • 父債子償
    • 子償還不了,合並,一起償還
    • 以此類推。。
  • 刪除簡化:
    • 子債父償
    • 父債子償
    • 子償不了
    • 合並償還

注意:由於這是平衡查找樹,所以中序遍歷為有序序列,其前驅(或 后繼)為左子樹下最大元素(或 右子樹下最小元素

總之,先求關鍵字個數范圍(⌈m/2⌉-1 ~ m-1),插入或刪除操作一定要滿足定義,不滿足,可以自己看着辦(不必那么復雜的記),只需調整到滿足定義即可。

B+樹

常用於數據庫操作系統文件系統中的一種查找的數據結構。

  • 基本性質(與B樹比較):
    1. 關鍵字與子樹
      • 在B+樹中,具有n個關鍵字的結點只含有n棵子樹,即每個關鍵字對應一棵子樹
      • 在B樹中,具有n個關鍵字的結點含有(n+1)棵子樹,即兩棵子樹指針夾着一個關鍵字
    2. 關鍵字個數范圍(其實與第一個一樣)
      • 在B+樹中,每個結點(非根內部結點)關鍵字個數n的范圍是⌈m/2⌉≤n≤m(根結點1≤n≤m
      • 在B樹中,每個結點(非根內部結點)關鍵字個數n的范圍是⌈m/2⌉-1≤n≤m-1(根結點1≤n≤m-1
    3. 記錄
      • 在B+樹中,葉子結點包含信息,所有非葉結點僅起到索引作用,非葉結點中的每個索引項只含有對應子樹的最大關鍵字和指向該子樹的指針,不含有該關鍵字對應記錄的存儲地址。
      • 在B樹中,每個關鍵字對應一個記錄存儲地址
    4. 葉結點
      • 在B+樹中,葉結點包含了全部關鍵字,即在非葉結點中出現的關鍵字也會出現在葉結點中,而且葉子結點的指針指向記錄
      • 在B樹中,葉結點包含的關鍵字和其他結點包含的關鍵字是不重復
    5. 在B+樹中,有一個指針指向關鍵字最小的葉子結點,所有葉子結點鏈接成一個單鏈表

散列結構

散列表

前面的線性表和樹表的查找中,記錄在表中的位置與記錄關鍵字之間不存在確定關系,這類查找方法建立在“比較”的基礎上,查找的效率取決於比較的次數。

那么如果需要快速查找到需要的關鍵字,而關鍵字不方便比較怎么辦?所以就引入了散列表,亦滿足了動態查找

  • 散列函數:一個把查找表中的關鍵字映射成該關鍵字對應的地址的函數,記為Hash(key)=Addr(這里的地址可以是數組下標、索引或內存地址等)

  • 沖突:散列函數可能會把兩個或兩個以上的不同關鍵字映射到同一地址

  • 同義詞:發生碰撞的不同的關鍵字

  • 散列表:根據關鍵字而直接進行訪問的數據結構。也就是說,散列表建立了關鍵字和存儲地址之間的一種直接映射關系。

  • 基本概念:
    散列表的特點是根據給定的關鍵字計算出關鍵字在表中的地址,就是將每個關鍵字通過一個算法來得出其對應的地址位置,例如最常見的的取余計算法,對關鍵字進行取余計算(除數一般用關鍵字的數量,也就是表長),即可得出每個關鍵字應該放的位置,而查找的時候根據待查找得關鍵字進行取余計算即可得到該關鍵字所在的位置,若不符,則查找失敗

  • 散列函數構造:
    1. 直接定址法
      取關鍵字的某個線性函數值為散列地址,即h(key)=a*key+b (a、b為常數)

      比如:查找年份,是一個線性關系

    2. 除留余數法
      散列函數為:h(key) mod(%) p
      • 一般p取表的大小,為了映射均勻p一般取不大於表長的素數
    3. 數字分析法
      分析數字關鍵字在各位上的變化情況,取比較隨機的位作為散列地址

      比如:取11位手機號碼key的后四位作為地址,或者18位的身份證號碼

    4. 平方取中法
      希望每一位都能影響到最后的函數結果值。

      如56793542,\({56793542}^2\)=325506412905764,取中間三位641

    5. 折疊法
      把數字分割成位數相同的幾個部分,然后疊加。

      如56793542,542+793+056=1391,h(56793542)=391

  • 處理沖突:
    1. 開放定址法(Open Addressing):(換個位置)

      所謂開放定址法,是指可存放新表項的空閑地址既向它的同義詞表項開放,又向它的非同義詞表項開放。
      發生聚集的主要原因是,解決沖突的方法選擇不當
      產生堆積現象,即 產生了沖突,它對存儲效率、散列函數和裝填因子均不會有影響,而平均查找長度會因為堆積現象而增大。(注意這里是存儲效率,而不是查找效率)

      一旦產生了沖突,就按某種規則去遞增尋找另一空地址。
      hi=(H(key)+di)%m
      式中,i代表沖突次數,m為表長,di為增量序列。

      這里形象化的比喻一下:設想一下你預定了一家旅館的房間,但后來發現這間房被人住了,於是旅館工作人員會給你重新分配一個房間。
      這就是開放定址法,當發現有哈希沖突時,則將元素往后面的空間存放,這里就有很多種分配方式,例如

      • 線性探測法(線性探測再散列)(Linear Probing):簡單說就是一個一個的往后面找,缺點是容易產生堆積(某個地方沖突越來越多)。
        \(d_i=0,1,2,...\)
      • 平方探測法(二次探測再散列):就是對當前的沖突位置+\(1^2\),-\(1^2\),+\(2^2\),-\(2^2\)……進行探查,直到找到空位置位置,這種算法不會出現堆積問題,但是不能探查到所有的空位
        \(d_i=0^2,1^2,-1^2,...\)

        有定理顯示:如果散列表長度TableSize是某個4k+3(k是正整數)形式的素數時,平方探測法可以探查到整個散列表空間

      • 再哈希法(雙散列法):使用兩個散列函數h1、h2
        \(H_i=(H(key)+i×Hash_2(key))%m\)

        i為沖突次數,m為表長;
        其實就是在m表上,不停的\(+Hash_2(key)\),循環,直到有空位為止。
        (如 Hash2(key)=6,那么就一直+6,直到有空位)

        • 探測序列還應該保證所有的散列存儲單元都應該能夠被探測到,選擇以下形式有良好的效果:
          h2(key) = p - (key mod p)
      • 偽隨機序列法:偽隨機放(di=偽隨機數序列)

      注意:在開放地址散列表中,刪除操作要很小心,通常只能“懶惰刪除”,即 需要增加一個“刪除標記(Deleted)”,而並不是真正刪除它,以便查找時不會“斷鏈”。其空間可以在下次插入時重用。

    2. 鏈地址法(Separate Chaining):(同一位置的沖突對象(同義詞)組織在一起)

      將相應位置上沖突的所有關鍵字存儲在同一個單鏈表中。

      這種方法有點像違建房,散列表本身不存儲信息,而保存指針,而每個記錄除了帶有關鍵字以外,還要帶一個指針域,對於某一個關鍵字進行計算后得出的值,與哈希表中某個值對應,就將指針指向這個關鍵字記錄,然后,如果有另外一個關鍵字計算也得到這個數,那么就將前一個關鍵字記錄的指針域指向這個關鍵字,形成一個鏈表。

    3. 建立公共溢出區:
      將哈希表分為基本表溢出表兩部分,凡是和基本表發生沖突的元素一律順着填入溢出表中。

  • 性能分析:
    散列表的性能指標可以這樣分析:
    對於散列表查找成功的ASL計算,通過對所有查找成功的關鍵字所需的比較次數進行相加再除以關鍵字的數量即可得出,查找失敗的ASL,則通過將從每個地址開始進行查找直到查找到空所需的地址比較次數相加除以地址數即可得出

    • 裝填因子(Loading Factor):散列表的裝填因子一般記為α,定義為一個表的裝滿程度,即
      \[α = {{表中記錄數n} \over {散列表長度m}}\]

      散列表的平均查找長度依賴於散列表的裝填因子α,而不直接依賴於n或m。α越大,表示裝填的記錄越滿,發生沖突的可能性越大,反之發生沖突的可能性越小。一般0.5~0.85較為合理。

    • 平均查找長度
      • 查找成功:是針對關鍵字
        \[ASL_{成功}={{關鍵字查找次數之和}\over{關鍵字個數}}\]

        查找成功:比較幾次找到關鍵字/關鍵字的個數(除以關鍵字的個數 即 查找關鍵字的概率相等)

      • 查找失敗:是針對可以散列的位置(就是模幾)(MOD p,即 可以散列的位置為0~p-1)(其實也就是說,只可能在散列這幾個位置后查找失敗,因為其他位置散列不到))
        \[ASL_{失敗}={{每個地址的查找次數之和}\over{地址個數}}\]

        查找失敗:每個0~p-1的散列地址中,比較幾次才能發現沒有這個關鍵字(即 比較幾次才能找到第一個,空則表示查找失敗了)(如 散列地址為0、1、2都不為空,3為空,那么發現地址0的查找失敗次數為4,1為3,2為2,3為1)

      注意:線性探測法查找到空,也算一次比較(如012都不為空,那么0的查找失敗次數為4)
      鏈地址法查找到空,不算一次比較,因為鏈地址法指向下一個結點的指針為空,就知道查找失敗了,不算一次比較。
      我們這里說的比較,是指需要查找的元素該地址上存儲的元素進行比較而鏈地址法中指向下一個結點的指針為空,沒有與查找元素比較,所以不算一次比較

    • 關鍵字的比較次數取決於產生沖突的多少
      影響產生沖突多少有以下三個因素:
      • 散列函數是否均勻
      • 處理沖突的方法
      • 散列表的裝填因子α
    • 散列表:

      散列地址 0 1
      關鍵字 -- --
      比較次數 -- --
  • 總結:
    • 選擇合適的h(key),散列表的查找效率期望是常數O(1),它幾乎與關鍵字的空間的大小n無關,也適合於關鍵字直接比較計算量大的問題。
    • 它是以較小的α為前提,因此,散列方法是一種空間換時間的方法。
    • 散列方法的存儲對關鍵字是隨機的,不便於順序查找關鍵字,也不適合范圍查找,或最大最小值查找。
    • 開放地址法:
      • 散列表是一個數組,存儲效率高,隨機查找。
      • 散列表有“堆積”現象
      • 關鍵字刪除需要“懶惰刪除”
    • 鏈地址法:
      • 散列表是順序存儲和鏈式存儲的結合,鏈表部分的存儲效率和查找效率都比較低。
      • 關鍵字刪除不需要“懶惰刪除”法,從而沒有存儲“垃圾”。
      • 太小的α可能導致空間浪費,大的α又將付出更多的時間代價。不均勻的鏈表長度導致時間效率的嚴重下降。
  • “沖突”是不是特別討厭?
    答:不一定!正因為有沖突,使得文件加密后無法破譯(不可逆,是單向散列函數,可用於數字簽名)。
    利用了哈希表性質:源文件稍稍改動,會導致哈希表變動很大。

本文參考:https://blog.csdn.net/qq_25940921/article/details/82224418


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM