面試:Java 實現查詢旋轉陣列的最小數字
在演算法面試中,面試官總是喜歡圍繞連結串列、排序、二叉樹、二分查詢來做文章,而大多數人都可以跟著專業的書籍來做到倒背如流。而面試官並不希望招收的是一位記憶功底很好,但不會活學活用的程式設計師。所以學會數學建模和分析問題,並用合理的演算法或資料結構來解決問題相當重要。
面試題:列印出旋轉陣列的最小數字
題目:把一個陣列最開始的若干個元素搬到陣列的末尾,我們稱之為陣列的旋轉。輸入一個遞增排序的陣列的一個旋轉,輸出旋轉陣列的最小元素。例如陣列 {3,4,5,1,2} 為陣列 {1,2,3,4,5} 的一個旋轉,該陣列的最小值為 1。
要想實現這個需求很簡單,我們只需要遍歷一遍陣列,找到最小的值後直接退出迴圈。程式碼實現如下:
public class Test08 {
public static int getTheMin(int nums[]) {
if (nums == null || nums.length == 0) {
throw new RuntimeException("input error!");
}
int result = nums[0];
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i + 1] < nums[i]) {
result = nums[i + 1];
break;
}
}
return result;
}
public static void main(String[] args) {
// 典型輸入,單調升序的陣列的一個旋轉
int[] array1 = {3, 4, 5, 1, 2};
System.out.println(getTheMin(array1));
// 有重複數字,並且重複的數字剛好的最小的數字
int[] array2 = {3, 4, 5, 1, 1, 2};
System.out.println(getTheMin(array2));
// 有重複數字,但重複的數字不是第一個數字和最後一個數字
int[] array3 = {3, 4, 5, 1, 2, 2};
System.out.println(getTheMin(array3));
// 有重複的數字,並且重複的數字剛好是第一個數字和最後一個數字
int[] array4 = {1, 0, 1, 1, 1};
System.out.println(getTheMin(array4));
// 單調升序陣列,旋轉0個元素,也就是單調升序陣列本身
int[] array5 = {1, 2, 3, 4, 5};
System.out.println(getTheMin(array5));
// 陣列中只有一個數字
int[] array6 = {2};
System.out.println(getTheMin(array6));
// 陣列中數字都相同
int[] array7 = {1, 1, 1, 1, 1, 1, 1};
System.out.println(getTheMin(array7));
}
}
列印結果沒什麼毛病。不過這樣的方法顯然不是最優的,我們看看有沒有辦法找出更加優質的方法處理。
有序,還要查詢?
找到這兩個關鍵字,我們不免會想到我們的二分查詢法,但不少小夥伴肯定會問,我們這個陣列旋轉後已經不是一個真正的有序陣列了,不過倒像是兩個遞增的陣列組合而成的,我們可以這樣思考。
我們可以設定兩個下標 low 和 high,並設定 mid = (low + high)/2,我們自然就可以找到陣列中間的元素 array[mid],如果中間的元素位於前面的遞增陣列,那麼它應該大於或者等於 low 下標對應的元素,此時陣列中最小的元素應該位於該元素的後面,我們可以把 low 下標指向該中間元素,這樣可以縮小查詢的範圍。
同樣,如果中間元素位於後面的遞增子陣列,那麼它應該小於或者等於 high 下標對應的元素。此時該陣列中最小的元素應該位於該中間元素的前面。我們就可以把 high 下標更新到中位數的下標,這樣也可以縮小查詢的範圍,移動之後的 high 下標對應的元素仍然在後面的遞增子陣列中。
不管是更新 low 還是 high,我們的查詢範圍都會縮小為原來的一半,接下來我們再用更新的下標去重複新一輪的查詢。直到最後兩個下標相鄰,也就是我們的迴圈結束條件。
說了一堆,似乎已經繞的雲裡霧裡了,我們不妨就拿題幹中的這個輸入來模擬驗證一下我們的演算法。
- input:{3,4,5,1,2}
- 此時 low = 0,high = 4,mid = 2,對應的值分別是:num[low] = 3,num[high] = 2,num[mid] = 5
- 由於 num[mid] > num[low],所以 num[mid] 應該是在左邊的遞增子陣列中。
- 更新 low = mid = 2,num[low] = 5,mid = (low+high)/2 = 3,num[mid] = 1;
- high - low ≠ 1 ,繼續更新
- 由於 num[mid] < num[high],所以斷定 num[mid] = 1 位於右邊的自增子陣列中;
- 更新 high = mid = 3,由於 high - mid = 1,所以結束迴圈,得到最小值 num[high] = 1;
我們再來看看 Java 中如何用程式碼實現這個思路:
public class Test08 {
public static int getTheMin(int nums[]) {
if (nums == null || nums.length == 0) {
throw new RuntimeException("input error!");
}
// 如果只有一個元素,直接返回
if (nums.length == 1)
return nums[0];
int result = nums[0];
int low = 0, high = nums.length - 1;
int mid;
// 確保 low 下標對應的值在左邊的遞增子陣列,high 對應的值在右邊遞增子陣列
while (nums[low] >= nums[high]) {
// 確保迴圈結束條件
if (high - low == 1) {
return nums[high];
}
// 取中間位置
mid = (low + high) / 2;
// 代表中間元素在左邊遞增子陣列
if (nums[mid] >= nums[low]) {
low = mid;
} else {
high = mid;
}
}
return result;
}
public static void main(String[] args) {
// 典型輸入,單調升序的陣列的一個旋轉
int[] array1 = {3, 4, 5, 1, 2};
System.out.println(getTheMin(array1));
// 有重複數字,並且重複的數字剛好的最小的數字
int[] array2 = {3, 4, 5, 1, 1, 2};
System.out.println(getTheMin(array2));
// 有重複數字,但重複的數字不是第一個數字和最後一個數字
int[] array3 = {3, 4, 5, 1, 2, 2};
System.out.println(getTheMin(array3));
// 有重複的數字,並且重複的數字剛好是第一個數字和最後一個數字
int[] array4 = {1, 0, 1, 1, 1};
System.out.println(getTheMin(array4));
// 單調升序陣列,旋轉0個元素,也就是單調升序陣列本身
int[] array5 = {1, 2, 3, 4, 5};
System.out.println(getTheMin(array5));
// 陣列中只有一個數字
int[] array6 = {2};
System.out.println(getTheMin(array6));
// 陣列中數字都相同
int[] array7 = {1, 1, 1, 1, 1, 1, 1};
System.out.println(getTheMin(array7));
// 特殊的不知道如何移動
int[] array8 = {1, 0, 1, 1, 1};
System.out.println(getTheMin(array8));
}
}
前面我們提到在旋轉陣列中,由於是把遞增排序陣列的前面的若干個數字搬到陣列後面,因為第一個數字總是大於或者等於最後一個數字,而還有一種特殊情況是移動了 0 個元素,即陣列本身,也是它自己的旋轉陣列。這種情況本身陣列就是有序的了,所以我們只需要返回第一個元素就好了,這也是為什麼我先給 result 賦值為 nums[0] 的原因。
上述程式碼就完美了嗎?我們通過測試用例並沒有達到我們的要求,我們具體看看 array8 這個輸入。先模擬計算機執行分析一下:
- low = 0, high = 4, mid = 2, nums[low] = 1, nums[high] = 1,nums[mid] = 1;
- 由於 nums[mid] >= nums[low],故認定 nums[mid] = 1 在左邊遞增子陣列中;
- 所以更新 high = mid = 2,mid = (low+high)/2 = 1;
- nums[low] = 1,nums[mid] = 1,nums[high] = 1;
- high - low ≠ 1,繼續迴圈;
- 由於 nums[mid] >= nums[low],故認定 nums[mid] = 1 在左邊遞增子陣列中;
- 所以更新 high = mid = 1,由於 high - low = 1,故退出迴圈,得到 result = 1;
但我們一眼瞭然,明顯我們的最小值不是 1 ,而是 0 ,所以當 array[low]、array[mid]、array[high] 相等的時候,我們的程式並不知道應該如何移動,按照目前的移動方式就預設 array[mid] 在左邊遞增子陣列了,這顯然是不負責任的做法。
我們修正一下程式碼:
public class Test08 {
public static int getTheMin(int nums[]) {
if (nums == null || nums.length == 0) {
throw new RuntimeException("input error!");
}
// 如果只有一個元素,直接返回
if (nums.length == 1)
return nums[0];
int result = nums[0];
int low = 0, high = nums.length - 1;
int mid = low;
// 確保 low 下標對應的值在左邊的遞增子陣列,high 對應的值在右邊遞增子陣列
while (nums[low] >= nums[high]) {
// 確保迴圈結束條件
if (high - low == 1) {
return nums[high];
}
// 取中間位置
mid = (low + high) / 2;
// 三值相等的特殊情況,則需要從頭到尾查詢最小的值
if (nums[mid] == nums[low] && nums[mid] == nums[high]) {
return midInorder(nums, low, high);
}
// 代表中間元素在左邊遞增子陣列
if (nums[mid] >= nums[low]) {
low = mid;
} else {
high = mid;
}
}
return result;
}
/**
* 查詢陣列中的最小值
*
* @param nums 陣列
* @param start 陣列開始位置
* @param end 陣列結束位置
* @return 找到的最小的數字
*/
public static int midInorder(int[] nums, int start, int end) {
int result = nums[start];
for (int i = start + 1; i <= end; i++) {
if (result > nums[i])
result = nums[i];
}
return result;
}
public static void main(String[] args) {
// 典型輸入,單調升序的陣列的一個旋轉
int[] array1 = {3, 4, 5, 1, 2};
System.out.println(getTheMin(array1));
// 有重複數字,並且重複的數字剛好的最小的數字
int[] array2 = {3, 4, 5, 1, 1, 2};
System.out.println(getTheMin(array2));
// 有重複數字,但重複的數字不是第一個數字和最後一個數字
int[] array3 = {3, 4, 5, 1, 2, 2};
System.out.println(getTheMin(array3));
// 有重複的數字,並且重複的數字剛好是第一個數字和最後一個數字
int[] array4 = {1, 0, 1, 1, 1};
System.out.println(getTheMin(array4));
// 單調升序陣列,旋轉0個元素,也就是單調升序陣列本身
int[] array5 = {1, 2, 3, 4, 5};
System.out.println(getTheMin(array5));
// 陣列中只有一個數字
int[] array6 = {2};
System.out.println(getTheMin(array6));
// 陣列中數字都相同
int[] array7 = {1, 1, 1, 1, 1, 1, 1};
System.out.println(getTheMin(array7));
// 特殊的不知道如何移動
int[] array8 = {1, 0, 1, 1, 1};
System.out.println(getTheMin(array8));
}
}
我們再用完善的測試用例放進去,測試通過。
總結
本題其實考察的點挺多的,實際上就是考察對二分查詢的靈活運用,不少小夥伴死記硬背二分查詢必須遵從有序,而沒有學會這個二分查詢的思想,這樣會導致只能想到迴圈查詢最小值了。
不少小夥伴在面試中表態,Android 原生態基本都封裝了常用演算法,對面試這些無作用的演算法表示抗議,其實這是相當愚蠢的。我們不求死記硬背演算法的實現,但求學習到其中巧妙的思想。只有不斷地提升自己的思維能力,才能助自己收穫更好的職業發展。
這也大概是大家一直到處叫大佬,埋怨自己工資總是跟不上別人的一方面原因吧。
相關文章
- 0二分查詢簡單 牛客NC.71旋轉陣列的最小數字 leetcode劍指 Offer 11. 旋轉陣列的最小數字陣列LeetCode
- 劍指offer:旋轉陣列的最小數字陣列
- 劍指offer 旋轉陣列的最小數字陣列
- 劍指offer面試題11:旋轉陣列的最小數字(Java版已在牛客網AC)面試題陣列Java
- 【劍指offer】7.旋轉陣列的最小數字陣列
- 【劍指 Offer】11. 旋轉陣列的最小數字陣列
- 劍指 Offer 11. 旋轉陣列的最小數字陣列
- 我請大家來刷題:旋轉陣列的最小數字陣列
- 旋轉陣列中的最小元素陣列
- (python版)《劍指Offer》JZ06:旋轉陣列的最小數字Python陣列
- 第二章 :查詢與排序-------2.17解題實戰_旋轉陣列的最小數字(改造二分法)排序陣列
- 劍指offer-轉陣列的最小數字-php陣列PHP
- 查詢陣列中出現次數大於陣列長度一半的數字陣列
- 153. 尋找旋轉排序陣列中的最小值排序陣列
- 面試最常問的陣列轉樹,樹轉陣列 c++ web框架paozhu實現面試陣列C++Web框架
- 153. 尋找旋轉排序陣列中的最小值(中)排序陣列
- LeetCode-153-尋找旋轉排序陣列中的最小值LeetCode排序陣列
- 【LeetCode】153. 尋找旋轉排序陣列中的最小值LeetCode排序陣列
- 【LeetCode(Java) - 33】搜尋旋轉排序陣列LeetCodeJava排序陣列
- Java中查詢陣列多數元素的4種方法Java陣列
- LeetCode 189 旋轉陣列LeetCode陣列
- 陣列中未出現的最小正整數陣列
- Python3實現旋轉陣列的3種演算法Python陣列演算法
- 每日一練(24):在排序陣列中查詢數字排序陣列
- LeetCodeHot100 二分查詢 35. 搜尋插入位置 74. 搜尋二維矩陣 34. 在排序陣列中查詢元素的第一個和最後一個位置 33. 搜尋旋轉排序陣列 153. 尋找旋轉排序陣列中的最小值LeetCode矩陣排序陣列
- 【LeetCode】189. 旋轉陣列LeetCode陣列
- LeetCode-189-旋轉陣列LeetCode陣列
- LC 189. 旋轉陣列陣列
- Java實現普通二維陣列和稀疏陣列的相互轉換Java陣列
- 【Java】陣列二分查詢元素Java陣列
- 【LeetCode-陣列】查詢大多數元素LeetCode陣列
- 陣列的主元素查詢陣列
- 如何查詢總和等於給定數字的整數陣列中的所有對陣列
- 劍指OFFER-數字在升序陣列中出現的次數(Java)陣列Java
- LeetCode 33——搜尋旋轉排序陣列LeetCode排序陣列
- 二維陣列查詢陣列
- 二維陣列中的查詢陣列
- 資料轉換-整數字節陣列陣列