二分查找的詳細分析--基於循環不變式的分析

二分查找:

1. 基本二分查找

給一個數組,已升序排序,即不存在重複元素,查找給定值target,如果不存在,返回該值在數組中可以插入的位置。二分查找本質是利用分治加剪枝不斷進行問題規模的縮小,到最後問題不可分解決問題。將一個區間分爲兩半(縮小規模),分別查找,因爲另一個子問題肯定無解,不需要查找(剪枝),所以本質是剪枝的分治。另外對二分查找循環不變式分析的過程要按查找值存在與否分類討論,且隨着迭代的深入,區間大小始終是在減半收縮。

1.1. 流程

1)將查找區間一分爲二,選取中間點,若是要查找元素,返回
2)如果中間點小於target值,查找右半區間,從 mid + 1 到 right
3)若中間點大於target值,查找左半區間,從 left 到 mid - 1
4)如果區間不存在,即left > right ,結束。

1.2. 樸素實現

int binary_search(int *arr, int size, int target) {
  int l = 0, r = size - 1;
  while (l <= r) {
    int mid = l + (r-l) / 2;
    if (arr[mid] == target) {
      return mid;
    } else if (arr[mid] > target) {
      r = mid - 1;
    } else {
      l = mid + 1;
    }
  }
  return l;
}

1.3. 分析

1.3.1 循環條件解釋

因爲循環的本質是在進行區間的收縮,因爲對於一個target來說,肯定要麼在左半區間,要麼在右半區間,或者剛好在區間中間點(會直接返回),其他情況都需要不斷查找target在哪個半區間,而區間最小的情況是隻有一個元素,即[left,left][left, left]或者[right,right][right,right]

1.3.2 循環不變式

循環不變式分爲兩種情況討論,如果target在數組中,如果target不在數組中
1. target在數組中
target一定在區間[left,right][left, right]中。

  • 初始情況
    target值一定在區間[left,right][left,right]中。
  • 迭代
    將區間一分爲二,如果mid所指值小於target,因爲target存在,那麼target一定在[mid+1,right][mid + 1, right]右半區間,如果mid所指的值大於target,同理target的值一定在[left,mid1][left, mid - 1]的左半區間,通過不斷的收縮區間大小爲原來一半,不斷縮小搜索範圍
  • 終止
    可以知道最終left會等於right,此時由不變式分析,target一定在[left,rihgt][left,rihgt]區間,此時區間只有一個數一定是target。

2. target不在數組
循環不變式: 如果要插入target則一定插入在區間中的某個位置,要麼就是插入在left,或者插入在right之後。

  • 初始情況
    初始情況右三種:

    • I. target在數組下標left之前
      此時區間中所有值均大於target,target插入必定插入在left
    • II. target在數組right之後
      此時target大於數組中所有值,插入一定插入在right之後
    • III. target在數組中某兩個數之間[i,i+1][i, i + 1]
      此時插入一定是插入在數組中間
  • 迭代

    • I. target在數組left之前
      此時區間中所有的數均大於target,所以一直查找[left,mid1][left, mid - 1]左半區間。left始終不變,插入位置在left。
    • II. target在right之後
      此時區間中所有數都小於target,一直查找[mid+1right][mid + 1, right]右半區間,right不變,插入位置在right之後。
    • III. target在數組中某兩個數之間[i,i+1][i,i+1]
      此時分類:
      • mid>i+1mid > i+1
        [i,i+1][i,i+1]在左半區間,有target<arr[i+1]<arr[mid]target < arr[i+1] < arr[mid], 那麼會查找左區間[left,mid1][left, mid - 1]mid1mid - 1更接近 i+1i + 1,區間在向[i,i+1]收縮。循環不變式滿足
      • mid<imid < i
        如果在右區間即arr[mid]<arr[i]<targetarr[mid] < arr[i] < target會查找右半區間[mid+1,right][mid + 1, right]mid+1mid + 1更接近ii,區間的邊界向[i, i + 1]收縮,循環不變式滿足。
      • mid=imid = i
        i等於mid,此時arr[mid]=arr[i]<targetarr[mid] = arr[i] < target,下一輪查找[mid+1,right]=[i+1,right][mid+1, right] = [i+1, right]後續迭代進入I的情況。left是要插入位置,循環不變式成立。
      • mid=i+1mid = i + 1
        mid等於i + 1,有target<arr[i+1]=arr[mid]target < arr[i+1] = arr[mid],下一輪查找[left,mid1]=[left,i][left, mid - 1] = [left, i]後續迭代進入II的情況

    總結迭代的情況,由於區間一直向[i,i+1][i,i+1]收縮,即區間收縮時,左邊界向i靠近,右邊界向i+1靠近,最後一定會進入迭代I、II的情況。

  • 終止

    • I. target在left之前
      此時,區間一直在收縮,直到left=right,最後一輪target<arr[left]=arr[mid]target < arr[left] = arr[mid], 下一輪查找區間[left,mid1]=[left,left1][left, mid - 1] = [left, left - 1] ,區間不存在停止。此時左邊界left指向了target可以插入的位置
    • II. target在right之前
      區間一直收縮,最後一輪arr[mid]=arr[right]<targetarr[mid] = arr[right] < target,下一輪查找[mid+1,right]=[right+1,right][mid + 1, right] = [right + 1, right],此時左邊界right+1指向了target可以插入的位置,也即循環終止時的left值指向。
    • III. target在數組[i,i+1][i,i+1]之間
      此時迭代會一直向[i,i+1][i,i+1]收縮,最後一定會使得迭代進入情況I或者II的終止情況,最終迭代均結束,終止時左邊界始終是target要插入的位置。

