自考研以来,第一次再次更新博客,接下来的时间会不断写新的文章。这道题目也是字节跳动的面试题,下面分享我的解题思路。
题目
内容:给定一个未排序的整数数组,找出其中没有出现的最小的正整数。
要求:你的算法的时间复杂度应为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