LeetCode #41 First Missing Positive
(Week 2 演算法作業)
題目
Given an unsorted integer array, find the smallest missing positive integer.
Example 1:
Input: [1,2,0]
Output: 3
Example 2:
Input: [3,4,-1,1]
Output: 2
Example 3:
Input: [7,8,9,11,12]
Output: 1
Note:
Your algorithm should run in O(n) time and uses constant extra space.
Difficulty: Hard
分析
我們要找到無序陣列升序排列後缺少的第一個正數。
在考慮演算法時,可以忽略那些 0 和負數。輸入數列有幾種情況:
所有的正數可以組成一個連續的數列
該數列從 1 開始,如 [ 1 2 3 4 5 ] ,答案為 6
該數列不從 1 開始,如 [ 2 3 4 ] ,答案為 1
所有的正數可以組成幾個連續的數列
第一個數列從 1 開始,如 [ [ 1 2 ] [ 5 6 7 ] ] ,答案為 3
第一個數列不從 1 開始,如 [ [ 2 3 4 ] [ 9 10 11 ] ] ,答案為 1
總的來說,就是要找這個無序數列忽略非正數進行排序後,第一個連續的數列,並看它的開頭和結尾
另外,輸入數列中可能有重複的數字,對某些演算法來說要考慮這一情況。我有一次的WA對應的輸入是 [ 2 2 4 0 1 3 3 3 4 3 ] 。
演算法1
這個演算法的思想是,建立一個 unsiged int
型別的陣列 record
,讓陣列中第 i 個(從 0 開始計)數的二進位制的第 n 位(最右邊為第 1 位)代表數列中是否存在這個數字 32 * i + j
。如果為 1 ,代表存在,否則不存在。
如:
record[0] = 1 = 0b0...0000001 // 代表數列中存在 1
record[2] = 38 = 0b0...0100110 // 代表數列中存在 32*2+2、32*2+3、32*2+6
record 陣列中第一個不等於 0xFFFFFFFF
的數,即為正數軸第一次出現斷點的地方。
#include <iostream>
#include <climits>
#include <vector>
#define N 100
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int size = nums.size();
unsigned int record[N] = {0};
int max = 0;
int min = INT_MAX;
// 第一次遍歷,去掉非正數,找到最大數和最小數
vector<int>::iterator iter = nums.begin();
while(iter != nums.end()){
int i = *iter;
if(i <= 0){
nums.erase(iter);
}
else{
if(i < min) min = i;
if(i > max) max = i;
iter++;
}
}
if(min != 1) return 1;
// 第二次遍歷,利用 record 陣列記錄數列中的正數
iter = nums.begin();
for(; iter != nums.end(); iter++){
int n = *iter;
int i = n - 1;
int quotient = i / 32;
int reminder = i % 32;
if(quotient < N) record[quotient] |= (1 << reminder);
}
// 確定 record 在邏輯上最大的索引,避免溢位
int indexmax = (max - 1) / 32;
if(indexmax >= size) indexmax = size - 1;
int result = 0;
for(int index = 0; index <= indexmax; index++){
if(record[index] < UINT_MAX){
// 找到 record 中第一個不是所有位都為 1 的數
result = index * 32;
// 尋找這個數中第一個為0的位
while(record[index] & 1){
record[index] >>= 1;
result++;
}
break;
}
}
result++;
return result;
}
};
這個演算法中的 record 陣列大小 N 是常量,如果所輸入的序列中的第一個連續數列的規模超出了 32*N ,就可能會出錯。考慮到網站測試的例子規模,我姑且把它設為 100 。或者也可以先確定輸入序列的規模,再確定陣列大小。
假設 n 為輸入數列的大小,k 為第一個缺失的正數,則時間複雜度為。
該演算法用時 4 ms。
演算法2
這一演算法的思路是,把數列中的數字和索引對應起來。比如,把數字 1 放到第 1 個位置(即 nums[0]
),把數字 2 放到第 2 個位置( nums[1]
),以此類推。然後對產生的新陣列,逐個比較索引和元素。由第一個沒有應有的元素的索引即可得到所求的答案。
對某些和順序有關的演算法題,陣列的索引可能很有用!
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
const int size = nums.size();
if(size == 0) return 1;
int result = 0;
int newnums[size] = {0};
for(int i = 0; i < size; i++){
if(nums[i] <= size && nums[i] > 0){
newnums[nums[i] - 1] = 1; // 1 表示存在與該索引對應的元素
}
}
for(int i = 0; i < size; i++){
if(newnums[i] != 1){ // 第一個不存在對應元素的索引
result = i + 1;
break;
}
}
if(result == 0) result = size + 1;
return result;
}
};
顯然,與演算法 1 相比,演算法 2 的思路更簡便。 但是演算法 1 可以一次性掃描32位數字,對第一個連續數列的規模比較大的情況來說,可能可以更快地確定答案所在的範圍。
假設 n 為輸入數列的大小,k 為第一個缺失的正數,則時間複雜度為。
該演算法用時 4 ms。
其他演算法
在討論區有一個演算法:
// Put each number in its right place.
// For example:
// When we find 5, then swap it with A[4].
// At last, the first place where its number is not right, return the place + 1.
class Solution
{
public:
int firstMissingPositive(int A[], int n)
{
for(int i = 0; i < n; ++ i)
while(A[i] > 0 && A[i] <= n && A[A[i] - 1] != A[i])
swap(A[i], A[A[i] - 1]);
for(int i = 0; i < n; ++ i)
if(A[i] != i + 1)
return i + 1;
return n + 1;
}
};
以 [ 2 2 4 0 1 3 3 3 4 3 ] 為例,經過第一輪 for 迴圈排序後產生 [ 1 2 3 4 2 0 3 3 4 3 ] ,經過第二輪 for 迴圈查詢到 5 。
總結
雖然所標的難度是 hard ,但是找到關鍵點(第一個連續的數列)後就其實很簡單。
平時還是要多練習演算法,鍛鍊腦筋,爭取更快地找到切入口(:з」∠)