Leetcode 704 二分查詢

一杯綠茶發表於2022-03-06

給定一個 n 個元素有序的(升序)整型陣列 nums 和一個目標值 target  ,寫一個函式搜尋 nums 中的 target,如果目標值存在返回下標,否則返回 -1

示例 1:

輸入: nums = [-1,0,3,5,9,12], target = 9
輸出: 4
解釋: 9 出現在 nums 中並且下標為 4

示例 2:

輸入: nums = [-1,0,3,5,9,12], target = 2
輸出: -1
解釋: 2 不存在 nums 中因此返回 -1

解題思路

二分查詢基本框架如下:

int binarySolution(int[] nums, int target) {
    int left = 0, right = ...;
    while (...) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...;
        } else if (nums[mid] > target) {
            right = ...;
        }
    }
    return ...;
}

分析二分查詢的一個技巧是:不要出現 else,而是把所有情況用 else if 寫清楚,這樣可以清楚地展現所有細節。

其中 ... 標記的部分,就是可能出現細節問題的地方,當你見到一個二分查詢的程式碼時,首先注意這幾個地方。

另外宣告一下,計算 mid 時需要防止溢位,程式碼中 left + (right - left) / 2 就和 (left + right) / 2 的結果相同,但是有效防止了 leftright 太大直接相加導致溢位。

對於本題的題解如下:

class Solution {
    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1; // 注意
        // 注意
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1; // 注意
            } else if (nums[mid] > target) {
                right = mid - 1; // 注意
            }
        }
        return -1;
    }
}

這裡探討一下其中的幾個細節。

1、為什麼 while 迴圈的條件中是 <=,而不是 <?

答:因為初始化 right 的賦值是 nums.length - 1,即最後一個元素的索引,而不是 nums.length

因為我們需要對所有的元素進行搜尋,使用 <= 搜尋的是兩端都閉區間 [left, right],如果使用 < 則相當於左閉右開區間 [left, right)

如果使用 <,然後 right 初始賦值改為 nums.length,也可實現 [left, right] 範圍的搜尋,但會漏掉最後一個元素,下面會講

此外還要注意下結束搜尋的條件。當找到目標值的時候可以終止:nums[mid] == target。如果沒找到,就需要 while 迴圈終止,然後返回 -1。那 while 迴圈什麼時候應該終止?搜尋區間為空的時候應該終止。

while(left <= right) 的終止條件是 left == right + 1,寫成區間的形式就是 [right + 1, right],或者帶個具體的數字進去 [3, 2]

while(left < right) 的終止條件是 left == right,寫成區間的形式就是 [right, right],或者帶個具體的數字進去 [2, 2],這時候區間非空,還有一個數 2,但此時 while 迴圈終止了。也就是說這區間 [2, 2] 被漏掉了,索引 2 沒有被搜尋,如果這時候直接返回 -1 就是錯誤的。

如果要用 while(left < right) 也是可以的,我們已經知道了出錯的原因,就打個補丁好了:

class Solution {
    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            }
        }
        // 對漏掉的元素進行判斷
        return nums[left] == target ? left : -1;
    }
}

2、為什麼 left = mid + 1right = mid - 1?我看有的程式碼是 right = mid 或者 left = mid

剛才明確了「搜尋區間」這個概念,而且本演算法的搜尋區間是兩端都閉的,即 [left, right]。那麼當我們發現索引 mid 不是要找的 target 時,下一步應該去搜尋哪裡呢?

當然是去搜尋 [left, mid-1] 或者 [mid+1, right] 對不對?因為 mid 已經搜尋過,應該從搜尋區間中去除。

3、此演算法有什麼缺陷?

比如說給你有序陣列 nums = [1,2,2,2,3]target2,此演算法返回的索引是 2,沒錯。但是如果我想得到 target 的左側邊界,即索引 1,或者我想得到 target 的右側邊界,即索引 3,這樣的話此演算法是無法處理的。

這樣的需求很常見,你也許會說,找到一個 target,然後向左或向右線性搜尋不行嗎?可以,但是不好,因為這樣難以保證二分查詢對數級的複雜度了。

總結

  • 不要使用 else,而是把所有情況用 else if 寫清楚
  • 計算 mid 時需要防止溢位,使用 left + (right - left) / 2 先減後加這樣的寫法
  • while 迴圈的條件 <= 對應 right 初始值為 nums.length - 1,終止條件是 left == right + 1,例如 [3, 2]
  • 如果 while 迴圈的條件 <,需要把 right 初始值改為 nums.length,此時終止條件是 left == right,例如 [2, 2],這樣會漏掉最後一個區間的元素,需要單獨判斷下
  • mid 不是要找的 target 時,下一步應該搜尋 [left, mid-1] 或者 [mid+1, right],對應 left = mid + 1 或者 right = mid - 1
  • 二分查詢時間複雜度 O(logn)

相關文章