LeetCode Prob.15 3Sum的各種解法

題目描述

給定一個整數(shù)列表秸仙,請問能否從中找出所有滿足a + b + c = 0的三元組袋坑?
例如,給定[-1, 0, 1, 2, -1, -4]优幸,那么答案為[[-1, 0, 1], [-1, -1, 2]]吨拍。

從2Sum說起

我們看一個簡化的問題:如果給定整數(shù)列表中,有且僅有一個 a + b = 0 的二元組网杆,那么該怎么求這個ab呢羹饰?

思路很簡單:我們設(shè)置一個字典,字典的key代表待尋找的數(shù)跛璧,keyvalue對應(yīng)為二元組严里。

那么接下來,我們只需要遍歷整個列表追城。對于遍歷到的數(shù)num

  • 如果num在字典里刹碾,那就找到了。
  • 否則座柱,我們令key0 - num迷帜,valuenum物舒。

于是代碼就是:

class Solution:
    def twoSum(self, nums, target):
        d = {}
        nlen = len(nums)
        i = 0
        while i != nlen:
            if nums[i] in d:
                return [d[nums[i]],i]
            else:
                d[target - nums[i]] = i
                i += 1

基于2Sum的3Sum

有了2Sum的鋪墊,只需要找到把3Sum問題轉(zhuǎn)化為2Sum問題的方法就好了戏锹。這個方法的思想是這樣的:

  1. 首先冠胯,我們對整數(shù)列表nums進(jìn)行排序。這可以幫助我們簡化思路锦针。
  2. 然后荠察,遍歷nums
  3. 我們設(shè)遍歷到的位置為fixpt奈搜。那么悉盆,現(xiàn)在的任務(wù)就是,在 fixpt + 1len(nums) - 1 這個區(qū)間內(nèi)馋吗,尋找到 a + b = 0 - nums[fixpt] 的二元組就可以了焕盟。

先來看看代碼:

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        #無解的情況有三個:元素個數(shù)不足,元素都大于或小于0宏粤。所以可以直接返回空列表脚翘。
        if len(nums) < 2 or nums[0] > 0 or nums[-1] < 0: return []

        ans = []
        fixpt = 0
        nlen = len(nums) 

        while fixpt < nlen - 2:
            #這里是兩個簡化條件:
            #第一,如果fixpt指向的數(shù)都大于0了绍哎,那么剩下的數(shù)肯定都大于0了
            #第二来农,當(dāng)我們在當(dāng)前的fixpt搜索完畢,那么也就是fixpt+1到nlen的所有解都被我們搜索完了蛇摸。
            #因此备图,如果fixpt+1指向的數(shù)和fixpt的相等,那么fixpt+2到nlen的可行解
            #當(dāng)然也已經(jīng)被fixpt時的搜索找到了赶袄。
            #所以才可以有這個直接跳過的步驟揽涮。
            if nums[fixpt] > 0: break
            if fixpt > 0 and nums[fixpt] == nums[fixpt - 1]:
                fixpt += 1
                continue

            mpt = fixpt + 1
            d = {}
            qd = {}
            #d的作用和2Sum時一樣。
            #qd的作用是為了省去查找解重復(fù)的時間饿肺。
            while mpt != nlen:
                mptn = nums[mpt]
                #下面這個if就是在判斷當(dāng)前數(shù)是否為d的key了蒋困。
                if mptn in d and (nums[fixpt],mptn) not in qd:
                    ans.append([nums[fixpt], d[mptn], mptn])
                    qd[(nums[fixpt],mptn)] = True
                #而這個if則是添加key。
                elif 0 - nums[fixpt] - mptn not in d:
                    d[0 - nums[fixpt] - mptn] = mptn
                mpt += 1
            fixpt += 1
        return ans 

總的核心就在最后那部分敬辣。前面的部分大多是簡化時間的雪标。

最終,LeetCode上運行的時間大約在1200+ms溉跃。

雙指針法對可行解搜索的簡化

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        if len(nums) == 0 or nums[0] > 0 or nums[-1] < 0: return []

        ans = []
        pt = 0

        while pt < len(nums) - 2: 
            if nums[pt] > 0: break
            if pt > 0 and nums[pt] == nums[pt - 1]:
                pt += 1
                continue

            left = pt + 1
            right = len(nums) - 1
            while left < right:
                if nums[pt] + nums[left] + nums[right] == 0:
                    ans.append([nums[pt], nums[left], nums[right]])
                    while left < right and nums[left] == nums[left + 1]: left += 1
                    while left < right and nums[right] == nums[right - 1]: right -= 1
                    left += 1
                    right -= 1
                elif nums[pt] + nums[left] + nums[right] > 0:
                    right -= 1
                else:
                    left += 1
            pt += 1
        return ans 

在這個算法中村刨,前面的簡化部分依然沒有什么變化,但是在可行解搜索的部分撰茎,用了速度更快的雙指針法嵌牺。

它的思想是這樣的,首先fixpt這里改叫pt,依然是一個定點逆粹,用來提供2Sum查找的目標(biāo)募疮。

但是,現(xiàn)在有兩個指針僻弹,leftright阿浓,從兩邊逐漸往中間縮,根據(jù)查找到的三數(shù)和決定是移動left還是移動right蹋绽。

具體到代碼上芭毙,縮的策略是這樣的:

while left < right:
    # 若滿足了a+b+c=0的條件,那么當(dāng)然就記錄答案了
    if nums[pt] + nums[left] + nums[right] == 0:
        ans.append([nums[pt], nums[left], nums[right]])
        #這里指針的再次移動是很關(guān)鍵的蟋字,不能用break代替
        #因為可行解可能不止一個稿蹲,在內(nèi)部還可能有別的可行解扭勉,所以跳過同類數(shù)字后再次進(jìn)入大循環(huán)查找鹊奖。
        while left < right and nums[left] == nums[left + 1]: left += 1
        while left < right and nums[right] == nums[right - 1]: right -= 1
        left += 1
        right -= 1
    #如果a+b+c>0,那么說明nums[left] + nums[right]的總和偏大涂炎,所以應(yīng)當(dāng)減小大數(shù)忠聚,即right -= 1。
    elif nums[pt] + nums[left] + nums[right] > 0:
        right -= 1
    #同樣的道理唱捣,只不過是為了讓總和增大两蟀。
    else:
        left += 1

最后調(diào)整總和大小還好理解,但是那兩行while是用來干什么的呢震缭?

舉個例子赂毯,就是[-1, 0, 1, 2, -1, -4]吧。排序之后拣宰,這個列表是[-4, -1, -1, 0, 1, 2]党涕。-4顯然沒有對應(yīng)項,但是對于第一個-1巡社,在這個算法下膛堤,第一個找到的可行解是[-1,-1,2]。然而晌该,如果就這么break了肥荔,就會丟失[0, 1]這個部分的搜索,而這部分恰恰是和-1能組成可行解朝群。

這個算法的時間大概是1000ms+燕耿。

正負(fù)數(shù)配對

在定點的思想下,暴力一點的解法就是在定點之后的搜索區(qū)間內(nèi)進(jìn)行配對了姜胖。這樣的方法肯定是比優(yōu)化過的方法慢一些的誉帅,因為這已經(jīng)接近n^2了,而雙指針只需要n的時間。

然而堵第,這種n^2的配對用得好的話吧凉,效果也是不錯的。我們可以分析一下這個問題:要滿足a + b + c = 0踏志,需要滿足什么條件阀捅?

  • 如果沒有0或者有一個數(shù)是0,那么必然需要有兩個數(shù)一正一負(fù)针余。
  • 如果有兩個數(shù)是0饲鄙,那么第三個也必須是0。

由此我們可以構(gòu)思圆雁,如果我們將正數(shù)忍级、負(fù)數(shù)、0分別歸入集合伪朽,并對數(shù)字進(jìn)行計數(shù)處理轴咱,那么就可以這么做:

  • 若0的個數(shù)大于2,那么可行解必然有[0, 0, 0]烈涮。
  • 我們從正數(shù)和負(fù)數(shù)中各取一個數(shù)朴肺,然后計算我們的目標(biāo),比如說坚洽,取了xy兩個數(shù)戈稿,那么需要尋找的就是s = -(x + y)
    • 有可能s == x or s == y讶舰。不妨設(shè)s == x鞍盗,那么只要看x的個數(shù)是否大于1,若如此跳昼,則有可行解[x, x, y]般甲。對于s == y的情況也類似。
    • 如果不等庐舟,那么我們只需要在x < s < y的時候欣除,以[x, s, y]作為我們的可行解。因為挪略,接下來的配對历帚,可能會選到數(shù)對sy,那么再計算的時候杠娱,x就成了我們的目標(biāo)挽牢。既然三個數(shù)必然存在x < s < y關(guān)系,那么我們就只在這個關(guān)系滿足的時候視為可行解摊求,這樣就避免了重復(fù)添加可行解的麻煩禽拔。

分析完了,代碼是這樣的:

class Solution:
    def threeSum(self, nums):
        d = {}
        for val in nums:
            d[val] = d.get(val, 0) + 1
        
        pos = [x for x in d if x > 0]
        neg = [x for x in d if x < 0]
        
        res = []
        if d.get(0, 0) > 2:
            res.append([0, 0, 0])
            
        for x in pos:
            for y in neg:
                s = -(x + y)
                if s in d:
                    if s == x and d[x] > 1:
                        res.append([x, x, y])
                    elif s == y and d[y] > 1:
                        res.append([x, y, y])
                    elif y < s < x:
                        res.append([x, y, s])
        return res

這里的一個巧妙的操作細(xì)節(jié)是,先用字典得到所有不重復(fù)的數(shù)字睹栖,以及出現(xiàn)的次數(shù)硫惕。然后就很好處理了。

既簡捷又快速野来。這個LeetCode的樣例得分是320ms恼除。

任意值3Sum算法的特殊情況

在LeetCode上最快的解法,有很多數(shù)字技巧曼氛,有些地方我也沒琢磨透豁辉。具體是這樣的:

class Solution:
    def threeSum(self, nums: 'List[int]') -> 'List[List[int]]':
        from bisect import bisect_left, bisect_right
       
        target = 0
        
        result = []
        length = len(nums)
        
        if length < 3:
            return result
        
        count ={}
        # map the counts
        for n in nums:
            if n in count:
                count[n] += 1
            else:
                count[n] = 1
            
        keys = list(count.keys())
        keys.sort()
      
        t3 = target // 3
        if t3 in keys and count[t3] >= 3:
            result.append([t3, t3, t3])

        begin = bisect_left(keys, target - keys[-1] * 2)
        end = bisect_left(keys, target * 3)

        for i in range(begin, end):
            a = keys[i]
            if count[a] >= 2 and target - 2 * a in keys:
                result.append([a, a, target - 2 * a])

            max_b = (target - a) // 2  # target-a is remaining
            min_b = target - a - keys[-1]  # target-a is remaining and c can max be keys[-1]
            b_begin = max(i + 1, bisect_left(keys, min_b))
            b_end = bisect_right(keys, max_b)

            for j in range(b_begin, b_end):
                b = keys[j]
                c = target - a - b
                if c in count and b <= c:
                    if b < c or count[b] >= 2:
                        result.append([a, b, c])
        return result

先解釋一下,bisect是二分查找?guī)煲ɑ迹琹eft和right的含義可以自己查查徽级,很好理解。

首先明確一下聊浅,3Sum問題的解一共四類:

  1. [0, 0, 0]餐抢,三數(shù)相等。
  2. [a, a, b]狗超;
  3. [a, b, b]弹澎,以上2和3都有a < b
  4. [a, b, c]努咐,a < b < c

下面我們會拿這個序號來指代解的類型殴胧。

我們一步步來分析:

        target = 0

這一行就已經(jīng)表明了這是一個通用的3Sum算法渗稍,可以用來算任意target = a + b + c值的3Sum。

然后是計數(shù)部分:

        count ={}
        # map the counts
        for n in nums:
            if n in count:
                count[n] += 1
            else:
                count[n] = 1

這里的計數(shù)和上面正負(fù)配對法的

        d = {}
        for val in nums:
            d[val] = d.get(val, 0) + 1

部分是一樣的喻频。

然后代碼就有趣起來了:

        keys = list(count.keys())
        keys.sort()
      
        t3 = target // 3
        if t3 in keys and count[t3] >= 3:
            result.append([t3, t3, t3])

為什么要這么算t3呢有梆?我沒看懂這里整數(shù)除法的意思盾鳞,不過對于我們target == 0的情況,可以直接簡化為0的個數(shù)是否大于等于3拱燃。也就是,判斷第一種解存不存在力惯。

然后就是很巧妙的搜索范圍簡化:

        begin = bisect_left(keys, target - keys[-1] * 2)
        end = bisect_left(keys, target * 3) #might use target // 3

        for i in range(begin, end):
            a = keys[i]
            if count[a] >= 2 and target - 2 * a in keys:
                result.append([a, a, target - 2 * a])

在這里碗誉,我們要規(guī)定a,三元組最小數(shù)的搜索范圍父晶。
begin的查找參數(shù)哮缺,意味著就算我們用兩個最大的數(shù)keys[-1]來構(gòu)成可行解,那么最小的數(shù)也只能是target - keys[-1] * 2甲喝。
而end的查找參數(shù)(我認(rèn)為這里應(yīng)該用整數(shù)除法尝苇,實際上這么改之后也ac了),它意味著最小數(shù)的最大值也不能超過t3,不然作為三元組的最小數(shù)糠溜,三個a相加都已經(jīng)超出target了淳玩。

下一步就是在可行的a中,看看有沒有可行的第二種解非竿。

懂了a的范圍處理方式之后凯肋,b的范圍處理自然也好理解了,而且方法也印證了上面 說的end參數(shù)要用整數(shù)除法的修改汽馋。

            max_b = (target - a) // 2  # target-a is remaining
            min_b = target - a - keys[-1]  # target-a is remaining and c can max be keys[-1]
            b_begin = max(i + 1, bisect_left(keys, min_b))
            b_end = bisect_right(keys, max_b)

一旦我們確定了ab侮东,那么c也就自然確定了:

            for j in range(b_begin, b_end):
                b = keys[j]
                c = target - a - b
                if c in count and b <= c:
                    if b < c or count[b] >= 2:
                        result.append([a, b, c])

由于bc可能在搜索過程中逆序出現(xiàn),因此我們就用b <= c的方法確定什么時候確定可行解豹芯。

最后這里if b < c or count[b] >= 2:的邏輯也有點巧妙悄雅,先判斷b < c是否成立,如果我們需要判斷count[b] >= 2铁蹈,那么就暗含了 c == b宽闲。

前者找出了第四種解,后者找出了第三種解握牧。那么這樣容诬,四種可能的解的類型都被我們找完了。

由此整個代碼就分析結(jié)束沿腰,它的條理非常清楚览徒,速度也很快, 在最快的情況下可以達(dá)到280ms颂龙。

4Sum习蓬?

這個就留到下次再說了,哈哈~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末措嵌,一起剝皮案震驚了整個濱河市躲叼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌企巢,老刑警劉巖枫慷,帶你破解...
    沈念sama閱讀 211,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異浪规,居然都是意外死亡或听,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評論 3 385
  • 文/潘曉璐 我一進(jìn)店門罗丰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來神帅,“玉大人,你說我怎么就攤上這事萌抵≌矣” “怎么了元镀?”我有些...
    開封第一講書人閱讀 157,435評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長霎桅。 經(jīng)常有香客問我栖疑,道長,這世上最難降的妖魔是什么滔驶? 我笑而不...
    開封第一講書人閱讀 56,509評論 1 284
  • 正文 為了忘掉前任遇革,我火速辦了婚禮,結(jié)果婚禮上揭糕,老公的妹妹穿的比我還像新娘萝快。我一直安慰自己,他們只是感情好著角,可當(dāng)我...
    茶點故事閱讀 65,611評論 6 386
  • 文/花漫 我一把揭開白布揪漩。 她就那樣靜靜地躺著,像睡著了一般吏口。 火紅的嫁衣襯著肌膚如雪奄容。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,837評論 1 290
  • 那天产徊,我揣著相機(jī)與錄音昂勒,去河邊找鬼。 笑死舟铜,一個胖子當(dāng)著我的面吹牛戈盈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播深滚,決...
    沈念sama閱讀 38,987評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼奕谭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了痴荐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,730評論 0 267
  • 序言:老撾萬榮一對情侶失蹤官册,失蹤者是張志新(化名)和其女友劉穎生兆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體膝宁,經(jīng)...
    沈念sama閱讀 44,194評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡鸦难,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,525評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了员淫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片合蔽。...
    茶點故事閱讀 38,664評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖介返,靈堂內(nèi)的尸體忽然破棺而出拴事,到底是詐尸還是另有隱情沃斤,我是刑警寧澤,帶...
    沈念sama閱讀 34,334評論 4 330
  • 正文 年R本政府宣布刃宵,位于F島的核電站衡瓶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏牲证。R本人自食惡果不足惜哮针,卻給世界環(huán)境...
    茶點故事閱讀 39,944評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坦袍。 院中可真熱鬧十厢,春花似錦、人聲如沸捂齐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辛燥。三九已至筛武,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挎塌,已是汗流浹背徘六。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留榴都,地道東北人待锈。 一個月前我還...
    沈念sama閱讀 46,389評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像嘴高,于是被迫代替她去往敵國和親竿音。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,554評論 2 349

推薦閱讀更多精彩內(nèi)容

  • 專業(yè)考題類型管理運行工作負(fù)責(zé)人一般作業(yè)考題內(nèi)容選項A選項B選項C選項D選項E選項F正確答案 變電單選GYSZ本規(guī)程...
    小白兔去釣魚閱讀 8,981評論 0 13
  • 在C語言中,五種基本數(shù)據(jù)類型存儲空間長度的排列順序是: A)char B)char=int<=float C)ch...
    夏天再來閱讀 3,333評論 0 2
  • 選擇題部分 1.(),只有在發(fā)生短路事故時或者在負(fù)荷電流較大時,變流器中才會有足夠的二次電流作為繼電保護(hù)跳閘之用拴驮。...
    skystarwuwei閱讀 12,837評論 0 7
  • 2017年全國統(tǒng)一高考數(shù)學(xué)試卷(文科)(新課標(biāo)Ⅰ) 一套啤、選擇題:本大題共12小題宽气,每小題5分,共60分潜沦。在每小題給...
    高考家庭教育研究閱讀 910評論 0 3
  • 習(xí)慣了做一個優(yōu)秀的人,習(xí)慣了盡全力在興趣或職責(zé)范圍內(nèi)的事做到出類拔萃的地位争占,習(xí)慣了向金字塔頂尖沖刺燃逻。盡管有很多不如...
    舒莉Jasmine閱讀 198評論 0 0