二分查找常用模板、例題

前言

二分法的思想:將一段區間(代碼中往往指一個數組)一分爲二,進而排除掉不符合條件的一半,選擇符合條件的一半繼續進行,直至尋找到正確答案(或無元素可供查找)

時間複雜度:因爲每次都能排除一半的區間,因此時間複雜度爲log(n)

適用條件:一般都要求數組有序,或間接有序(後面有例題)

二分查找法的思想在 1946 年就被提出,但是第 1 個沒有 Bug 的二分查找法在 1962 年纔出現。

《計算機程序藝術設計 · 第三卷》

如上,思想雖然簡單,但是往往在邊界條件的處理上讓人頭疼。在比賽或者面試過程中,沒有太多時間

模板

步驟

二分法的處理步驟:

  1. 查看數組是否有序或間接有序,即每次將數組分成2部分時,是否能夠確認目標值在哪一部分
  2. 構造判斷條件
  3. 處理邊界條件(由於基本都是(left + right) / 2,每次縮減一半,因此通常只需分析當數組長度爲2時的特殊情況即可)

1. 常用模板

給定一個有序數組nums,再給定一個target,查找target在數組中的下標,沒有則返回-1
nums = [1, 2, 3 ,5 ,6 ,7 ,8] target = 5
ans = 3

   // 常見二分法
   int binarySearch(int[] nums, int target) {
       int left = 0;
       int right = nums.length - 1;
       int mid = 0;
       while(left <= right) {
           mid = (left + right) / 2;
           if (target == nums[mid]) {
               return mid;
           } else if (nums[mid] < target) {
               left = mid + 1;
           } else {
               right = mid - 1;
           }
       }
       return -1;
   }
  1. 數組有序
  2. 構造判斷條件,即當target == nums[mid]時,返回下標。當nums[mid] < target時,說明左半部分區間均< target,因此可以排除左半區間,left = mid + 1繼續查找。反之同理
  3. 當待搜索的數組長度爲2時,假設搜索[4, 5],此時left = 0 right = 1,則mid = 0,因此先搜索4,4 < 5排除左半區間,left = mid + 1 = 1,則下次搜索只剩[5],得出正確答案。如還未搜索到,此時left+1left > right跳出循環,返回-1

2. 左邊界模板

給定一個有序數組nums,再給定一個target,查找target在數組中的下標,沒有則返回-1
nums = [1, 2, 3 ,3 ,3 ,7 ,8] target = 3
ans = 2

// 左邊界
    public int searchLeft(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int mid = 0;

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

        return nums[left] == target ? left : -1;
    }
  1. 數組有序
  2. 判斷條件,上一題中當nums[mid] == target時,直接return。此時由於尋找的是左邊界,因此當nums[mid] == target,依舊需要向左繼續查找
  3. 與上一題邊界條件不同,此時如果依舊寫while(left <= right),上一題中當nums[mid] == target時,直接return。而本題由於沒有return,而是right = mid,因此將會陷入死循環。例如:nums = [5],target = 5,left = 0,right = 0,mid = 0,此時如果條件while(left <= right)將會陷入死循環。因此在邊界條件的處理上,我們將==條件獨立出來判斷

爲了與下方右邊界模板統一,我們也可以將左邊界模板的右指針初始話成right = nums.length,但是由此帶來的問題就是。當target大於數組內所有數時,left會不斷右移,直到left = right = nums.length,此時就會發生數組越界。因此需要特殊判斷

    public int searchLeft(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        int mid = 0;

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

        if(left == nums.length) return -1;
        return nums[left] == target ? left : -1;
    }

3. 右邊界模板

給定一個有序數組nums,再給定一個target,查找target在數組中的下標,沒有則返回-1
nums = [1, 2, 3 ,3 ,3 ,7 ,8] target = 3
ans = 4

    // 右邊界
    public int searchRight(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        int mid = 0;

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

        if(left == 0) return nums[left] == target ? left : -1;
        return nums[left - 1] == target ? left - 1 : -1;
    }
  1. 數組有序
  2. 判斷條件,此時由於尋找的是右邊界,因此當nums[mid] == target,依舊需要向右繼續查找
  3. 值得注意的是,int right = nums.length,此時的右指針不再指向最右側節點,而是最右側+1節點。其實跟左邊界最大的區別在於(0 + 1) / 2 = 0,由於向下取整的緣故,每次除都是向左靠齊的。而現在搜索的是右邊界,使用了left = mid + 1;,其實left是偏向右一位去進行嘗試的,實際可能出現正確答案其實在[left-1],同時還需要考慮特殊情況,即target在最左側的情況if(left == 0) return nums[left] == target ? left : -1;,否則left - 1越界

力扣真題

力扣35(easy) 0ms

給定一個排序數組和一個目標值,在數組中找到目標值,並返回其索引。如果目標值不存在於數組中,返回它將會被按順序插入的位置。
示例 1:
輸入: [1,3,5,6], 5
輸出: 2

示例 2:
輸入: [1,3,5,6], 2
輸出: 1

此題是典型的二分法應用題,通過二分法搜索插入位置,數組搜索範圍只剩1個元素時,如果 該元素 <= target時,則返回當前位置。否則返回target下一個位置

