示例 1:
輸入: nums = [4,5,6,7,0,1,2], target = 0
輸出: 4
示例 2:
輸入: nums = [4,5,6,7,0,1,2], target = 3
輸出: -1
問題分析
首先個人認為題目中的“旋轉”可能並不直觀,不利於理解,在這里旋轉也可以認為是數組向右循環移動,何為循環移動,看下面
1, 2, 3, 4, 5 循環向右移動一位 為 5, 1, 2, 3, 4
1, 2, 3, 4, 5 循環向右移動兩位 為 4, 5, 1, 2, 3
題目要求時間復雜度O(logN),而且數組是排序的,肯定是二分法了。但是這個數組卻並不是一個完全遞增的數組,他的特點是
- 數組中存在一個位置index,A = [0, index)遞增序列,B = [index, size)是一個遞增序列
- 遞增序列A中的任意一個元素均大於遞增序列B的任意一個元素,用數學表示的話就是
對於特點2可能不是很明顯,理由如下:
- 題中的旋轉可以認為進行了X次的向右循環移動
- 每次向右循環移動都會將最后一位數移動到最左邊
- 根據 特點1 和 理由2 可知每次被移動到左邊的數字為序列B中最大的元素,移動過后這個元素進入了序列A的最左邊,也就是說這個元素成為了序列A中最小的元素
- 根據上述三條理由確定一個在每次循環右移之后都成立的條件:
每次循環右移到數組最左邊的元素都是大於遞增序列B的任何一個數的,且這個元素是遞增序列A中最小的一個元素
或者可以表述為
每次循環右移到遞增序列A最左邊的元素都是大於遞增序列B中的任何一個元素,且這個元素是遞增序列A中最小的一個元素
- 所以進行了X次向右循環移動的數組依舊滿足 理由4 的結論,所以 特點2 成立
解法一:數組中的最小值,對左右兩邊各進行一次二分查找
很明顯,數組的最小值就是遞增序列B的第一項。最小值的左邊(不包括旋最小值)是一個遞增序列,最小值的右邊(包括最小值)是一個遞增數列。
不是很清楚的讀者可以在下圖,紅線圈住的值就是最小值,也可以很輕易地看出上述規律。
由於題目要求時間復雜度O(lgn),所以我們必須在O(lgn)的時間內找到最小值,具體如何找呢?肯定還是二分搜索的思想,不過要分情況考慮了。
當 nums[mid] > nums[0] 時
應該是下圖所示的情況,很明顯,最小是在二分點的右邊,所以應該是移動左指針,即
left = mid + 1;
當 nums[mid] < nums[0] 時
應該是下圖所示的情況,很明顯,最小值在二分點的左邊,應該移動右指針,即
right = mid - 1;
不過我們少考慮的一種情況,也就是如果整個數組是一個完全遞增的數組,也就是並沒有經過任何變化,那么上述方法的到的最終結果就是會出出錯,所以在二分結果后需要再做一個判斷,判斷數組的第一位和二分查找得到的結果那個小,即
if nums[0] < nums[mid]
minIndex = 0;
else
minIndex = mid;
所以尋找旋轉點的最終偽代碼為:
findMin(nums)
// nums is a array
left = 0;
right = nums.length - 1;
while left <= right
mid = (left + right) / 2;
// 判斷是否是最小值,也就是左右都比它大
if nums[mid] is smaller than left and right values
break;
else if nums[mid] > nums[0]
left = mid + 1;
else
right = mid - 1;
if nums[0] < nums[mid]
minIndex = 0;
else
minIndex = mid;
return midIndex;
現在我們找到了最小值了,就可以以最小值為分界點分別對左右兩個遞增序列進行二分查找了,所以整個程序的偽碼如下:
search(nums,target)
/*
nums is a array
target is a number
*/
minIndex = findMin(nums);
left = 0;
right = minIndex - 1;
while left <= right
mid = (left + right) / 2;
if nums[mid] == target
return mid;
else if target < nums[mid]
right = mid - 1;
else
left = mid + 1;
left = minIndex;
right = nums.length - 1;
while left <= right
mid = (left + right) / 2;
if nums[mid] == target
return mid;
else if target < nums[mid]
right = mid - 1;
else
left = mid + 1;
時空復雜度分析
找出旋轉點O(lgn),兩次二分查找均為O(lgn),算法總時間復雜度為O(lgn)
算法使用了常數空間,空間復雜度為O(1)
解法二:直接通過二分法找出目標值
這里會稍微復雜一些,依舊先分情況討論
當 nums[mid] > nums[0] 且 nums[mid] < target
從圖上來看,二分點在目標值的左邊,所以應該移動左指針,即
left = mid + 1;
當 nums[mid] > nums[0] 且 nums[mid] > target 且 nums[0] < target
從圖上看,二分點在目標值右邊,所以應該移動右指針,即
right = mid - 1;
當 nums[mid] > nums[0] 且 nums[mid] > target 且 nums[0] > target
從圖上來看,二分點在目標值的左邊,應該移動左指針,即
left = mid + 1;
當 nums[mid] < nums[0] 且 nums[mid] > target
從圖上來看,二分點在目標值的右邊,應該移動右指針,即
right = mid - 1;
當 nums[mid] < nums[0] 且 nums[mid] < target 且 nums[0] > target
從圖上來看,二分點在目標值的左邊,應該移動左指針,即
left = mid + 1;
當 nums[mid] < nums[0] 且 nums[mid] < target 且 nums[0] < target
從圖上來看,二分點在目標值的右邊,應該移動右指針,即
right = mid - 1;
當 nums[mid] = nums[0]
從圖上來看,目標值肯定在二分點的右邊(暫不考慮目標值在最左端的情況),應該移動左指針,即
left = mid + 1;
當目標值位於數組兩端
在程序最開始時做判斷即可
所以整個程序的偽代碼應該為:
search(nums, target)
/*
nums is a array
target is a number
*/
left = 0;
right = nums.length - 1;
if nums[0] == target
return 0;
if nums[nums.length - 1] == target
return nums.length - 1;
while left <= right
mid = (left + right) / 2;
if nums[mid] == target
return mid;
else
if nums[mid] > nums[0]
if target > nums[mid]
left = mid + 1;
else
if target > nums[0]
right = mid - 1;
else
left = mid + 1;
else if nums[mid] < nums[0]
if target < nums[mid]
right = mid - 1;
else
if target < nums[nums.size() - 1]
left = mid + 1;
else
right = mid - 1;
else
left = mid + 1;
return -1;
時空復雜度分析
時間復雜度為O(lgn),空間復雜度O(1)