java 判断一个数是正整数_LeetCode 41—缺失的第一个正整数

前言

从这篇题解开始,我会尽量同时使用 Java 和 C++ 来刷题,从而来学习 Java 的基础使用,正所谓“刷题学习法”。

关键字:数组,BitMap

归航return:(Trivial) LeetCode 468—验证 IP 地址​zhuanlan.zhihu.com
归航return:(Trivial) LeetCode 剑指 Offer 29—螺旋矩阵​zhuanlan.zhihu.com

Problem

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

示例 1:

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

示例 2:

输入: [3,4,-1,1]
输出: 2

示例 3:

输入: [7,8,9,11,12]
输出: 1

提示:

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

41. 缺失的第一个正数 - 力扣(LeetCode)​leetcode-cn.com

Solution

先暂时不考虑空间复杂度的要求,尝试在线性时间复杂度下完成这道题目。为方便起见,在本题中,称呼数组的名字为 nums,符合要求的正整数称作 f(nums)

首先证明:

,且等号可以成立。证明如下:

一方面,令 nums 数组是从 1 递增到 nums.size() ,步长为 1,那么此时

另一方面,假设存在某个 nums 数组,使得

,那么这意味着
nums 数组中出现了 1 到 nums.size() 所有的整数,而这已经使得数组的长度达到了 nums.size(),因此这是不可能的,矛盾!

综上所述:

另一方面:

,这是 trivial 的。

基于上述事实,我们可以维护一个 boolean 类型的 visited 数组,用来标记 1 到 nums.size() 中出现过的正整数,然后扫描完整个数组之后遍历这个 visited 数组,如果遇到了第一个 visited.get(i)==false,那么答案就是 i+1,否则答案就是 nums.size()+1。Java 代码如下,在时间上击败了 88.08% 的 Java 提交:

class Solution {
    public int firstMissingPositive(int[] nums) {
        ArrayList<Boolean>visited = new ArrayList<Boolean>(Collections.nCopies(nums.length, false));
        for (int i = 0;i < nums.length; ++i){
            if (nums[i] > 0 && nums[i] <= nums.length)
                visited.set(nums[i]-1, true);
        }
        for (int i = 0;i < visited.size(); ++i){
            if (visited.get(i) == false)
                return i+1;
        }
        return nums.length + 1;
    }
}

C++ 代码如下,在时间上击败了 59.41% 的 C++ 提交:

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        vector<bool>visited(nums.size(), 0);
        for (const int &x:nums){
            if (x>0 && x<= nums.size())
            visited[x-1] = true;
        }
        for (int i = 0;i < visited.size(); ++i){
            if (!visited[i])
            return i+1;
        }
        return visited.size() + 1;
    }
};

但是题目还要求常数空间复杂度,因此上述方法尽管打败了 88.08% 的 Java 提交,但仍然是错误的。之后我尝试使用一些方法来简化问题分析,但一无所获,最后还是选择去题解区找答案,答案让我颇为吃惊:将输入的 nums 数组本身利用起来,作为判断问题的方法!

官方题解的描述晦涩难懂,经过我的思考,我将官方题解拆分成两个部分:

首先考虑一个简化的情况1<=nums[i]<=nums.length,对于任意的元素。那么这个时候参考答案的做法是:使用元素的正负来标明元素是否出现。逐个遍历 nums 数组,如果某个元素是 x ,就将 nums[x-1] 加上负号作为标记,显然由于出现过多次和出现一次是一样的,那么更准确地说,应该是将 nums[x-1] 用其绝对值的相反数来代替。当然,这样做之后,后续的 x 可能是小于 0 的。

因此,我们需要将条件修改为:如果某个元素的绝对值是 abs(x),那么就将 num[abs(x)-1] 用其绝对值的相反数来进行标记。将整个数组遍历完毕之后,从头到尾开始扫描,如果某个地方第一次出现某个元素大于 0,那么意味着这个地方的元素并没有出现过,返回下标+1;否则,返回 num.length+1

接下来开始考虑到更一般的情况nums[i] 可以是任何的合法 int 数。注意到,如果整个数组中,1 未曾出现过,那么答案就是 1,这是 trivial 的。如果出现过 1,那么将不属于 [1,nums.length] 的元素替换为 1,是不影响答案的,这个结论也是 trivial 的,参考上方的 visited 数组的定义即可证明。

将数组处理完毕之后,那么问题就化为了上述简化的情况,如法炮制即可。当然,由于在这种情况下 f(nums)>=2,因此扫描的顺序不需要从头开始,而是改为从第 1 个元素开始扫描即可。Java 代码如下:

class Solution {
    public int firstMissingPositive(int[] nums) {
        int n = nums.length;
        int numberOfOnes = 0;
        for (int i = 0; i < n; ++i){
            if (nums[i] == 1)
                ++numberOfOnes;
            if (nums[i] <= 0 || nums[i] > n)
                nums[i] = 1;
        }
        if (numberOfOnes == 0)
            return 1;
        for (int i = 0; i < n; ++i){
            int indexToBeReversed = Math.abs(nums[i])-1;
            nums[indexToBeReversed] = -Math.abs(nums[indexToBeReversed]);
        }
        for (int i = 1; i < n; ++i){
            if (nums[i] > 0)
                return i+1;
        }
        return n+1;
    }
}

C++ 代码如下,时间上击败了 100% 的 C++ 提交:

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        int countOfOnes = 0;
        int lengthOfNums = nums.size();
        for (int &x:nums){
            if (x == 1)
                ++countOfOnes;
            if (x <= 0 || x > lengthOfNums)
                x = 1;
        }
        if (countOfOnes == 0)
            return 1;
        for (int &x:nums){
            int indexToBeReversed = abs(x)-1;
            nums[indexToBeReversed] = -abs(nums[indexToBeReversed]);
        }
        for (int i = 1; i < lengthOfNums; ++i){
            if (nums[i] > 0)
                return i+1;
        }
        return lengthOfNums+1;
    }
};
必须承认,C++ 的 range-based for loop 是个好东西,极大的提高了代码可读性。这里必须使用 int& 类型,不能 pass by value 或者 pass by const reference,也最好别使用 C++ 11 的 auto type deduction,因为这样可能会导致达不到修改原始数组内容的目的。

这样,就将空间复杂度成功降低到了 O(1)。当然,这里有一个负面作用就是改变了原始的数组,不能忘记。

经过更多的资料查阅,我发现:事实上,本题是一种数据结构,BitMap 的变式。简要来说,BitMap 就是利用二进制数中,二进制位的 0 和 1 用来代表元素是否存在的一种思想。例如: short 类型总共有 16 位,那么便可以从最高位到最低位是否为 1,来代表 0 到 15 是否在某个数组中出现过,如果元素更多,那么可以维护一个 short 类型的数组,来处理这些问题。基于这样的思路,添加元素,删除元素,查找元素只需要使用精巧的位运算即可完成,当然这个数据结构的缺点也是很明显的—无法记录重复情况,只能记录是否出现过。

回到原题。为何经过我的思考我将其称作 BitMap 数据结构的变式呢?因为这道题在经过数据预处理之后,实际上可以理解成一个 boolean 类型的数组,然后使用元素的正负号来映射没有出现过元素,和出现过元素。这里之所以是说“理解成”,是因为这里事实上是一个 int 类型的数组,而 int 的最高位是符号标志位,从这个角度来看,理解成一个“奢侈的” boolean 数组自然是无可厚非。

EOF。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值