【LeetCode】35. 搜尋插入位置
35. 搜尋插入位置
知識點:陣列,二分查詢;
題目描述
給定一個排序陣列和一個目標值,在陣列中找到目標值,並返回其索引。如果目標值不存在於陣列中,返回它將會被按順序插入的位置。
請必須使用時間複雜度為 O(log n) 的演算法。
示例
輸入: nums = [1,3,5,6], target = 5
輸出: 2
輸入: nums = [1,3,5,6], target = 2
輸出: 1
輸入: nums = [1,3,5,6], target = 7
輸出: 4
解法一:二分查詢
這道題目就是典型的二分搜尋。下面介紹一個重要的查詢方法,二分查詢。
二分查詢
可檢視二分查詢總結 :
如果想要在陣列中查詢一個數,最基本的方法就是暴力解法:一次遍歷,這時候時間複雜度是O(N),二分查詢就是其中的一種優化,時間複雜度是O(logN);具體做法是一步一步逼近直到找到。前提是陣列需要是一個排序陣列。
Knuth 大佬(發明 KMP 演算法的那位)怎麼說的:
Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky...
思路很簡單,細節是魔鬼;
二分查詢的模板:
//迴圈寫法; public int binarySearch(int[] nums, int target){ int left = 0, right = num.length-1; while(left <= right){ //細節1:迴圈條件; int mid = left + ((right-left) >> 1); //細節2:防止溢位,此外需要注意由於優先順序的原因,需要新增括號; if(nums[mid] == target){ return mid; }else if(nums[mid] < target){ left = mid + 1; //細節3:注意加減1; }else{ right = mid - 1; } } return -1 } //遞迴寫法: public int binarySearch(int[] nums, int target, int left, int right){ if(left <= right){ int mid = left + ((right-left)-1); if(nums[mid] == target){ return mid; //查詢成功; }else if(nums[mid] > target){ return binarySearch(nums, target, left, mid-1); //新的區間:左半區間; }else{ return binarySearch(nums, target, min+1, right); //新的區間: 右半區間; } } return -1; }
上述功能就是如果能在陣列中找到目標值,就返回其索引,如果找不到,就返回其下標;如果目標值比中間值還大,那肯定在中間值右側(因為陣列已經排序好了),如果目標值比mid值小,那肯定在mid左側。
- 細節1:為什麼while迴圈的條件時<=?
因為我們初始化的時候右側區間是nums.length-1;所以是包括right的,即我們的區間是[left,right],這樣一個左閉右閉的區間,把這個區間理解成搜尋區間,即我們是在這樣一個區間上搜索,那什麼時候停止呢,兩個原因:- 1.找到了目標值,那就停止;
- 2.沒找到目標值,但是搜尋區間為空了,沒得找了,這時候停止;
所以在最後一個=的時候,比如[2,2]這時候區間還不為空,萬一就是這個2呢。
- 細節2:為什麼寫成left+((right-left) >> 1);
這主要是為了防止溢位,記住就可以了,注意除以2,用位運算的話會比較快一點,而且記得帶外面那個大括號; - 細節3: 為什麼left = mid + 1,right = mid - 1?
想一下剛才搜尋區間的概念,如果發現了索引mid不是要找的target,那自然要從將mid剔除掉,從mid的左邊或者右邊找起來了。
缺陷:上述演算法存在一個缺陷就是不能返回左右側邊界,比如陣列是[1,2,2,2,3], target是2,這時候返回的索引是2,沒有辦法返回左右邊界。
好了,現在回到本題,本題和標準二分查詢唯一區別就是如果找不到的時候返回它應該在的位置,,這個應該在的位置其實就是第一個比target大的元素位置,其實,和上面的程式一模一樣,唯一的區別就是最後不再返回-1,而返回left。left就一定是最後應該插入的位置嗎?在上述過程中,如果找到了,那就直接返回;如果原陣列中不包含target,那while最後一次執行的一定是left=right=mid,這時候mid左邊的數全部小於target,mid右邊的數全部大於target,所以此時返回的插入位置分為兩種情況:
- 1.nums[mid] > target, 此時執行right=mid-1,直接返回mid也就是left就行。
- 2.nums[mid] < target,此時執行left=mid+1,這時候就到了第一個比target大的值,返回left就可以了;
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length-1;
while(left <= right){ //細節1
int mid = left + ((right-left) >> 1);
if(nums[mid] == target) return mid;
else if(nums[mid] > target){
right = mid-1;
}else{
left = mid+1;
}
}
return left;
}
}
時間複雜度:O(logN);
體會
- 注意檢視上面中提到的二分法的模板和細節,注意什麼時候帶等號,什麼時候不帶等號;
- 二分查詢本質上就是一個不斷縮小搜尋空間的過程,比如我們找出某個值,就是在不斷的把空間縮小,找出哪個數字亂序了,也在不斷的把空間縮小,求一個數的開根號,不斷的縮小空間然後拿值去逼近。這個過程要多想一下,就是我們不斷的縮小空間,然後每次都是拿這個空間上的中間值去做某種判斷,然後去逼近我們的結果。
- 二分查詢應用的前提就是一定是一個有序的,或者半有序的,如果是半有序的話原則是就是不斷向有序上去趕,因為只有在有序的時候才能去二分;
- 上面提供的二分查詢的模板要始終明白在最後一步跳出迴圈前兩點:
- 1.最後一步一定是right=left=mid。
- 2.mid左側和右側一定是已經有了規律的了。比如查詢值的時候,mid左側都比t值小,mid右側都比t值大;比如判斷從哪個數字開始亂了,mid左側一定是整齊的,mid右側一定是亂的。那這時候只要去判斷一下mid的值就可以了。如果mid>t或者說mid亂了,那正好返回left也就是mid,如果mid<t或者說mid沒亂,那left=mid+1,這不就正好是第一個大於t或者第一個亂的了嗎。