LeetCode刷題實戰15題: 三數之和

算法的重要性,我就很少說了吧,想去大廠,就必需要通過基礎知識和業務邏輯面試+算法面試。面試

今天和你們聊的問題叫作三數之和 ,咱們先來看題面:算法

 

 

 

描述數組

 

給定一個整數的數組,要求尋找當中全部的a,b,c三個數的組合,使得三個數的和爲0.注意,即便數組當中的數有重複,同一個數也只能使用一次。app

 

Given an array nums of n integers, are there elements a , b , c in
nums such that a + b + c = 0? Find all unique triplets in the array
which gives the sum of zero.ide

 

Note:優化

The solution set must not contain duplicate triplets.3d

 

樣例

 

給定數組 nums = [-1, 0, 1, 2, -1, -4],

知足要求的三元組集合爲:
[
  [-1, 0, 1],
  [-1, -1, 2]
]指針

 

題解

 

這道題是以前LeetCode第一題2 Sum的提高版,在以前的題目當中,咱們尋找的是和等於某個值的兩個數的組合。而這裏,咱們須要找的是三個數。從表面上來看彷佛差異不大,可是實際處理起來要麻煩不少。blog

 

  暴力求解  

 

咱們先理一下思路,從最簡單的方法開始入手。這題最簡單的方法固然就是暴力法,咱們已經明確了要找的是三個數的和,既然數量肯定了,就好辦了,咱們直接枚舉全部三個數的組合,而後全部和等於0的組合就是答案。可是這裏有一個小問題,當咱們找到了答案以後,咱們並不能直接返回,由於數組當中重複的元素頗有可能會致使答案的重複,咱們必需要去掉這些重複的答案,保證答案當中每個都是惟一的。排序

 

那咱們先對原數組作處理,去除掉其中重複的元素以後再來尋找答案可不能夠呢?

 

很遺憾,這個想法很好,可是不可行。緣由也很簡單,由於答案不能重複,可是答案裏的數是能夠重複的。

 

舉個例子,好比數組是[-1, -1, 2, 0, -2],那麼[-1, -1, 2]是一個答案,若是一開始就出去掉了重複的-1,那麼這個答案顯然就沒法構成了。惟一的解決方法是用容器來維護答案,保證容器內的答案是惟一的,不過這個會帶來額外的時間和空間開銷。

 

因此,整體看來,暴力枚舉並非個好方法,複雜度不低,若是使用C++和Java等語言的話,使用容器也很麻煩。

  •  
ret = set()
for i in range(n): for j in range(i+1, n): for k in range(j+1, n): if a[i] + a[j] + a[k] == 0: ret.add((i, j, k))
return list(ret)

 

  利用2 Sum  

 

還有一個思路是利用以前的2 Sum的解法,在以前的2 Sum問題當中,咱們經過巧妙地使用map,來達成了在watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=複雜度內找到了全部和等於某個值的元素對。因此,咱們能夠先枚舉第一個數的大小,而後在剩下的元素當中進行2 Sum操做。

 

假設咱們枚舉的數是a[i],那麼咱們在剩下的元素當中作2 Sum,來尋找和等於-a[i]的兩個數。最後,將這三個數組成答案。若是遺忘2 Sum解法的同窗能夠點擊下方連接回到以前的文章。LeetCode 1 Two Sum——在數組上遍歷出花樣

 

這個方法看起來巧妙不少,可是仍是逃不掉重複的問題。舉個例子:[-1, -1, -1, -1, -1, 2]。若是咱們枚舉-1,那麼會出現多個[-1, -1, 2]的結果。因此咱們依然免不了手動過濾重複的答案。不過利用2 Sum的解法要比暴力快一些,由於2 Sum的時間複雜度是watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=,再乘上枚舉元素的複雜度,不考慮去重狀況下的總體複雜度是O(n^2),要比枚舉的O(n^3)更優。

 

咱們利用2 sum寫出新的算法:

 

  •  
def two_sum(array, idx, target):    """    two sum的部分    """    n = len(array)    ret = []    # 用來記錄全部出現過的元素    appear = set()    # 用來判斷2 sum的答案出現重複    used = set()    for i in range(idx + 1, n):        # 若是 target - array[i]以前出現過,說明能夠構成答案        if target - array[i] in appear:            # 判斷答案是否重複            if array[i] in used or target - array[i] in used:                continue            # 記錄            used.add(array[i])            used.add(target - array[i])            ret.append((array[i], target - array[i]))        appear.add(array[i])    return ret

def three_sum(array): n = len(array) # 記錄枚舉過的元素 used = set() ret = [] # 防止答案重複 duplicated = set() for i in range(n): # 若是出現過,說明已經枚舉過,跳過 if array[i] in used: continue # 拿到2 sum的答案 combinations = two_sum(array, i, -array[i]) if len(combinations) > 0: for combination in combinations: # 組裝答案 answer = tuple(sorted((array[i], *combination))) # 判斷答案是否重複 if answer in duplicated: continue # 記錄 ret.append(answer) duplicated.add(answer) used.add(array[i]) return ret

 

  尺取法  

 

這題的另外一個解法是尺取法,也就是two pointers,也叫作兩指針算法。這個在咱們以前的文章當中也有過介紹,有遺忘或者錯過的同窗能夠點擊下方的連接回顧一下。LeetCode3 一題學會尺取算法

 

尺取法的精髓是經過兩個指針控制一個區間,保證區間知足必定的條件。在這題當中,咱們要控制的條件實際上是三個數的和。因爲咱們的指針數量是2,也就是說咱們只有兩個指針,可是咱們卻須要找到三個數組成的答案。顯然,咱們直接使用尺取法是不行的。咱們稍做變通就能夠解決這個問題,就是第一個解法的思路,咱們先枚舉一個數,而後再經過尺取法去尋找另外兩個數。

 

使用尺取法須要咱們根據如今區間內的信息,制定策略如何移動區間。顯然,若是區間裏的數雜亂無章,咱們是很難知道應該怎麼維護區間的。因此咱們首先對數組當中的元素進行排序,保證元素的有序性。區間裏的元素有序了,那麼咱們就方便了。

 

假設咱們當前枚舉的數是a[i],那麼咱們就須要找到另外的兩個數b和c,使得b + c = -a[i]。對於每個i來講,這樣的b和c可能存在,也可能不存在,咱們必需要尋找過了才知道。

 

和2 Sum同樣,爲了優化時間複雜度,加快算法的效率,咱們須要人爲設置一些限制。咱們限制b和c只能在a的右側,固然也能夠限制在一左一右,總之,咱們須要把這三個數的順序固定下來。由於三個數調換順序只會產生重複,因此咱們固定順序能夠避免重複。因此咱們枚舉a的位置以後,在a的右側經過尺取法尋找另外兩個元素。

 

方法也很簡單,咱們一開始設置b的位置是i+1, c的位置是n。若是b+c > -a,那麼說明二者的和過大,由於b已是最小值了,因此只能將c向左移動。若是b+c < -a,說明二者的和太小,須要增大,因此應該將b往右側移動增大數值。如此往復,當這個區間遍歷完成以後,繼續移動a的位置,尋找下一組解,這裏須要注意,a須要跳過全部重複的數字,避免重複。

 

咱們寫出代碼:

 

  •  
def three_sum(array):    n = len(array)    # 先對array進行排序    array = sorted(array)    ret = []    for i in range(n-2):        # 判斷第一個數是否重複        if i > 0 and array[i] == array[i-1]:            continue        used.add(array[i])        # 進行two pointers縮放        j = i + 1        k = n - 1        target = -array[i]        if target < 0:            break        while j < k:            cur_sum = array[j] + array[k]            # 判斷當前區間的結果和目標的大小            if cur_sum < target:                    j += 1                continue            elif cur_sum > target:                k -= 1                continue            # 記錄            ret.append(answer)            # 繼續縮放區間,尋找其餘可能的答案            j += 1            while j < k and array[j] == array[j-1]:                j += 1            k -= 1            while j < k-1 and array[k] == array[k+1]:                k -= 1    return ret

 

 

寫出代碼以後,咱們來分析一下算法的複雜度。一開始的時候,咱們對數組進行排序,衆所周知,排序的複雜度是watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=。以後,咱們枚舉了第一個數,開銷是watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=,咱們進行區間縮放的複雜度也是watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=,因此整個主體程序的複雜度是watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=。看似和上面一種方法區別不大,可是咱們節省了set重複的判斷,因爲hashset讀取會有時間開銷,因此雖然算法的量級上沒什麼差異,可是常數更小,真正運行起來這種算法要快不少。

 

這題官方給的難度是Medium,但實際上我以爲比通常的Medium要難上一些,代碼量也要大上一些。今天文章當中列舉的並非所有的解法,其餘的作法還有不少,好比對全部數進行分類,分紅負數、零和正數,而後再進行組裝等等。感興趣的同窗能夠本身思考,看看還有沒有其餘比較有趣的方法。

 

 

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

相關文章
相關標籤/搜索