1. 程式人生 > >LeetCode #41 First Missing Positive

LeetCode #41 First Missing Positive

(Week 2 演算法作業)

題目

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。

其他演算法

在討論區有一個演算法:


// 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 ,但是找到關鍵點(第一個連續的數列)後就其實很簡單。

平時還是要多練習演算法,鍛鍊腦筋,爭取更快地找到切入口(:з」∠)