數據結構-查找


 
        

寫在前面:這些內容是以考研的角度去學習和理解的,很多考試中需要用到的內容在實際應用中可能用不上,比如其中的計算問題,但是如果掌握這些東西會幫你更好的理解這些內容。

  • 這篇關於查找的博客也只是用來記錄以便於后續復習的,所以很多地方只是淺談,並沒有代碼的實現

  • 如果有緣發現這篇文章想要深入了解或者因為作者表達能力差而看不懂以及有錯的地方,歡迎留言指出來,我會盡快去完善的,期待有緣人

  • 內容多和雜,如果有機會我進一步進行梳理,將其重新梳理一片文章(會更注重於代碼)

  • 本來只是想簡單寫一下的,但是不小心就get不到重點了

  • 本來打算等逐步完善和優化后再發出來的,但那樣繼續往前總感覺有所顧及,所以就先給這幾天查找的復習暫時告一段落吧。


導學

概覽

總體

(一)概念

  • 查找:在數據集合中查找特定元素的過程

  • 查找表(查找結構):同一類型數據元素構成的集合

    • 靜態查找表:只涉及查找,不存在修改

      • 適用:順序查找,折半查找,散列查找等

    • 動態查找表:動態插入和刪除,對查找表進行修改

      • 適用:二叉排序樹,散列查找等

所有數據結構都可以看作是查找表,對於折半查找和順序查找這些都屬於查找算法

  • 關鍵字:數據元素中唯一標識該元素的某數據項的值

    • 主關鍵字:此關鍵字能唯一表示一個數據元素

    • 次關鍵字:此關鍵字用以識別若干記錄(一對多)

說明:在查找表中每個數據元素就相當於一條記錄,包含有不同的數據項,例如拿學生為例,一個學生作為數據元素,那么學號,身高,姓名就是這個元素中的數據項,每個學生都有特定的學號,因此學號可以作為關鍵字。(當然如果數據項包含身份證號,你用身份證號走位關鍵字也可以)

0x01平均查找長度(重點)

  • 注意:作為查找算法效率衡量的主要指標,那么查找算法的性能分析肯定是重點分析平均查找長度的,因此必須熟練掌握。

提一嘴,算法效率的度量前面學過時間和空間復雜度,但是算法效率的度量不是只取決於時間和空間復雜度,針對不同的算法還可能會有其他一些輔助度量,如查找算法中的平均查找長度。(不過需要注意后面學排序算法時,其穩定性並不是用來衡量算法的優劣的,只是用來描述算法性質的),下面開始說明平均查找長度

  • 平均查找長度:所有查找過程中進行關鍵字比較次數的平均值

    • 一次查找長度是指需要比較的關鍵字次數

    • 成功平均查找長度:所有元素查找成功的情況

    • 失敗平均查找長度:所有元素查找失敗的情況

  • 數學公式定義:

image-20210411222237211

沒有特殊說明的情況下,默認每個數據元素的查找概率是相等的,記住就是每個元素查找時比較的次數乘以概率然后加起來

舉個例子:

下標 0 1 2
查找集中的值 4 3 5

每個元素的查找概率是1/3,而查找成功時對應的比較次數依次是1,2,3,因此ASL=2(這是成功平均查找長度)

查找失敗時對應的比較次數是4,4,4因此ASL=4

  • 對於不同的概率計算方法是一樣的

(成功)平均查找長度就是1/2*1 + 1/3 *2 + 1/6 * 3 = 5/3

(二)查找算法

  • 算法的實現取決於數據結構的選取

  • 數據結構三要素:邏輯結構,存儲結構,數據的運算

  • 下面的學習是以邏輯結構進行划分的

這里我有個疑惑,就是散列結構屬不屬於線性結構

線性結構:集合中數據元素之間只存在一對一的關系

非線性結構有更細的划分(集合---沒關系,樹形結構---一對多,圖形結構---多對多)

那么散列結構中數據元素之間存不存在一對一的關系呢?

0x02線性結構

1.順序查找(一般線性表)

  • 思想:又稱線性查找,從前往后或者從后往前依次對比進行查找

  • 優點:對數據元素存儲沒有要求,順序表、鏈表都可以

  • 缺點:當n較大時,平均查找長度較大,效率低

  • 重點注意:對於線性的鏈表,只能進行順序查找

(1)實現代碼

typedef struct { // 查找表的結構
Elemtype * elem;  // 指針是采用動態分配,使用時分配
   int TableLen; // 查找表長度
}Stable;
int Search_Seq(Stable S,Elemtype key) {
   S.elem[0] = key;   // 哨兵
   for(i = S.TableLen;i != key;i--) {
       return i;
  }
}
  • 代碼不難,但是這里要介紹一個“哨兵”的概念,在這份代碼中,S.elem[0]就是哨兵,引入“哨兵"可以避免許多不必要的判斷語句,並且隨着程序規模增大,效率提升也會越大

  • "哨兵"需要額外分配一個空間,這里是將數組下標為0的位置留空作為"哨兵

  • 注意:在查找時,元素若是查找失敗是會和“哨兵”進行一次額外比較的,但是有些考題對於哨兵的這一次比較是不算在查找次數之內的,因此查找長度計算會有所不同,看學校