總結:對於target不在數組中,在迭代過程中由於區間一直向收縮,最終都會落入I或者II類情況,即target已經在搜索區間之外的情況,最終迭代都會結束

1.4. 終止過程分析

本節討論循環如何終止的情況,分爲最後只剩1個元素,2個元素,3個元素的情況,討論如下

  1. 只有一個元素
    如果只有一個元素,那麼left=right。
    • target<nums[mid]target < nums[mid]
      如果迭代發現target小於mid的值,查找左半區間邊,right = mid - 1 = left - 1,此時left指向插入點位置。
    • target>nums[mid]target > nums[mid]
      如果發現target大於mid的值,查找右半區間left = mid + 1 = right + 1,此時left仍然停在插入點位置。
  2. 有兩個元素
    此時 mid = left = right - 1
    • target<nums[mid]target < nums[mid]
      此時查找左半區間,right = mid - 1 = left - 1,left停在插入點位置
    • target>nums[mid]target > nums[mid]
      此時查護照右半區間, left = mid + 1 = right,進入一個元素的情況
  3. 有三個元素
    left + 1 = mid = right - 1。
    • target<nums[mid]target < nums[mid]
      查找左半區間, right = mid -1 = left, 進入一個元素的情況
    • target>nums[mid]target > nums[mid]
      查找右半區間, left = mid + 1 = right, 進入一個元素的情況

1.5. 收縮區間的邊界問題

在收縮過程中,區間只可以向左半區間或者右半區間收縮,如果區間元素個數大於2,那麼mid一定是處於left和right中間的,無論向左向右區間一定是可以正常縮小的,但是如果區間只有2個元素或者一個元素時,由於mid的計算方式導致mid會等於left或者right,如果left和right賦值爲mid,區間可能會出現無法進一步縮小的情況,所以left和right要如何賦值來使區間收縮是非常重要的細節,一不小心就可能死循環需要注意,尤其是left切記不可以賦值爲mid,right賦值爲mid時循環條件需要改爲while (left < right)

  • left = mid 向右收縮邊界問題
    當區間元素大於2時,區間都可以正常向右縮小,但是當區間收縮到只有兩個元素時可能無法繼續向右收縮,因爲如果下一步需要在右半區間查找,向右收縮,此時right=left+1right = left + 1,且mid=left+(rightleft)/2=left+1/2=leftmid = left + (right - left) / 2 = left + \lfloor 1 /2\rfloor = left,如果令left=mid, 那麼mid會一直等於left,left也會一直等於mid,區間無法繼續收縮,導致下一輪迭代區間還是不變,陷入死循環。但是如果確實需要使用,可以考慮修改計算中點的算法,避免陷入死循環。
  • right = mid 向左收縮問題邊界
    此時區間元素大於1時均可正常向左收縮,但是在區間只有最後一個元素時,left=right,mid=left=rightleft=right, mid = left = right,如果需要進一步查找左半區間會使得right一直停留在原處,無法繼續向左收縮區間陷入死循環。但是可以改變收縮條件爲while (left < right)這樣就可以避免陷入死循環,但是引出的問題就是最後出循環後需要多一輪判斷,因爲最後的值到底存在與否不可知道,或者也可以將right置爲取不到的值,也就是首輪時right在區間尾部,類似STL中的end,這樣也可以保證,但是邏輯上肯定有問題,得修修補補。

2. 變形

2.1. 下界

如果數組非降序排列,可能存在重複元素,給定target,若存在返回該值的最小下標,如果不存在返回可插入的位置。

2.1.1. 分析

