在排序數(shù)組中查找元素的第一個和最后一個位置
這個就是考察二分法的進(jìn)階題目了
在排序數(shù)組中查找元素的第一個和最后一個位置
給定一個按照升序排列的整數(shù)數(shù)組 nums,和一個目標(biāo)值 target。找出給定目標(biāo)值在數(shù)組中的開始位置和結(jié)束位置。
如果數(shù)組中不存在目標(biāo)值 target,返回 [-1, -1]。
進(jìn)階:你可以設(shè)計并實現(xiàn)時間復(fù)雜度為 O(log n) 的算法解決此問題嗎?
示例 1:
- 輸入:nums = [5,7,7,8,8,10], target = 8
- 輸出:[3,4]
示例 2:
- 輸入:nums = [5,7,7,8,8,10], target = 6
- 輸出:[-1,-1]
示例 3:
- 輸入:nums = [], target = 0
- 輸出:[-1,-1]
思路
這道題目如果基礎(chǔ)不是很好,不建議大家看簡短的代碼,簡短的代碼隱藏了太多邏輯,結(jié)果就是稀里糊涂把題AC了,但是沒有想清楚具體細(xì)節(jié)!
對二分還不了解的同學(xué)先做這兩題:
- 704.二分查找
- 35.搜索插入位置
下面我來把所有情況都討論一下。
尋找target在數(shù)組里的左右邊界,有如下三種情況:
- 情況一:target 在數(shù)組范圍的右邊或者左邊,例如數(shù)組{3, 4, 5},target為2或者數(shù)組{3, 4, 5},target為6,此時應(yīng)該返回{-1, -1}
- 情況二:target 在數(shù)組范圍中,且數(shù)組中不存在target,例如數(shù)組{3,6,7},target為5,此時應(yīng)該返回{-1, -1}
- 情況三:target 在數(shù)組范圍中,且數(shù)組中存在target,例如數(shù)組{3,6,7},target為6,此時應(yīng)該返回{1, 1}
這三種情況都考慮到,說明就想的很清楚了。
接下來,在去尋找左邊界,和右邊界了。
采用二分法來去尋找左右邊界,為了讓代碼清晰,我分別寫兩個二分來尋找左邊界和右邊界。
剛剛接觸二分搜索的同學(xué)不建議上來就像如果用一個二分來查找左右邊界,很容易把自己繞進(jìn)去,建議扎扎實實的寫兩個二分分別找左邊界和右邊界
尋找右邊界
先來尋找右邊界,至于二分查找,如果看過704.二分查找就會知道,二分查找中什么時候用while (left <= right),有什么時候用while (left < right),其實只要清楚循環(huán)不變量,很容易區(qū)分兩種寫法。
那么這里我采用while (left <= right)的寫法,區(qū)間定義為[left, right],即左閉又閉的區(qū)間(如果這里有點看不懂了,強(qiáng)烈建議把704.二分查找這篇文章先看了,704題目做了之后再做這道題目就好很多了)
確定好:計算出來的右邊界是不包好target的右邊界,左邊界同理。
可以寫出如下代碼
- // 二分查找,尋找target的右邊界(不包括target)
- // 如果rightBorder為沒有被賦值(即target在數(shù)組范圍的左邊,例如數(shù)組[3,3],target為2),為了處理情況一
- int getRightBorder(vector<int>& nums, int target) {
- int left = 0;
- int right = nums.size() - 1; // 定義target在左閉右閉的區(qū)間里,[left, right]
- int rightBorder = -2; // 記錄一下rightBorder沒有被賦值的情況
- while (left <= right) { // 當(dāng)left==right,區(qū)間[left, right]依然有效
- int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
- if (nums[middle] > target) {
- right = middle - 1; // target 在左區(qū)間,所以[left, middle - 1]
- } else { // 當(dāng)nums[middle] == target的時候,更新left,這樣才能得到target的右邊界
- left = middle + 1;
- rightBorder = left;
- }
- }
- return rightBorder;
- }
尋找左邊界
- // 二分查找,尋找target的左邊界leftBorder(不包括target)
- // 如果leftBorder沒有被賦值(即target在數(shù)組范圍的右邊,例如數(shù)組[3,3],target為4),為了處理情況一
- int getLeftBorder(vector<int>& nums, int target) {
- int left = 0;
- int right = nums.size() - 1; // 定義target在左閉右閉的區(qū)間里,[left, right]
- int leftBorder = -2; // 記錄一下leftBorder沒有被賦值的情況
- while (left <= right) {
- int middle = left + ((right - left) / 2);
- if (nums[middle] >= target) { // 尋找左邊界,就要在nums[middle] == target的時候更新right
- right = middle - 1;
- leftBorder = right;
- } else {
- left = middle + 1;
- }
- }
- return leftBorder;
- }
處理三種情況
左右邊界計算完之后,看一下主體代碼,這里把上面討論的三種情況,都覆蓋了
- class Solution {
- public:
- vector<int> searchRange(vector<int>& nums, int target) {
- int leftBorder = getLeftBorder(nums, target);
- int rightBorder = getRightBorder(nums, target);
- // 情況一
- if (leftBorder == -2 || rightBorder == -2) return {-1, -1};
- // 情況三
- if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1};
- // 情況二
- return {-1, -1};
- }
- private:
- int getRightBorder(vector<int>& nums, int target) {
- int left = 0;
- int right = nums.size() - 1;
- int rightBorder = -2; // 記錄一下rightBorder沒有被賦值的情況
- while (left <= right) {
- int middle = left + ((right - left) / 2);
- if (nums[middle] > target) {
- right = middle - 1;
- } else { // 尋找右邊界,nums[middle] == target的時候更新left
- left = middle + 1;
- rightBorder = left;
- }
- }
- return rightBorder;
- }
- int getLeftBorder(vector<int>& nums, int target) {
- int left = 0;
- int right = nums.size() - 1;
- int leftBorder = -2; // 記錄一下leftBorder沒有被賦值的情況
- while (left <= right) {
- int middle = left + ((right - left) / 2);
- if (nums[middle] >= target) { // 尋找左邊界,nums[middle] == target的時候更新right
- right = middle - 1;
- leftBorder = right;
- } else {
- left = middle + 1;
- }
- }
- return leftBorder;
- }
- };
這份代碼在簡潔性很有大的優(yōu)化空間,例如把尋找左右區(qū)間函數(shù)合并一起。
但拆開更清晰一些,而且把三種情況以及對應(yīng)的處理邏輯完整的展現(xiàn)出來了。
總結(jié)
初學(xué)者建議大家一塊一塊的去分拆這道題目,正如本題解描述,想清楚三種情況之后,先專注于尋找右區(qū)間,然后專注于尋找左區(qū)間,左右根據(jù)左右區(qū)間做最后判斷。
不要上來就想如果一起尋找左右區(qū)間,搞著搞著就會顧此失彼,繞進(jìn)去拔不出來了。
其他語言版本
Java
- class Solution {
- int[] searchRange(int[] nums, int target) {
- int leftBorder = getLeftBorder(nums, target);
- int rightBorder = getRightBorder(nums, target);
- // 情況一
- if (leftBorder == -2 || rightBorder == -2) return new int[]{-1, -1};
- // 情況三
- if (rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1};
- // 情況二
- return new int[]{-1, -1};
- }
- int getRightBorder(int[] nums, int target) {
- int left = 0;
- int right = nums.length - 1;
- int rightBorder = -2; // 記錄一下rightBorder沒有被賦值的情況
- while (left <= right) {
- int middle = left + ((right - left) / 2);
- if (nums[middle] > target) {
- right = middle - 1;
- } else { // 尋找右邊界,nums[middle] == target的時候更新left
- left = middle + 1;
- rightBorder = left;
- }
- }
- return rightBorder;
- }
- int getLeftBorder(int[] nums, int target) {
- int left = 0;
- int right = nums.length - 1;
- int leftBorder = -2; // 記錄一下leftBorder沒有被賦值的情況
- while (left <= right) {
- int middle = left + ((right - left) / 2);
- if (nums[middle] >= target) { // 尋找左邊界,nums[middle] == target的時候更新right
- right = middle - 1;
- leftBorder = right;
- } else {
- left = middle + 1;
- }
- }
- return leftBorder;
- }
- }
- // 解法2
- // 1、首先,在 nums 數(shù)組中二分查找 target;
- // 2、如果二分查找失敗,則 binarySearch 返回 -1,表明 nums 中沒有 target。此時,searchRange 直接返回 {-1, -1};
- // 3、如果二分查找成功,則 binarySearch 返回 nums 中值為 target 的一個下標(biāo)。然后,通過左右滑動指針,來找到符合題意的區(qū)間
- class Solution {
- public int[] searchRange(int[] nums, int target) {
- int index = binarySearch(nums, target); // 二分查找
- if (index == -1) { // nums 中不存在 target,直接返回 {-1, -1}
- return new int[] {-1, -1}; // 匿名數(shù)組
- }
- // nums 中存在 targe,則左右滑動指針,來找到符合題意的區(qū)間
- int left = index;
- int right = index;
- // 向左滑動,找左邊界
- while (left - 1 >= 0 && nums[left - 1] == nums[index]) { // 防止數(shù)組越界。邏輯短路,兩個條件順序不能換
- left--;
- }
- // 向左滑動,找右邊界
- while (right + 1 < nums.length && nums[right + 1] == nums[index]) { // 防止數(shù)組越界。
- right++;
- }
- return new int[] {left, right};
- }
- /**
- * 二分查找
- * @param nums
- * @param target
- */
- public int binarySearch(int[] nums, int target) {
- int left = 0;
- int right = nums.length - 1; // 不變量:左閉右閉區(qū)間
- while (left <= right) { // 不變量:左閉右閉區(qū)間
- int mid = left + (right - left) / 2;
- if (nums[mid] == target) {
- return mid;
- } else if (nums[mid] < target) {
- left = mid + 1;
- } else {
- right = mid - 1; // 不變量:左閉右閉區(qū)間
- }
- }
- return -1; // 不存在
- }
- }
Python
- class Solution:
- def searchRange(self, nums: List[int], target: int) -> List[int]:
- def getRightBorder(nums:List[int], target:int) -> int:
- left, right = 0, len(nums)-1
- rightBoder = -2 # 記錄一下rightBorder沒有被賦值的情況
- while left <= right:
- middle = left + (right-left) // 2
- if nums[middle] > target:
- right = middle - 1
- else: # 尋找右邊界,nums[middle] == target的時候更新left
- left = middle + 1
- rightBoder = left
- return rightBoder
- def getLeftBorder(nums:List[int], target:int) -> int:
- left, right = 0, len(nums)-1
- leftBoder = -2 # 記錄一下leftBorder沒有被賦值的情況
- while left <= right:
- middle = left + (right-left) // 2
- if nums[middle] >= target: # 尋找左邊界,nums[middle] == target的時候更新right
- right = middle - 1;
- leftBoder = right;
- else:
- left = middle + 1
- return leftBoder
- leftBoder = getLeftBorder(nums, target)
- rightBoder = getRightBorder(nums, target)
- # 情況一
- if leftBoder == -2 or rightBoder == -2: return [-1, -1]
- # 情況三
- if rightBoder -leftBoder >1: return [leftBoder + 1, rightBoder - 1]
- # 情況二
- return [-1, -1]
- # 解法2
- # 1、首先,在 nums 數(shù)組中二分查找 target;
- # 2、如果二分查找失敗,則 binarySearch 返回 -1,表明 nums 中沒有 target。此時,searchRange 直接返回 {-1, -1};
- # 3、如果二分查找成功,則 binarySearch 返回 nums 中值為 target 的一個下標(biāo)。然后,通過左右滑動指針,來找到符合題意的區(qū)間
- class Solution:
- def searchRange(self, nums: List[int], target: int) -> List[int]:
- def binarySearch(nums:List[int], target:int) -> int:
- left, right = 0, len(nums)-1
- while left<=right: # 不變量:左閉右閉區(qū)間
- middle = left + (right-left) // 2
- if nums[middle] > target:
- right = middle - 1
- elif nums[middle] < target:
- left = middle + 1
- else:
- return middle
- return -1
- index = binarySearch(nums, target)
- if index == -1:return [-1, -1] # nums 中不存在 target,直接返回 {-1, -1}
- # nums 中存在 targe,則左右滑動指針,來找到符合題意的區(qū)間
- left, right = index, index
- # 向左滑動,找左邊界
- while left -1 >=0 and nums[left - 1] == target: left -=1
- # 向右滑動,找右邊界
- while right+1 < len(nums) and nums[right + 1] == target: right +=1
- return [left, right]
- # 解法3
- # 1、首先,在 nums 數(shù)組中二分查找得到第一個大于等于 target的下標(biāo)(左邊界)與第一個大于target的下標(biāo)(右邊界);
- # 2、如果左邊界<= 右邊界,則返回 [左邊界, 右邊界]。否則返回[-1, -1]
- class Solution:
- def searchRange(self, nums: List[int], target: int) -> List[int]:
- def binarySearch(nums:List[int], target:int, lower:bool) -> int:
- left, right = 0, len(nums)-1
- ans = len(nums)
- while left<=right: # 不變量:左閉右閉區(qū)間
- middle = left + (right-left) //2
- # lower為True,執(zhí)行前半部分,找到第一個大于等于 target的下標(biāo) ,否則找到第一個大于target的下標(biāo)
- if nums[middle] > target or (lower and nums[middle] >= target):
- right = middle - 1
- ans = middle
- else:
- left = middle + 1
- return ans
- leftBorder = binarySearch(nums, target, True) # 搜索左邊界
- rightBorder = binarySearch(nums, target, False) -1 # 搜索右邊界
- if leftBorder<= rightBorder and rightBorder< len(nums) and nums[leftBorder] == target and nums[rightBorder] == target:
- return [leftBorder, rightBorder]
- return [-1, -1]
- # 解法4
- # 1、首先,在 nums 數(shù)組中二分查找得到第一個大于等于 target的下標(biāo)leftBorder;
- # 2、在 nums 數(shù)組中二分查找得到第一個大于等于 target+1的下標(biāo), 減1則得到rightBorder;
- # 3、如果開始位置在數(shù)組的右邊或者不存在target,則返回[-1, -1] 。否則返回[leftBorder, rightBorder]
- class Solution:
- def searchRange(self, nums: List[int], target: int) -> List[int]:
- def binarySearch(nums:List[int], target:int) -> int:
- left, right = 0, len(nums)-1
- while left<=right: # 不變量:左閉右閉區(qū)間
- middle = left + (right-left) //2
- if nums[middle] >= target:
- right = middle - 1
- else:
- left = middle + 1
- return left # 若存在target,則返回第一個等于target的值
- leftBorder = binarySearch(nums, target) # 搜索左邊界
- rightBorder = binarySearch(nums, target+1) -1 # 搜索右邊界
- if leftBorder == len(nums) or nums[leftBorder]!= target: # 情況一和情況二
- return [-1, -1]
- return [leftBorder, rightBorder]