1. 程式人生 > >LeetCode一求陣列第一個缺失正數問題

LeetCode一求陣列第一個缺失正數問題

1、題目要求及分析:

(1)題目要求:

給一個未排序的陣列,找出第一個缺失的正整數。

例如,
[1,2,0] 返回 3
[3,4,-1,1] 返回 2

你的演算法應該在 O(n) 的時間複雜度內完成並且使用常數量的空間。
(2)題目分析:

此題是對陣列的靈活運用,跟很多陣列的題一樣,都是迴圈->比較->輸出問題,但是這裡注意時間複雜度為O(n)與常數量的空間(也就是另外定義初始化空間的時候長度是常量,是固定不變的),具體解決辦法分析請看下面!

2、解決辦法與分析

方法一:關鍵是找到陣列中最小的元素與最大的數,然後從最小元素遞增到最大元素為止,逐漸與陣列比較,若不在陣列中並且此時該數是大於0的立馬返回結束

(1)簡單演算法描述:

(1)找到陣列中最小的元素與最大元素(採用方法的方法很多,但儘可能只能迴圈一次滿足時間複雜度)
(2)從最小元素min自增到最大元素max為止,逐一與陣列元素比較
(3)如果不相等且min此時大於0則說明該正整數缺失,返回結束
(4)如果相等則說明該數已經在陣列中,繼續(2)

(2)java程式碼:

public int firstMissingPositive1(int[] nums) {

        //陣列為空,陣列長度為1(元素小於等於0、元素等於1,、元素大於1)特殊情況直接返回,不用繼續迴圈判斷
        if (nums.length
== 0 || (nums.length == 1 && nums[0] <= 0) || (nums.length == 1 && nums[0] > 1)) { return 1; } else if (nums.length == 1 && nums[0] == 1) { return 2; } //陣列變list集合 Integer arr[] = new Integer[nums.length]; for
(int i = 0; i < nums.length; i++) { Integer integer = nums[i]; arr[i] = integer; } //陣列排序找到最小最大的值 int min = Collections.min(Arrays.asList(arr)) > 0 ? Collections.min(Arrays.asList(arr)) : 0; int max = Collections.max(Arrays.asList(arr)); //最小的陣列元素大於1,直接返回第一個缺失的正整數位1 if (min > 1) { return 1; } //迴圈找到缺失最小的正整數 while(min != max ) { if (!Arrays.asList(arr).contains(min) && min > 0) { return min; } min++; } return max+1; }

(3)優缺點分析:
優點:

  • 關鍵迴圈部分簡單,容易理解;
  • 時間複雜度較低,關鍵部分只有一個while迴圈。

缺點:

  • 該方法較為麻煩的就是先將陣列變為List集合。
  • 雖然利用了Collections集合工具的的minmax方法,但對於[1,2,3,4...,10000]這樣的情況,尋找最大值與最小值的Collections內部實現就會造成一定的時間浪費,從而對整個程式效率就造成一定的影響。
  • 如果最小值是很小的負數,如[-100,1,100],這樣也會在min++遞增迴圈比較過程中造成很大的浪費(我們只需要正數)。

(4)演算法優化:

我們發現我們找到最大元素後,執行while(min != max)一開始會以為這裡當max足夠大時會造成很大的浪費,但後來發現我是錯的,因為從min開始一旦找到最小缺失的正整數就會return返回。
比如:我們輸入陣列nums = [1,2,3,100],則max = 100,迴圈體while內:

while:
min = 1;
min = 2;
min = 3;
min = 4;
end while;

其實後來發現我們只需要將min自增到nums.length+1即可,無需理會max最大值,即while(min != nums.length+1)
事實上這是因為除了[1,2,3,4,5,...n]這樣連續的情況外,大部分缺失的正整數都會在最小值min附近,並且在num.length+1長度下min ++自增得到的數肯定能找到最小缺失正整數。
比如:我們輸入陣列nums = [1,2,3,100],則nums.length + 1 = 5迴圈體while內:

while:
min = 1;
min = 2;
min = 3;
min = 4;
end while

結果是一樣,所以我們可以進一步對方法一進行優化修改程式碼。

public int firstMissingPositive1(int[] nums) {

        //陣列為空,陣列長度為1(元素小於等於0、元素等於1,、元素大於1)特殊情況直接返回,不用繼續迴圈判斷
        if (nums.length == 0 || (nums.length == 1 && nums[0] <= 0) || (nums.length == 1 && nums[0] > 1)) {
            return 1;
        }
        else if (nums.length == 1 && nums[0] == 1) {
            return 2;
        }
        //陣列變list集合
        Integer arr[] = new Integer[nums.length];
        for (int i = 0; i < nums.length; i++) {
            Integer integer = nums[i];
            arr[i] = integer;
        }
        //最小值小於0時直接從0開始,不需要從負數開始
        int min = Collections.min(Arrays.asList(arr)) > 0 ? Collections.min(Arrays.asList(arr)) : 0;
        //不需要最大元素了,節省了時間
        //int max = Collections.max(Arrays.asList(arr));
        //最小的陣列元素大於1,直接返回第一個缺失的正整數位1
        if (min > 1) {
            return 1;
        }
        //迴圈找到缺失最小的正整數
        while(min != nums.length+1 ) {
            if (!Arrays.asList(arr).contains(min) && min > 0) {
                    return min;
                }
            min++;
          }
        return min;
  }

方法二:其實質為濃縮繼承方法一的思想,高效地完成迴圈比較

(1)簡單演算法描述:

(1)迴圈陣列比較元素小於nums.length+1並且大於0的元素
(2)若存在這樣的元素num[j],則該元素減1作為索引加入boolean陣列中,並且記為true
(3)若沒有這樣的元素則繼續(1),直到nums末尾結束返回1
(4)迴圈遍歷boolean陣列,直到false結束,返回i+1

(2)java程式碼:

public int firstMissingPositive2(int[] nums) {

        boolean []b = new boolean[nums.length];
        if(nums.length == 0){
            return 1;
        }
        for(int j = 0; j < nums.length; j++){
            //以為nums.leng+1為界,找到大於0的正整數元素
            if(nums[j] > 0 && nums[j] < nums.length+1){
                System.out.println("match: "+nums[j]);
                b[nums[j]-1] = true;
            }
         }
         int i = 0;
         while(i < nums.length && b[i] == true){
            i++;//i++還原回nums[j]
         }
         return i+1;
    }

(3)結果分析:

我們以輸入[5,1,3,1000]為例,分析過程程式碼得到如下的表格。
這裡寫圖片描述

要徹底理解此演算法,則要知道以下兩點:

1)為什麼要以if(nums[j] > 0 && nums[j] < nums.length+1)為條件?

  • 這是因為方法一中缺點以及演算法優化中提及到的我們只需要關注大於0的元素即可;
  • 同時當極限特殊情況[1,2,3,...n]這樣連續的陣列nums出現時,正整數為n+1,也就是nums.length+1,那麼我們就可以使用極限情況判別,不需要對大於nums.length+1的元素進行匹配操作,從而節約時間。

