LeetCode刷題總結-數組篇(上)

       數組是算法中最常用的一種數據結構,也是面試中最常考的考點。在LeetCode題庫中,標記爲數組類型的習題到目前爲止,已累計到了202題。然而,這202道習題並不是每道題只標記爲數組一個考點,大部分習題都有兩到三個考點。比如,考查數組+哈希表、數組+動態規劃+數學、數組+回溯等。

       看到如此多考點標籤,如果盲目地按照一個標籤內部所有習題的順序去刷題,會讓人有點錯亂感。對於時間比較緊湊的同學來說,題目的數量比較多,想在較短時間內刷完是一個很大的挑戰。因此,本文針對時間較緊湊的同學精選一些數組類型的代表性題目,進行分類總結,希望能夠起到一點幫助(PS:其實是作者希望藉此進一步加深自己對考點的認知,建立一個有效的知識體系… …)。

       標記爲數組類型的題目有200多道題,本文的重點關注那些主要考察數組的題目。對於考察點主要放在其它考點(比如:二分查找、雙指針、哈希表等)上的題目,作者計劃把這些題目放在後序總結篇章中。按照作者刷題的情況,在屬於數組考點系列的題目中,劃分爲四個常考問題:子數組問題、矩陣問題、O(n)類型問題和思維轉換類型問題。

  • 子數組問題:就是給定一個數組,圍繞該數組的子數組列出諸多難題,等待我們來解答。
  • 矩陣問題:給定一個矩陣(或者稱爲二維數組),圍繞該矩陣列出不同方式遍歷矩陣中元素等難題,等待我們來解答。
  • O(n)類型問題:O(n)是指時間複雜度爲O(n),給定的題目題意一般很容易理解,其一般解法(俗稱暴力解法,時間複雜度一般爲O(n^2),甚至更高)也很簡單,但是題目要求你的解法時間複雜度爲O(n)。看到這些題目的某些解答後,會讓我們忍不住誇讚:真乃神人、好厲害、奇異特解、巧妙、強、優雅。
  • 思維轉換類型問題:其解答不屬於上述三種類型問題,但是解答方式有點巧妙,或者說該類型題目較爲基礎,很可能考察你的快速應用代碼能力的題目。(PS: 其實是作者自己也不好劃分,但是認爲有點價值的題目… …)

       本文是《LeetCode刷題總結-數組篇(上)》,總結歸納有關子數組問題的題目。本期題目數量共17題,其中難度爲簡單有1題,難度爲中等的有12題,難度爲困難的有4題。具體題目信息及解答見下文。

 

例1 最大子序和

題號:53,難度:簡單

題目描述:

 

解題思路:

本題最爲經典和廣泛的解法是應用動態規劃的思想來解答,其時間複雜度爲O(n)。題目中鼓勵嘗試使用更爲精妙的分治法求解,通過翻閱相關解答和評論發現,分治法並沒有動態規劃解答的優雅,其時間複雜度爲O(nlogn),也並不是最優。所以,介紹一下應用動態規劃解題的思路。

從數組第一個元素開始遍歷,用一個一維數組存儲遍歷到當前元素的最大連續子數組的和。

當遍歷到第i個元素時,如果前i-1和元素中連續子數組和加上第i個元素時比第i個元素的值要大,那麼就更新dp[i] = dp[i-1] + nums[i],否則dp[i] = nums[i]。

具體代碼:

class Solution {
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length + 1];
        int result = nums[0];
        for(int i = 0;i < nums.length;i++) {
            dp[i+1] = Math.max(dp[i]+nums[i], nums[i]);
            result = Math.max(dp[i+1], result);
        }
        
        return result;
    }
}

執行結果:

 

例2 乘積最大子序列

題號:152,難度:中等

題目描述:

 

解題思路:

這題其實是例1 最大子序和一個變例,由加法變換成了乘法操作(依舊是應用動態規劃的思路)。此時需要做的改變是定義兩個變量來存儲當前子序列的乘積,一個是保存最大值,一個是保存最小值(包含負數的子序列)。

具體代碼:

class Solution {
    public int maxProduct(int[] nums) {
        int result = nums[0], n_max = 1, n_min = 1;
        for(Integer n: nums) {
            if(n < 0) {
                int temp = n_max;
                n_max = Math.max(n_min * n, n);
                n_min = Math.min(temp * n, n);
            } else {
                n_max = Math.max(n_max * n, n);
                n_min = Math.min(n_min * n, n);
            }
               
            result = Math.max(n_max, result);
        }
        
        return result;
    }
}

執行結果:

 

例3 子集

題號:78,難度:中等。(可參考子集II, 題號90,難度:中等)

題目描述:

 

解題思路:

本題考查我們應用回溯來求解所有子集的問題,在一些算法教材中最經典的問題時求解全排列的問題,解法和這道題類似。

此題需要特別注意的是,首先採用鏈表在遞歸過程中添加元素,在回溯時刪除元素,能夠有效提高時間效率。其次,給遞歸調用程序設計一個start參數,可以避免同一個元素被重複遞歸調用,達到了剪枝效果。

最後,在結果列表中採用重新創建一個列表存儲子集的結果,是因爲在遞歸函數中列表參數只對應一個地址,採用重新創建相當於應用了深拷貝的思想,避免了結果均爲空集的情況。

具體代碼:

class Solution {
    private List<List<Integer>> result;
    
    public List<List<Integer>> subsets(int[] nums) {
        result = new ArrayList<>();
        if(nums.length <= 0)
            return result;
        dfs(nums, 0, new LinkedList<Integer>());
        
        return result;
    }
    
    public void dfs(int[] nums, int start, LinkedList<Integer> list) {
        result.add(new ArrayList<Integer>(list));
        for(int i = start;i < nums.length;i++) {
            list.addLast(nums[i]);
            dfs(nums, i + 1, list);
            list.removeLast();
        }
    }
}

執行結果:

 

例4 最長連續序列

題號:128,難度:困難

題目描述:

 

解題思路:

採用哈希表存儲數組中所有元素,然後應用哈希表查詢當前元素的左右兩邊序列數字是否存在,查詢操作的時間複雜度爲O(1),所以整體的時間複雜度爲O(n)。

具體代碼:

class Solution {
    public int longestConsecutive(int[] nums) {
        int result = 0;
        Set<Integer> set = new HashSet<>();
        for(Integer n: nums)
            set.add(n);
        for(Integer n: nums) {
            if(set.contains(n)) {
                int len = 1;
                int temp = n;
                while(set.contains(--temp)) {
                    len++;
                    set.remove(temp);
                }
                temp = n;
                while(set.contains(++temp)) {
                    len++;
                    set.remove(temp);
                }
                result = Math.max(result, len);
            } 
        }
        
        return result;
    }
}

執行結果:

 

例5 乘積小於K的子數組

題號:713,難度:中等

題目描述:

 

解題思路:

本題考查應用雙指針的思想,一前一後同時往後遍歷。

具體代碼:

class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        int result = 0, left = 0, right = 0;
        int target = 1;
        while(right < nums.length) {
            target *= nums[right++];
            while(left < right && target >= k)
                target = target / nums[left++];
            result += (right - left);     
        }
      
        return result;
    }
}

執行結果:

 

例6 和爲K的子數組

題號:560,難度:中等

題目描述:

 

解題思路:

本題採用哈希表存儲從數組第一個元素不斷往後的子序列和,然後判斷到當前元素的序列總和減去K的值在哈希表中有多少個,即爲包含當前元素的子序列可以得到目標結果,利用前後子序列的差可以得到目標子序列和爲K。

具體代碼:

class Solution {
    public int subarraySum(int[] nums, int k) {     
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        int sum = 0, result = 0;

        for(int i = 0; i < nums.length; ++i) {
            sum += nums[i];
            if(map.containsKey(sum-k))
                result += map.get(sum-k);
            map.put(sum, map.getOrDefault(sum, 0)+1);
        }
        
        return result;
    }
}

執行結果:

 

例7 可被K整除的子數組

題號:974,難度:中等

題目描述:

 

解題思路:

從第一個元素開始,求取連續子數組的餘數(sum % k),採用Map存儲每個餘數的個數。

相同餘數的子數組個數大於等於2時,任意選取其中兩個子數組餘數相減,即餘數抵消,可得到一個符合題目要求的sum。(此處的個數計算方式爲:n*(n-1) / 2)

但是,此處有兩個需要注意的點:

(1) 如果餘數爲0,最終0的餘數個數只有一個時(1*(1-1)/2 = 0),這樣計算會漏掉(如果爲多個,也會有遺漏,可以自己計算,可以自己稍微琢磨)。所以,在初始化Map時,添加以下代碼:

map.put(0, 1); 

(2) 如果餘數爲負數,就不能執行相同餘數相減抵消的操作。此時,需要做以下處理:

// sum % K 正常計算方法

((sum % K) + K) % K   // 如果爲負數時,需要轉換爲正數,這個轉換原

具體代碼:

class Solution {
    public int subarraysDivByK(int[] A, int K) {
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        int result = 0;
        int sum = 0;
        
        for(Integer a: A) {
            sum += a;
            map.put(((sum % K) + K) % K , map.getOrDefault(((sum % K) + K) % K, 0)+1);
        }      
        // System.out.println("map = "+map);
        for(Integer key: map.keySet())
            result += map.get(key) * (map.get(key) - 1) / 2;
        
        return result;
    }
}

執行結果:

 

例8 三個無重疊子數組的最大和

題號:689,難度:困難

題目描述:

 

解題思路:

採用動態規劃求解,狀態轉移方程:dp[2][n] = max(dp[2][n-1], dp[1][n-k] + sumRange(n, n -k+1))。其中一維長度爲3,表示三個子數組。

具體代碼(代碼引用自LeetCode的一個題解):

class Solution {
     public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
        int[][] dp = new int[3][nums.length];
        int[] cummulative = new int[nums.length];
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
            cummulative[i] = sum;
        }
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < nums.length; j++) {
                if (j < (i + 1) * k - 1) {
                    dp[i][j] = 0;
                } else {
                    if (i == 0) {
                        // 易錯點: 當k=1的時候,邊界條件需要處理一下。
                        dp[i][j] = Math.max(j > 0 ? dp[i][j - 1] : 0, rangeSum(cummulative, j - k + 1, j));
                    } else {
                        dp[i][j] = Math.max(j > 0 ? dp[i][j - 1]: 0, rangeSum(cummulative, j - k + 1, j) + dp[i - 1][j - k]);
                    }
                }

            }
        }
        int[] ans = new int[3];
        int length = dp[2].length - 1;
        for (int i = 2; i >= 0; i--) {
            int[] row = dp[i];
            for (int j = length - 1; j >= 0; j--) {
                if (row[j] != row[length]) {
                    ans[i] = j - k + 2;
                    length = j - k + 1;
                    break;
                }
            }
        }
        return ans;
    }

    private int rangeSum(int[] cummulative, int left, int right) {
        if (left == 0) {
            return cummulative[right];
        } else {
            return cummulative[right] - cummulative[left - 1];
        }
    }

}

執行結果:

 

例9 最長重複子數組

題號:718,難度:中等

題目描述:

 

解題思路:

本題既可以用哈希表來解答,也可以用動態規劃的思想來解答。應用動態規劃的思路解答的時間效率最高。此處介紹一下動態規劃的解題思路。dp[i][j]表示A [i-1]爲終點,B[j-1]爲終點時兩者的最長公共子數組。具體更新策略見代碼。

具體代碼:

