利用迴圈不變式寫出正確的二分查詢及其衍生演算法

2021-08-15 10:55:23 字數 3569 閱讀 8573

利用迴圈不變式寫出正確的二分查詢及其衍生演算法

先看看定義

二分查詢的搜尋過程從陣列的中間元素開始,如果中間元素正好是要查詢的元素,則查詢成功;如果某一特定元素大於或者小於中間元素,則在陣列大於或小於中間元素的那一半中查詢,而且跟開始一樣從中間元素開始比較。如果在某一步驟陣列為空,則代表找不到。這種搜尋演算法每一次比較都使搜尋範圍縮小一半。

思路很簡單以至於大多數人都能講出來,但是有多少人能一次寫出bug-free的**?《程式設計之美》第2.16節的最長遞增子串行演算法,如果想實現o(n2)到o(nlogn)的時間複雜度下降,必須借助於二分演算法的變形。其實很多演算法都是這樣,如果出現了在有序序列中元素的查詢,使用二分查詢總能提公升原先使用線性查詢的演算法的效能。

很多人覺得二分演算法簡單,但隨手一寫會發現諸如死迴圈,邊界條件無法確定的問題,提供充足的時間,僅有約10%的專業程式設計師能夠完成乙個正確的二分查詢,看來,二分查詢並不是我們想象的那麼簡單。網路上的資料大多對邊界值的確定避而不談,即使進行講解,思路也有些凌亂並且不適合我這種初學者,在參考多篇文章之後,我對二分演算法有了乙個新的認識。

————==寫出這篇博文獻給和我一樣對二分查詢的思路毫無頭緒但又不甘於死記硬背的人們==。

note: 

1. 本文中,文字說明部分的』=』究竟是賦值還是邏輯判斷請根據上下文推斷。 

2. 演算法推導過程中的mid的計算採用(low+high)/2的方式,因為只是推導,所以無需考慮溢位的問題,但是**中全部採用low+(high-low)/2,推導過程中可認為兩者值相等。 

3. 在理論推導每次陣列減少的長度時,mid是不能代換成left + (right - left)/2的。這種形式代表了非整型的運算,沒有捨去小數部分,而在實際的執行過程中mid是會捨去小數部分的。

迴圈不變式主要用來幫助理解演算法的正確性。形式上很類似與數學歸納法,它是乙個需要保證正確斷言。對於迴圈不變式,必須證明它的三個性質:

初始化:它在迴圈的第一輪迭代開始之前,應該是正確的。

保持:如果在迴圈的某一次迭代開始之前它是正確的,那麼,在下一次迭代開始之前,它也應該保持正確。

終止:迴圈能夠終止,並且可以得到期望的結果。

例1:找出陣列中值為v的元素,不存在時返回-1

初始化:初始區間為[0,n-1],low初始值為0,high初始值為n-1,若v存在於陣列中,則其必然存在於區間[low,high中],正確。

保持:若array[mid]< v,則v應該存在於[mid+1,high]區間內,捨棄區間[low,mid],陣列減小長度為(mid-low+1),則陣列每次至少減少1個單位,並且low應該指向mid+1的位置;

若array[mid]==v,則說明找到,返回mid;

若array[mid]>v,則v應該存在於[low,mid-1]區間內,捨棄區間[mid,high],陣列減小長度為(high-mid+1),則陣列每次至少減少1個單位,並且high指標應該指向mid-1位置;

終止:根據上面的討論,我們明確了每個if判斷之後high,low指標的移動情況,但是我們還需要確定迴圈條件,我們看到,無論是情況1還是情況3,陣列每次至少減少1個單位,考慮當low==high時,可以推出low或high==mid,但是我們的陣列減小長度要麼為(mid-low+1),要麼為(high-mid+1),不管什麼情況總是保證每次減少1,所以不會出現死迴圈的情況。

根據上面的分析,我們確定迴圈條件為:low<=high

**如下:

class solution 

return -1;

}};

例2: 

二分查詢返回key(可能有重複)第一次出現的下標x,如果不存在返回-1

初始化:初始區間為[0,n-1],low初始值為0,high初始值為n-1,若v存在於陣列中,則其必然存在於區間[low,high],正確。

保持: 

1. 若array[mid]

class solution 

if(nums[low]==target)

res[0]=low;

}};

