题目
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 为第一个缺失的正数,则时间复杂度为 O ( n ) + O ( k ) O(n)+O(k) O(n)+O(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 为第一个缺失的正数,则时间复杂度为 O ( n ) + O ( k ) O(n)+O(k) O(n)+O(k)。
该算法用时 4 ms。
其他算法
在讨论区有一个算法:
My short c++ solution, O(1) space, and O(n) time
// 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 ,但是找到关键点(第一个连续的数列)后就其实很简单。
平时还是要多练习算法,锻炼脑筋,争取更快地找到切入口_(:з」∠)_