第六章:查找
1.查找的基本概念
查找
:在數據集合中尋找滿足某種條件的數據元素的過程。
查找的結果
查找成功和查找失敗
查找表
:用於查找的數據集合,由同一種數據類型(或記錄)的組成,可以是一個數組或鏈表等數據類型
操作
- 查詢某個特定的數據元素是否在查找表中
- 檢索滿足條件的某個特定的數據元素的各種屬性
- 在查找表中插入一個數據元素
- 從查找表中刪除一個數據元素
如果只有前連個操作的查找表稱爲:靜態查找表,有上面四個操作的稱爲:動態查找表
關鍵字
:數據元素中唯一標識該元素的某個數據項的值,使用基於關鍵字的查找,查找結果應該是唯一的
平均查找長度
:查找時,關鍵字比較次數的平均值ASL
Pi:編號爲
i
的數據元素的查找概率,我們一般假設每個元素的查找概率是相同的,那麼n個元素的查找概率就是1/n
,Ci
:編號爲i
的元素的查找長度(注意這個公式代表的是查找成功的查找長度)
2.順序查找
順序查找
:又稱線性查找,主要用於在 線性表
中進行查找
其中這個線性表又分爲有序和無序的,他們的查找方式不同。例如下圖:我們給每一個人加上一個序號,這個序號就是關鍵字
2.1無序線性表
然後我們現在要查找7號人,我們則需要依次遍歷整個線性表,纔可以說此表中沒有7號;即
對無序線性表進行順序查找,查找失敗時要遍歷整個線性表
代碼實現:
結構體是我們需要查找的查找表,查找函數中的
ST.elem[0]=key
,我們稱爲哨兵,它的作用下面講:首先我們查找是從後往前查找的,如果我們找到了關鍵字,那麼返回下標,注意我們那個哨兵的值是key,所以這個循環一定會查找到,但是如果是匹配到最後一個元素,他返回值是0,0代表哨兵的位置,那麼我們就可以判斷查找失敗,所以加這個哨兵其實就是相當於查找函數中可以少寫一個判斷。
這裏每個元素的查找長度爲
n-i+1
,每個元素的查找概率Pi=1/n
,則它的平均查找長度爲:
ASL失敗=n+1
2.2有序線性表
我們要查找3號人,我從開始比較,最後在第三個位置找到,我們可以發現如果要查找的元素在查找表中,那麼它的查找方式和無序表方式相同。那麼如果我們如果查找4號人,我們從頭開始比較,直到比較到5號後,還不相等,那麼我們就不需要繼續進行判斷了,即
對關鍵字有序線性表進行順序查找,查找失敗時不一定要遍歷整個線性表
這裏查找成功的查找長度和上面的無序表一樣,爲了找查找失敗的查找長度我們需要學習判定樹
2.2判定樹
判定樹
:描述查找過程的二叉排序樹
例如有序線性表:(10,20,30,40,50),我們首先講該有序表構造成一顆二叉排序樹如下圖:
二叉排序樹:所有左子樹值都比根節點小,所有右子樹值都比根節點大
下面我們標出,每個結點之間查找失敗的元素範圍
我們稱這些橘黃色的矩形範圍內的結點爲:失敗結點
例如我們利用上面的二叉排序樹查找值爲30的結點,首先10!=20,根據二叉排序的特點我們繼續查找右子樹,然後20!=30,繼續查找右子樹,30==30查找成功;如果我們查找值爲25的結點,首先10!=25,20!=25,30<25,所以我們要比較30的左子樹,則查找失敗。查找失敗一定會走到對於的失敗節點,並且我們發現對於順序查找失敗節點的層數減一就是比較次數
ASL失敗=n/2 + n/(n+1)
這裏我們看的是一個區間的失敗結點的平均查找長度,整個數據元素我們是沒法計算的,因爲有無窮個元素
3.折半查找
上面我們講的是順序查找,即依次進行查找,比如我們查找6號元素,就如圖所示6號元素比較靠後,那麼我們查找的效率就比較慢,我們如果不是從第一個位置依次向後進行查找,而是從後往前或者從後半段進行查找,那麼效率相比會高一點。
折半查找
:又稱二分查找,僅適用於有序的順序表
算法思想
- 首先將給定值key與表中中間位置元素的關鍵字比較
- 若相等,則返回該元素的位置
- 若不等,則在前半部分或者是後半部分進行查找
- 重複該過程,直到找到查找的元素爲止;或查找失敗。(遞歸)
查找序列升序時
若key小於中間元素,則查找前半部分
若key大於中間元素,則查找後半部分
這裏我們就可以看出爲啥這裏要求是順序表;如果是鏈表,我們想找中間元素位置則需要遍歷整個鏈表,而對於順序表來說我們只需要除以2就可以找到中間元素的下標了
代碼實現:
三個初始變量分別代表:我們查找部分的第一個元素下標,最後一個元素的下標和中間元素的下標
我們使用一個循環語句來執行這個折半查找過程,注意這裏是low<=high,其中(low+high)/2,肯定會出現小數,而我賦值給一個整數,相當於取下界了,然後當前mid只需的關鍵字的值和我們要查找的相等則直接返回mid即可,如果大於我們要查找的關鍵字的值,此時我們要查找左邊部分,所以令high=mid-1;如果小於我們要查找的關鍵字的值,我們要查找右邊部分,所以令low=mid+1;如果循環結束還沒查找到,則返回-1
折半查找的判定樹
我們可以看出折半查找的二叉樹的層數是比較少的,而層數代表比較的次數直接相關
ASL(成功)=
Log2(n+1) -1
(判定樹是滿二叉樹)對於上圖
ASL(成功)=(1*1+2*2+3*4+4*4)/11=3
,對於上圖ASL(失敗),我們沒有給出失敗的例子,所以直接按照失敗結點爲單位進行計算ASL(失敗)=(4*3+8*4)/12=11/3
,這裏一共有12個失敗結點,第4層有4個失敗結點,所以是4*(4-1)
,第5層有8個失敗結點所以是8*(5-1)
折半查找的時間複雜度爲O(log2n)
(以2爲底,n的對數)
總結:順序查找適用於順序存儲和鏈式存儲,序列有序無序皆可。折半查找只適用於順序存儲,且要求序列一定有序
4.分塊查找
分塊查找我們將要查找的序列分成塊,然後每一個塊有一個名字,我查找的俺塊進行查找,每一個塊的名字是這塊元素中值最大的那個。我們在查找的時候首先可以比較該元素和塊的名稱的大小,如果比該塊大的話,找下一塊,小或等於的話可以進入塊進行比較。
分塊查找:又稱索引順序查找,它吸取了順序查找和折半查找各自的有點,既有動態結構,又適於快速查找。
如何分塊 ?
- 將查找表分爲若干子塊。塊內的元素可以無序,但塊間是有序的,即對於所有塊有第 i 塊的最大關鍵字小於第 i+1 塊的所有記錄的關鍵字。
- 建立索引表,索引表中的每個元素含有各塊的最大關鍵字和各塊中的第一個元素的地址,索引表按關鍵字有序排列。
即:塊內無序塊間有序
如何查找?
1、在索引表中確定待查記錄所在的塊,可以順序查找或折半查找索引表
2、在塊內進行順序查找
比如我們要查找53這個元素,首先查找在哪一塊,這裏我們可以使用順序查找也可以使用折半查找,加入我們使用順序查找,此時查找到下標爲3的這個索引表位置的時候因爲 53<65,所以我們進入塊內查找,此時只能使用順序查找,接着我們查找到53元素。
平均查找長度
5.B樹
B樹
又稱多路平衡查找樹,B樹中所有結點的孩子結點數
的最大值稱爲B樹的階。
一棵m階B樹或爲空樹,或爲滿足如下特性的m叉樹:
- 1)樹中每個結點至多有
m
棵子樹(即至多含有m-1
個關鍵字) - 2)若根結點不是終端結點,則至少有兩棵子樹
- 3)除根結點外的所有非葉結點至少有
m/2 取上界
棵子樹(即m/2-1取上界-1
個關鍵字) - 4)非葉結點的結構
- 5)所有的葉子結點都出現在同一層次上,並不帶任何信息
n是結點關鍵字的個數:
m/2-1<n<m-1
Ki(i=1,2,...,n)
爲結點的關鍵字,k1<k2<....<kn
Pi(i=1,2,...,n)
爲子樹根結點的指針,Pi-1
所指子樹的關鍵字均小於Ki,Pi所指子樹的關鍵字均大於Ki,
觀察上面的B樹可以發現其實B樹是由二叉排序樹更改來的,這樣的排序結果方便我們查找
這裏需要注意,上邊的B樹的葉子結點都在第四次上,最後一層的結點有的地方作爲虛擬的結點,有的則作爲實際的一層存在
n個關鍵字,階數爲m,高度爲h的B樹,它對這個高度h有什麼要求?
首先我們求h最小值,h何時最小?當B樹每一個結點關鍵字的個數達到最大,即
解釋:每個結點關鍵字個數達到最大,第一層有1個結點,第二層有m個結點,第三層有m^2個結點…每一個結點最多有m-1個關鍵字,所以乘以m-1,最後化簡n<=m^h -1
,移項化簡即可得到h和n得關係
然後我們求h最大值,h何時最小?當B樹每個結點關鍵字的個數達到最小,即
首先根節點至少有一個關鍵字, 那麼他有兩個子樹,而且這兩個子樹的每一個節點至少有m/2 -1
個關鍵字,而且每個結點至少有m/2個子樹,同理可以計算出第三層有多少個子樹和關鍵字…最後得到最後一層2(m/2)^(h-1)<=n+1
,最終化簡得到: ,這個式子不需要記憶,一般都是給出要給一個例子,手動計算即可。
5.1查找
- 在B樹中找結點(磁盤)
- 在節點中找關鍵字(內存)
5.2插入
1.定位
查找插入該關鍵字的位置,即最底層中的某個非葉子結點;規定一定是插入在最底層的某個非葉子結點內
2.插入
若插入後,不破會m階二叉樹的定義,即插入後結點關鍵字個數在屬於區間[m/2 -1,m-1]
,則直接插入
例如同樣是上圖B樹,我們插入結點13:
若插入後,關鍵字數量大於m-1,則對插入後的結點進行分裂操作
分裂:插入後的結點中間位置(m/2
)關鍵字併入父結點中,中間結點左側結點留在原先的結點中,右側結點放入新的節點中,若併入父節點後,父結點關鍵字數量超出範圍,繼續向上分裂,直到符合要求爲止
例如同樣是上圖B樹,我們插入結點19:
插入之後我們發現此時不滿足,B樹的定義了,所以我們要進行向上分裂操作,即將中間位置的結點併入父結點當中,然後左側的19留在原來的結點當中,右側的21我要加入到新的結點當中如下圖:
此時我們發現分裂後任然不符合B樹的定義,同樣也要進行分裂操作:
然而分裂完成後,任然不符合B樹定義,我們繼續分裂,接着符合B樹的定義:
5.3刪除
刪除操作我們要分情況進行討論,把所有的結點分爲非終端結點
和終端結點
:
對於終端節點的刪除操作:
-
- 直接刪除:若被刪除關鍵字所在結點關鍵字總數>m/2 -1,表明刪除後仍滿足B樹定義可以直接刪除
-
- 兄弟夠借:若被刪除關鍵字所在結點關鍵字總數=m/2 -1(關鍵字的最小值),且與此結點鄰近的兄弟結點的關鍵字個數≥m/2,則需要從兄弟結點借一個關鍵字,此過程需要調整該結點、雙親結點和兄弟結點的關鍵字
同樣上圖的B樹,我們刪除結點24:
此時我需要從兄弟節點中借一個關鍵字,假設我們從左兄弟借一個結點,首先我們從這兩個兄弟結點中夾着的雙親結點23,放到我們刪除24位置,然後我們將左兄弟結點中最大的那個放到剛纔的23結點的位置上
- 3)兄弟不夠借:若被刪除關鍵字所在結點關鍵字總數=m/2 -1,且與此結點鄰近的兄弟結點的關鍵字個數=m/2-1,則刪除關鍵字,並與一個不夠借的兄弟結點和雙親結點中兩兄弟子樹中間的關鍵字合併。合併後若雙親結點因減少一個結點導致不符合定義,則繼續執行2、3步驟
如下圖的B樹,我們刪除結點30
我們可以發現30這個結點的兩側的兄弟結點都不夠借,所以我們要與一個不夠借的兄弟結點和雙親結點中兩兄弟子樹中間的關鍵字合併。
對於非終端節點的刪除操作:
- 1)若小於k的子樹中關鍵字個數>m/2 -1,則找出k的前驅值k‘,並用k’來取代k,再遞歸地刪除k即可。
- 2)若大於k的子樹中關鍵字個數>m/2 -1,則找出k的後繼值k‘,並用k’來取代k,再遞歸地刪除k即可。
- 3)若前後兩子樹關鍵字個數均爲m/2 -1,則直接兩個子結點合併,然後刪除k即可。
首先進行合併
然後再進行刪除
6.B+樹
B+樹
一棵m階B+樹滿足如下特性:
- 1)每個分支結點最多有m棵子樹(子結點)
- 2)若根結點不是終端結點,則至少有兩棵子樹
- 3)除根結點外的所有非葉結點至少有
m/2
棵子樹,子樹和關鍵字個數相等
- 4)所有
葉結點
包含全部關鍵字及指向相應記錄的指針,葉結點中將關鍵字按大小順序排列,並且相鄰結點按大小順序連接起來 - 5)所有分支結點 (可視爲索引的索引) 中僅包含他的各個子結點(下一級索引塊)中關鍵字的最大值及指向其子結點的指針
B+樹 vs B+樹
-
1)在B+樹中,具有n個關鍵字的結點值含有n棵子樹,即每個關鍵字對應一棵子樹;在B樹中,具有n個關鍵字的結點含有n+1棵子樹
-
2)在B+樹中,葉結點包含信息,所有非葉結點僅起索引作用,非葉結點中的每個索引項只含有對應子樹的最大關鍵字和指向該子樹關鍵字的指針,不含有該關鍵字對應記錄的存儲地址
-
3)在B+樹中,葉結點包含全部關鍵字即在非葉結點中出現的關鍵字也會出現在葉結點中;在B樹中,葉結點包含的關鍵字和其他結點包含的關鍵字是不重複的
6.1查找
在B+樹種,和B樹不同的是,我們有兩種查找方式,我們可以從樹的根節點進行 多路查找,也可以從樹的最後一層進行順序查找。
注意:在B+樹中查找時,無論查找成功還是失敗一定是查找大葉節點當中的值爲止。
7.散列
7.1散列表
我們上面學習的順序、折半、分塊或者B樹查找都是基於比較進行查找的,與下面我們講的散列查找不同。首先我們講一個小例子:
如上面的查找,如果我們使用平常的比較進行查找按照順序依次進行查找。但是如果我們知道紅色就是1號,黃色就是2號…那我們如果將這種特性進行映射其實就相當於我們的散列。
散列函數:一個把查找表中的關鍵字映射成該關鍵字對應的地址的函數
也就是通過這個函數傳入一個關鍵字值,他就能返回此關鍵字對應的地址;但是我們需要注意的是這裏的地址並不侷限於只是地址,它還可以是數組的下標或索引的,只要能代表關鍵字即可。
例如:我們有一個數組,此時我們假設Hash(key)=key%3;此時我們用散列出的下標代表關鍵字的位置:6%3=0,所以對應的下標爲0
散列表
:根據關鍵字而直接進行訪問的數據結構。他建立了關鍵字與存儲地址之間的一種直接映射關係
那麼既然它的查找效率更高,爲什麼沒有得到推廣呢?
因爲這個方法存在衝突的問題,如上面我們通過取餘3來找關鍵字存儲的下標,假設我們現在還要存儲關鍵字3,那麼3%3=0;而0我們已經存儲了6,這就衝突了。
衝突
:散列函數可能會把多個不同的關鍵字映射到同一地址下的情況
這樣的衝突其實是不可避免的,所以我們接下來的學習中主要是設計散列函數如何減小這種衝突發生的可能性,以及就算髮生了衝突怎麼樣讓其不影響我們的查找。
7.2散列函數
散列函數:一個把查找表中的關鍵字映射成該關鍵字對應的地址的函數;(Hash(key)=Addr)
散列函數構造要求:
- 1)散列函數的定義域必須包含全部需要存儲的關鍵字,而值域的範圍則依賴於散列表的大小或地址範圍。
- 2)散列函數計算出來的地址應該能等概率均勻分佈在整個地址空間中,從而減少衝突的發生。
- 3)散列函數應儘量簡單,能夠在較短時間內計算出任一關鍵字對應的散列地址。
直接定值法: 直接取關鍵字的某個線性函數值爲散列地址。 Hash(key)=a*key+b (其中a,b爲常數)
特點:方法簡單,不會產生衝突,若關鍵字分佈不連續,則會浪費空間。
除留取餘法: Hash(key)=key%p;p的取法:假定散列表表長爲m,取一個不大於m但最接近或等於m的質數p
特點:選好p是關鍵,可以減少衝突的可能。
數字分析法: 適用於關鍵字已知的集合,若更換關鍵字則需要重新構造散列函數
如上圖有8個關鍵字的二進制位,我們可以看到每個二進制數的前8爲都是一樣的,而後四位各不相同,所以如果我們將這後四位當作各個關鍵字的散列地址的計算的話,這樣的方法就是數字分析法。
平方取中法: 這種方法取關鍵字的平方值的中間幾位數作爲散列地址
例如:512的平方是271441,假如我們選擇7144作爲該關鍵字的散列地址,那麼這種方法就叫平方取中法。
特點:適用於關鍵字的每位取值不均勻或均小於散列地址所需要的位數。
摺疊法: 將關鍵字分割成位數相同的幾部分,然後取這幾部分的疊加和作爲散列地址。
例如:5121252 可以分成521+125+2=648
特點:適用於關鍵字的位數多?而且關鍵字中的每位上數字分佈大致均勻
7.3衝突處理
我們可以爲產生衝突的關鍵字尋找下一個空的Hash地址;
上面的過程叫:開放地址法
,我們還有一種方法叫拉鍊法
。
**開放定址法:**是指可存放新表項的空閒地址既向它的同義詞表項開放,又向它的非同義詞表項開放。
如何計算增量序列?
- 線性探查法:容易產生
堆積現象
,堆積現象會大大降低查找效率 - 平方探測法
- 再散列法
- 僞隨機序列法
在開放定址法中不能隨便刪除某個元素
拉鍊法
:是指把所有同義詞存放在一個線性鏈表中,這個線性鏈表由地址唯一標識,即散列表中每個單元存放該鏈表頭指針。
拉鍊法適用於經常進行插入和刪除的情況
查找步驟:初始化:Addr=Hash(key
)
①檢測查找表中地址爲Addr
的位置上是否有記錄,若無記錄,則返回查找失敗;若有記錄,則比較它與key值,若相等則返回成功,否則執行步驟②
②用給定的處理衝突方法計算“下一散列地址”,把Addr
置爲此地址,轉入步驟①
例如我們在拉鍊法中查找28這個關鍵字,首先通過Hash(key)找到它的頭指針地址,然後遍歷這個頭指針指向的單鏈表,一次比較即可。如果在開放定址法中,直接通過Hash(key)找到它的下標即可,如果下標對應的無記錄,說明查找失敗。28的Hash(key)爲1,然後下標爲1的值不是28,然後用給定的處理衝突方法計算“下一散列地址”,把Addr
置爲此地址,繼續查找。
查找效率
散列表的平均查找長度依賴於散列表的填裝因子
8.字符串模式匹配
8.1串
若兩個串長度相等且每個對應位置的字符都相等時,稱這兩個串是相等的
子串
串中任意個連續的字符組成的子序列稱爲該串的子串,包含子串的串爲主串
通常稱字符在序列中的序號爲該字符在串中的位置。子串在主創中的位置是以子串的第一個字符在主串的位置來表示的
我們用子串的第一個字符在主串的位置來表示字串的位置;例如:S2='World'
位置爲7
8.2串的存儲結構
1、定長順序存儲
# define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8cXBGWil-1593687034743)(C:\Users\12783\AppData\Roaming\Typora\typora-user-images\image-20200702181910816.png)]
2、堆分配存儲
typedef struct{
char *ch;
int lenght;
}HString;
使用malloc()/free
進行申請和釋放空間
3、塊鏈存儲表示
8.3串的基本操作
最小操作子集
8.4模式匹配
StrString(&Sub,S,pos,len) 子串截取
StrCompare(S,T) 字串比較
不依賴模式匹配的上面的兩個函數(這裏假設是定長的字符串,下標爲0的地方不存儲字符)
7.理木客
數據結構相關知識,公衆號理木客同步更新中,歡迎關注