此時需要返回第一個位置,比較時如果相等,不可以直接返回,因爲該位置可能是衆多target中間的位置,所以需要繼續查找,但是對於等於的情況要如何處理,是查找左半區間,還是查找右半區間。

  • 查找左半區間
    此時如果左半區間存在target值,根據以上對樸素二分查找循環不變式分析,如果左半區間存在,一定找得到,如果不存在,則區間中元素均小於target,會一直查找右半,那麼最後停止時left一定指向right的右邊,即之前的中間值位置,由於之前等於是查找左半區間arr[mid]>=target,mid=right+1,arr[right+1]>=targetarr[mid] >= target, mid = right + 1,arr[right + 1] >= target,那麼終止時left一定指向最小的下標,或者插入的位置。
  • 查找右半區間
    此時如果後面有target,那麼一定找的到位置,如果不存在,則知道區間中所有元素均大於target的值,會一直查找最左半,right最後停止在left的右邊,$arr[left - 1] <= target $,則right可能指向最大下標,但是left一定是指向了插入位置。
    綜上等於的情況查找左半區間最後left可能是最小下標,可能是插入位置,查找右半區間則right可能是最大下標,否則left是插入位置
int lower_bound(int *arr, int n, int target) {
  int left = 0, right = n - 1;
  while (left <= right) {
    if (target <= arr[mid]) {
      right = mid - 1;
    } else {
      left = mid + 1;
    }
  }
  return left;
}
  1. 中間值等於目標值時,搜索左半區間
    如果左半區間中沒有目標值,那麼計算出的所有mid均小於target,一直執行l=mid + 1, 那麼最後l=r時,l = mid + 1 = r + 1,即最小下標。如果左半區間有目標值,如果mid小於目標值,l = mid + 1, 如果mid等於目標值(只可能等於,因爲上一輪mid是等於),那麼 r = mid - 1。進行不斷的區間收縮迭代,一定可以找到最下下標。

2.2. 上界

給定數組非降序排列,給定target,若存在返回該值的最大下標,不存在,則返回可插入的位置。和下界問題類似,等於的情況查找右半區間即可。要麼存在

int upper_bound(int *arr, int n, int target) {
  int left = 0;
  int right = n - 1;
  while (left <= right) {
    if (target >= arr[mid]) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  if (right >= 0 && arr[right] == target) {
    return target;
  } else {
    return left;
  }
}

2.2.3. 循環不變式分析

上述的上界和下界問題,循環不變式分析,如果不存在target,那麼情況和普通的查找一致,如果存在target,分析核心見2.2.1節的思想,即分析找到target後要如何查找區間,對區間做是否存在target來做分類討論。

2.3. 尋找帶有重複值的旋轉數組最小值

leetcode 154
這個問題也是二分的思想求解問題,因爲二分的核心思想是收縮區間,所以在這個問題也是一樣,但是這個問題對於中間值等於左右值時,無法確定查找值在左半區間還是右半區間,但是可以確定一定在左右之間,所以此時只能簡單的通過同時縮減左右一個單位來減少區間大小以減小問題規模。

int findMin(vector<int>& nums) {
    int size = nums.size();
    if (size == 1) return nums[0];
    int l = 0; int r = size - 1;
    while (l < r) {  // pivot must in [l,r], while condition
        int mid = l + (r - l) / 2;
        if (nums[l] < nums[r]) break;
        if (nums[mid] > nums[r]) {  // pivot must in right half range [mid + 1, r]
            l = mid + 1;
        } else if(nums[mid] < nums[r]) {  // pivot must in left half range [l, mid]
            r = mid;
        } else {
            if (nums[l] > nums[r]) {
                r = mid;
            } else {  //  no more info to know whether pivot is located in left or right of mid, just shrink the range by adjust left and right
                ++l;
                --r;
            }
        }
    }
    return nums[l];
}

3. 總結

對於二分查找問題,本質是分治,只不過其餘問題不用解而已。在一個區間[left,right][left,right]中查找一個值,值要麼在,要麼不在。如果在要麼在[left,mid1][left, mid - 1],要麼在[mid+1,right][mid + 1, right],或者就在mid處。如果值不在,則討論target的插入位置是否在區間中。通過想像一個區間被mid一分爲二兩個半區間,固定不動,然後用target和mid比較以確定target所在半區間,以及要如何收縮,以此思考問題的解決過程

  • 區間收縮
    分析時只考慮中間點將區間一分爲二,然後確定下一步需要搜索的區間,只有下圖的模型,考慮target在哪個區間,以及如何收縮區間的問題。而考慮left和right之間有幾個元素屬於終止條件的問題,是邊界問題,迭代的一般過程無需考慮,只需知道區間一直在減小即可。
    //left ..................mid....................right//
    
  • 終止條件和邊界
    由於區間收縮到最後會出現只有兩個元素和只有一個元素的情況,此時mid的值可能等於left,也可能等於right,是收縮區間的時候屬於特殊情況,見1.4分析。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章