一個函數秒殺 2Sum 3Sum 4Sum 問題

2021-03-02 數據結構與算法
經常刷 LeetCode 的讀者肯定知道鼎鼎有名的 twoSum 問題,我們的舊文 Two Sum 問題的核心思想 對 twoSum 的幾個變種做了解析。
但是除了 twoSum 問題,LeetCode 上面還有 3Sum,4Sum 問題,我估計以後出個 5Sum,6Sum 也不是不可能。那麼,對於這種問題有沒有什麼好辦法用套路解決呢?本文就由淺入深,層層推進,用一個函數來解決所有 nSum 類型的問題。一、twoSum 問題力扣上的 twoSum 問題,題目要求返回的是索引,這裡我來編一道 twoSum 題目,不要返回索引,返回元素的值:如果假設輸入一個數組 nums 和一個目標和 target,請你返回 nums 中能夠湊出 target 的兩個元素的值,比如輸入 nums = [5,3,1,6], target = 9,那麼算法返回兩個元素 [3,6]。可以假設只有且僅有一對兒元素可以湊出 target。我們可以先對 nums 排序,然後利用前文「雙指針技巧匯總」寫過的左右雙指針技巧,從兩端相向而行就行了:

vector<int> twoSum(vector<int>& nums, int target) {
    // 先對數組排序
    sort(nums.begin(), nums.end());
    // 左右指針
    int lo = 0, hi = nums.size() - 1;
    while (lo < hi) {
        int sum = nums[lo] + nums[hi];
        // 根據 sum 和 target 的比較,移動左右指針
        if (sum < target) {
            lo++;
        } else if (sum > target) {
            hi--;
        } else if (sum == target) {
            return {nums[lo], nums[hi]};
        }
    }
    return {};
}

這樣就可以解決這個問題,不過我們要繼續魔改題目,把這個題目變得更泛化,更困難一點:nums 中可能有多對兒元素之和都等於 target,請你的算法返回所有和為 target 的元素對兒,其中不能出現重複

vector<vector<int>> twoSumTarget(vector<int>& nums, int target);

比如說輸入為 nums = [1,3,1,2,2,3], target = 4,那麼算法返回的結果就是:[[1,3],[2,2]]。對於修改後的問題,關鍵難點是現在可能有多個和為 target 的數對兒,還不能重複,比如上述例子中 [1,3] 和 [3,1] 就算重複,只能算一次。

vector<vector<int>> twoSumTarget(vector<int>& nums, int target {
    // 先對數組排序
    sort(nums.begin(), nums.end());
    vector<vector<int>> res;
    int lo = 0, hi = nums.size() - 1;
    while (lo < hi) {
        int sum = nums[lo] + nums[hi];
        // 根據 sum 和 target 的比較,移動左右指針
        if      (sum < target) lo++;
        else if (sum > target) hi--;
        else {
            res.push_back({lo, hi});
            lo++; hi--;
        }
    }
    return res;
}

但是,這樣實現會造成重複的結果,比如說 nums = [1,1,1,2,2,3,3], target = 4,得到的結果中 [1,3] 肯定會重複。出問題的地方在於 sum == target 條件的 if 分支,當給 res 加入一次結果後,lo 和 hi 不應該改變 1 的同時,還應該跳過所有重複的元素:所以,可以對雙指針的 while 循環做出如下修改:

while (lo < hi) {
    int sum = nums[lo] + nums[hi];
    // 記錄索引 lo 和 hi 最初對應的值
    int left = nums[lo], right = nums[hi];
    if (sum < target)      lo++;
    else if (sum > target) hi--;
    else {
        res.push_back({left, right});
        // 跳過所有重複的元素
        while (lo < hi && nums[lo] == left) lo++;
        while (lo < hi && nums[hi] == right) hi--;
    }
}

這樣就可以保證一個答案只被添加一次,重複的結果都會被跳過,可以得到正確的答案。不過,受這個思路的啟發,其實前兩個 if 分支也是可以做一點效率優化,跳過相同的元素:

vector<vector<int>> twoSumTarget(vector<int>& nums, int target) {
    // nums 數組必須有序
    sort(nums.begin(), nums.end());
    int lo = 0, hi = nums.size() - 1;
    vector<vector<int>> res;
    while (lo < hi) {
        int sum = nums[lo] + nums[hi];
        int left = nums[lo], right = nums[hi];
        if (sum < target) {
            while (lo < hi && nums[lo] == left) lo++;
        } else if (sum > target) {
            while (lo < hi && nums[hi] == right) hi--;
        } else {
            res.push_back({left, right});
            while (lo < hi && nums[lo] == left) lo++;
            while (lo < hi && nums[hi] == right) hi--;
        }
    }
    return res;
}

這樣,一個通用化的 twoSum 函數就寫出來了,請確保你理解了該算法的邏輯,我們後面解決 3Sum 和 4Sum 的時候會復用這個函數。這個函數的時間複雜度非常容易看出來,雙指針操作的部分雖然有那麼多 while 循環,但是時間複雜度還是 O(N),而排序的時間複雜度是 O(NlogN),所以這個函數的時間複雜度是 O(NlogN)。二、3Sum 問題題目就是讓我們找 nums 中和為 0 的三個元素,返回所有可能的三元組(triple),函數籤名如下:

vector<vector<int>> threeSum(vector<int>& nums);

這樣,我們再泛化一下題目,不要光和為 0 的三元組了,計算和為 target 的三元組吧,同上面的 twoSum 一樣,也不允許重複的結果:

vector<vector<int>> threeSum(vector<int>& nums) {
    // 求和為 0 的三元組
    return threeSumTarget(nums, 0);
}

vector<vector<int>> threeSumTarget(vector<int>& nums, int target) {
    // 輸入數組 nums,返回所有和為 target 的三元組
}

這個問題怎麼解決呢?很簡單,窮舉唄。現在我們想找和為 target 的三個數字,那麼對於第一個數字,可能是什麼?nums 中的每一個元素 nums[i] 都有可能!那麼,確定了第一個數字之後,剩下的兩個數字可以是什麼呢?其實就是和為 target - nums[i] 的兩個數字唄,那不就是 twoSum 函數解決的問題麼🤔可以直接寫代碼了,需要把 twoSum 函數稍作修改即可復用:

/* 從 nums[start] 開始,計算有序數組
 * nums 中所有和為 target 的二元組 */
vector<vector<int>> twoSumTarget(
    vector<int>& nums, int start, int target) {
    // 左指針改為從 start 開始,其他不變
    int lo = start, hi = nums.size() - 1;
    vector<vector<int>> res;
    while (lo < hi) {
        ...
    }
    return res;
}

/* 計算數組 nums 中所有和為 target 的三元組 */
vector<vector<int>> threeSumTarget(vector<int>& nums, int target) {
    // 數組得排個序
    sort(nums.begin(), nums.end());
    int n = nums.size();
    vector<vector<int>> res;
    // 窮舉 threeSum 的第一個數
    for (int i = 0; i < n; i++) {
        // 對 target - nums[i] 計算 twoSum
        vector<vector<int>> 
            tuples = twoSumTarget(nums, i + 1, target - nums[i]);
        // 如果存在滿足條件的二元組,再加上 nums[i] 就是結果三元組
        for (vector<int>& tuple : tuples) {
            tuple.push_back(nums[i]);
            res.push_back(tuple);
        }
        // 跳過第一個數字重複的情況,否則會出現重複結果
        while (i < n - 1 && nums[i] == nums[i + 1]) i++;
    }
    return res;
}

需要注意的是,類似 twoSum,3Sum 的結果也可能重複,比如輸入是 nums = [1,1,1,2,3], target = 6,結果就會重複。關鍵點在於,不能讓第一個數重複,至於後面的兩個數,我們復用的 twoSum 函數會保證它們不重複。所以代碼中必須用一個 while 循環來保證 3Sum 中第一個元素不重複。至此,3Sum 問題就解決了,時間複雜度不難算,排序的複雜度為 O(NlogN),twoSumTarget 函數中的雙指針操作為 O(N),threeSumTarget 函數在 for 循環中調用 twoSumTarget 所以總的時間複雜度就是 O(NlogN + N^2) = O(N^2)。三、4Sum 問題

vector<vector<int>> fourSum(vector<int>& nums, int target);

都到這份上了,4Sum 完全就可以用相同的思路:窮舉第一個數字,然後調用 3Sum 函數計算剩下三個數,最後組合出和為 target 的四元組。

vector<vector<int>> fourSum(vector<int>& nums, int target) {
    // 數組需要排序
    sort(nums.begin(), nums.end());
    int n = nums.size();
    vector<vector<int>> res;
    // 窮舉 fourSum 的第一個數
    for (int i = 0; i < n; i++) {
        // 對 target - nums[i] 計算 threeSum
        vector<vector<int>> 
            triples = threeSumTarget(nums, i + 1, target - nums[i]);
        // 如果存在滿足條件的三元組,再加上 nums[i] 就是結果四元組
        for (vector<int>& triple : triples) {
            triple.push_back(nums[i]);
            res.push_back(triple);
        }
        // fourSum 的第一個數不能重複
        while (i < n - 1 && nums[i] == nums[i + 1]) i++;
    }
    return res;
}

/* 從 nums[start] 開始,計算有序數組
 * nums 中所有和為 target 的三元組 */
vector<vector<int>> 
    threeSumTarget(vector<int>& nums, int start, int target) {
        int n = nums.size();
        vector<vector<int>> res;
        // i 從 start 開始窮舉,其他都不變
        for (int i = start; i < n; i++) {
            ...
        }
        return res;

這樣,按照相同的套路,4Sum 問題就解決了,時間複雜度的分析和之前類似,for 循環中調用了 threeSumTarget 函數,所以總的時間複雜度就是 O(N^3)。四、100Sum 問題?在 LeetCode 上,4Sum 就到頭了,但是回想剛才寫 3Sum 和 4Sum 的過程,實際上是遵循相同的模式的。我相信你只要稍微修改一下 4Sum 的函數就可以復用並解決 5Sum 問題,然後解決 6Sum 問題……那麼,如果我讓你求 100Sum 問題,怎麼辦呢?其實我們可以觀察上面這些解法,統一出一個 nSum 函數:

/* 注意:調用這個函數之前一定要先給 nums 排序 */
vector<vector<int>> nSumTarget(
    vector<int>& nums, int n, int start, int target) {

    int sz = nums.size();
    vector<vector<int>> res;
    // 至少是 2Sum,且數組大小不應該小於 n
    if (n < 2 || sz < n) return res;
    // 2Sum 是 base case
    if (n == 2) {
        // 雙指針那一套操作
        int lo = start, hi = sz - 1;
        while (lo < hi) {
            int sum = nums[lo] + nums[hi];
            int left = nums[lo], right = nums[hi];
            if (sum < target) {
                while (lo < hi && nums[lo] == left) lo++;
            } else if (sum > target) {
                while (lo < hi && nums[hi] == right) hi--;
            } else {
                res.push_back({left, right});
                while (lo < hi && nums[lo] == left) lo++;
                while (lo < hi && nums[hi] == right) hi--;
            }
        }
    } else {
        // n > 2 時,遞歸計算 (n-1)Sum 的結果
        for (int i = start; i < sz; i++) {
            vector<vector<int>> 
                sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
            for (vector<int>& arr : sub) {
                // (n-1)Sum 加上 nums[i] 就是 nSum
                arr.push_back(nums[i]);
                res.push_back(arr);
            }
            while (i < sz - 1 && nums[i] == nums[i + 1]) i++;
        }
    }
    return res;
}

嗯,看起來很長,實際上就是把之前的題目解法合併起來了,n == 2 時是 twoSum 的雙指針解法,n > 2 時就是窮舉第一個數字,然後遞歸調用計算 (n-1)Sum,組裝答案。需要注意的是,調用這個 nSum 函數之前一定要先給 nums 數組排序,因為 nSum 是一個遞歸函數,如果在 nSum 函數裡調用排序函數,那麼每次遞歸都會進行沒有必要的排序,效率會非常低。比如說現在我們寫 LeetCode 上的 4Sum 問題:

vector<vector<int>> fourSum(vector<int>& nums, int target) {
    sort(nums.begin(), nums.end());
    // n 為 4,從 nums[0] 開始計算和為 target 的四元組
    return nSumTarget(nums, 4, 0, target);
}

再比如 LeetCode 的 3Sum 問題,找 target == 0 的三元組:

vector<vector<int>> threeSum(vector<int>& nums) {
    sort(nums.begin(), nums.end());
    // n 為 3,從 nums[0] 開始計算和為 0 的三元組
    return nSumTarget(nums, 3, 0, 0);        
}

那麼,如果讓你計算 100Sum 問題,直接調用這個函數就完事兒了。

相關焦點

  • 秒殺2Sum 3Sum 4Sum 算法題
    2 Sum這道題題意就是,給一個數組和一個目標值,讓你在這個數組裡找到兩個數,使得它倆之和等於這個目標值的。比如題目中給的例子,目標值是 9,然後數組裡 2 + 7 = 9,於是返回 2 和 7 的下標。
  • sum函數應用教程
    很多初學者對sum函數不屑一顧,覺得sum函數不過爾爾。sum函數語法雖然簡單,但是功能十分強大。當你深挖其內涵時,你會被sum函數的博大精深所震動。我們要深挖每個函數的功能和應用,才能深刻體會每個函數的魅力。 Sum函數語法:.sum(number1,number2....),為 1 到 254 個需要求和的參數。 Sum函數規則如下: 1、邏輯值及數字的文本表達式將被計算。
  • sumproduct函數的使用方法 sumproduct函數怎麼使用
    excel函數有個函數身兼數職那就是sumproduct函數,它有SUM、PRODUCT、COUNTIF、SUMIF、SUMIFS等函數的功能,下面我們就一起來學習sumproduct函數的使用方法吧。
  • python的內置函數:sum求和函數
    前言看到sum,我們就知道這是一個求和函數,無論是java、javascript還是mysql中,求和是簡單的,但在python中,可以進行一些複雜的元組求和,具體是怎樣的呢?python中的sum求和函數1.sum的使用語法sum(iterable[, start]) iterable -- 可迭代對象,如:列表、元組、集合。
  • sum() 函數的妙用
    2, 3], [4, 5]]# 想得到結果:newlist = [1, 2, 3, 4, 5]原始數據是一個二維列表,目的是獲取該列表中所有元素的具體值。F 同學貢獻了一個思路:# 方法三,巧用sum:newlist = sum(oldlist,[])說實話,這個方法令我大感意外!sum() 函數不是用於求和的麼?怎麼竟然有此用法?這個寫法利用了什麼原理呢?由於我開始時不知道 sum() 函數可以接收兩個參數,不清楚它們是怎麼用於計算的,所以一度很困惑。
  • MySQL函數sum使用場景解讀
    文章簡介 今天分享一下MySQL中的sum函數使用。該函數已經成為大家操作MySQL資料庫中時常用到的一個函數,這個函數統計滿足條件行中指定列的和,想必肯定大家都知道了,本身就沒什麼講頭了,這篇文章主要是通過幾個小案例深入了解一下該函數,以及在做MySQL查詢時如何使用sum函數做優化。
  • SUM函數,從入門到精通
    相信很多表親都學過sum函數,就覺得sum函數很簡單。其實很多事情不過是你覺得很簡單罷了,sum函數內涵比較深刻。今天我們一起來學習sum函數。01函數語法我們來看官方說明:SUM函數是一個數學和三角函數,可將值相加。可以將單個值、單元格引用或是區域相加,或者將三者的組合相加。(1)語法:SUM(number1,[number2],...)
  • Sum函數的4個巧妙用法
    1、小計求和對小計行求和,一般是=小計1+小計2+小計3...有多少小計行加多少次。換換一種思路,總計行=(所有明細行+小計行)/2,所以公式可以簡化為:=SUM(C2:C11)/22、多表求和多個工作表如果格式完全相同,可以用sum函數的多表求和功能。如下圖所示,要求在匯總表裡設置合計公式,匯總前19個工作表B列的和。
  • EXCEL函數學習:SUM函數
    今天來學習一個EXCEL的函數,SUM求和函數SUM函數是大家比較熟悉的一個函數,主要是計算某一區域所有數字的和。語法:SUM(number1,number2, ……) Number1,Number2, …… 求和的參數。有些頭疼,還是案例來學習快一些。來個小學生的題目:4+5=?
  • excel中sum函數小結運用
    之前發布的文章的中介紹了if、sumif、包括表示邏輯與and和*的運用,今天給大家介紹一個sum的小結運用。可能有些朋友會說sum就是一個求和函數,小學的時候就會用,還介紹幹嘛?你說對,sum函數是一個求和函數,不過還請你忍住聒噪之言,經過今天的介紹,也許會給你帶來對sum函數全新的認識。
  • Excel中sum函數使用方法
    Sum函數可以說是Excel中最簡單的一個函數了,作用是將數據進行求和。你可以將多個值或者區域進行組合相加。語法是 SUM(number1,[number2],...)函數參數說明:number1:這是必需的參數,這個參數可以是阿拉伯數字,單元格引用(例如A2等)或者是A1:A3這樣的單元格範圍。number2-255:這個是可選參數,可以按照這種方式輸入255個參數。
  • 電子表格sum函數別樣運用
    在以前的文章給大家介紹了有關average函數的別樣的運用,相信大家對函數的使用不僅僅只是停留在原始的使用層面上,有些問題或是換一種思路去思考,便有柳暗花明的感覺。本篇的文章介紹有關sum函數別樣運用,在求和的基礎上添加其他條件,希望給讀者有一點點幫助。
  • 「Excel技巧」sumproduct函數的含義及各種用法
    今天跟大家一起來認識一個很好用的函數:sumproduct函數。sumproduct函數,sumproduct是由兩個英文單詞組成,即sum和product。Sum代表求和,product代表乘積,組成的sumproduct就是乘積之和。
  • 詳解Python的max、min和sum函數用法
    max()、min()、sum()這三個內置函數分別用於計算列表、元組或其他可迭代對象中所有元素最大值、最小值以及所有元素之和,sum()只支持數值型元素的序列或可迭代對象
  • Excel 公式之 SUM 統計函數
    1、SUM 常用簡單的基本求和統計公式:=SUM(number1,[number2]…) ,括號中 number1 參數以逗號分隔,可以輸入N多個參數進行統計求和。2、SUMIF 條件求和,主要是先分析數據達到指定的條件後才進行統計函數語法:=SUMIF(range,criteria,[sum_range])如下圖所示,使用 SUMIF 函數分別統計主操及
  • 你真的了解SUM函數的用法嗎?
    2、累計加 求累計求和的時候,SUM函數也是相當的簡單,上期中我們用N函數也可以簡單的求出累計和,本次這裡,小熙把兩種方法都列舉出來。 用N函數,簡單實用:
  • Excel教程:sumproduct函數多條件求和
    Excel技巧37:sumproduct函數多條件求和
  • excel中的sumproduct函數太強大了!一個頂多個,不服不行
    本文就詳細給大家介紹一下sumproduct函數的用法吧。一、基本用法。對於sumproduct函數,公式參數特別簡單,即=SUMPRODUCT(數組1,數組2,數組3, ……),每個數組之間用逗號隔開,表示數組之間先相乘再求和。
  • 你確定你會用sum函數嗎?別再說sum函數簡單!多表格求和看過來
    多表格一次性求和可以用sum函數先看下素材,把1月到3月的產品金額求和,不用手動一個個相加,直接操作多表格求和詳細操作:選中C2單元格,輸入「=sum()」,滑鼠點擊「1月」sheet工作表,再按住「shift
  • Oracle聚合函數AVG/SUM/MIN/COUNT詳解
    Oracle聚合函數同時可以對多行數據進行操作,並返回一個結果。比如經常用來計算一些分組數據的總和和平均值之類,常用函數有AVG()、SUM()、MIN()、MAX()等等。下列案例所需表結構參考:學生信息系統。