1. 程式人生 > 實用技巧 >【演算法題】LeetCode刷題(六)

【演算法題】LeetCode刷題(六)

資料結構和演算法是程式設計路上永遠無法避開的兩個核心知識點,本系列【演算法題】旨在記錄刷題過程中的一些心得體會,將會挑出LeetCode等最具代表性的題目進行解析,題解基本都來自於LeetCode官網(https://leetcode-cn.com/),本文是第六篇。

1.顏色分類(原第75題)

給定一個包含紅色、白色和藍色,一共 n 個元素的陣列,原地對它們進行排序,使得相同顏色的元素相鄰,並按照紅色、白色、藍色順序排列。
此題中,我們使用整數 0、 1 和 2 分別表示紅色、白色和藍色。
注意:
不能使用程式碼庫中的排序函式來解決這道題。

示例:

輸入: [2,0,2,1,1,0]
輸出: [0,0,1,1,2,2]

(1)知識點

原地演算法

(2)解題方法

方法轉自:https://leetcode-cn.com/problems/sort-colors/solution/yan-se-fen-lei-by-leetcode/

方法:一次遍歷

相比之前的題目經常用到的雙指標法,這個其實是個三指標。事實上,一般遇到要排序的問題,無非就是多加幾個指標就可以解決了。
我們用三個指標(p0, p2 和curr)來分別追蹤0的最右邊界,2的最左邊界和當前考慮的元素。本解法的思路是沿著陣列移動 curr 指標,若nums[curr] = 0,則將其與 nums[p0]互換;若 nums[curr] = 2 ,則與 nums[p2]互換。

  • 時間複雜度:O(n),由於對長度 N的陣列進行了一次遍歷。
  • 空間複雜度:O(1),不需要額外的空間開銷。

(3)虛擬碼

函式頭:void sortColors(int[] nums)

方法:一次遍歷

  • 定義p0=curr=0,p2=len-1
  • 第一重迴圈:(curr<=p2)
    • 如果curr=0,交換p0和curr的值,p0++,curr++
    • 如果curr=1,curr++
    • 如果curr=2,交換p2和curr的值,p2--

(4)程式碼示例




2.子集(原第78題)

給定一組不含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)。
說明:解集不能包含重複的子集。

示例:

