二分查詢演算法詳講(三種版本寫法)原創

小程xy發表於2024-05-28

介紹:

二分查詢演算法(Binary Search)是一種在有序陣列中查詢目標元素的演算法。
它的基本思想是透過將目標元素與陣列的中間元素進行比較,從而將搜尋範圍縮小一半。

  • 如果目標元素等於中間元素,則搜尋結束;
  • 如果目標元素小於中間元素,則繼續在左半部分查詢;
  • 如果目標元素大於中間元素,則在右半部分查詢。

透過不斷地將搜尋範圍縮小一半,最終可以找到目標元素或確定目標元素不存在。

接下來透過例題介紹二分的不同寫法

例題:

輸入一個整數 n, 接下來一行輸入 n 個整數(保證整數序列有序), 最後輸入一個整數 m, 查詢 m 在序列中的起始下標和結束下標

示例1:

輸入:

5
1 2 2 4 5
2

輸出:

1 2

解釋:

2 在序列中的起始和結束位置是下標 1 和 2

程式碼講解:

二分程式碼按照退出條件分為

  1. while (l <= r)
  2. while (l < r)

程式碼中的所有 lr 都是序列的左右閉區間

程式碼中的所有 l + r >> 1l + r + 1 >> 1 分別相當於 (l + r) / 2(l + r + 1) / 2>>是按位右移, 整數向右位移一位相當於除2

程式碼中的所有 x, 都是目標值, 也就是要查詢的值; 所有的 idx, 是答案, 也就是要查詢數的起始下標或結束下標

先講第一種: while (l <= r), 在l > r時退出

// 查詢起始下標
int l = 0, r = n - 1, idx = 0;
while (l <= r)
{
    int mid = l + r >> 1;  // 一分為3, [l, mid), [mid, mid], (mid, r]
    if (a[mid] < x) l = mid + 1;  // 如果當前中間值比 x 小, 需要去序列的右區間, 因為mid位置的數比 x 小, 那麼左邊的區間(l, mid]的所有數都比 x 小
    else if (a[mid] > x) r = mid - 1;  // 同上
    else if (a[mid] == x)  // 等於答案時
    {
        idx = mid;
        r = mid - 1;  // 我們要找的時起始的下標, 雖然此時a[mid] == x, 但是mid的左邊可能還有等於x的值, 所以我們要繼續往左區間去找
    }
}

// 查詢結束下標(程式碼中只有註釋的地方和上面的程式碼不一樣)
int l = 0, r = n - 1, idx = 0;
while (l <= r)
{
    int mid = l + r >> 1;  // 一分為3, [l, mid), [mid, mid], (mid, r]
    if (a[mid] < x) l = mid + 1;  
    else if (a[mid] > x) r = mid - 1;  
    else if (a[mid] == x)  
    {
        idx = mid;
        l = mid + 1;  // 我們要找的時結束的下標, 雖然此時a[mid] == x, 但是mid的右邊可能還有等於x的值, 所以我們要繼續往右區間去找
    }
}

觀察上面程式碼我們可以把a[mid] == x的情況跟其他兩種情況合併

// 查詢起始下標
int l = 0, r = n - 1, idx = 0;
while (l <= r)
{
    int mid = l + r >> 1;  // 一分為3, [l, mid), [mid, mid], (mid, r]
    if (a[mid] < x) l = mid + 1;  
    else if (a[mid] >= x)
    {
         idx = mid;
         r = mid - 1;  // 繼續往左區間找
    }
}

// 查詢結束下標
int l = 0, r = n - 1, idx = 0;
while (l <= r)
{
    int mid = l + r >> 1;
    if (a[mid] <= x)
    {
        idx = mid;
        l = mid + 1;  // 繼續往右區間找
    }
    else if (a[mid] > x) r = mid - 1;
}

下面講第二種: while (l < r) 在l == r時退出

大家可以發現這種寫法不需要 idx 這個變數來記錄最終查詢的x的起始下標或結束下標了, 因為最後l就是對應的起始下標或結束下標。(r等於l, 所以用r也行)

查詢起始下標
int l = 0, r = n - 1;
while (l < r)
{
    int mid = l + r >> 1;  // 區間分成了兩個 [l, mid] 和 (mid, r]
    if (a[mid] < x) l = mid + 1;

    // 當a[mid] == x的時候, r一直往左, 所以當有多個相同的x的話, 會查詢到第一個
    else if (a[mid] >= x) r = mid;  // 因為a[mid]可能 == x, 因為mid也可能滿足條件, 所以區間變成[l, mid]
}

查詢結束下標
int l = 0, r = n - 1, idx = 0;
while (l < r)
{				
    int mid = l + r + 1 >> 1;  // 區間分成了兩個 [l, mid) 和 [mid, r]
    if (a[mid] > x) r = mid - 1;

    // 當a[mid] == x的時候, l一直往右, 所以當有多個相同的x的話, 會查詢到最後一個
    else if (a[mid] <= x) l = mid;  // 因為a[mid]可能 == x, 因為mid也可能滿足條件, 所以區間變成[mid, r]
}

解決常見疑惑

接下來講一下第二種查詢結束下標的時候 為什麼是 mid = l + r + 1 >> 1,而不是 mid = l + r >> 1;
原因有下面兩點:

  1. c++預設向0取整, 對於正整數你可以說是向下取整, 也就是 5 / 2 = 2,
  2. 當出現 l = r - 1 的時候, 此時 mid = (l + r) / 2 向下取整後等於 r - 1 , 如果此時進入了a[mid] <= x的分支, 那麼 l = mid = r - 1, 這時會發現 l 沒有發生變化, 那麼就會一直陷入死迴圈

對於沒有專門if(a[mid] == mid )這個分支的程式碼,如果查詢的數不存在的話,上面兩種程式碼找到的下標分別是:

  1. 查詢起始下標的程式碼 : 第一個大於x的數的下標, 如果大於x的數不存在, 那麼找到的下標在最後面

  2. 查詢結束下標的程式碼: 最後一個小於x的數的下標, 如果小於x的數不存在, 那麼找到的下標在最前面

if(a[mid] == mid )這個分支的程式碼,如果查詢的數不存在的話,那麼idx的值還是原來初始化的值!

覺得寫的不錯的話, 點個贊吧

相關文章