引入
情景一: 假設要在電話簿中找一個名字以K打頭的人,可以從頭開始翻頁,直到進入以K打頭的部分。但你很可能不這樣做,而是從中間開始,因爲你知道以K打頭的名字在電話簿中間。
情景二: 假設要在字典中找一個以O打頭的單詞,你也將從中間附近開始。
情景三: 假設你登錄QQ。當你這樣做時,QQ必須覈實你是否有其網站的賬戶,因此必須在其數據庫中查找你的用戶名。如果你的用戶名爲kkk,QQ可從以A打頭的部分開始查找,但更合乎邏輯的做法是從中間開始查找。
總結: 以上查找是一個算法問題,幾乎每個類似的情景都可以使用按順序查找的方式,但是大多數情況下,這是不合理的,由此可以引申出二分或者說折半查找的算法。
二分查找
二分查找是一種算法,其輸入是一個有序的元素列表(必須是有序的)。如果要查找的元素包含在列表中,二分查找返回其位置;否則返回null。
舉一個猜數字的例子
1. 給你一個數組,長度爲100,從1開始到100結束:
2. 系統隨機產生一個範圍在1到100的正整數
3. 用戶來猜這個數字的大小,大了程序返回猜大了,反之返回猜小了,知道用戶猜中,程序結束。
參考代碼:
import random as r
def guess():
n = 0
rmin = 1
rmax = 100
ranNum = r.randint(rmin,rmax)
while True:
n += 1
userNum = int(input(f'請猜一個範圍[{rmin},{rmax}]的整數:'))
if ranNum == userNum:
print(f'猜對了!<{userNum}>,猜了{n}次')
break
elif userNum > ranNum:
print(f'猜大了!<{userNum}>')
elif userNum < ranNum:
print(f'猜小了!<{userNum}>')
if __name__ == '__main__':
guess()
一種做法:
像上圖中這樣從小到大猜100次,這顯然是一種糟糕的方法。
如果我們採用二分法思想呢,每次都猜中間數。
不難發現,如果使用二分查找,每次查找的元素個數都會減一半,大大提高了效率,猜數字1到100,最多不超過7次,就能猜中答案!
優化後的猜數字代碼:
import random as r
def guess():
n = 0
rmin = 1
rmax = 100
ranNum = r.randint(rmin,rmax)
while True:
n += 1
#機器算法,折半查找
userNum = (rmin + rmax) // 2
if ranNum == userNum:
print(f'猜對了!<{userNum}>,猜了{n}次')
break
elif userNum > ranNum:
rmax = userNum
print(f'猜大了!<{userNum}>')
elif userNum < ranNum:
rmin = userNum
print(f'猜小了!<{userNum}>')
if __name__ == '__main__':
guess()
二分法例題
輸入:一個有序列表,一個待查找數
輸出:位置索引或者none
def binary_search(list,item):
low = 0#low和high用於跟蹤要在其中查找的列表部分
high = len(list) - 1
while low <= high:
mid = (low + high) // 2 #只要範圍沒有縮小到只包含一個元素,就檢查中間的元素
guess = list[mid]
if guess == item:#找到了元素
return mid
if guess > item:#猜的數字大了
high = mid - 1 #向下放縮範圍,因爲索引從零開始,如果不-1就會產生死循環
else:#猜的數字小了
low = mid + 1#向上放縮範圍
return None#沒有指定的元素
if __name__ == '__main__':
my_list = [1, 3, 5, 7, 9]
print (binary_search(my_list, 3)) # => 1
print (binary_search(my_list, 9)) # => 4
print (binary_search(my_list, -1)) # => None
簡單查找和二分查找的對比
二分法缺點
- 必須有序,我們很難保證我們的數組都是有序的。
- 它必須是數組,數組讀取效率是O(1),可是它的插入和刪除某個元素的效率卻是O(n)。
參考書籍《圖解算法》