class Solution {
      public int findLength(int[] A, int[] B) {
        
        int[][] dp = new int[A.length + 1][B.length + 1];
        int res = 0;
        for (int i = 1; i <= A.length; i++)
            for (int j = 1; j <= B.length; j++) {
                
                if (A[i - 1] == B[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                
                res = Math.max(res, dp[i][j]);
            }
        
        return res;
    }
}

執行結果:

 

例10 匹配子序列的單詞數

題號:792,難度:中等

題目描述:

 

解題思路:

要特別注意子序列的含義,子序列是按照從前往後的順序任意多個元素組成的序列,其中的順序不能更改。因此,不能應用哈希表統計字母的個數來判斷是否包含某個單詞。此處可採用暴力法直接匹配查找,時間效率較低。此處可採用二分查找來優化匹配結果,能提高時間效率。

具體代碼(貼一個LeetCode上評論的代碼):

class Solution {
    List<Integer> index[]=new ArrayList[26];
    
    public int numMatchingSubseq(String S, String[] words) {
         for(int i=0;i<S.length();i++){
            char ch=S.charAt(i);
            if(index[ch-'a']==null)
                index[ch-'a']=new ArrayList();
            index[ch-'a'].add(i);
         }
         int res=0,pre;
         for(String str:words){
            pre=-1;
            for(int i=0;i<str.length();i++){
                pre=helper(str.charAt(i)-'a',pre);
                if(pre==-1)
                    break;
            }
            if(pre!=-1)
                res++;
         }
         return res;
    }
    
    private int helper(int i,int pre){
        if(index[i]==null)
            return -1;
        
        int l=0,r=index[i].size()-1;
        if(pre==-1)
            return index[i].get(0);
        if(index[i].get(r)<=pre)
            return -1;
        
        while(l<r){
            int mid=(l+r)/2;
            if(index[i].get(mid)<=pre)
                l=mid+1;
            else
                r=mid;
        }
        
        return index[i].get(l);
   }
}

執行結果:

 

例11 區間子數組個數

題號:795, 難度:中等

題目描述:

 

解題思路:

最大元素滿足大於等於L小於等於R的子數組個數 = 最大元素小於等於R的子數組個數 - 最大元素小於L的子數組個數。

具體代碼:

class Solution {
    public int numSubarrayBoundedMax(int[] A, int L, int R) {
        return numSubarrayBoundedMax(A, R) - numSubarrayBoundedMax(A, L - 1);
    }

    private int numSubarrayBoundedMax(int[] A, int Max) {
        int res = 0;
        int numSubarry = 0;
        for (int num : A) {
            if (num <= Max) {
                numSubarry++;
                res += numSubarry;
            } else {
                numSubarry = 0;
            }
        }
        return res;
    }
}

執行結果:

 

例12 子數組的最小值之和

題號:907,難度:中等

題目描述:

 

解題思路:

參考自LeetCode的評論解答:計算每個數在子數組中最小的次數。

具體代碼:

class Solution {
    public int sumSubarrayMins(int[] A) {
        long res = 0;
        long mod = 1000000007;
        for (int i = 0; i<A.length; i++) {
            int l = i-1;
            for (; l>=0 && A[i] < A[l]; l--) ;
            int r = i+1;
            for (; r<A.length && A[i] <= A[r]; r++) ;
            
            res += (i-l)*(r-i)*A[i];
        }
        return (int)(res % mod);
    }
}

執行結果:

 

例13 子序列寬度之和

題號:891,難度:困難

題目描述:

 

解題思路:

具體可參考LeetCode的一篇題解

具體代碼:

class Solution {
    public int sumSubseqWidths(int[] A) {
        final int MOD = (int) (1e9 + 7);
        Arrays.sort(A);
        int n = A.length;
        long res = 0;
        long p = 1;
        for (int i = 0; i < n; ++i) {
            res = (res + (A[i] - A[n - 1 - i]) * p) % MOD;
            p = (p << 1) % MOD;
        }
        return (int) ((res + MOD) % MOD);
    }
}

執行結果:

 

例14 環形子數組的最大和

題號:918, 難度:中等

題目描述:

 

解題思路:

因爲題目要求有環形,所以需要定義兩個變量。一個變量存儲當前無環形是的連續最大子數組和,一個存儲無環形連續最小子數組和。最後採用數組的總和減去最小和,和已經保存的最大和進行比較。另外,需要注意一點如果數組全部爲負數時,此時直接返回子數組的最大值(因爲此時,最小子數組和就是數組的和)。

具體代碼:

class Solution {
    public int maxSubarraySumCircular(int[] A) {
        int max = A[0];
        int min = A[0];
        int maxSoFar = A[0];
        int minSoFar = A[0];
        int sum = A[0];
        for (int i=1;i<A.length;i++) {
          sum += A[i];
          maxSoFar = Math.max(A[i],maxSoFar+A[i]);
          minSoFar = Math.min(A[i],minSoFar+A[i]);
          max = Math.max(max,maxSoFar);
          min = Math.min(min,minSoFar);
        }
        if (max < 0) 
            return max;
        return Math.max(max,sum-min);
    }
}

執行結果:

 

例15 最長湍流子數組

題號:978,難度:中等

題目描述:

 

解題思路:

採用連續三個位置數據是否符合湍流特徵來判斷,時間複雜度爲O(n)。

具體代碼(引用自LeetCode一個評論代碼):

class Solution {
    public int maxTurbulenceSize(int[] A) {
        int N = A.length;
        int ans = 1;
        int anchor = 0;

        for (int i = 1; i < N; ++i) {
            int c = Integer.compare(A[i-1], A[i]);
            if (i == N-1 || c * Integer.compare(A[i], A[i+1]) != -1) {
                if (c != 0) ans = Math.max(ans, i - anchor + 1);
                anchor = i;
            }
        }

        return ans;
    }
}

執行結果:

 

例16 兩個非重疊子數組的最大和

題號:1031,難度:中等

題目描述:

 

解題思路:

採用滑動窗口的思路來解答,對長度爲L的數組,採用大小爲L的滑動窗口,對於長度爲M的數組採用大小爲M的窗口。然後,通過兩個窗口之間的距離來遍歷。

具體代碼:

class Solution {
    public  int maxSumTwoNoOverlap(int[] A, int L, int M) {
        int len = A.length, dpL[] = new int[len - L + 1], dpM[] = new int[len - M + 1], max = 0;
        for (int i = 0; i < L; i++)
            dpL[0] += A[i];
        for (int i = 0; i < M; i++)
            dpM[0] += A[i];
        for (int i = 1; i < len - L + 1; i++)
            dpL[i] = dpL[i - 1] + A[i + L - 1] - A[i - 1];
        for (int i = 1; i < len - M + 1; i++)
            dpM[i] = dpM[i - 1] + A[i + M - 1] - A[i - 1];
        for (int i = 0; i < len - L - M + 1; i++) {
            int count = len - i - L - M;
            while (count >= 0) {
                max = Math.max(max, Math.max(dpL[i] + dpM[i + L + count], dpM[i] + dpL[i + M + count]));
                count--;
            }
        }
        return max;
    }
}

執行結果:

 

例17 子數組中佔絕大多數的元素

題號:1157,難度:困難

題目描述:

 

解題思路:

採用哈希數組來解答,一旦哈希數組中目標元素值大於等於threshold,就返回目標數字,否則返回-1。

具體代碼:

class MajorityChecker {

    private int[] nums;
    private int[] ans;
    private int max;
    
    public MajorityChecker(int[] arr) {
        nums = arr;
        max = arr[0];
        for(int x : arr)
            if(x > max)
                max = x;
        
    }
    
    public int query(int left, int right, int threshold) {
        ans = new int[max + 5];
        for(int i = left;i <= right;i++){
            if(++ans[nums[i]] >= threshold)
                return nums[i];
        }
        return -1;
    }
}

執行結果:

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章