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

  • 2019 年 11 月 3 日
  • 筆記

       數組是演算法中最常用的一種數據結構,也是面試中最常考的考點。在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;      }  }

執行結果: