力扣300——最長上升子序列

這道題主要涉及動態規劃,優化時能夠考慮貪心算法和二分查找。
<!-- more -->java

原題

給定一個無序的整數數組,找到其中最長上升子序列的長度。git

示例:github

輸入: [10,9,2,5,3,7,101,18]
輸出: 4 
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。

說明:算法

  • 可能會有多種最長上升子序列的組合,你只須要輸出對應的長度便可。
  • 你算法的時間複雜度應該爲 O(n2) 。

進階: 你能將算法的時間複雜度下降到 O(n log n) 嗎?segmentfault

解題

暴力法

這也是最基礎的想法,利用遞歸,從每個數開始,一個一個尋找,只要比選中的標準大,那麼就以新的數爲起點,繼續找。所有找完後,找出最長的序列便可。數組

也看一下代碼:優化

class Solution {
    public int lengthOfLIS(int[] nums) {
        // 遞歸查詢
        return recursiveSearch(nums, Integer.MIN_VALUE, 0);
    }

    public int recursiveSearch(int[] nums, int standard, int index) {
        if (nums.length == index) {
            return 0;
        }
        
        // 若是包含當前index的數字,其遞增加度
        int tokenLength = 0;
        if (nums[index] > standard) {
            tokenLength = 1 + recursiveSearch(nums, nums[index], index + 1);
        }

        // 若是不包含當前index的數字,其遞增加度
        int notTokenLength = recursiveSearch(nums, standard, index + 1);

        // 返回較大的那個值
        return tokenLength > notTokenLength ? tokenLength : notTokenLength;
    }
}

提交以後報超出時間限制,這個也是預料到的,那麼咱們優化一下。spa

記錄中間結果

仔細分析一下上面的暴力解法,假設 nums 是: [10,9,2,5,3,7,101,18],那麼從 7 到 101 這個查找,在二、五、3的時候,都曾經查找過一遍。code

那麼針對這種重複查找的狀況,咱們能夠用一個二維數組,記錄一下中間結果,這樣就能夠達到優化的效果。好比用int[][] result標記爲記錄中間結果的數組,那麼result[i][j]就表明着從 nums[i - 1] 開始,不管包含仍是不包含 nums[j] 的最大遞增序列長度。這樣就能保證再也不出現重複計算的狀況了。遞歸

讓咱們看看代碼:

class Solution {
    public int lengthOfLIS(int[] nums) {
        // 記錄已經計算過的結果
        int result[][] = new int[nums.length + 1][nums.length];
        for (int i = 0; i < nums.length + 1; i++) {
            for (int j = 0; j < nums.length; j++) {
                result[i][j] = -1;
            }
        }
        // 遞歸查詢
        return recursiveSearch(nums, -1, 0, result);
    }

    public int recursiveSearch(int[] nums, int preIndex, int index, int[][] result) {
        if (nums.length == index) {
            return 0;
        }

        // 若是已經賦值,說明計算過,所以直接返回
        if (result[preIndex + 1][index] > -1) {
            return result[preIndex + 1][index];
        }

        // 若是包含當前index的數字,其遞增序列最大長度
        int tokenLength = 0;
        if (preIndex < 0 || nums[index] > nums[preIndex]) {
            tokenLength = 1 + recursiveSearch(nums, index, index + 1, result);
        }

        // 若是不包含當前index的數字,其遞增序列最大長度
        int notTokenLength = recursiveSearch(nums, preIndex, index + 1, result);

        // 返回較大的那個值
        result[preIndex + 1][index] = tokenLength > notTokenLength ? tokenLength : notTokenLength;
        return result[preIndex + 1][index];
    }
}
提交OK,可是結果感人,幾乎是最慢的了,不管時間仍是空間上,都只戰勝了`5%`左右的用戶,那就繼續優化。

### 動態規劃

假設我知道了從 nums[0] 到 nums[i] 的最大遞增序列長度,那麼針對 nums[i + 1],我只要去跟前面的全部數比較一下,找出前面全部數中比 nums[i + 1] 小的數字中最大的遞增子序列,再加1就是 nums[i + 1] 對應的最大遞增子序列。

這樣我只要再記錄一個最大值,就能夠求出整個數組的最大遞增序列了。

讓咱們看看代碼:

class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }

        // 動態規劃,以前幾個數字中,有幾個比當前數小的,不斷更新

        // 存儲中間結果
        int[] dp = new int[nums.length];
        // 最大值,由於數組中至少有一個,因此最小是1
        int max = 1;
                // 遍歷
        for (int i = 0; i < dp.length; i++) {
            // 當前下標i的最大遞增序列長度
            int currentMax = 0;
            for (int j = 0; j < i; j++) {
                // 若是nums[i]比nums[j]大,那麼nums[i]能夠加在nums[j]後面,繼續構成一個遞增序列
                if (nums[i] > nums[j]) {
                    currentMax = Math.max(currentMax, dp[j]);
                }
            }

            // 加上當前的數
            dp[i] = currentMax + 1;

            max = Math.max(dp[i], max);
        }

        return max;
    }
}

提交OK,執行用時:9 ms,只打敗了75.15%的 java 提交,看來仍是能夠繼續優化的。

貪心算法 + 二分查找

貪心算法意味着不須要是最完美的結果,只要針對當前是有效的,就能夠了。

咱們以前在構造遞增序列的時候,實際上是在不斷根據以前的值進行更新的,而且十分準確。但其實並不須要如此,只要保證序列中每一個數都相對較小,就能夠得出最終的最大長度。

仍是以 [10,9,2,5,3,7,101,18,4,8,6,12]舉例:

  1. 從10到2,都是沒法構成的,由於每個都比以前的小。
  2. 當以最小的2做爲起點後,2,52,3都是能夠做爲遞增序列,但明顯感受2,3更合適,由於3更小。
  3. 由於7大於3,所以遞增序列增加爲2,3,7
  4. 由於101也大於7,所以遞增序列增加爲2,3,7,101
  5. 由於18小於101,可是大於7,所以咱們能夠用18替換101,由於18更小,序列更新爲2,3,7,18
  6. 此時遇到4,4大於3可是小於7,咱們能夠用它替換7,雖然此時新的序列2,3,4,18並非真正的結果,但首先長度上沒有問題,其次若是出現新的能夠排在最後的數,必定是大於4的,由於要先大於如今的最大值18。序列更新爲2,3,4,18
  7. 同理,8大於4小於18,替換18,此時新的序列2,3,4,8,這樣是否是你們開始懂得了這個規律。
  8. 遇到6以後,更新爲2,3,4,6
  9. 遇到12後,更新爲2,3,4,6,12

這樣也就求出了最終的結果。

結合一下題目說明裏提到的O(nlogn),那麼就能夠想到二分查找,運用到這裏也就是找到當前數合適的位置。

接下來讓咱們看看代碼:

class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }

        // 貪心 + 二分查找

        // 一個空數組,用來存儲最長遞增序列
        int[] result = new int[nums.length];
        result[0] = nums[0];
        // 空數組的長度
        int resultLength = 1;
        // 遍歷
        for (int i = 1; i < nums.length; i++) {
            int num = nums[i];
            // 若是num比當前最大數大,則直接加在末尾
            if (num > result[resultLength - 1]) {
                result[resultLength] = num;
                resultLength++;
                continue;
            }
            // 若是和最大數相等,直接跳過
            if (num == result[resultLength - 1]) {
                continue;
            }

            // num比最大值小,則找出其應該存在的位置
            int shouldIndex = Arrays.binarySearch(result, 0, resultLength, num);
            if (shouldIndex < 0) {
                shouldIndex = -(shouldIndex + 1);
            }
            // 更新,此時雖然得出的result不必定是真正最後的結果,但首先其resultLength不會變,以後就算resultLength變大,也是相對正確的結果
            // 這裏的更新,只是爲了讓result數組中每一個位置上的數,是一個相對小的數字
            result[shouldIndex] = num;
        }

        return resultLength;
    }
}

提交OK,執行用時:2 ms,差很少了。

總結

以上就是這道題目個人解答過程了,不知道你們是否理解了。這道題目用動態規劃其實就已經能解決了,但爲了優化,還須要用到貪心算法和二分查找。

有興趣的話能夠訪問個人博客或者關注個人公衆號、頭條號,說不定會有意外的驚喜。

https://death00.github.io/

公衆號:健程之道

相關文章
相關標籤/搜索