前言
二分法的思想:將一段區間(代碼中往往指一個數組)一分爲二,進而排除掉不符合條件的一半,選擇符合條件的一半繼續進行,直至尋找到正確答案(或無元素可供查找)
時間複雜度:因爲每次都能排除一半的區間,因此時間複雜度爲log(n)
適用條件:一般都要求數組有序,或間接有序(後面有例題)
二分查找法的思想在 1946 年就被提出,但是第 1 個沒有 Bug 的二分查找法在 1962 年纔出現。
《計算機程序藝術設計 · 第三卷》
如上,思想雖然簡單,但是往往在邊界條件的處理上讓人頭疼。在比賽或者面試過程中,沒有太多時間
模板
步驟
二分法的處理步驟:
- 查看數組是否有序或間接有序,即每次將數組分成2部分時,是否能夠確認目標值在哪一部分
- 構造判斷條件
- 處理邊界條件(由於基本都是
(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;
}
- 數組有序
- 構造判斷條件,即當
target == nums[mid]
時,返回下標。當nums[mid] < target
時,說明左半部分區間均< target
,因此可以排除左半區間,left = mid + 1
繼續查找。反之同理 - 當待搜索的數組長度爲2時,假設搜索[4, 5],此時
left = 0 right = 1
,則mid = 0
,因此先搜索4,4 < 5
排除左半區間,left = mid + 1 = 1
,則下次搜索只剩[5],得出正確答案。如還未搜索到,此時left
再+1
,left > 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;
}
- 數組有序
- 判斷條件,上一題中當
nums[mid] == target
時,直接return
。此時由於尋找的是左邊界,因此當nums[mid] == target
,依舊需要向左繼續查找 - 與上一題邊界條件不同,此時如果依舊寫
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;
}
- 數組有序
- 判斷條件,此時由於尋找的是右邊界,因此當
nums[mid] == target
,依舊需要向右繼續查找 - 值得注意的是,
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] = 1
而target = 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:
如果我們能知道數組在具體的哪個點處被倒置,那麼我們剩下的工作就簡單多了,只需要把target
與nums[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;
// 待更新