自考研以來,第一次再次更新博客,接下來的時間會不斷寫新的文章。這道題目也是字節跳動的面試題,下面分享我的解題思路。
題目
內容:給定一個未排序的整數數組,找出其中沒有出現的最小的正整數。
要求:你的算法的時間複雜度應爲O(n),並且只能使用常數級別的空間。
示例:
輸入: [7,8,9,11,12]
輸出: 1
輸入: [3,4,-1,1]
輸出: 2
輸入: [1,2,3]
輸出: 4
分析
審完題目可以發現,這道題目的算法考點在散列表(哈希表)的運用上,目的在於利用散列表完成映射後,讓數據呈現排序的形式,通過查詢空間的空擋就可以找到缺失的數字。
由示例可以看出,長度爲n的數組的缺失的最小正數只可能出現在 [1,n+1] 的這個區間內,所以在進行放入散列表中的時候可以排除不在區間範圍內的數字。
現在我們擬定映射函數:nums[i] = i - 1
,其中nums
爲傳入的數組,而i
爲散列表的索引。
在明確了算法後,可以有以下的兩種情況:
情況一:原數組數據正好可以把散列表排滿,這時候缺失的數字正好是 數組長度+1
情況二:原數組的數據只能放進散列表的一部分,這時候缺失的數字是散列表的 第一個空擋
代碼實現:
方式一:使用額外的標記數組作爲散列表,遍歷數組一在找到範圍區間內的數據就更改標記數組的相應位置。最後進行遍歷找到 未被標記的位置,根據映射規則可以計算得出缺失的數字。如果標記數組沒有缺失,那麼缺失的數字就是 數組長度+1;
public static int firstMissingPositive(int[] nums) {
int len = nums.length;
//創建等長的標記數組(boolean數組默認值爲false,且每個元素佔用1字節)
boolean[] temp = new boolean[len];
//根據映射規則對相應的標記數組進行更改
for (int num : nums) {
if(num>0 && num<=len)temp[num-1]=true;
}
//情況二
for(int i = 0;i < len;i++){
if(temp[i] == false)return i+1;
}
//情況一
return len+1;
}
優點:實現簡單,效率高,典型的空間換時間的實現方式,時間複雜度爲 O(n)。
缺點:並不符合題目的空間複雜度要求,散列表的空間與原數組的空間線性相關,空間複雜度爲O(n)
方式二:對原數組進行調整,在遍歷過程中每遇到一個區間範圍內的數據便把它與它所映射的地址的數據進行交換,直到當前位置放置了正確的數據或者無效的數據,再對下一個元素的操作。最後遍歷的時候,只要找出不符合映射規則的位置即可知道缺失的數字。如果還不能理解,可以結合以下調整過程的圖例和代碼進行理解。
public static int firstMissingPositive(int[] nums) {
int len = nums.length;
//調整數組
for (int i = 0; i < len; i++) {
//當元素 在區間範圍內 並且 還未遵循映射規則時 進行交換調整
while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
int temp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = temp;
}
}
//情況二
for (int i = 0; i < len; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
//情況一
return len + 1;
}
優點:時間複雜度O(n),空間複雜度爲O(1),符合題目要求
缺點:打亂了原有數據順序
總結:
兩種實現方式各有優劣,本題最重要的是提供一種結合散列表的思路,開發過程中根據相應的需求進行選擇。
Tips:
標記數組爲什麼不用 byte類型數組,而是使用boolean類型數組?
-
boolean 類型被編譯成 int 類型來使用,佔 4 個 byte 。
-
boolean 數組被編譯成 byte 數組類型,每個 boolean 數組成員佔 1 個 byte
具體的解釋可以看下這個:https://zhuanlan.zhihu.com/p/101105514