(2)平均查找長度

  • 成功平均查找長度(這里與哨兵的比較是計算在內的):

    • 第i個數查找成功需要比較n-i+1次,如果是從前往后進行比較第i個數比較的次數就是i

    • 每一個元素查找成功的概率是1/n,也就是說P為1/n

    • 平均查找長度為

  • 失敗平均查找長度為:

(3)有序表的順序查找

  • 思想:略微特殊的線性表,其中的關鍵字是有序的,在查找失敗時就不用繼續往下查找了(比如升序序列,發現第4個元素大於查找的元素,那么后續的元素一定是都大於此元素的,就沒有繼續查找的必要了),從而降低失敗平均查找長度

  • 成功平均查找長度和一般線性表一樣

  • 有序表的順序查找和折半查找不一樣,有序表的順序查找一樣適用於鏈表,而折半查找則無法用在鏈表的查詢上

2.折半查找

  • 又稱二分查找

  • 基本思想:以升序序列為例,將key值與表中間位置進行比較,相等則查找成功,若小於則在表的左邊繼續查找,大於則在右邊繼續查找,每次都是與中間位置進行對比

  • 注意:只適用於有序的順序表,同時折半查找需要方便的定位查找區域,所以要求線性表必須具有隨機存取的特性,因此只適用於順序存儲結構,不適用於鏈式存儲結構,同時要求元素按關鍵字有序排列

(1)代碼實現

int BinarySearch(sqLList L, int data) {
int low = 0, high = L.length - 1, mid;
while (low <= high) {
mid = (low + high) / 2;   // 取中間位置,向下取整
printf("%d\n", mid - 1);
if (L.data[mid - 1] == data) {
return mid;
}
else if (L.data[mid - 1] < data) {
low = mid + 1;
}
else
high = mid - 1;
}
}

(2)查找過程

  • 設定一個mid值存儲中間位置的值,這個中間位置mid的值為low和high的和除以2然后向下或者向上取整

0 1 2 3 4 5
7 10 13 16 19 29

查找29(這里采用向下取整)

第一次:low=0指向7,high=5指向29,此時mid=2指向13

13<29,此時范圍縮小至[3,5],(因為13已經比較過了,所以直接從下一個開始)

第二次:low=3指向16,high=5指向29,此時mid=(3+5)/2=4指向19

13<20,此時范圍縮小至[5,5]

第三次:low=5指向29,high=5指向29,此時mid=5指向29

相等-->結束

 

  • 解題注意:向上取整和向下取整生成的判定樹是有區別的

    • 向上取整:左子樹節點數 >= 右子樹節點數

    • 向下取整:右子樹節點數 >= 左子樹節點數

    • 也就是說:一顆判定樹中要么左子樹節點數全部大於等於右子樹節點數,要么右子樹節點數全部大於左子樹節點數

(3)判定樹(重點)

  • 是折半查找過程生成的一棵二叉樹,准確講是用來折半查找過程的一種描述

  • 判定樹是一顆平衡二叉樹同時也是一顆二叉排序樹

注意:判定樹描述的是折半查找過程中所有節點的查找過程,而折半查找中關鍵字比較序列指的是這個二叉排序樹上的一條路徑而已

看下面題目:

(4)判定樹說明

例子:7 10 13 16 19 29 32 33 37 41 43

對應的判定樹:

說明:

  • 葉節點(方形節點)代表查找不成功的情況,但是注意:其方形節點是虛構的,它不計入比較的次數之中

計算ASL時,查找失敗節點不適合放行節點,而是方形節點上層的圓形節點

  • 有序序列有n個元素--->判定樹有n個非葉節點和n+1個方形的葉節點

  • 折半查找關鍵字的比較次數是和其關鍵字在判定樹中的層數有關的,關鍵字在判定樹的第i層,那么查找成功的比較次數就是i

(5)構建判定樹(要會)

  • 構建過程個人感覺就像告訴你先序遍歷和中序遍歷然后讓你確定二叉樹

    不同的是這里每次需要的根節點不是通過先序遍歷告訴你,而是通過low+high然后向下取整計算得到

    擴展:什么樣的情況下能確定一顆二叉樹呢?

    答案:知道先序遍歷和中序遍歷,或知道后序遍歷和中序遍歷,或知道層序遍歷和中序遍歷

(動圖).ing--->盡快補上

(6)平均查找長度(重點)

平均查找長度公式:

  • 失敗查找長度也是根據此公式進行計算,不同的是在判定樹上的高度和節點而已

  • 而一般在計算時采用的公式是

此公式也是計算判定樹樹高的公式,關於這個,我的理解是:

使用第一個求和公式能夠准確的計算平均查找長度,而這個一般計算公式求得的結果是比實際查找長度要大的,因為向上取整了,比如本來平均查找長度是5.00023,但是在題目中需要的是整數,因此向上取整就成了6,根據題目來

看個題目,這個題目雖然屬於分塊查找的,但是主要運算的是折半查找

(7)關於折半查找題目

  • 很多就是給你一個長度已知的順序表,然后讓你求關鍵字比較次數或平均查找長度

  • 解題最重要的思路就是構建判定樹,不一定要全部畫出來,但是給你一個元素個數要直到判定樹的高度


  • 這個題考的是判定樹高度公式

選A,記住即可

  • 求關鍵字比較次數的最值

思路:對於折半查找,關鍵字、節點數目什么的,第一反應就應該是構建判定樹(不要真傻傻的畫出來)關鍵字折半查找的比較次數是和判定樹高度相關的的

求解:順序表長度16,判定樹高度是5,所以關鍵字最多的比較次數就等於樹高5

  • 求平均查找長度

思路:這種題目就需要畫出判定樹然后進行計算了,

取有序表元素為0 1 2 3 4 5 6 7 8 9 10 11

畫出來是這個樣子

查找成功平均查找長度(圓形節點):判定樹的每一層節點數*節點所在高度之和然后除以總個數

(1* 1 + 2 * 2 + 4 * 3 + 5 * 4)/12=37/12

查找失敗平均查找長度(方型也就是葉子節點):判定樹的每一層節點數*(節點所在高度-1)之和然后除以葉子共個數

(3 * 3 + 10 * 4)/13 = 49/13

查找失敗平均查找長度中與節點相乘的並不是節點所在高度,而是所在高度-1是因為方形節點在實際上是不存在的,當我們找到方形節點上一層的圓形節點時就已經知道是否查找失敗了

  • 判定樹

思路:可能,前面知識點講過,一棵折半查找判定樹所有左子樹要么全部大於等於右子樹節點數,要么全部小於等於。因此選A

BCD中都同時存在左子樹大於右子樹節點數,右子樹大於左子樹節點數的子樹

以D為例,根節點左子樹有4個節點,右子樹有5個節點,右子樹大於左子樹

而根節點的左子樹那個節點的左子樹有2個節點,右子樹有1個節點,左子樹大於右子樹

因此不可能成為折半查找判定樹

  • 分塊查找最值問題(分塊問題不多,就與折半查找放到一起了)

思路:最好情況就是分塊最理想,每個塊中有根號n個元素,同時元素之間都是有序的,索引與塊內都可以使用折半查找

求解:

  • 折半查找中關鍵字比較序列

思路:這個題其實問的就是下面選項中那些關鍵字的序列不能構成二叉排序樹的一條查找路徑

3.分塊查找

  • 概念:又稱索引順序查找

  • 特點:既有動態結構又便於快速查找,吸取了順序查找和折半查找的優點

  • 基本思想:將查找表分為若干子塊,塊內元素可以無序,但塊之間必須是有序的(也就是說前一塊中的最大關鍵字必須小於后一塊中的最小關鍵字),然后就是索引表,索引表中存放的是每個塊之中最大的關鍵字和第一個元素的位置。索引表中的關鍵字是有序的

  • 算法描述:先索引查找,然后在在塊中查找。索引查找采用折半查找,塊中查找則可以根據情況采用折半查找或者順序查找

平均查找長度

  • 分塊查找平均長度一般求的是成功平均查找長度

  • 由於塊內查找和索引查找可以采用順序查找或者折半查找,因此,平均查找長度計算有所不同,但整體思路是一樣的,就是將折半查找和順序查找的平均查找長度進行組合,公式:

注意:對n個記錄的索引順序表進行分塊,最理想的塊長是

$$
\sqrt{n}
$$

 

  • 分塊查找因為不同的查找組合會出現不同的平均查找長度,因此就有最理想,最好情況和最壞情況,看例題

0x03樹形結構

1.二叉排序樹(BST)

  • 也稱二叉查找樹

  • 是一種動態樹表,特點是樹的結構通常不是一次生成的,而是在查找過程中,當樹中不存在關鍵字等於給定值的節點時進行插入。

  • 性質:

    • 左子樹非空則左子樹關鍵字值均不大於(不小於)右子樹

    • 右子樹非空則右子樹關鍵字值均不小於(不大於)左子樹

    • 左右子樹又各是一顆二叉排序樹

    很多時候二叉排序樹默認是左子樹值小於右子樹的值,但若有特殊說明則右子樹小於左子樹也是排序二叉樹

  • 如果對二叉排序樹進行中序遍歷就可以得到一個遞增(遞減)的有序序列

  • 存儲結構