class Solution {
    public int searchInsert(int[] nums, int target) {
        if(nums.length == 0) return 0;
        int left = 0;
        int right = nums.length - 1;
        int mid = 0;
        // 此時沒有等號,因爲當數組搜索只剩一個元素,即left == right時,我們停止,自行判斷
        while(left < right) {
            mid = (left + right) / 2;
            if(nums[mid] == target) {
                return mid;
            } else if(nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        if(nums[left] == target) {
            return left;
        }else if(nums[left] < target) {
            return left + 1;
        }else {
            return left;
        }
    }
}

該代碼只是爲了方便讀者理解,其實最後一段代碼是沒有必要的,完全可以合併到循環中,因此最終代碼爲

class Solution {
    public int searchInsert(int[] nums, int target) {
        if(nums.length == 0) return 0;
        int left = 0;
        int right = nums.length;
        int mid = 0;

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

        return left;
    }
}

力扣34(mid) 0ms

給定一個按照升序排列的整數數組 nums,和一個目標值 target。找出給定目標值在數組中的開始位置和結束位置。
你的算法時間複雜度必須是 O(log n) 級別。
如果數組中不存在目標值,返回 [-1, -1]。

示例 1:
輸入: nums = [5,7,7,8,8,10], target = 8
輸出: [3,4]

這題很明顯就是搜索左右邊界問題,左邊界我們有2套模板,右邊界一套,直接代入即可

class Solution {
    public int[] searchRange(int[] nums, int target) {
        if(nums.length == 0) return new int[]{-1, -1};
        
        int[] ans = new int[2];
        ans[0] = searchLeft(nums, target);
        ans[1] = searchRight(nums, target);

        return ans;
    }

    public int searchLeft(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        int mid = 0;

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

        if(left == nums.length) return -1;
        return nums[left] == target ? left : -1;
    }

    public int searchRight(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        int mid = 0;

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

        if(left == 0) return nums[left] == target ? left : -1;
        return nums[left - 1] == target ? left - 1 : -1;
    }
}

力扣33(mid) 0ms

假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。
搜索一個給定的目標值,如果數組中存在這個目標值,則返回它的索引,否則返回 -1 。
你可以假設數組中不存在重複的元素。
你的算法時間複雜度必須是 O(log n) 級別。

示例 1:
輸入: nums = [4,5,6,7,0,1,2], target = 0
輸出: 4

解法1
這題呼應上面所說,數組應該有序或間接有序。如示例1,假設此時nums[mid] = 1target = 4,通常當nums[mid] < target時,我們需要往右半部分查找,但是此時因爲數組倒置,也就是4 > nums[nums.length - 1],所以實際我們應該往左半部分查找。由此會有以下僞代碼。這是一種做法,分析出所有可能,筆者認爲稍微複雜了一些

if(nums[mid] < target && taget >= nums[nums.length - 1]) {
	// 查找左半區
}
if(nums[mid] < target && taget < nums[nums.length - 1]) {
	// 查找右半區
}
...

解法2
如果我們能知道數組在具體的哪個點處被倒置,那麼我們剩下的工作就簡單多了,只需要把targetnums[0]做一下比較,就可以知道應該在前半段還是在後半段進行搜索。而尋找倒置點的工作也恰好時一個二分法的過程。

class Solution {
    public int search(int[] nums, int target) {
        if (nums.length == 0) return -1;
        
        int left = 0;
        int right = nums.length - 1;
        // 尋找倒置點,如果數組沒倒置則返回-1
        int mid = searchMid(nums);
        if (mid == -1) {
        	// 數組沒倒置嗎,直接搜索全數組
            return searchTarget(nums, target, left, right);
        } else if (target >= nums[left]) {
        	// 數組倒置,但是target大於nums[0],所以在左半部分找
            return searchTarget(nums, target, left, mid);
        } else {
        	// 在右半部分找
            return searchTarget(nums, target, mid + 1, right);
        }
    }

    public int searchMid(int[] nums) {
        if (nums.length <= 1) {
            return -1;
        }

        int left = 0;
        int right = nums.length - 1;
        int mid = 0;
        while (left <= right) {
            mid = (left + right) / 2;
            // 如果mid數大於mid+1,說明改點就是倒置點
            if (mid + 1 < nums.length && nums[mid] > nums[mid + 1]) {
                return mid;
            } else if (nums[mid] >= nums[left]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return -1;
    }
	// 常用的二分法搜索模板
    public int searchTarget(int[] nums, int target, int left, int right) {
        int mid = 0;
        while (left <= right) {
            mid = (left + right) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] > target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return -1;
    }
}

力扣4(hard) 2ms

給定兩個大小爲 m 和 n 的正序(從小到大)數組 nums1 和 nums2。
請你找出這兩個正序數組的中位數,並且要求算法的時間複雜度爲 O(log(m + n))。
你可以假設 nums1 和 nums2 不會同時爲空。

示例 1:
nums1 = [1, 3]
nums2 = [2]
則中位數是 2.0

這道題當真有點難,首先看到2個數組時有序的,大概可以想到二分法。而這道題的解題關鍵則在於需要找到二分法的目標,即最終目標是找到第K小的數

當nums1與nums2的數組長度和爲奇數時,則求第 (m + n + 1) / 2小的數。如果爲偶數,則需要求第(m + n + 1) / 2小與(m + n + 2) / 2小的數的平均值。又因爲當長度和爲奇數時(m + n + 1) / 2等於(m + n + 2) / 2,所以其實問題可以簡化(該思想可大量用在奇偶數的例題中)

// findK爲尋找第k小的數
return (findK(m + n + 1) + findK(m + n + 2)) / 2;

// 待更新

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章