【演算法題】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
方法一:遞迴
- 定義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中
- 定義List<List
方法二:回溯
- 定義一個回溯方法 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)解題方法
方法:回溯法
這個題同樣考慮回溯法,因為每跑到一個點就會遇到幾種不同的情況,比如下圖中的'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)解題方法
方法:回溯法
這個題和子集那題不同,如果依舊按照那個方法的話會得出很多重複的結果。我們以[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
方法:回溯法
- 需要定義兩個全域性變數陣列(一個二維陣列用於返回最後的結果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)程式碼示例