今天這篇博客就聊聊幾種常見的查找算法,當然本篇博客只是涉及了部分查找算法,接下來的幾篇博客中都將會介紹關於查找的相關內容。本篇博客主要介紹查找表的順序查找、折半查找、插值查找以及Fibonacci查找。本篇博客會給出相應查找算法的示意圖以及相關代碼,並且給出相應的測試用例。當然本篇博客依然會使用面向對象語言Swift來實現相應的Demo,並且會在github上進行相關Demo的分享。
查找在生活中是比較常見的,本篇博客所涉及的這幾種查找都是基於線性結構的查找。也就是說我們的查找表是一個線性表,我們要查找某個元素在線性表中的位置。順序查找就是從頭到尾一個個進行比較,直到找到為止,此方法適用於無序的查找表。而折半查找、插值查找以及Fibonacci查找的查找表都是有序的,下方的內容會詳細的介紹到。進入今天博客的主題。
一、查找協議的定義
因為本篇博客我們涉及查找表的多種查找方式,而且查找表的數據結構都是線性結構。基於Swift面向對象語言的特征以及面向接口編程的原則,我們先給我們所有的查找方式定義一個協議。本篇博客中所有的查找方式都會遵循這個查找類型,這樣便於外部統一調用,也方便我們測試和擴展。
下方這個SearchType協議就是我們所定義的查找協議。下方這個協議雖然比較簡單,但是還是比較重要的,協議中定義了本篇博客所涉及的查找方式對外的調用方式。協議中的search()方法就是外部要調用的方法。該函數第一個參數就是要查找的查找表,第二個參數就是要查找的關鍵字。該函數的返回值就是關鍵字在查找表中的位置。如果沒有找到就會返回0。
二、順序查找
上面也簡單的提了一下,順序查找表是從頭到尾以此進行對比,直到找到我們要查找的元素位置。如果未找到,就返回0。當然從順序查找的這個過程中我們就可以看出來順序查找適用於無序的查找表。也就是說,當我們使用順序查找作用於查找表時,我們是不用關心查找表的順序的。
為了更直觀的理解順序查找,我們可以看一下下方的示意圖。在查找表中存儲着A~H的元素,我們要查找G元素在該查找表中的位置,我們需要從A開始以此匹配,當找到G時,就返回G在查找表中的位置。
根據上面我們不難給出代碼實現,下方代碼這個SequentialSearch這個類就是我們創建的賦值順序查找的類。當然該類要遵循SearchType,並且給出search()方法的實現。search()方法中的實現內容比較簡單,就是一個for循環,依次從頭到尾進行匹配。匹配成功后就返回該關鍵字在線性表中的位置。代碼比較簡單在此就不做過多贅述了。
對於順序查找,我們可以將其進行優化。在的search實現中,i是從范圍中取的,所以每次得判斷i是否在特定范圍中。在我們優化后的代碼中就不用做此判斷。優化的手段就是將我們要匹配的關鍵字item追加到查找表的尾部,我們稱之為哨兵,如果查找的結果是哨兵的位置,那么說明查找失敗,search()函數就返回零。當然你也可以將哨兵放在第一個位置,從后往前的進行查找,不過如果你的查找表是順序存儲的話,不建議將哨兵插入到第一個位置,因為順序表的插入操作是比較費時的。
根據上面這個示意圖,我們不難給出相應的代碼實現。下方這個代碼片段就是設置了哨兵的順序查找方法。因為代碼比較簡單,在此就不做過多贅述了。
三、折半查找
折半查找又稱為二分查找,折半查找的作用對象是有序的查找表,也就是說,我們的查找表是已經排好序的。之所以稱為折半查找,是因為在每次關鍵字比較時,如果不匹配,則根據匹配結果將查找表一份為二,排除沒有關鍵子的那一半,然后在含有關鍵字的那一半中繼續折半查找。
下方就是折半查找的示意圖,在下方示意圖中,我們查找A--H這個查找表中關鍵字G的位置。下方就是每個步驟的具體說明
-
(1)標記查找表的范圍,查找表的初識范圍就是整張表,所以查找表的下邊界 low=1,查找表的上邊界 high=8。查找表的中間位置 mid=low+(high-low)/2=(high+low)/2 = 4。所以我們將G與mid所對應的D比較大小。 比較結果為G>D。
-
(2)由上一步的比較結果,我們得知上面一輪中,前一半的數據是沒有我們要查找的關鍵字G的。所以將前一半查找表中的數據進行丟棄,重新定義查找表的范圍,因為mid處的元素以及匹配完畢了,要想丟棄前半部分的的數據,我們只需更新查找表的下邊界移動到mid后方即可。 也就是將查找表的范圍縮小到上一步查找表范圍的后半部分。此刻查找表的下邊界 low=mid + 1 = 4+1 = 5。查找表的下邊界更新后,mid的位置也會變化,所以我們要對mid進行更新,mid的位置仍然是low和high的中心, mid = (high + low)/2 = (8+5)/2=6。此刻mid處的元素為F, 將G與F比較,可知G > F。
-
(3)由G>F這個結果,我們得出,上一輪查找表的前半部分的數據需要丟棄,所以要還需要更新low的值, low= mid + 1 = 6+1 = 7。 mid = (8+7)/2=7。此刻的mid處的元素是G, 所以找到的我們要找的值,返回 mid = 7。
上面是一個完整的二分查找的實例,不過在上述實例中,只對low和mid的值進行了更新,因為都是拋棄了前半部分。當item<items[mid]時,我們就需要丟棄查找表的后半部分,更新上邊距high的值。不難得出,上邊邊界high的值更新為high=mid-1。將查找表的范圍縮小到前半部分繼續查找。根據這些敘述,我們不難給出代碼實現,下方代碼段就是折半查找的Swift語言的實現。如下所示:
四、插值查找
插值查找其實說白了就是上面二分查找的優化,因為從中間對查找表進行拆分並不是最優的解決方案。因為我們的查找表是有序的,當我們感覺一個值比較大時,會直接從后邊來查找。比如舉個現實生活中的例子,當你在翻字典是,查找“zhi”相關的字,如果讓你直接翻內容的話,你肯定從奔着字典的后邊幾頁去了,而不是從中間進行二分對吧。
插值查找就是讓mid更趨近於我們要查找的值,將查找表縮小到更小的范圍中,這樣查找的效率肯定會提升的。至於如何將mid更趨近於我們要查找的值呢,那么這就是我們“插值查找”要做的事情了。在折半查找中我們知道mid = low + 1/2(high-low)。因為high-low前面的權值是1/2,所以會將查找表進行折半。插值查找就是將這個1/2權值修改成一個更為合理的一個值。
我們將上述的表達式進行修改mid = low + weight*(high-low),從這個表達式中我們可以看出weight的值越大,mid的值也就也靠后,這符合我們想要的規則。因為我們的查找表是有序的,查找的關鍵字越大,有越往后,我們就可以根據要查找的關鍵字來求出weight的值。我們不難求出weight=(key - low)/(high-low)。上面這個表達式就可以求出在當前查找表范圍中,我們要查找的這個key值在查找表中的權值。
說這么多,其實插值查找與折半查找的區別就在於mid的計算方法上。下方就是插值查找的一個完整實例。我們要查找82在相應查找表中的位置。具體步驟如下所示:
-
(1)、首先初始化我們查找表的范圍low=1, high=8。計算我們關鍵字82在當前查找表范圍內的權值 weight=(key - low)/(high-low)=(82-10)/(98-10)=0.82。由權值,我們就可以容易的求出mid的值 mid = low + weight*(high-low) = 1 + 0.82*(8-1)=6。所以我們將82與items[mid]=79進行比較, 可知82>79。
-
(2)、由上面82>79的比較結果可知,mid之前的查找表可以被拋棄,所以我們可以查找表的下邊界更新為 low=mid+1=7。在更新后的查找表中,82對應的權值 weight=(82-82)/(98-82)=0。由此刻的weight我們可以求出 mid=7+0*(8-7) = 7。此刻我們將82於mid對應的值進行比較,發現匹配成功,將mid進行返回。
上述過程的代碼實現並不復雜,只需要將折半查找中的mid的計算方式進行替換即可。下方的InterpolationSearch類就是我們插值查找的類,當然該類也要遵循SearchType協議。在下方代碼段中,除了紅框部分中的代碼,其余的與折半查找的代碼完全一致。代碼比較簡單,在此就不做過多贅述了。
五、Fibonacci查找
接下來我們來聊聊斐波那契(Fibonacci)查找。其實就是按照Fibonacci數列來分隔查找表。如果你之前了解過Fibonacci數列的話,那么Fibonacci查找應該好理解。下方我們生成Fibonacci數列,然后使用該數列對我們的查找表進行分割。
1.生成Fibonacci數列
首先我們要生成Fibonacci數列以供我們Fibonacci查找使用。在Fibonacci數列中下一項的值等於前兩項的值的和,如果用數學公式來表示的話即為F(n)=F(n-1)+F(n-2)(n>1), F(0)=0, F(1)=1, 根據此規則就可以生成我們的Fibonacci數列。在Fibonacci數列中,n越大,F(n-1)/F(n)的zh值就越接近於0.618,我們知道0.618是黃金分割比,所以斐波那契數列又叫做黃金分割數列。所以我們要實現的Fibonacci查找也可以被稱為黃金分割查找。
首先我們先根據Fibonacci數列的規則,來生成Fibonacci數列備用。下方這個就是我們生成Fibonacci數列的方法。下方的FibonacciSearch類就是我們Fibonacci查找的類,其中的fibonacciSequence中存儲的就是我們的fibonacci數列。下方的createFibonacciSequence()方法就是創建Fibonacci數列的方法。如下所示:
2.Fibonacci查找示意圖
Fibonacci查找其實就是利用Fibonacci數列將查找表進行拆分,拆分成F(n-1)和F(n-2)兩部分。也就是說如果我們的查找表元素的個數為F(n),那么low到mid(查找表的前半部分)的元素的個數為F(n-1), 而后半部分(min---high)的元素個數就是F(n-2)。有上述的分割關系,我們可知mid = low + F(n-1) - 1。
說白了,Fibonacci查找其實就是使用Fibonacci數列將查找表進行分割,然后求出mid的位置,將關鍵字與mid進行比較,然后決定是拋棄后半部分還是前半部分。下方是使用Fibonacci數列查找82在相應查找表的具體步驟。
-
(1)、首先准備好Fibonacci數列備用,然后計算查找表元素的個數n在Fibonacci數列中的范圍。下方實例中的查找表的個數為9,由 F(6)=8 < 9 < F(7)=13這個關系,我們可知查找表從9個元素擴展到13個元素就可以使用斐波那契數列進行分割了,因為F(7)=13, 我們將對7進行標記,也就是key=7。
-
(2)、為了可以使用Fibonacci數列進行分割,我們將查找表擴充到13個元素( F(7) = 13)。查找表后邊擴充的元素的值與原查找表最后一個元素的保持一致即可。
-
(3)、將擴充后的查找表使用Fibonacci數列進行第一輪的分割。因為 F(7)=13=F(6) + F(5) = 8 + 5, 所以我們將查找表分為兩部分,前半部分的元素個數為 F(6)=8個,而后半部分的個數為 F(5)=5個,此刻我們的mid的值為 mid=low + F(6) -1 = 1+8-1=8。我們將82於mid出的元素進行比較(82<98)。
-
(4)、由 82<98這個結果我們可以將查找表的范圍縮小到上面分割的前半部分。所以我們將high的值進行更新high = mid - 1 = 7。我們繼續將前半部分使用Fibonacci數列進行分割,前半部分的個數為 F(6)=8, 因為 F(6)=F(5)+F(4) = 5+3, 所以我們可以將新的查找表在此分為 F(5)=5和 F(4)=3兩部分。此刻的 mid=low+F(5)-1=1+5-1 = 5。82與 items[5]=58比較,可以得出82>58,此刻 key=6。
-
(5)、由82>58這個結果我們可以知道,上一輪的查找表的前半部分應該被丟棄掉。我們將查找表縮小到后半部分(F(4)對應的部分)。后半部分的元素個數為F(4)=3個,我們可以繼續將查找表進行拆分,此刻的 key=4。我們先更新low的位置, low=mid+1=5+1 = 6。那么 mid = low+F(3)-1 = 6+2-1=7。此刻 82=items[mid]=items[7]=82, 查找成功將mid返回。
3、Fibonacci查找的代碼實現
原理分析完畢后,給出代碼實現不是什么難事呢。大體結構與二分查找依然類似。就是根據Fibonacci數列來計算mid的值,然后不斷的縮小查找表的范圍。首先我們需要查找當前查找表需要擴展到幾個元素可以被Fibonacci數列進行分割。下方這個函數就是計算查找表擴展后的元素的個數。findNumberInFibonacci()方法有一個參數,這個參數就是當前查找表的元素的個數,該方法的返回值就是擴充后查找表的個數。
求出要擴充的個數,接下來我們就需呀給查找表進行擴充了。下方這個方法就是對查找表進行擴充。擴充時使用的元素是原查找表最后一個值。
對查找表擴充完畢后,接下來就該進行查找了。下方是Fibonacci查找的核心代碼。代碼的具體步驟與上述的示例圖是一一對應的。需要注意的一點是key值的更新。下方代碼中的key其實就是Fibonacci數列的下標,當前范圍內查找表的個數==F[key]。因為我們查找表的范圍是不斷縮小的,所以key值也是會變化的。我們將查找表(查找表的元素個數為F[key])分割為F[key-1](前半部分)與F[key-2](后半部分)兩部分,如果將后半部分進行拋棄,那么key值就為key-1, 如果將前半部分拋棄,那么key=key-2,這一點需要注意。
六、測試用例
至此、我們順序查找、折半查找、插值查找、斐波那契查找聊完了,並且給出了相應的代碼實現。接下來就到了我們測試的時間了。因為上面所有的查找類都遵循了一個SearchType協議,所有我們的測試用例可以共用一份,這也是面向接口編程的好處之一。下方就是我們本篇博客的測試用例。
上方的測試用例我們使用的是一個,只要傳入不同的查找類的對象,我們就可以使用相應的查找方法進行查找。下方就是我們本篇博客測試用例的輸出結果。
本篇博客的篇幅也夠長的了,就先到這兒吧,上述實例的完整Demo會在github上進行分享, 下篇博客我們將要介紹其他幾種查找方式。
github鏈接地址:https://github.com/lizelu/DataStruct-Swift/tree/master/SearchDemo