1. 程式人生 > 實用技巧 >淺談二分查詢框架+劍指 Offer 53 - I (C++)

淺談二分查詢框架+劍指 Offer 53 - I (C++)

目錄

二分查詢目標分類及其框架

以下討論的場景的前提都包括:給定一個正向排序的陣列vector<int> nums和給定需要求取的目標值target,來進行討論。

查詢數值的下標位置

因為二分查詢的目標是確定某個具體下標,所以每次取中值mid後,只要其對應的數值與target相等就可以進行函式的返回。如果在迴圈退出後,仍然沒有查詢到確定值,就可以判定未不存在合理解。

因此我們考慮使用閉口區間進行while迴圈的篩選,當mid值不等於target時,左邊界變動未left = mid + 1

,右邊界變動為right = mid - 1while(left <= right)迴圈退出的條件即為left = right - 1

基本框架如下:

// 給定vector陣列
// 給定目標值
vector<int> nums{ ... };
int target;

int left = 0;
// right邊界初始化為末尾元素
int right = nums.size() - 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;

查詢目標值的左邊界

而搜尋左邊界我們使用的迴圈終止條件是半開半閉區間。其大致框架如下:

// 給定vector陣列
// 給定目標值
vector<int> nums{ ... };
int target;

int left = 0;
// right邊界初始化為陣列長度
int right = nums.size();
while (left < right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] == target)
        right = mid;
    else if (nums[mid] < target)
        left = mid + 1;
    else if (nums[mid] > target)
        right = mid;
}

與具體下標的程式碼框架有所不同,重點在於對於nums[mid] == target的處理。

if (nums[mid] == target)
	right = mid;

當找到目標值並沒有立刻退出迴圈返回下標,而是縮小搜尋區間的上界right,在區間[left, mid)中繼續搜尋,不斷向左收縮,達到鎖定左側邊界的目的。

並且同時右邊界也沒有向之前一樣修改為right = mid - 1,而是:

if (nums[mid] > target)
	right = mid;

造成右邊界變換方式改變的原因,主要是搜尋區間由閉區間轉變成左閉右開區間的緣故

  • 閉區間[left, right]討論完mid後,下一步搜尋空間去掉mid並分割成[left, mid-1][mid+1, right]
  • 左閉右開區間討論完mid後,剩下的區間應該按照迴圈條件的格式分割成分割成[left, mid)[mid+1, right)

另外,關於右邊界初始化的細節:

int left = 0, right = nums.size();

不同於求具體下標的題目,將右邊界初始為right = nums.size()-1,即末位陣列元素。其實也是跟搜尋區間有關:

  • 對於閉區間[lef, right],如果取陣列長度為下標回越界出錯。
  • 而對於[left, right),右側開區間取不到,可以嘗試此型別的初始化。

迴圈退出的條件為left == right,所以函式只會在退出迴圈後進行函式值的返回。我們需要對返回的left或者right進行討論和篩選,錯誤的情況直接返回-1。

對於上圖的情況,該演算法回返回1。可以理解成nums中小於2的元素有1個

  • 對於target小於有序陣列所有元素的情況,比如對於有序陣列 nums = [2,3,5,7] , target = 1 ,演算法會返回 0,含義 是:nums 中⼩於 1 的元素有 0 個

  • 對於target大於有序陣列所有元素的情況,比如說 nums = [2,3,5,7], target = 8 ,演算法會返回 4,含義是: nums 中⼩於 8 的元素有 4 個

因此對於循壞外的篩選程式碼如下:

if (left == nums.size()) return -1;
if (nums[left] != target) return -1;
return left;

通過上述討論,我們知道退出迴圈後left可能有以下幾種情況:為0也可能為nums.size()

  • left == 0,說明陣列中沒有比target小的元素。但同時我們也要篩選掉目標值target就在下標為0的情況,所以我們需要對下標left處的元素值進行篩選。if (nums[left] != target),那麼二分查詢就是失敗的,否則函式就返回下標值。
  • left == nums.size(),說明陣列中沒有比target大的元素。二分查詢失敗,如果不經過此步驟討論直接進行left下標對應元素值的判斷,可能回出現數組越界的段錯誤,所以上述兩句篩選缺一不可,並且程式碼順序也不可更改。

查詢目標值的右邊界

與查詢左邊界類似,程式碼框架如下:

// 給定vector陣列
// 給定目標值
vector<int> nums{ ... };
int target;

int left = 0;
// right邊界初始化為陣列長度
int right = nums.size();
while (left < right) {
    int mid = left + (right - left) / 2;
    if (nums[mid] == target)
        left = mid + 1;
    else if (nums[mid] < target)
        left = mid + 1;
    else if (nums[mid] > target)
        right = mid;
}

類似地,關鍵點還是在這裡:

if (nums[mid] == target)
	left = mid + 1;

nums[mid] == target時不要立即返回,而是增大搜索區間的下界left,使得區間不斷向右收縮,達到鎖定右側邊界的目的。

同樣,對於迴圈終止時的left == right,我們也要進行篩選,對於二分查詢失敗的情況返回-1。

對於上圖的情況,該演算法回返回3。可以理解成nums中小於等於target2的元素有3個

  • 對於target小於有序陣列所有元素的情況,比如對於有序陣列 nums = [2,3,5,7] , target = 1 ,演算法會返回 0,含義 是:nums 中⼩於等於 target1 的元素有 0 個

  • 對於target大於有序陣列所有元素的情況,比如說 nums = [2,3,5,7], target = 8 ,演算法會返回 4,含義是: nums 中小於等於target 8 的元素有 4 個

if (left == 0) return -1;
if (nums[left - 1] != target) return -1;
return left - 1;

不要跟左邊界的區別弄混,雖然兩者都涉及到0下標和陣列長度下標的討論。由於我們對於下標nums的理解,查詢左邊界時,確定的下標時target的首位元素位置,而查詢右邊界時,確定的下標是末位target元素位置的後一位!!!。因此對於查詢右邊界的情況,如果返回的下標是0,可以肯定二分查詢失敗了。而取右邊界時,返回下標前的值是小於等於target的,所以需要討論前一位nums[left - 1]的值是否等於target

例題:劍指 Offer 53 - I. 在排序陣列中查詢數字 I

題目描述

統計一個數字在排序陣列中出現的次數。

示例 1:

輸入: nums = [5,7,7,8,8,10], target = 8
輸出: 2

示例 2:

輸入: nums = [5,7,7,8,8,10], target = 6
輸出: 0

限制:

  • 0 <= 陣列長度 <= 50000

題解

class Solution {
public:
    int search(vector<int>& nums, int target) {
        // 先尋找左邊界
        int left = 0, right = nums.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target)
                right = mid;
            else if (nums[mid] < target)
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid;
        }
        // 兩種篩選缺一不可
        // 篩選不存在目標元素的情況
        if (left == nums.size()) return 0;
        // 篩選左右指標重合到0位置,但對映元素不為目標元素的情況
        if (nums[left] != target) return 0; 
        
        // 開區間尋找左邊界是可取的目標值
        // 開區間尋找有邊界是不可取的非目標值
        int leftBorder = left;
        right = nums.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target)
                left = mid + 1;
            else if (nums[mid] < target)
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid;
        }
        int rightBorder = left;
        return (rightBorder - leftBorder);
    }
};