[LeetCode] 41、缺失的第一个正数

本文介绍了一种高效算法,用于在未排序的整数数组中找到最小的缺失正整数,采用比特位标记和桶排序策略,实现O(n)时间复杂度与常数级空间复杂度。

题目描述

给定一个未排序的整数数组,找出其中没有出现的最小的正整数。

你的算法的时间复杂度应为O(n),并且只能使用常数级别的空间。

示例 1:

输入: [1,2,0]
输出: 3

解题思路

这种题还是有一些trick的(太难,面试不友好),理解并尽量记住。本题可用“哈希表”实现时间 O ( n ) O(n) O(n),但是这时空间复杂度不满足要求。“脏工作环境” 的解决方法是将一个字符串 hash_str 分配 n 个 0,并且用类似于哈希表的方法,如果在数组中出现数字 i 则将字符串中 hash_str[i] 修改为 1 。(空间其实还是 O ( n ) O(n) O(n)

bitmap题解(有改动,见下),桶排序题解(推荐)

  • bitmap法:(思路:使用索引作为“哈希键”以及元素的符号作为“哈希值”来实现是否存在的检测拿自己当bitmap,不再另外设置哈希表,绝妙!)

    首先我们可以不考虑负数和零,因为不需要考虑。同样可以不考虑大于 n 的数字,因为首次缺失的正数一定小于或等于 n + 1 。缺失的正数为 n + 1 的情况会单独考虑。(n个数只可能缺失1~(n+1))

    • 检查 1 是否存在于数组中。如果没有,则已经完成,1 即为答案。

    • 如果 nums = [1],答案即为 2

    • 将负数,零,和大于 n 的数替换为 1 。(目的是将nums数组全变为正的,便于后面用负号来表示一个数曾经出现过

    • 遍历数组。当读到数字 nums[i] 时,替换第 nums[i] 个元素的符号。(下标为nums[i] - 1

      注意重复元素:只能改变一次符号。(见代码,我的实现

    • 再次遍历数组。返回第一个正数元素的下标(+1)。

    • 如果之前的步骤中没有发现nums 中有正数元素,则返回n + 1

    例如,nums[2] 元素的负号意味着数字 3 出现在 nums 中。nums[3]元素的正号表示 4 没有出现在 nums中。

    为了完成此操作,我们遍历一遍数组(该操作在数据预处理使得数组中只有正数的操作后),检查每个元素值 elem(下标) 以及将nums[nums[elem] - 1] 元素的符号变为负号来表示数字 nums[elem] 出现在 nums 中。注意,当数字出现多次时需要保证符号只会变化 1 次。

  • 桶排序(推荐):看题解幻灯片,解法非常优雅,强烈推荐。(上面那个解法了解即可,主要是会用“负号”表示一个数曾经出现过的思路)

    • 核心思想:一个萝卜一个坑,最后再看一下哪个坑出了问题。

    • 拿上面的示例举例,数字 1 应该放在索引为 0 的位置上,数字 3 应该放在索引为 2 的位置上,数字 4 应该放在索引为 3 的位置上。一个数字放在它应该放的位置上,我们就认为这个位置是“和谐”的,看起来“顺眼”的。

    • 按照以上规则排好序以后,缺失的第 1 个正数一下子就看出来了,那么“最不和谐”的数字的索引 +1,就为所求。那如果所有的数字都“和谐”,数组的长度 +1 就为所求。

参考代码

bitmap法

class Solution {
public:
	int firstMissingPositive(vector<int>& nums) {
		int length = nums.size();
		if (length == 0)
			return 1;

        // 基本情况,先看含不含1
		bool contain_1 = false;
		for (auto i : nums) {
			if (i == 1) {
				contain_1 = true;
				break;
			}
		}
		
		// 两种特殊情况:数组不含1 和 nums = [1]
		if (!contain_1)
			return 1;
		//if (contain_1 && length == 1)  // 这个特判其实不需要
		//	return 2;
        
        // 用 1 替换负数,0,和大于 n 的数。在转换以后,nums 只会包含正数
		for (int i = 0; i < length; i++){
            if (nums[i] <= 0 || nums[i] > length)
				nums[i] = 1;
		}	
        
        // 使用索引和数字符号作为检查器
        // 例如,如果 nums[i] 是负数表示在数组中出现了数字 `i + 1`
        // 如果 nums[2] 是正数 表示数字 3 没有出现
		for (int i = 0; i < length; i++) {
			int curNum = abs(nums[i]);
			// 如果发现了一个数字 curNum,则改变 nums[curNum - 1] 元素的符号
            // 注意重复元素只需操作一次(这里不要出错)
            nums[curNum - 1] = -abs(nums[curNum - 1]);
		}
        
        // 现在第一个正数的下标,就是第一个缺失的数
		for (int i = 0; i < length; i++){
            if (nums[i] > 0)
				return i + 1;
		}
		return length + 1;
	}
	
};

桶排序(推荐!!!爱死我自己哈哈哈哈哈)

// 基于桶排序。
class Solution {
public:
	int firstMissingPositive(vector<int>& nums) {
		int length = nums.size();
		if (length == 0)
			return 1;
        
        // 小心[1, 1]这种例子会造成死循环,要特殊处理。(nums[i] != nums[nums[i]-1])
        for(int i = 0; i < length; i++){
            while(nums[i] >= 1 && nums[i] <= length && i != nums[i]-1 && nums[i] != nums[nums[i]-1])  // 注意这里要用while
                swap(nums[i], nums[nums[i]-1]);
        }
            
        for(int i = 0; i < length; i++){
            if(i + 1 != nums[i])
                return i+1;
        }        
        return length + 1;
	}
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值