【動畫筆記】二分查找(折半查找)


進入大二以來一直把學數據結構和算法這事藏在心里,畢竟大學破事太多了,最近實在是按捺不住了。拿起《算法圖解》,結合網絡上的總結,准備循序漸進入門數據結構和算法...

目前筆記里代碼示例我都是用的Python語言(正好學校在教,趁熱打鐵),日后打C/C++基礎后我應該會回來再增加示例。

alright-2021-11-29

二分查找的前提

  1. 查找的數據目標需要是有順序的儲存結構,比如Python中的列表list
  2. 這個數據目標還需要按一個順序排列(升序or降序)。

寫寫練練

Don't try to understand it.Feel it.

廢話不多說了,關於二分查找,我最開始寫了個錯誤的玩意

# 錯誤寫法例子
my_list = [1, 3, 4, 5, 8, 16, 24, 56, 78]

def find(from_list, which_one):
    start = 0
    end = len(from_list)-1
    while start < end:
        middle_ind = (start+end)//2
        middle = from_list[middle_ind]
        print(start, end)
        if middle == which_one:
            return middle_ind
        elif middle > which_one:
            end = middle_ind  # 搜索范圍尾部前移
        elif middle < which_one:
            start = middle_ind  # 搜索范圍頭部后移
    return False

found_ind = find(my_list, 78)
print(found_ind)

這個例子的問題體現在兩個方面:

  1. 一旦尋找的是順序儲存結構中沒有的值,會卡在循環里

  2. 一旦尋找的是末尾的邊緣值,也會卡在循環里

  3. 無論是奇數個數據還是偶數個數據都有這個問題。

從輸出能看到原因:startend滿足不了end == start的條件,到最后兩值會永遠差1

我試着用動畫展示一下這個有問題的執行過程:

whatswrong-2021-11-29

到最后死循環的時候因為我們將中間值采用了向下取整,導致當start指向倒數第二個索引時卡死在start=7


通過觀察,咱發現每次卡死的時候 start和end永遠會相差 1

005-2021-11-30

很明顯了,問題就出現在 每次查找后對startend的處理 上。就上面這個有問題的代碼而言,其實每次搜索后 如果中間項 < 搜索項目,進行start=middle_ind+1的操作就可以,列個表格:

  • 搜尋的值:78
搜尋范圍 (start,end) middle_ind (中間項的索引) start=middle_ind+1 end 循環繼續
(0,8) 4 5 8
(5,8) 6 (向下取整) 7 8
(7,8) 7 (向下取整) 8 8

↑ 這樣的話就能順利達到查找循環的終止條件start=end


我的理解:在本次搜索中我們已經檢查了middle_ind對應的項目,下一次搜索范圍的開始(start)就應該從這一項的下一位開始,也就是middle_ind+1


091-2021-11-30

但是吧,上面我們找的是末尾的值78,如果找的是開頭的值 1 呢?

照葫蘆畫瓢唄~依照上面的思路,每次搜索后的下一次搜索就應該從這一項的前一位了,也就是middle_ind-1。(上面動畫中能直觀看出來范圍下限索引start是趨於增大的,反之范圍上限索引end值就是趨於減小的),再列個表:

  • 搜尋的值:1
搜尋范圍 (start,end) middle_ind (中間項的索引) start end=middle_ind-1 循環繼續
(0,8) 4 0 3
(0,3) 1 (向下取整) 0 0

方便直觀對照,我放個靜態圖在這里:

list-2021-11-30

在尋找既不是開頭也不是末尾的值時,搜索過程中往往會交替有startend,所以在二分查找程序中關鍵部分就要兼顧上述兩種處理:

if middle == which_one:
    return middle_ind
elif middle > which_one:
    end = middle_ind-1  # 搜索范圍尾部前移,注意是middle_ind-1
elif middle < which_one:
    start = middle_ind+1  # 搜索范圍頭部后移,注意是middle_ind+1

(這也是為什么之前尋找列表中沒有的值時會卡在循環里)

經過這些處理后,咱成功彌補了這個問題

NaNi-2021-11-30

等等...在搜索列表中開頭或末尾的值時循環在start=end后就停止了...程序仍然無法搜索到開頭或末尾的值,而是返回了False

通過觀察發現,其實只要再執行一次循環,問題就完美解決了:把循環條件start < end 換成 start <= end (start等於end時繼續循環一次)


最后寫成的二分查找代碼如下:

def find(from_list, which_one):
    start = 0  # 開始的索引
    end = len(from_list)-1  # 結尾的索引
    while start <= end:  # 當范圍沒有縮減至start>end時,不停二分查找(易錯點:為什么用<=?因為當start=end的時候會遺漏一個處理項)
        middle_ind = (start+end)//2  # 找到二分中間項目的索引,這里向下取整(floordiv)
        middle = from_list[middle_ind]  # 獲得中間項
        if middle == which_one:  # 找到了,返回對應的索引
            return middle_ind # 找到就停車跑路
        elif middle > which_one:  # 要尋找的值小於中間值
            end = middle_ind-1  # 將范圍尾部索引減小到中間值索引-1(易錯點)
        elif middle < which_one:  # 要尋找的值大於中間值
            start = middle_ind+1  # 將范圍頭部索引start增大到中間值索引+1(易錯點)
    return False  # 啥都沒找到

最后咱基於上面的寫法整了幾個動畫:

  1. 尋找列表最開頭的值:

    my_list = [1, 3, 4, 5, 8, 16, 24, 56, 78]
    found_ind = find(my_list, 1) # 0
    

    findBeginning-remade-2021-12-01

  2. 尋找列表中間的一個值:

    my_list = [1, 3, 4, 5, 8, 16, 24, 56, 78]
    found_ind = find(my_list, 16) # 5
    

    findMedian-remade-2021-12-01

  3. 尋找一個找不到的值:

    my_list = [1, 3, 4, 5, 8, 16, 24, 56, 78]
    found_ind = find(my_list, 6) # False
    

    cannotFind-2021-12-01

時間復雜度

通過大O表示法咱可以寫成 O(f(n)) 這樣,其中:

  • n是操作的數據的規模
  • f(n)操作的次數(程序執行的次數)
  • 大O的量和f(n)成正比(這是不是說明看O其實就可以粗略地看f(n)?)。

043-2021-12-01

上面例子的列表中有9個元素,數據規模 n=9,這些例子中除了卡死循環的情況外,我發現程序 最多 執行檢查4次,而不是9次——

——因為每次執行操作后會將搜索范圍折半,也就是每次操作后數據規模會成半縮減

timeComplexity-2021-12-01

(字丑勿cue ┑( ̄Д  ̄)┍)

所以二分查找的時間復雜度表示為 O(log2n)

為什么說到“最多”這個詞呢?因為大O時間體現的是最不理想情況下的運行時間,也就是該算法的時間復雜度的上界

更多的寫法

這篇筆記里的二分查找寫法只是所有寫法中的一種。我認為掌握一個算法並不是要對每種寫法都了如指掌,而是要去理解其中的原理

這里貼個知乎問題貼:

注意這個鏈接里的回答說 median = ( low + high ) / 2 寫法會溢出是 C/C++ 里的一個小坑。


該筆記於Github倉庫撰寫: https://github.com/cat-note/bottleofcat/blob/main/Algo/BinarySearch.md


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM