在演算法面試中,面試官總是喜歡圍繞連結串列、排序、二叉樹、二分查詢來做文章,而大多數人都可以跟著專業的書籍來做到倒背如流。而面試官並不希望招收的是一位記憶功底很好,但不會活學活用的程式設計師。所以學會數學建模和分析問題,並用合理的演算法或資料結構來解決問題相當重要。
面試題:列印出旋轉陣列的最小數字
題目:把一個陣列最開始的若干個元素搬到陣列的末尾,我們稱之為陣列的旋轉。輸入一個遞增排序的陣列的一個旋轉,輸出旋轉陣列的最小元素。例如陣列 {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 - low) / 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 原生態基本都封裝了常用演算法,對面試這些無作用的演算法表示抗議,其實這是相當愚蠢的。我們不求死記硬背演算法的實現,但求學習到其中巧妙的思想。只有不斷地提升自己的思維能力,才能助自己收穫更好的職業發展。
這也大概是大家一直到處叫大佬,埋怨自己工資總是跟不上別人的一方面原因吧。
我是南塵,只做比心的公眾號,歡迎關注我。
做不完的開源,寫不完的矯情。歡迎掃描下方二維碼或者公眾號搜尋「nanchen」關注我的微信公眾號,目前多運營 Android ,儘自己所能為你提升。如果你喜歡,為我點贊分享吧~