輸入: nums = [1,2,3]
輸出:
[
[3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

(1)知識點

子集

(2)解題方法

方法轉自:https://leetcode-cn.com/problems/subsets/solution/zi-ji-by-leetcode/

方法一:遞迴

根據題目可以判斷,對於n個元素,每個元素都有放or不放兩種狀態,那麼總共就有2^n種情況。
比如集合[1,2,3]:

不放,不放,不放:[]
放,不放,不放:[1]
不放,放,不放:[2]
不放,不放,放:[3]
放,放,不放:[1,2]
放,不放,放:[1,3]
不放,放,放:[2,3]
放,放,放:[1,2,3]

另外,我們考慮:

如果沒有元素[],他的子集是[]
如果有一個元素[1],他的子集是[],[1]
如果有兩個元素[1,2],他的子集是[],[1],[2],[1,2]
如果有三個元素[1,2,3],他的子集是[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]

發現什麼了?每次增加一個元素,只需要在前一個的基礎上的末尾新增一個元素就行了,所以總的子集數量是呈指數級別的增長的。這就容易想到遞迴演算法了。

  • 時間複雜度:O(N×2^N),生成所有子集,並複製到輸出結果中。
  • 空間複雜度:O(N×2^N),這是子集的數量。
  • 對於給定的任意元素,它在子集中有兩種情況,存在或者不存在(對應二進位制中的 0 和 1)。因此,N 個數字共有 2^N個子集。

方法二:回溯法

我們知道,在遇到這種排列組合的情況下,基本上都能用到回溯法,而回溯法本身是一棵樹,這裡就需要每一層的情況都得加到最後的集合中去

  • 時間複雜度:O(N×2^N),生成所有子集,並複製到輸出結果中。
  • 空間複雜度:O(N×2^N),儲存所有子集,共 nn 個元素,每個元素都有可能存在或者不存在。

方法三:字典排序(二進位制排序)

將每個子集對映到長度為 n 的位掩碼中,其中第 i 位掩碼 nums[i] 為 1,表示第 i 個元素在子集中;如果第 i 位掩碼 nums[i] 為 0,表示第 i 個元素不在子集中。

例如,位掩碼 0..00(全 0)表示空子集,位掩碼 1..11(全 1)表示輸入陣列 nums。
因此要生成所有子集,只需要生成從 0..00 到 1..11 的所有 n 位掩碼。

  • 時間複雜度:O(N×2^N),生成所有子集,並複製到輸出結果中。
  • 空間複雜度:O(N×2^N),儲存所有子集,共 nn 個元素,每個元素都有可能存在或者不存在。

(3)虛擬碼

函式頭:List<List> subsets(int[] nums)

方法一:遞迴

  • 定義List<List> res用作返回
  • 將空集加入到res
  • 第一重迴圈:(num:nums)
    • 定義List<List> sub用作添加當前num的集合
    • 將res裡的每一個List都填上num然後放進sub裡面(注意java有一個簡單的寫法(curr是res中的某個list):sub.add(new ArrayList(curr){{add(num);}});)
    • 將sub裡面的每個list再加到res中

方法二:回溯

  • 定義一個回溯方法 backtrack(first, curr),第一個引數為索引 first,第二個引數為當前子集 curr。
  • 如果當前子集構造完成,將它新增到輸出集合中。
  • 否則,第一重迴圈從i: first->n-1
    • 將整數 nums[i] 新增到當前子集 curr。
    • 繼續向子集中新增整數:backtrack(i + 1, curr)。
    • 從 curr 中刪除 nums[i] 進行回溯。

方法三:字典排序(二進位制排序)

  • 生成所有長度為 n 的二進位制位掩碼。
  • 將每個子集都對映到一個位掩碼數:位掩碼中第 i 位如果是 1 表示子集中存在 nums[i],0 表示子集中不存在 nums[i]。

(4)程式碼示例




3.單詞搜尋(原第79題)

給定一個二維網格和一個單詞,找出該單詞是否存在於網格中。
單詞必須按照字母順序,通過相鄰的單元格內的字母構成,其中“相鄰”單元格是那些水平相鄰或垂直相鄰的單元格。同一個單元格內的字母不允許被重複使用。

示例:

board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
給定 word = "ABCCED", 返回 true
給定 word = "SEE", 返回 true
給定 word = "ABCB", 返回 false

(1)知識點

回溯法

(2)解題方法

方法轉自:https://leetcode-cn.com/problems/word-search/solution/zai-er-wei-ping-mian-shang-shi-yong-hui-su-fa-pyth/

方法:回溯法

這個題同樣考慮回溯法,因為每跑到一個點就會遇到幾種不同的情況,比如下圖中的'C',到這裡就有兩個路可以走,如果“走錯了”,就需要回來換一條路,這裡就用到了回溯演算法。

我們考慮從第一個點開始遍歷,直到找到待查的第一個字母,然後上下左右(可以自定義順序)來找到下一個,如果不對就回溯。並且在行進的過程中,需要標記已經走過的點,因為不能重複。

(3)虛擬碼

函式頭:boolean exist(char[][] board, String word)

方法:回溯法

  • 兩重迴圈遍歷board中的每個元素,呼叫backtrack函式,判斷以該字元開頭滿不滿足情況。

boolean backtrack(int i, int j, int start):start是word中的某個字元

  • 如果start是word的最後一個字元,直接判斷是否和board[i][j]相等返回。——遞迴終止條件
  • 如果start位置的字元和board[i][j]相等,進一步判斷:
    • 標記i,j位置已經走過(這裡需要一個全域性變數陣列來記錄)
    • 第一重迴圈(k:0->3):這裡是分別走i,j位置的上下左右四個方向(這裡也需要定義一個全域性變數陣列來記錄偏移量,具體就是int[][]direct={{0,1},{0,-1},{1,0},{-1,0}})
      • 定義newX=i+direct[k][0],newY=j+direct[k][1]
      • 如果newX和newY均未走過,並且newX和newY都在board的範圍裡面(需要提前定義好全域性變數board的範圍,所有direct的四個方向不一定都能走到),呼叫backtrack(newX,newY,start+1)
    • 回溯(標記i,j未走過)
  • 返回false

(4)程式碼示例




4.子集2(原第90題)

給定一個可能包含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)。
說明:解集不能包含重複的子集。

示例:

輸入: [1,2,2]
輸出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]

(1)知識點

回溯法

(2)解題方法

方法轉自:https://leetcode-cn.com/problems/subsets-ii/solution/shi-yong-mapji-shu-yi-bian-sao-miao-jia-ru-suo-you/

方法:回溯法

這個題和子集那題不同,如果依舊按照那個方法的話會得出很多重複的結果。我們以[1,2,2]為例,利用回溯法,並且對每種情況進行判斷。要實現同層比較的關鍵就是比較for迴圈裡的j和回溯函式的實參i,j是當前檢查元素下標;i是本層開始檢查的元素下標
如果nums[j]是在同層的在for迴圈裡重複出現的,那麼j肯定大於i,否則比較的兩個元素就處於不同層。實際上就是判斷j是否大於i,然後還需要nums[j]!=[j-1]
下圖中第一層第三個說明有點錯誤。(應該是nums[2]==nums[1])

(3)虛擬碼

函式頭: List<List> subsetsWithDup(int[] nums)

方法:回溯法

  • 需要定義兩個全域性變數陣列(一個二維陣列用於返回最後的結果res,一個一位陣列用來裝臨時的子集tmp)
  • 先對nums排序
  • 呼叫backtrack(0)

void backtrack(int i)

  • 如果i=len,直接返回——遞迴終止條件
  • 第一重迴圈(j:i->len-1):
    • 如果j>i且nums[j]==nums[j-1]跳過
    • tmp新增nums[j]
    • res新增一個新List(拷貝tmp)
    • backtrack(i+1)
    • tmp.remove(最後一個元素)——回溯

(4)程式碼示例