墮落了一天,那麼接着來刷leetcode,今天作的一題不算複雜,題目來自leetcode153. 尋找旋轉排序數組中的最小值,題目描述以下:javascript
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。html
( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。java
請找出其中最小的元素。算法
你能夠假設數組中不存在重複元素。數組
示例 1:app
輸入: [3,4,5,1,2] 輸出: 1示例 2:.net
輸入: [4,5,6,7,0,1,2] 輸出: 0
在以前JS leetcode 旋轉數組 題解分析一文中,咱們已經作過旋轉數組的題目,因此這裏所說的旋轉其實只是將數組尾部的元素依次加入數組頭部的操做。咱們簡單分析題目,再說怎麼實現。code
由題目提供的信息可知,數組爲升序排列數組,並且在某個未知的點進行了旋轉,因此它可能沒轉。htm
不過但站在找出數組中最小元素來講,咱們能夠不考慮這些條件,直接祭出Math.min()
大法:blog
/** * @param {number[]} nums * @return {number} */ var findMin = function(nums) { return Math.min(...nums); //ES5 //return Math.min.apply(null, nums); };
因爲數組本來就是排序好的,只是可能進行了旋轉,咱們能夠可恥的將數組再排序而後取出索引0位元素:
/** * @param {number[]} nums * @return {number} */ var findMin = function (nums) { return nums.sort((a, b) => a - b)[0]; };
雖然能達到目的,總以爲差了點意思,提交後發現執行時間排名也很低,說明有更好的作法。
在看了官方的解題思路後,發現本題用二分法查找會更好,不過對於我來講,二分法是啥都比較疑惑,因此這裏先簡單說說什麼是二分法。
好比,咱們要在數組[1,2,3,4,5,6,7,8,9]
裏面找到目標9的下標,按照常規遍歷,咱們只有從頭遍歷到最後一位才能找到,時間複雜度爲O(n),使用二分法就不同,以下:
/** * @desc 二分法查找目標元素索引 * @param {*} arr 數組 * @param {*} target 目標元素 */ function binarySearch(arr, target) { // 數組起始索引 var low = 0; // 數組最後一項索引 var high = arr.length - 1; while (low <= high) { // 獲取數組中間項索引 var mid = Math.floor((low + high) / 2); if (target === arr[mid]) { // 返回目標元素下標 return mid; // 根據比較來決定下次查找的數組應該是左仍是右 } else if (target > arr[mid]) { low = mid + 1; } else { high = mid - 1; }; }; // 沒找到返回-1 return -1; }; binarySearch([1, 2, 3, 4, 5, 6, 7, 8, 9], 9); //8
咱們一開始就能夠按照某個特定規則找到數組中的中間項元素,從而將數組一分爲二,而後將中間項與目標元素比較,好比一開始咱們找到了5,因爲比目標元素9小,因此9確定在5右邊的數組中。
那麼下次遍歷就從[6,7,8,9]
開始,此次中間元素找到7,仍然比9小,繼續上述操做,又在[8,9]
中尋找。直到最後找到目標元素9,從而獲取到索引。
你看,這樣對半分的查找,是否是比咱們常規從頭至尾的遍歷要快不少,二分法的時間複雜度爲O(logn),注意,二分法適合有序序列,否則咱們也不知道下次應該去左邊仍是右邊比較了。
前面說了,二分法適合有序數組,這樣咱們纔好根據特定條件來決定下次應該從哪邊開始,好比上文中的target > arr[mid]
。而本題雖然也是有序,但很遺憾的是數組通過了旋轉操做,因此通常的二分法並不適用。
比較麻煩的是,雖然題目說了作了旋轉,不保證數組旋轉了一整圈結果並沒有變化的狀況。因此第一步咱們能夠先判斷數組到底有沒有被旋轉,判斷條件很簡單
已知數組是有序數組,若是數組發生了旋轉,那麼數組第一位必定大於最後一位,反之若是數組未發生旋轉,那麼第一位必定小於最後一位,這種狀況咱們直接返回第一位便可。
那若是數組已發生了旋轉,咱們又該怎麼判斷呢,其實有一個這樣的特色:
縱觀整個數組,咱們以相鄰兩個元素來看,假設當前中間元素爲mid,若是arr[mid-1]
大於arr[mid]
,那麼mid爲咱們想要的元素。或者arr[mid]
大於arr[mid+1]
,那麼此時mid+1是咱們想要找的元素。爲啥這麼說,由於數組雖然旋轉了,但相鄰且知足如上任意條件之一的狀況只存在一次,不信你們隨便寫個有序數組看。
因此每次肯定中間元素,咱們都得走以下條件,只要知足其一便是咱們想要找的元素。
if (nums[mid] > nums[mid + 1]) { return nums[mid + 1]; }; if (nums[mid - 1] > nums[mid]) { return nums[mid]; };
這是找到目標元素的條件,那不知足咱們如何知道應該查找左邊數組仍是右邊數組呢?其實有這樣一個規律:
如上圖,咱們將9=>1之間稱爲變化點,變化點左邊的元素都必定比數組第一位元素大,變化點右邊的全部元素,必定比數組第一位元素小。
因此找到中間元素mid,若是mid比第一位還大,那說明咱們應該去數組右邊找,若是mid比第一位還小,那麼應該去左邊找。
你可能在想,萬一我這個mid第一次就是最小元素咋辦,若是運氣真這麼好,它早就被咱們上面定的兩個條件之一給返回了,能走到這一步說明這個mid必定不是咱們想找的目標元素,這纔要分去哪邊找啊。
那麼咱們上代碼:
/** * @param {number[]} nums * @return {number} */ var findMin = function(nums) { // 假設數組只有一項,直接返回 if (nums.length == 1) { return nums[0]; }; var left = 0, right = nums.length - 1; // 假設數組最後一項大於第一項,說明數組未旋轉,直接返回 if (nums[right] > nums[0]) { return nums[0]; }; // 既然能走到這一步,那說明數組必定旋轉了,套用以前的規則,使用二分法進行查找 while (right >= left) { // Find the mid element var mid = Math.floor((left + right) / 2); // 知足以下條件之一說明就是最小元素,直接返回便可 if (nums[mid] > nums[mid + 1]) { return nums[mid + 1]; }; if (nums[mid - 1] > nums[mid]) { return nums[mid]; }; // 比較當前中間元素與第一位 if (nums[mid] > nums[0]) { // 若是要大,那就去右邊找 left = mid + 1; } else { // 反之就去左邊找 right = mid - 1; }; }; return -1; };
執行圖解以下:
其實這段代碼我分析了好久,對於我以爲比較難的是什麼狀況返回mid,我以爲mid若是是最小,它必定比mid+1小,但這樣是不成立的,例如1比2小,2也比3小,這個條件無法用。
因此官方分析我以爲讓我佩服的是,整個數組中,當mid是1時,mid-1必定比mid大,整個數組你找不出第二個這樣的狀況。同理,當mid是9時,mid必定比mid+1大,你一樣找不出第二個這樣的狀況...
因此變化點相關的兩個元素9和1纔是解題關鍵。
那麼關於本題就分析到這裏了。
另外,二分法參考以下: