LeetCode #41 First Missing Positive

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SquirrelYuyu/article/details/82704126

(Week 2 算法作业)

题目

First Missing Positive

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 )

该算法用时 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 )

该算法用时 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 ,但是找到关键点(第一个连续的数列)后就其实很简单。

平时还是要多练习算法,锻炼脑筋,争取更快地找到切入口(:з」∠)

猜你喜欢

转载自blog.csdn.net/SquirrelYuyu/article/details/82704126