例3.二分查詢返回key(可能有重複)最後一次出現的下標x,如果不存在返回-1

初始化:

初始區間為[0,n-1],low初始值為0,high初始值為n-1,若v存在於陣列中,則其必然存在於區間[low,high],正確。

保持:若array[mid]< v,則v存在於區間[mid+1,high],捨棄區間[low,mid],區間減少的長度為(mid-low+1),至少為1,同時令low指向mid+1。

若array[mid]==v,則v存在於區間[mid,high],捨棄區間[low,mid-1],區間減小長度為(mid-low),同時令low指向mid,根據前面的分析,我們知道這種情況下如果low==high就會出現死迴圈,同時,如果high==low+1也會導致死迴圈,因為mid=(low+high)/2。

若array[mid]>v,則v存在於區間[low,mid-1],捨棄區間[mid,high],區間減小長度為(high-mid+1),至少為1,同時令high指向mid-1的位置。

終止: 

根據上面的討論,我們知道low必須小於high,且low+1也要小於high。

故迴圈條件為low+1< high

另外由於是尋找最後一次出現的下標,所以迴圈結束後還需要檢查array[low+1]是否等於target,不相等則檢查array[low],若還不相等則查詢失敗。

**如下:

class solution 

};

結合以上各個演算法,可以找出根據需要寫二分查詢的規律和具體步驟,比死記硬背要強不少,萬變不離其宗嘛:

(1)大體框架必然是二分,那麼迴圈的key與array[mid]的比較必不可少,這是基本框架;

(2)迴圈的條件可以先寫乙個粗略的,比如原始的while(left<=right)就行,這個迴圈條件在後面可能需要修改;

(3)確定每次二分的過程,要保證所求的元素必然不在被排除的元素中,換句話說,所求的元素要麼在保留的其餘元素中,要麼可能從一開始就不存在於原始的元素中;

(4)檢查每次排除是否會導致保留的候選元素個數的減少?如果沒有,分析這個邊界條件,如果它能導致迴圈的結束,那麼沒有問題;否則,就會陷入死迴圈。為了避免死迴圈,需要修改迴圈條件,使這些情況能夠終結。新的迴圈條件可能有多種選擇:while(left< right)、while(left< right-1)等等,這些迴圈條件的變種同時意味著迴圈終止時候選陣列的形態。

(5)結合新的迴圈條件,分析終止時的候選元素的形態,並對分析要查詢的下標是否它們之中、同時是如何表示的。

對於(3),有一些二分演算法實現不是這樣的,它會使left或right在最後一次迴圈時越界,相應的left或right是查詢的目標的最終候選,這一點在理解時需要注意。當然,不利用這個思路也可以寫出能完成功能的二分查詢,而且易於理解。

應用:1.查詢排序陣列中某個數出現的次數。

解法:二分查詢確定第一次和最後一次出現的下標,差值+1就是出現次數,時間複雜度o(logn).

參考**:

二分查詢的詳細分析 基於迴圈不變式的分析

給乙個陣列,已公升序排序,即不存在重複元素,查詢給定值target,如果不存在,返回該值在陣列中可以插入的位置。二分查詢本質是利用分治加剪枝不斷進行問題規模的縮小,到最後問題不可分解決問題。將乙個區間分為兩半 縮小規模 分別查詢,因為另乙個子問題肯定無解,不需要查詢 剪枝 所以本質是剪枝的分治。另外...

如何寫出乙個正確的二分查詢

總結基於分治思想的乙個很簡單的演算法,但若想寫對,卻也不是那麼容易 使用條件 二分查詢也稱折半查詢 binary search 它是一種效率較高的查詢方法。但是,折半查詢要求線性表必須採用順序儲存結構,而且表中元素按關鍵字有序排列 查詢過程我們l來看下邊 二分查詢,找到該值在陣列中的下標,否則為 1...

寫好正確的二分搜尋

今天再次解決乙個需要使用二分查詢的問題,再一次的,我又沒有一次過寫對.為什麼我說 又 抓狂了,似乎開始有一些 二分查詢恐懼症 為了以後能夠一次將這個基本的演算法寫對,我決定再仔細研究一下.我之前有寫過乙個二分查詢的演算法,在 這裡,這一次再以這個問題為例來說明.我今早寫下的錯誤 類似於下面的樣子 i...