二分查詢不得不說的事

W24-發表於2020-10-11

關於二分查詢

參考二分查詢

二分查詢演算法的編寫框架大致如下:

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

    while(...) {
        int mid = (right + left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) { // 寫成else更簡化
            right = ...
        }
    }
    return ...;
}

計算mid時有技巧可以防止溢位,寫成mid = left + (right - left) / 2

查詢一個數(基本的二分查詢)

這個場景是最簡單的,也是我們最熟悉的。搜尋一個數,如果存在則返回其下標,否則返回 -1。

  • 假設陣列中有n個元素,下標範圍為[0 : n-1],要查詢的元素為key
  • 初始化時,令min = 0, max = n-1。這裡提出一個搜尋區間的定義,此時演算法的搜尋區間就是閉區間[min, max],搜尋區間的特點決定了min, max的移動方式。
  • 迴圈條件為while(min <= max),計算中間元素mid = (left + right) / 2
  • if A[mid] > key, then max = mid - 1
  • if A[mid] < key, then min = mid + 1
  • 因為搜尋區間兩邊都是閉的,所以min, max移動時都是需要加一或者減一
  • 該演算法的終止條件要麼為nums[mid] = target,要麼為left = right + 1
int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1; // 注意

    while(left <= right) { // 注意
        int mid = (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;
}
  • 注意啊注意,如果不修改min, max移動方法的話,最基本的二分查詢不可以直接修改為min=0. max=n, while(min < max)這種寫法,可以舉個例子,nums = [1, 2, 3], target = 1min=0, max=3, mid=1, nums[1] > target,移動max之後min = max直接結束,所以這種寫法是錯的。
  • 如果非要寫成min = 0, max = n, while(min < max),那麼移動時left = mid + 1不用動,但是right = mid
  • 初始化,min = 0, max = n。那麼此時演算法的搜尋區間為左閉右開的[left, right)
  • 迴圈條件為while(min < max),計算中間元素mid = (left + right) / 2
  • if A[mid] > key, then max = mid!!!!!
  • if A[mid] < key, then min = mid + 1
  • 該演算法的終止條件要麼為nums[mid] = target,要麼為left = right
int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length; // 注意

    while(left < right) { // 注意
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1;
        else if (nums[mid] > target)
            right = mid; // 注意
    }
    return -1;
}

也就是說,初始化的min, max決定了搜尋區間的特性,進而決定了演算法中迴圈的判斷條件以及min, max的移動方式。(即,修改min. max的初始化條件的話,只需要修改while內的判斷條件以及min, max的移動方式即可)

二分查詢的變種

參考二分查詢的變種:其中連結中的的2.5, 2.6寫的有問題,沒有考慮min可能越界的情況,正確寫法參考下面的5, 6

關於二分查詢,如果條件稍微變換一下,比如:陣列之中的資料可能可以重複,要求返回匹配的資料的最小(或最大)的下標;更進一步, 需要找出陣列中第一個大於key的元素(也就是最小的大於key的元素的)下標,等等。 這些,雖然只有一點點的變化,實現的時候確實要更加的細心。

二分查詢的變種和二分查詢原理一樣,主要就是變換判斷條件(也就是邊界條件)。

1. 查詢第一個與key相等的元素

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1;

    while(left <= right) {
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            right = mid - 1; // 演算法能夠搜尋第一個key的關鍵所在!!
        else if (nums[mid] < target)
            left = mid + 1;
        else if (nums[mid] > target)
            right = mid - 1;
    }
    
    if(left < nums.leghth && nums[left] == target) {
        return left; 
    }
    
    return -1; // 還有可能沒有找到!
}
  • 因為初始化left = 0, right = n - 1,迴圈條件中為left <= right,因此迴圈終止條件left = right + 1,所以left的取值範圍為[0, n],所以最後需要判斷left < nums.length?
  • 假設搜尋過程的中間過程中就遇到了第一個與key相等的元素,我們稱此時的midfinal,那麼下一步搜尋區間會變成[left, final - 1],因為這個區間裡面全是小於 key 的元素,所以最後結束時,left = final, right = final - 1left整好指到了第一個與 key 相等的元素位置。
  • 假設搜尋的最後一步才遇到與 key 相等的元素,也就是left = right = final,下一步left = final, right = final - 1,不滿足迴圈條件終止。此時left同樣指向了第一個也是唯一一個與 key 相等的元素位置。

分析的時候,將nums[mid]target的關係分三部分進行討論,分析完可以合併:

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1;

    while(left <= right) {
        int mid = (right + left) / 2;
        if(nums[mid] >= target) // 合併
            right = mid - 1;
        else if (nums[mid] < target)
            left = mid + 1;
    }
    
    if(left < nums.leghth && nums[left] == target) {
        return left;
    }
    
    return -1;
}

2. 查詢最後一個與key相等的元素

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1;

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

3. 查詢最後一個小於或等於key的元素

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1;

    while(left <= right) {
        int mid = (right + left) / 2;
        if(nums[mid] <= target)
            left = mid + 1;
        else if (nums[mid] > target)
            right = mid - 1;
    }
    
    if(right >= 0 && nums[right] <= target) {
        return right;
    }
    
    return -1; // 還有可能沒有找到!
}

其實最後可以直接寫成reutrn right,因為對於特殊情況,沒有小於等於 key 的元素,也就是所有的都大於 key,那麼在搜尋過程中left一直不變,right一直在減小,結束時left = 0, right = -1,要返回的就是-1;

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1;

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

4. 查詢最後一個小於key的元素

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1;

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

5. 查詢第一個大於或等於key的元素

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1;

    while(left <= right) {
        int mid = (right + left) / 2;
        if(nums[mid] >= target)
            right = mid - 1;
        else if (nums[mid] < target)
            left = mid + 1;
    }
    
    if(left < nums.length && nums[left] >= key) {
        return left;
    }
    return -1;
}

6. 查詢第一個大於key的元素

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1;

    while(left <= right) {
        int mid = (right + left) / 2;
        if(nums[mid] > target)
            right = mid - 1;
        else if (nums[mid] <= target)
            left = mid + 1;
    }
    
    if(left < nums.length && nums[left] > key) {
        return left;
    }
    return -1;
}

相關文章