typedef struct BTNode {
int data;          // 節點存儲的數據,這里代表關鍵字
struct BTNode *lchild;
struct BTNode *rchild;
}

(1)二叉排序樹的查找

  • 基本思想:(默認二叉排序樹)並不復雜,從根節點開始進行比較,相等即返回,key值小於根節點關鍵字,進入左子樹,反之進入右子樹

  • 遞歸算法

BTNode *BSTSearch(BTNode* bt,int key) {
if(bt == NULL) {
       return NULL;
  } else {
       if(bt->data == key) {
           return bt;
      } else if(bt->data > key) {
           return BSTSearch(bt->lchild,key);
      } else {
           return BSTSearch(bt->rchild,key);
      }
  }
}
  • 非遞歸算法

BSTNode *BST_Search(BiNode T,ElemType key) {
while(T != NULL && T->data != key) {
       if(T->data > key) {
           T = T->lchild;
      } else
           T = T->rchild;
  }
   return T;
}

二叉排序樹的查找並不難,難點和重點在於經過二叉排序樹擴展過來的二叉平衡樹等

(2)二叉排序樹的插入

  • 插入思路:

    • 二叉樹為空,直接插入節點

    • 二叉樹非空,則進行查找,插入的前提是查找,找到屬於該節點的空位置,然后插入

  • 二叉樹插入的關鍵字一定是存儲在新創建的葉子上

int BSTInsert(BTNode *bt,int key) {
   if(bt == NULL) {   // 這里也是先找到空節點,然后進行插入
       bt = (BTNode *)malloc(sizeof(BTNode)); // 因為非空所以先開辟空間
       bt->lchild=bt->rchild=NULL;
       bt->data = key;
       return 1;
  } else {
       if(key == bt->data)
           return 0;   //關鍵字已存在樹中,插入失敗
       else if(key < bt->data) {
           return BSTInsert(bt->lchild,key);
      } else
           return BSTInsert(bt->rchild,key);
  }
}

(3)二叉排序樹的構造

  • 二叉排序樹的構造就是將關鍵字逐個插入到一顆空樹中

void CreateBST(BSTNode *bt,int key[],int n) {
   bt = NULL;  // 一顆空樹或者將樹清空
   for(int i = 0;i < n;i++) {
       BSTInsert(bt,key[i]);
  }
}

(4)二叉排序樹的刪除(略微復雜一點)

  • 二叉排序樹的節點刪除要稍微復雜一點,因為需要 保證在刪除節點后的樹仍讓是一顆二叉排序樹

    假設刪除的是p節點,f節點為其雙親

  1. p節點為葉子節點,可以直接刪除,刪除后不會破壞二叉排序樹的特性

  2. p節點只有右子樹或者左子樹,此時將p刪除然后用p的子樹代替p即可

  3. 若p節點既有右子樹又有左子樹,此時令p的直接后繼(或直接前驅)替代p,然后將直接后繼(或直接前驅)刪除即可。重點在下面

    什么?你說我怎么知道哪一個是直接后繼或者直接前驅?

    來,首先沿着p節點的右子樹的根節點的左指針一直往左走,直到來到最左邊的那個節點就是直接后繼了,直接前驅就是沿着p節點的左子樹的右指針一直往右走,直到來到最右邊的那個節點就是直接前驅了,別看我放在這里,這是重點(敲黑板)

(5)平均查找長度

看到這個你想到了什么? ----------對對對,就是那個,那個折半查找的判定樹

  • 需要說明的是二叉排序樹和判定樹還是有很大區別的,如果此時二叉排序樹剛好是一棵二叉平衡樹,那么此時平均查找長度和折半查找的判定樹是一樣的

  • 關於不同:二分查找的判定樹是唯一的,而二叉排序樹是不唯一的,相同關鍵字因插入順序可能生成不同的二叉排序樹

    要問為什么的話?

    因為判定樹是根據二分查找生成的,而二分查找的查找集是有序的,要么升序要么降序,然后從中間開始生成,因此生成順序是唯一的,而對於一個相同元素的集合,二叉排序樹的插入順序可以是隨意的,因此生成的二叉樹也是不唯一的

  • 當二叉排序樹的平衡因子大於1時,平均查找長度就不一樣了,極端情況下,二叉排序樹的輸入序列是有序的,此時是一顆傾斜的單枝樹,平均查找長度就和順序查找一樣了,性能顯著變壞(就像下圖右邊一樣)

    所以后面才出現了平衡二叉樹,平衡二叉樹里不會出現這種極端情況,因為其左右子樹的高度差是被限制了的。(當人們發現二叉排序樹越極端,效率越低時,就在想能不能通過控制二叉排序樹的高度來保證二叉排序樹的效率,因此平衡二叉樹就出現了)

    極端情況相關題目:

(6)二叉排序樹相關題目

  • 查找路徑(序列)問題

  • 插入刪除相關題目

