刷leetcode——和為K的子數組

這道題主要是找規律,優化的時候可以利用哈希表和數組的特性。

原題

給定一個整數數組和一個整數 k,你需要找到該數組中和為 k 的連續的子數組的個數。

示例 1 :

<code>輸入:nums = [1,1,1], k = 2輸出: 2 , [1,1] 與 [1,1] 為兩種不同的情況。/<code>

說明 :

  1. 數組的長度為 [1, 20,000]。
  2. 數組中元素的範圍是 [-1000, 1000] ,且整數 k 的範圍是 [-1e7, 1e7]。

原題url:https://leetcode-cn.com/problems/subarray-sum-equals-k/

解題

一開始的想法肯定就是利用暴力解法了,三層 for 循環的那種,第1層和第2層選擇起點和終點,第3層則是計算起點到終點的總和。這樣的想法最簡單,但很可惜,直接超時了,讓我們稍微優化一下。

子數組之和

因為題目要求子數組下標連續,那麼我們想想,如果要求sum(i, j)(也就是從下標 i 到下標 j 的子數組之和),其實可以轉化成sum(0, i - 1) - sum(0, j)。這樣的話,就可以把上面的三層for循環優化成兩層。

接下來我們直接看看代碼:

<code>class Solution {    public int subarraySum(int[] nums, int k) {        // sum(i, j) = sum(0, j) - sum(0, i - 1)        int[] sumArray = new int[nums.length];        sumArray[0] = nums[0];        for (int i = 1; i < nums.length; i++) {            sumArray[i] = sumArray[i - 1] + nums[i];        }        int result = 0;        int sum = 0;        for (int i = sumArray.length - 1; i >= 0; i--) {            // 前i個數之和            if (sumArray[i] == k) {                result++;            }            // 查詢從(j + 1)到i的和            for (int j = i - 1; j >= 0 && j != i; j--) {                sum = sumArray[i] - sumArray[j];                if (sum == k) {                    result++;                }            }        }        return result;    }}/<code>

提交OK,但時間複雜度是O(n^2),優化一下。

用哈希表優化

我們想想,上面使用使用第二層for循環,主要是為了查出 sumArray 中是否還存在等於sumArray[i] - k的數,這明顯是一個映射關係,因此我們用一個 map 去記錄中間的求和結果。

<code>class Solution {    public int subarraySum(int[] nums, int k) {        // sum(i, j) = sum(0, j) - sum(0, i - 1)        // 用map存儲,key為sum,value為第i個數        Map<integer>> map = new HashMap<>(nums.length * 4 / 3 + 1);        // 數組求和        int sum = 0;        // 記錄一共有哪些和        Set<integer> sumSet = new HashSet<>();        for (int i = 0; i < nums.length; i++) {            sum += nums[i];            sumSet.add(sum);            // 查看當前是否已經記錄            List<integer> indexList = map.get(sum);            if (indexList == null) {                indexList = new LinkedList<>();            }            indexList.add(i);            map.put(sum, indexList);        }        int result = 0;        for (Integer subSum : sumSet) {            List<integer> list = map.get(subSum);            // 如果list為null,說明是被移除了,說明之前已經計算過            if (list == null) {                continue;            }            if (subSum == k) {                result += map.get(subSum).size();            }            // 剩餘的和            int remainSum = subSum - k;            List<integer> remainList = map.get(remainSum);            // 如果為null,說明不存在            if (remainList == null) {                continue;            }            // 如果不為null,說明存在,則可以進行配對            // 讓list和remainList進行配對            for (int index1 : list) {                for (int index2 : remainList) {                    if (index1 <= index2) {                        continue;                    }                    result++;                }            }        }        return result;    }}/<integer>/<integer>/<integer>/<integer>/<integer>/<code>

提交OK,雖然較之前的方法時間效率上有所提高,但並沒有本質區別。特別是最後的雙重 for 循環,因為下標只有大的減小的才有意義,這樣也就給自己額外增加了運算。

那麼反思一下,是否真的有必要提前算好子數組的和?如果一邊遍歷一邊求和,並將求和的結果存入map中,那麼 map 中存在的,一定是下標小於自己的求和結果。針對這點,我們可以再做一步優化:

<code>public class Solution {    public int subarraySum(int[] nums, int k) {        // 最終結果的數量        int count = 0;        // 求和        int sum = 0;        // key為中間求出了哪些和,value為當前和有幾種情況        HashMap<integer> map = new HashMap<>();        // 和為0有1中情況,就是一個數都不選        map.put(0, 1);        // 遍歷數組        for (int i = 0; i < nums.length; i++) {            // 求和            sum += nums[i];            // map中是否有記錄剩餘的值            if (map.containsKey(sum - k)) {                // 累加,此處可以直接添加,是因為求和是從前往後進行的,現在能找到的,說明一定是之前的                count += map.get(sum - k);            }            // 記錄當前的值            map.put(sum, map.getOrDefault(sum, 0) + 1);        }        return count;    }}/<integer>/<code>

提交OK,這樣時間複雜度就變為了O(n)。

數據結構的優化

現在,我們用哈希表來存儲中間結果。雖然哈希表的查找效率理論上可以達到O(1),但考慮到哈希衝突,最壞情況下還是有可能達到O(n)。真正能夠保證達到O(1)的數據結構,是數組(用空間換取時間)。

那這個用來存儲的一維數組究竟長度該設置為多少呢?自然就是找出數組中子數組之和的最大值和最小值,兩者求差,結果就是最終的數組長度。利用這個數組去存儲子數組求和的結果,這樣就能保證在查找時的效率了。

<code>class Solution {    public int subarraySum(int[] nums, int k) {        int sum = 0;        // sum的最小值和最大值,因為可以一個數值都不選,因此0作為初始值        int min = 0;        int max = 0;        // 求和        for (int n : nums) {            sum += n;            min = Math.min(min, sum);            max = Math.max(max, sum);        }        // sums[i]代表從下標為0到下標為i的子數組之和        // 用一個數組存儲,相比於map,取值更快,用空間換取時間        int[] sums = new int[max - min + 1];        // 最終結果        int count = 0;        sum = 0;        // 遍歷數組        // 需要重新求和,因為沒有類似set這樣的結構存儲了結果        for (int n : nums) {            // 求和            sum += n;            // 新的目標值            int target = sum - min - k;            // 是否有超過範圍            if (target >= 0 && target < sums.length) {                count += sums[target];            }            sums[sum - min]++;        }        // 再加上本身就是k的子數組的數量        if (k - min >= 0 && k - min < sums.length) {            count += sums[k - min];        }        return count;    }}/<code> 

提交OK,執行時間上更快了。

總結


以上就是這道題目我的解答過程了,不知道大家是否理解了。這道題主要是找規律,優化的時候可以利用哈希表和數組的特性。

有興趣的話可以訪問我的博客或者關注我的公眾號、頭條號,說不定會有意外的驚喜。

https://death00.github.io/


分享到:


相關文章: