給定一個 n
個元素有序的(升序)整型陣列 nums
和一個目標值 target
,寫一個函式搜尋 nums
中的 target
,如果目標值存在返回下標,否則返回 -1
。
示例 1:
輸入: nums = [-1,0,3,5,9,12], target = 9
輸出: 4
解釋: 9 出現在 nums 中並且下標為 4
示例 2:
輸入: nums = [-1,0,3,5,9,12], target = 2
輸出: -1
解釋: 2 不存在 nums 中因此返回 -1
解題思路
二分查詢基本框架如下:
int binarySolution(int[] nums, int target) {
int left = 0, right = ...;
while (...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...;
} else if (nums[mid] > target) {
right = ...;
}
}
return ...;
}
分析二分查詢的一個技巧是:不要出現 else,而是把所有情況用 else if 寫清楚,這樣可以清楚地展現所有細節。
其中 ...
標記的部分,就是可能出現細節問題的地方,當你見到一個二分查詢的程式碼時,首先注意這幾個地方。
另外宣告一下,計算 mid
時需要防止溢位,程式碼中 left + (right - left) / 2
就和 (left + right) / 2
的結果相同,但是有效防止了 left
和 right
太大直接相加導致溢位。
對於本題的題解如下:
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 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;
}
}
這裡探討一下其中的幾個細節。
1、為什麼 while 迴圈的條件中是 <=,而不是 <?
答:因為初始化 right
的賦值是 nums.length - 1
,即最後一個元素的索引,而不是 nums.length
。
因為我們需要對所有的元素進行搜尋,使用 <=
搜尋的是兩端都閉區間 [left, right]
,如果使用 <
則相當於左閉右開區間 [left, right)
。
如果使用<
,然後right
初始賦值改為nums.length
,也可實現[left, right]
範圍的搜尋,但會漏掉最後一個元素,下面會講
此外還要注意下結束搜尋的條件。當找到目標值的時候可以終止:nums[mid] == target
。如果沒找到,就需要 while
迴圈終止,然後返回 -1
。那 while 迴圈什麼時候應該終止?搜尋區間為空的時候應該終止。
while(left <= right)
的終止條件是 left == right + 1
,寫成區間的形式就是 [right + 1, right]
,或者帶個具體的數字進去 [3, 2]
。
while(left < right)
的終止條件是 left == right
,寫成區間的形式就是 [right, right]
,或者帶個具體的數字進去 [2, 2]
,這時候區間非空,還有一個數 2
,但此時 while
迴圈終止了。也就是說這區間 [2, 2]
被漏掉了,索引 2
沒有被搜尋,如果這時候直接返回 -1
就是錯誤的。
如果要用 while(left < right)
也是可以的,我們已經知道了出錯的原因,就打個補丁好了:
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length;
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 nums[left] == target ? left : -1;
}
}
2、為什麼 left = mid + 1
,right = mid - 1
?我看有的程式碼是 right = mid
或者 left = mid
剛才明確了「搜尋區間」這個概念,而且本演算法的搜尋區間是兩端都閉的,即 [left, right]
。那麼當我們發現索引 mid
不是要找的 target
時,下一步應該去搜尋哪裡呢?
當然是去搜尋 [left, mid-1]
或者 [mid+1, right]
對不對?因為 mid
已經搜尋過,應該從搜尋區間中去除。
3、此演算法有什麼缺陷?
比如說給你有序陣列 nums = [1,2,2,2,3]
,target
為 2
,此演算法返回的索引是 2
,沒錯。但是如果我想得到 target
的左側邊界,即索引 1
,或者我想得到 target
的右側邊界,即索引 3
,這樣的話此演算法是無法處理的。
這樣的需求很常見,你也許會說,找到一個 target
,然後向左或向右線性搜尋不行嗎?可以,但是不好,因為這樣難以保證二分查詢對數級的複雜度了。
總結
- 不要使用
else
,而是把所有情況用else if
寫清楚 - 計算
mid
時需要防止溢位,使用left + (right - left) / 2
先減後加這樣的寫法 while
迴圈的條件<=
對應right
初始值為nums.length - 1
,終止條件是left == right + 1
,例如[3, 2]
- 如果
while
迴圈的條件<
,需要把right
初始值改為nums.length
,此時終止條件是left == right
,例如[2, 2]
,這樣會漏掉最後一個區間的元素,需要單獨判斷下 - 當
mid
不是要找的target
時,下一步應該搜尋[left, mid-1]
或者[mid+1, right]
,對應left = mid + 1
或者right = mid - 1
- 二分查詢時間複雜度
O(logn)