2.平衡二叉樹

(1)知識點

  • 已知平衡二叉樹高度求最少節點==已知節點數求最大深度(高度)--->用平衡二叉樹遞推公式求解

  • 以最少節點數構造平衡二叉樹的遞推公式:(這個公式在解題中很重要)---->

    $$
    N_h=N_{h-1}+N_{h-2}+1\\ 如N_0=0,N_1=1;N_2=N_0+N_1+1=2 \\其中N_2=2代表構造高度為2的平衡二叉樹最少需要2個節點
    $$

  • 所有非葉節點的平衡因子均為1,即平衡二叉樹滿足平衡的最少節點的情況

答案解釋在平衡二叉樹相關題目模塊中

(2)概念

好了,講完了二叉排序樹,接下來開始平衡二叉樹(AVL樹),還記得平均查找長度的英文縮寫嗎?不記得就上去看(哼~)

  • 又稱AVL樹,(也可簡稱為平衡樹),是一種特殊的二叉排序樹,因此它具有二叉排序樹的性質,同時其左右子樹都是平衡二叉樹

    二叉平衡樹是排序二叉樹的改進

  • 定義;所有樹的左右子樹高度之差不超過1,也就是平衡因子不大於1(只能為-1,0,1)

    平衡因子:左子樹和右子樹的高度之差

    下圖中每個節點中的數據就是該樹(以該節點為根)的平衡因子

(3)平衡二叉樹的插入

這個就比較復雜了,復雜的地方在於如果因為插入節點導致樹不再平衡就需要進行調整

那么,如何保證平衡呢?

  • 基本思路:當刪除或插入導致平衡樹的平衡被打破,首先找到插入路徑上離插入節點最近的平衡因子大於1的節點p,再對以p節點為根的樹進行調整,使之重新達到平衡。調整規律分為(LL,RR,LR,RL)四種情況:

    下面的調整描述是王道書上的,如果不好理解建議看一下天勤的描述,較之王道要更通俗一點,這里就不貼出來了

    1. LL平衡旋轉(右單旋轉)

      由於在結點A的左孩子(L)的左子樹(L)上插入了新結點,導致 A平衡因子由1增至2,以A為根的子樹失去平衡,因此需要一次向右旋轉操作。

      具體操作過程是將A的左孩子B向右上旋轉代替A成為根結點,將A結點向右下旋轉成為B的右子樹的根結點,而B的原右子樹則作為A結點的左子樹。

    2. RR平衡旋轉(左單旋轉)

      由於在結點A的右孩子(R)的右子樹(R)上插入了新結點,A的平衡因子由-1減至-2,導致以A為根的子樹失去平衡,需要一次向左的旋轉操作。

      具體操作過程是:將A的右孩子B向左上旋轉代替A成為根結點,將A結點向左下旋轉成為B的左子樹的根結點,而B的原左子樹則作為A結點的右子樹,如圖5.29所示。

    3. LR平衡旋轉(先左后右雙旋轉)

      由於在A的左孩子(L)的右子樹(R)上插入新結點,A的平衡因子由1增至2,導致以A為根的子樹失去平衡,需要進行兩次旋轉操作,先左旋轉后右旋轉。

      具體操作過程:先將A結點的左孩子B的右子樹的根結點C向左上旋轉提升到B結點的位置,然后再把該C結點向右上旋轉提升到A結點的位置,如圖5.30所示。

    4. RL平衡旋轉(先右后左雙旋轉)

      由於在A的右孩子(R)的左子樹(L)上插入新結點,A的平衡因子由-1減至-2,導致以A為根的子樹失去平衡,需要進行兩次旋轉操作,先右旋轉后左旋轉。

      具體操作過程:先將A結點的右孩子B的左子樹的根結點C向右上旋轉提升到B結點的位置,然后再把該C結點向左上旋轉提升到A結點的位置,如圖5.31所示。

  • 在RL和LR平衡旋轉中,新節點究竟是插入第二次子樹后的左子樹還是右子樹不影響旋轉過程。

  • 這里說一下,平衡二叉樹的構建和二叉排序樹一樣就是不斷的將節點插入一棵空樹,通過調整不斷維持其平衡。

  • 調整方式的命名:LL,RR,LR,RL並不是對調整過程的描述,而是對不平狀態的描述,比如LL是在左子樹(L)的左子樹(L)上插入節點導致不平衡(調整就是向着反方向調整,因此是右單旋轉,向右),LR是指在其左子樹(L)的右子樹(R)上插入節點導致不平衡,調整因此是先向左后向右進行兩次旋轉,RR和RL同理。

(4)平均查找長度

  • 和折半查找生成的判定樹的平均查找長度是相同的

(5)平衡二叉樹相關題目

  • 給定節點求深度最值

思路:已知節點求最大深度--->遞推公式