2)為什麼要建立boolean陣列?

  • 利用陣列索引遞增記錄符合條件的每一個元素nums[j]-1遞增到nums.length-1是否在nums陣列中。符合條件的nums[1]-1=0的遞增集合為{0,1,2,3}nums[2]-1=2遞增集合為{2,3},這也是對方法一中利用min
    最小值然後遞增比較的思想的濃縮應用。
    這裡寫圖片描述

  • 建立boolean陣列初始化全部為false,這樣就只需要對符合條件的更新為true即可,節省時間提高效率,同時if判斷更快速。

3、思考總結

(1)其中第二種方法經過查資料看到大神寫的,但是卻沒有任何解釋的地方,之後進行了研究分析。發現此演算法解決問題甚是高效,在這裡便加以記錄了。

(2)其實兩種方法都比較高效,都可以在解決此問題的很多方法中靠前(個人yy的,敬請見諒!)。但是效率最高的就是第二種方法,時間空間上都很高效!
(3)通過此題總結了如何不利用常規的方法求解陣列最值問題(即利用相關的工具類),主要有以下四種方法。

  • 通過轉為List集合,然後利用Collections.min(list)Collections.max(list)
  • 通過轉為List集合,然後利用排序int min = Collections.sort(list).get(0)int max = Collections.sort(list).get(list.size()-1)
  • 通過Arrays.sort(array)排序,得到int min = array[0]int max = array[array.length-1]
  • 通過利用Java8 stream將一個數組放進 stream 裡面,然後直接呼叫 streammin/max方法,int min = Arrays.stream(a).min().getAsInt();int max = Arrays.stream(a).max().getAsInt();