思考,是指將抽象和具象進行平衡,然後留存在心中。在這個過程中,最爲重要的特性是清晰。
由於我對python3相對比較熟悉,所以編程語言採用python3.
問題
問題:給定一個非空浮點數升序列表,和一個浮點數,如果這個浮點數在列表中,則返回最小索引,否則返回-1
問題的形式化
問題的形式化描述:
輸入:非空浮點數升序列表alist,浮點數num
輸出:索引index
涉及的基本概念
不管是使用迭代的寫法還是使用遞歸的寫法,在思考編程中不可避免要對基本概念進行明確。基本概念是思考的基石,通過它們我們實現對問題的抽象,並且在具象的考慮中有錨點供我們檢驗。
那麼有哪些基本概念呢?查找問題,需要考慮查找的目標和查找的地方,也就是查找對象和查找域。然後要考慮查找的範圍,針對數組,就是查找區間,這個區間可以使用子數組表示,也可以使用數組的索引區間表示,這裏考慮到需要返回索引,我們使用索引區間表示。還要考慮的一個點就是有序數組的元素大小比較關係,也就是序關係和相等。最後,還要考慮返回值。
二分查找的想法,就是通過將查找對象與二分點值進行大小比較將查找區間長度縮減一半,最終查找區間長度只有1,就可以通過一次大小比較確定輸出index還是-1。這個過程中需要考慮的概念是初始查找區間,查找區間長度,二分點,繼續縮減條件, 查找區間變換公式,查找區間長度變換公式,最終狀態,返回值。
基本概念的代碼化
下面的代碼化,我不追求簡潔,而是追求清晰,即更好地呈現出基本概念。
查找問題的基本概念
- 查找對象:
num
- 查找域:
alist
- 查找區間:
[left, right]
- 序關係:
<,>
- 相等:
def isEqual(a, b):
if abs(a-b)<1e-7:
return True
else:
return False
注意,相等關係要先於序關係進行判斷,也就是:相等,不相等(包含<,>
)
- 返回值:
index
或者-1
二分查找問題的基本概念
- 初始查找區間:
[left=0, right=len(alist)-1]
- 查找區間長度:
length = right-left+1
- (左)二分點:
mid = left + (right - left) // 2
- (右)二分點:
mid = right - (right - left) // 2
- 繼續縮減條件:查找區間長度大於1,即
length > 1
- 查找區間變換公式:如果是返回最小索引則使用左二分點,如果是返回最大索引則使用右二分點,此部分以及本節後續部分顯示的都是隻考慮返回最小索引的情形
if isEqual(num, alist[mid]):
left, right = left, mid
elif num<alist[mid]:
left, right = left, mid - 1
elif num>alist[mid]:
left, right = mid + 1, right
- 查找區間長度變換公式:下面只是爲了講解,並不是嚴謹的代碼
if isEqual(num, alist[mid]):
length = mid - left + 1
elif num<alist[mid]:
length = mid - 1 - left + 1
elif num>alist[mid]:
length = right - (mid+1)+ 1
命題:在繼續縮減條件length>1
下,(左)二分點在[left,right]
中,並且變換後的查找區間長度均大於等於0且小於變換前的長度(特別地,如果查找對象在查找域中,則長度始終大於等於1,但是如果查找對象不在查找域中,則長度可能爲0,即right-left=-1
)
證明:我們使用變化前的區間表達變換後的區間長度:
isEqual(num, alist[mid])
時有
1=<mid-left+1 = (right-left)//2+1<right-left+1
num<alist[mid]
時有
0=<mid-left=(right-left)//2<right-left+1
num>alist[mid]
時有
0=<right-mid=(right-left)-(right-left)//2<right-left+1
這個命題,保證了縮減持續進行,直到達到最終狀態
- 最終狀態:
[left, left]
即length=1
即right-left=0
(在查找對象不在查找域時,是會出現[left, left-1]
的狀態的,所以有兩個最終狀態,雖然編程時可以同等處理,但是概念上要清晰)
注意,我們將查找區間作爲我們的這三個狀態表達中的最基本概念。一般最終狀態對應的是最小規模的原問題。最小規模非空有序列表就是隻含有一個元素的列表(但是由於縮減過程,導致[left, left-1]
,也是最小規模問題,所以我們加一個length
判斷,爲了讓alist[left]
總有意義,我們加上越界判斷)。
此時,就剩下最後的比較,並據此給出返回值了:
# 判斷越界條件,雖然此時left肯定不會越界,但是爲了代碼更加通用加上了
if left>=len(alist) or right<0 or right<left:
return -1
if isEqual(num, alist[left]):
return left
else:
return -1
總結以上,得到求解代碼:
def binarySearch(alist, num):
# 初始化查找區間和查找區間長度
left, right = 0, len(alist)-1
length = right - left + 1
# 在縮減條件成立時應用查找區間變換公式,直到達到最終狀態
while length>1:
mid = left + (right - left) // 2
if isEqual(num, alist[mid]):
left, right = left, mid
elif num<alist[mid]:
left, right = left, mid - 1
elif num>alist[mid]:
left, right = mid + 1, right
length = right - left + 1
# 依據最終狀態,給出返回值
if left>=len(alist) or right<0 or right<left:
return -1
if isEqual(num, alist[left]):
return left
else:
return -1
def isEqual(a, b):
if abs(a-b)<1e-7:
return True
else:
return False
if __name__ == "__main__":
alist1 = [1., 2., 2., 3.2, 5., 6.]
alist2 = [2., 2]
alist3 = [1., 1]
alist4 = [3,3]
num = 2
print(binarySearch(alist1, num))
驗證
一個問題是,上一節的返回值確實給出了問題的解嗎?轉化到基本概念上,也就是如果查找對象在查找域中,那麼最終狀態的查找區間是否就是最小索引呢?
最一開始,最小索引當然在查找區間中,於是只需證明下面的命題。
命題:如果查找對象在查找域中,那麼縮減查找區間的操作會讓最小索引保持在縮減後的查找區間中。
證明:還是分三種情況討論。
-
isEqual(num, alist[mid])
時
因爲[left, right]
中有最小索引,而此時的mid
必定大於等於最小索引,所以[left,mid]
中必有最小索引。 -
num<alist[mid]
時,可知最小索引小於等於mid-1
於是可以。 -
num>alist[mid]
時,可知最小索引大於等於mid+1
於是可以。
左二分點vs右二分點
這一節我們討論最微妙的細節。
我們首先明確,因爲要保留最小索引,在 isEqual(num, alist[mid])
時,我們要將查找區間變換到[left,mid]
。而爲了讓區間長度嚴格減小,必須保證mid<right
,這就要求二分點mid
必須是左二分點。如果不好理解的話,考慮查找對象2和查找域[2.,2.],則查找區間爲[left, right=left+1]
,則此時的左二分點就是left
而右二分點就是right
,此時只有取左二分點才能縮減區間長度。
左右指針框架
這一節,我們做一點抽象化的擴展。把上面的代碼形成一種思維框架。上面的問題,輸入的是列表,輸出的是索引,所以我們使用索引區間表示查找區間,而最爲關鍵的就是查找區間的兩個端點指針
第一部分:左右指針初始化,循環條件(循環變量)初始化
left, right = xxx, yyy
循環條件初始化
第二部分:在循環條件滿足時,對左右指針進行更新,並更新循環條件(循環變量)
while 循環條件:
左右指針更新公式
循環變量更新公式
第三部分:確定最終狀態,即循環條件的臨界破壞情況,一般對應最小規模的問題。在最終狀態下,求解最小規模問題。
求解最小規模問題,使用左右指針確定返回值
把這三部分結合起來,一般寫代碼就不會有遺漏了,而且條理清晰。
所以,我們發現什麼了呢?其實不管是遞歸也好,迭代也好,我們的策略都是具象和抽象的平衡:
- 具體的問題,求解最小規模問題
- 抽象化概念,確定縮減變量,縮減條件,縮減變量的更新公式,把大規模問題縮減爲最小規模問題。這個過程要保持解的不變性,以及縮減的有效性。爲此,可以使用集合論的思考方法,確定”可行域“,縮減”可行域“,考慮"可行域"的度量。
上面的左右指針框架,實際上就是用左右指針表達出可行域和可行域的度量,並且用左右指針來驗證解的不變性,以及縮減的有效性。
最後還是以某某大神的話收尾,要給對象一個名字,你才能記住她。