$$
N_{0}=0 \qquad N_1=1 \qquad N_2=N_0+N_1+1=2 \\N_3=N_2+N_1=4 \qquad N_4=N_3+N_2+1=7 \\N_5=N_4+N_3+1=12 \qquad N_6=N_5+N_4+1=20
$$

從遞推公式可以看到20是大於13小於21,因此最大深度為6

  • 給定高度求節點最值

思路:已知高度求最少節點

根據上面的公式:5層需要至少12個節點

  • 節點總數問題

思路:重點是非葉子節點的平衡因子均為1代表此時平衡二叉樹擁有最少的節點,題目就變成了已知高度求最少節點

根據上面的公式:6層知道需要20個節點

  • 二叉樹的構建問題

思路:此題考查的是平衡二叉樹的構建,本質是考平衡二叉樹插入時的平衡調整

  • 綜合選項

樹中最大元素一定無左子樹,但可能有右子樹,不一定是葉子節點

  • 對插入的理解

答案:A

注意最后一種平衡調整(這里是LR平衡旋轉),先以1為根節點進行左旋,然后以3為節點進行右旋

3.B樹

  • B樹(B-樹),是一種多路平衡查找樹,也是由二叉樹變換而來的

  • 記住:B樹不支持順序查找(B+樹是支持的)

  • B樹中所有結點孩子節點個數最大的值稱為B樹的階,通常用m表示

  • 一棵m階B樹或空樹性質:

    • 每個節點最多有m棵子樹,至多含有m-1個關鍵字

  • 若根節點不是終端節點,則至少有兩棵子樹

    • 除根節點之外所有非葉節點的子樹至少有

     

  • 除根節點之外所有非葉節點的關鍵字至少有

  • 所有非葉節點結如下:

    說明,K代表的是節點的關鍵字,且K1<K2<...Kn;P代表的是指向子樹根節點的指針,注意P(i-1)指向的子樹中的關鍵字都是小於Ki

     

  • 所有葉節點都出現在同一層次上,並且不帶信息(實際上這些節點並不存在,指向這些的節點指針為空),這些也是查找失敗到達的位置

    雖然實際上並不存在,邏輯上是被當作存在的,因此說葉子節點的時候指的就是這一層節點

  • B樹屬於平衡m叉查找樹,其左右子樹的高度差不會超過1

  • B樹的關鍵字樹比子樹數目要小一,就好像一個正方形,關鍵字是正方形中的內容,而指向子樹的指針就是正方形下面的兩個端點

  • 如果已經規定或者知道了樹是m階B樹,那么即使畫出來的樹的結構圖中節點中沒有m-1數目的關鍵字,其在空間分配中也還是占據了m-1個關鍵字應該占據的空間。

  • 放在前面(理解)

    B樹的插入和刪除操作后需要進行分裂和合並的目的是使得被修改節點的關鍵字數目n滿足

(1)B樹的查找

  • B樹的查找是二叉排序樹的擴展,二叉排序樹是二路查找,B樹是多路

  • 兩個基本操作:查找節點和在節點中查找關鍵字

    • 前一個操作時在磁盤上進行的,后一個操作是在內存中進行的(在找到目標節點后將節點信息讀入內存,然后采用順序查找或折半查找)

  • 基本思路:(此處以升序為例)

    • 先從根節點開始,相等即結束,關鍵字A比key大,就順着關鍵字A前面的指針進入子樹中,關鍵字A比key小就進入關鍵字A后面的指針進入子樹中,重復上述過程,直到查找成功或者查找到葉節點時就查找失敗(對應指針為空指針)。

(2)B樹的插入

  • 和所有樹的構建一樣B樹的構建也是在一棵空樹上不斷插入關鍵字

  • B樹比二叉排序樹要稍微復雜一點

二叉排序樹在找到終端節點后插入即可,而B樹則需要做額外的處理以使得插入節點后的樹滿足B樹的定義

  • 過程:

    • 1-->定位,首先利用上述的查找算法找出最底層的某個非葉節點(注意插入是插入至葉節點),如果樹中已存在該關鍵字就無法進行插入,不存在的話會找到查找失敗的葉節點,則個時候指向查找失敗葉節點的上層節點就是我們需要的非葉節點。

    • 2-->插入,插入的時候就需要注意情況了,因為在插入一個關鍵字后可能該節點的關鍵字數量會超過每個節點允許的最大關鍵字數目,因此就需要進行調整。所以,兩種情況

      • 第一種就是插入一個關鍵字后節點的關鍵字數量不大於B樹允許的每個節點的最大關鍵字數量m,直接插入即可(即找到的節點關鍵字數目小於m-1)

      • 第二種就是進行插入后節點的關鍵字數大於m-1,這個時候就需要進行分裂

        具體過程:

        從中間位置[m/2(向上取整)]將節點關鍵字分為三部分,分別是中間位置的左邊,中間位置的那個數,中間位置的右邊,然后將中間位置那個關鍵字添加到父節點中去,如果此時父節點關鍵字溢出則將父節點進行分裂。

(3)B樹的刪除

  • B樹的刪除重點就是要掌握在刪除最底層非葉節點時的不同情況下的合並操作

  • B樹的刪除要稍微復雜一點,分為幾種處理情況

    • 關鍵字k位於最底層非葉節點中時,用k的直接前驅(或直接后繼)k2替代k,然后刪除k2即可

    刪除最底層非葉節點就是不斷的替換然后轉換成刪除最最曾非葉節點

    直接前驅看刪除關鍵字前一個指針下指向的節點的最大的關鍵字,后繼看關鍵字后一個指針指向的節點的最小的關鍵字

    • 重點:關鍵字k位於最底層非葉節點,有三種情況

(4)解題知識點

  • 根節點為非終端節點時,最少的字節點數和關鍵書分別為2和1

  • 已知B樹中的關鍵字求最大高度和最小高度

    公式推導不難,不用公式做題也不難,但這樣就慢了

這種思想就是求最小高度,就是每棵樹都有最大的孩子m

最大高度就是根節點有兩個孩子節點,其他非葉節點都有m/2向上取整孩子節點數

  • 具有n個關鍵字的B樹有n+1個葉子節點

    B樹的葉節點對應的查找失敗的情況,對n個關鍵字的查找集合進行查找,失敗可能性有n+1種

    理解的話:給你一批有序的數字,然后讓你查找k,找不到無非就是三種情況,1落在了那一批數字中間的間隔之中(比如在5、7中找6,失敗的原因就是6落在了5和7中間) 2所有數都大於k 3所有數都小於k

(5)B樹相關問題

  • 關於的B樹和B+樹,選擇題一般考察的就是對B樹概念的理解以及節點或者關鍵字的最值問題

其實關於樹最多的計算問題就是知道樹的深度求最多或者最少的節點數,知道節點數求樹的最大深度或者最小深度

知識點中有,答案n+1

思路:給定高度求節點的最值,考的就是對於B樹的特性,哪些節點至少和至多擁有的子樹數目和關鍵字的數目

8--->至少:根節點含有兩個子節點,其他非葉節點都含有3/2向上取整為2個節點,所以是一顆完全二叉樹,個數為2的5次方-1=31

至多:所有非葉節點都含有3個子節點,1+3+9+27+81=121(此處是等比數列),一般求至多需要用到等比求和公式

9--->至少:此處不再是節點而是關鍵字,但也是含有最少節點數,本質是一樣的

至少:根節點含有2個子節點,其他非葉節點含有的子節點數為5/2向上取整-1為2,但此處高度只有2,都不需要考慮那么多

直接算,第一層一個節點,第二層2個節點,2*2+1=5,注意:根節點的關鍵字數可以為1

 

4.B+樹

  • B+樹是應數據庫所需而生的一種B樹的變形樹

  • 性質:

    • 相同性質

      • 每個分支節點最多有m棵子樹(孩子節點)

      • 非根葉節點至少有兩棵子樹

      • 除根節點之外所有非葉節點的子樹至少有

        $$
        \lceil{m/2}\rceil
        $$
    • 不同性質:

      • 節點的子樹個數和關鍵字個數相同

      • 所有葉節點包含全部關鍵字及指向其相應記錄的指針,葉節點中關鍵字按有序排列

      • 所有非葉節點僅起到一個索引的作用,即節點中的每個索引項只含有對應子樹的最大關鍵字和指向該子樹的指針,不含有該關鍵字對應記錄的存儲地址,而在B-樹中,每個關鍵字對應一個記錄的存儲地址。

  • 通常在B+書中有兩個頭指針,一個指向根節點,另一個指向關鍵字最小的葉節點

  • B+樹的應用方面從這個題的解析中就能了解

0x04散列結構

這一節主要還是將書上的內容內容放上來了,后面還是重點介紹一下題目

1.散列表

  • 記錄在表中的位置和關鍵字之間存在着確定的關系

敲黑板:對於考研這一部分要求記住一句話---根據給定的關鍵字計算出關鍵字在表中的位置

(1)概念介紹

  • 散列表:根據關鍵字進行直接訪問的數據結構

散列表建立了關鍵字和存儲地址之間的一種映射關系

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

    $$
    Hash(key)=Addr
    $$
  • 沖突:散列函數將兩個或兩個以上的不同關鍵字映射到同一地址上

  • 同義詞:發生碰撞(沖突)的不同關鍵字之間稱為同義詞

(2)散列表的建立

  • 散列表的建立分為兩部分,首先是通過散列函數將關鍵字插入表中,然后如果產生沖突就進行沖突處理,然后將數據插入其中

下面實際中常用的散列函數:

  • 一個好的散列函數應該盡可能的避免產生沖突,但是任何散列函數都會產生沖突,我們所能做的就是盡量降低產生沖突的可能性。

處理沖突的方法:

  • 取定某一增量序列后,對應的處理方法就是確定的,一般有下面四種取法:

  • 在采用開放定址的情形下,刪除元素只能進行邏輯刪除,就是在想要刪除的元素位置做一個刪除標記,不能進行物理刪除。

    因為若進行物理刪除元素就會截斷其他具有相同散列地址元素的查找地址。這樣做的副作用就是進行多次刪除之后,表面上看散列表很滿,實際上有許多位置未利用,因此需要定期維護散列表,將刪除標記的元素進行物理刪除。

  • 另一種沖突處理方法:

image-20210409235216596

(3)散列表的查找

  • 散列表的查找與構造基本一致

    • 首先根據散列函數查找關鍵字對應地址是否有記錄,若無記錄則查找失敗,有記錄則進行比較,若相等就返回查找成功標志,失敗則執行接下來的步驟

    • 用給定的沖突處理方法計算“下一個散列地址”

  • 散列表的查找效率取決於三個因素:散列函數,沖突處理的方法和裝填因子

  • 散列表的平均查找長度依賴於散列表的裝填因子,而不依賴於散列表的長度m和表中記錄數n

(4)平均查找長度

  • 平均查找長度會因為散列函數和沖突處理的不同而不同

    • 下面的例子:散列函數--H(key)=key%13 沖突處理--線性探測

      散列表

    查找成功關鍵字比較次數:

計算方法:首先14%13=1(地址),而1地址存放的剛好就是14,因此比較次數為1

01%13=1(地址),1地址存放的是14,發生沖突因此采用線性探測(從2位置從前往后查找)繼續查找,發現第二個位置是01,因此比較次數為2

看27%13=1(地址),1地址存放的非27,開始使用沖突處理繼續查找,依次往后找,發現在第四個位置,從前往后依次比較過1,2,3,4,比較次數為4

依次類推

  • 至於查找失敗平均查找長度:在后續題目中會進行講解(emmmm,主要是不曉得咋個說)

  • 散列查找的平均查找長度的計算有些細節東西是需要注意的,不然很容易出錯,具體看最后的題目

(5)其他

  • 散列表的裝填因子(定義為一個表的裝滿程度):

    • 裝填因子越大,發生沖突的可能性越大。

(6)散列表相關題目

這里答案是D,但我還是覺得是(K-1)K/2,因為第一個是沒有發生沖突的,也就是說進行線性探測的只有K-1個數

這種題就是比較簡單的了,依次將關鍵字帶入散列函數然后看結果為1的個數即可,有4個

會借着這個題目詳細記錄一下散列查找的平均查找長度的計算

首先拿到題目先將散列表畫出來,先將所有關鍵字帶入散列函數中得到

關鍵字 87 40 30 6 11 22 98 20
散列計算 3 5 2 6 4 1 0 6

計算過后發現只有6和20發生了沖突,因為6計算在前,因此對20進行沖突處理得到最終的散列地址

散列地址 0 1 2 3 4 5 6 7 8
關鍵字 98 22 30 87 11 40 6 20 NULL

每個關鍵字的查找失敗長度就是從該關鍵字的散列地址出發依次往后對比(數),直到找到為空結束

以關鍵字98為例,依次往后找,空的散列地址是8,那么一共比較了9次(0~8)

其他依次類推,散列地址從0~8的關鍵字對應比較的是 (9~3次) 然后將所有關鍵字的查找失敗的查找長度相加除以m即可,這里的m是7並不是表長8

最后結果就是:(9+8+7+6+5+4+3)/7=6

需要注意的是:你只能將從0-6地址的關鍵字的比較次數加起來然后除以7,第7個地址的關鍵字沒有算進來的

這里很容易出錯,很多人都以為是除以表長,其實不然,因為你的散列函數中模長是小於表長的,對於超過模長的關鍵字比如散列地址為7的關鍵字散列函數是找不到的

  • 總結一下:平均查找長度的計算是如果模長小於表長,是只算到模長的

  • 說一下線性探測再散列就是線性探測法

因為王道書上的開放定址法有四種,其中第三個的名字叫再散列法,搞得我以為線性探測再散列法是先線性探測再進行再散列法

然后發現數據結構教材上開放定址法只寫了三個,分別是線性探測再散列,二次探測再散列,偽隨機探測再散列,是沒有的再散列法的,不過有個再哈希法,這個再哈希法和王道書上的再散列法是不一樣的,()這個哈希法采用的是另一種哈希函數)而且不屬於開放定址法


一些無關的話題

個人認為最好的學習狀態是以實際應用或考試為驅動的學習

算法最核心的就是思想,其次就是數據結構,不同的數據結構搭配上算法組成各種各樣的程序

$$
\sum_{i=1}^n\sum_{j=1}^i\sum_{k=1}^j1
$$


免責聲明!

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



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