LeetCode 刷題知識總整理
前言
這篇用來記錄刷leetcode 的各類主題,以及不同情境下要用的對應解題策略是哪些,也連接到之前所做的筆記跟部落格
Roadmap
這邊是參考 NeetCode 官網的roadmap 按照不同主題進行刷題的
Big-O
Two Pointers
Two pointer 如果出現在陣列題目中,代表兩個索引(index),如果出現在 Linked List題目中,代表兩個不同的指標。但核心目的都是一樣的,透過移動pointer位置來減少溶於計算提高效率。
常見的Two pointer類型:
- 反向指針: 一個指向頭部一個指向尾部,逐漸向中間靠攏,檢查回文, 兩數之和通常很常用這種類型
- 快慢指針: 一個指針較快,另一個指針較慢,可以用於檢測Linked List 中是否有環。
某些情況下會搭配 Sliding Windows 來去動態調整子陣列的大小,Ex.右指針擴展Windows,左指針收斂windows。以下整理常見的 Two Pointer 使用時機:
- $O(N^2)$ 降低成 $O(N)$,題目要求降低複雜度
- 輸入資料是有序的
- 題目要求將不同索引作比較
- 題目要求要在不同索引間交換
- 題目要求將陣列分區
注意,如果要用反向指針逐步收斂問題範圍,會有條件,那就是資料必須是已排序的
但如果題目是要搭配 Sliding Windows 或者linked list 則資料不一定要排序
- LeetCode#392. Is Subsequence
- LeetCode#125. Valid Palindrome
- LeetCode#167. Two Sum II - Input Array Is Sorted
- LeetCode#15. 3Sum
Sliding Window
Sliding Window 算是 Two Pointer 的其中一種變化,可以透過兩個指標 left
以及 right
來去建立窗口(Window), 通常題目會要求返回特定條件的最大或最小子範圍,利用 [Left, Right]
夾出來的 Window 來在運算過程中滑動(收縮和擴展)來找出最佳的範圍。
三個關鍵步驟
- Expand out window
- Meet the condition and process the window
- Shrink the window
通常會由 right
指針去往外擴展,一旦滿足條件時,則可透過增加 left
來收斂 window,其解題邏輯如下:
1 | void slidingWindow(){ |
在 sliding window 題目中一定會出現三種變數:
- Window 邊界:
left
,right
- 紀錄條件的變數,看是否到達expansion 停止條件
- 紀錄回傳值的變數
解題流程
- 定義停止擴展window的條件:先明確在什麼情況下需要停止擴展窗口
- 擴展window直到滿足條件:在擴展window之前,先處理當前
right
指標所指向的元素 - 當滿足停止擴展的條件時,處理當前window:在滿足條件時,針對當前window進行需要的處理
- 收縮當前window:在收縮window之前,先處理當前
left
指標所指向的元素 - 處理邊界情況:確保對特殊情況(例如空輸入、極端值等)進行適當處理
題目
- leetcode#487. Max Consecutive Ones II
- LeetCode#424. Longest Repeating Character Replacement 這題在窗口擴展的時候同時透過雜湊表記錄字母出現頻率,接著收窄窗口直到窗口等於最高字母頻率次數 + K(題目給的替換操作次數),窗口收窄後再去紀錄最大子字串長度(窗口長度)
參考:
https://shannonhung.github.io/posts/lecture-two-pointer-and-sliding-window
https://medium.com/@timpark0807/leetcode-is-easy-sliding-window-c44c11cc33e1
Hash Table
基本用法
1 |
|
迭代存取 unordered_map
元素
1 | for(const auto &n : dp){ |
查找特定值
1 | if(dp.find("KEY")!=dp.end()){ |
清空 unordered_map
容器
1 | dp.clear(); |
使用時機1: 用於快速存取元素
一旦建立好 Hash Table 就可以用 $O(1)$ 的時間複雜度來存取元素
使用時機2: 比對無序資料
在 Python 中實踐 Hash Table的方式就是 Dictionary,而在C++中則是透過 unordered_map
,他們的特點就是都是 Key-Value Pair,這代表他們就是一個無序元素映射的集合。因此在比較無序資料時也會用到 Hash Table,像是可以用來記錄特定單字在某個文章出現的頻率那也可以使用 Hash Table
使用時機3: Deep Copy
如果今天對於 Linked List 的結構有Deep Copy 的需求 (Ex. 錄製出一個一模一樣的Linked List結構),則會需要儲存舊節點的鏈結關係以及新節點的鏈結關係。這時就可以用 Hash Table 來去做映射。
- leetCode#138. Copy List with Random Pointer: 這裡宣告一個 Hash Table 來分別儲存舊的list跟複製後的list。
1 | unordered_map <Node*, Node*> randomMap; |
unordered_map vs unordered_set
1 | unordered_map <int, int> umap; |
從宣告上就可以看出差異, unordered_map
會是 Key-Value Pair 然而 unordered_set
會只有 Key 或 value,總之他並不是 pair,也並不能儲存映射關係。 兩者的共同點就是都是無序的 ,並且實踐都是基於 Hash Table,因此 unordered_set
中的元素都會是唯一的
以下是 unordered_map
和 unordered_set
在解 LeetCode 題目時的使用時機整理:
功能/特性 | unordered_map |
unordered_set |
---|---|---|
結構定義 | 雜湊表存儲鍵值對(Key-Value Pair) | 雜湊表存儲唯一的鍵(Key) |
典型使用情境 | 當需要同時儲存一個值(Value)與其對應的鍵(Key)時,例如計數、鍵值對查詢 | 當只需要快速查詢某元素是否存在,或需要儲存唯一元素集合時,例如去重、檢查存在性 |
插入/刪除/查詢操作時間複雜度 | 平均 $ O(1) $,最差 $ O(n) $ | 平均 $O(1)$,最差 $O(n)$ |
重點功能 | - 可用於統計出現次數(如頻率計數)。 - 快速通過鍵查詢值 |
- 快速判斷元素是否存在。 - 快速存儲唯一元素(無需額外邏輯進行去重) |
典型應用場景 | 1. 頻率計數:統計字元、數字或其他資料的出現次數,例如異位詞檢查、子陣列和問題 2. 映射查詢:需要通過鍵快速找到值 3. 分組:按某些條件將資料分組並存儲對應的值 |
1. 集合操作:判斷某元素是否存在,例如兩數和問題 2. 去除重複:快速生成唯一元素集合,例如找出數組中的唯一值 |
常見 LeetCode 題目範例 | - Two Sum (#1):用於記錄目標數字的補數和其索引 - Group Anagrams (#49):統計異位詞分組 |
- Contains Duplicate (#217):檢查數組中是否存在重複值 - Intersection of Two Arrays (#349):找出兩數組交集 |
注意事項 | - 若只需要檢查元素存在性,使用 unordered_set 更高效,避免存儲多餘的值 |
- 若需要儲存並操作鍵對應的值,應使用 unordered_map ,unordered_set 無法完成此需求 |
Example
1 |
|
Stack
Stack 筆記整理: 介紹如何實作Stack,以及一些Stack STL 的基本操作
使用時機1: LIFO 問題
像是 括號匹配問題(Parentheses) {()}
這種檢查括號是否閉合的問題就是後進先出(LIFO)的問題,可以由左至右將左括號依序 Push 進入 Stack,碰到右括號就 Pop 出來看是否匹配。或是像用相對路徑存取檔案的這種題目,像是 /var/www/html
這種 檔案路徑的情境題 也很適合用 Stack 去做,先 push 進先前的目錄,而如果要從子目錄移動到上一層,則也需要先將子目錄 Pop 出來,這也會是 LIFO 的情境。
Queue
Queue 筆記整理: 介紹怎麼實作Queue,並且介紹Queue STL 的基本操作
使用時機1: FIFO 問題
如果題目情境很講求順序,例如系統接收請求的順序這種 FIFO的問題,就很適合用到Queue,但我目前並未做到這類的 Queue題目,只有類似資料結構實作題目
Linked List
Linked List 筆記整理 當時主要是用C來進行實作,但是C與C++在Linked List實踐邏輯中其實一樣,語法稍有差異而已,但不太影響解題
建立節點
1 | typedef struct ListNode { |
使用時機1. Two Pointer
當需要遍歷Linked List,並且需要快速找到特定節點或結構時,快慢指針可以在一次遍歷中完成查找,適合處理效率要求較高的問題,例如尋找中間節點或判斷是否有循環。或者今天需要比較或操作多個指針時,雙指針可以用來實現倒數第 N 個節點的定位,或在兩條list間進行同步操作
- LeetCode#876 Middle of the Linked List
快慢指針找中點 - LeetCode#141 Linked List Cycle
快慢指針檢測循環 - LeetCode#19 Remove Nth Node From End of List
雙指針刪除倒數第 N 個節點 - LeetCode#143 Reorder List
快慢指針找中點 + 鏈表翻轉 + 交替合併
使用時機2. Dummy Head
通常如果要新增或刪除節點,抑或是頻繁的操作頭節點,dummy head 就會試一種簡化操作的基礎技巧,也可以用來避免特殊判斷
- LeetCode#203 Remove Linked List Elements
使用 dummy head 刪除指定值的節點 - LeetCode#21 Merge Two Sorted Lists
使用 dummy head 合併Linked List - LeetCode#2 Add Two Numbers
使用 dummy head 構建新Linked List處理多位數相加
使用時機3. 反轉與結構轉換
處理single Linked List反轉、旋轉等結構轉換相關的問題,或是將其他資料結構轉換成linked list
- LeetCode#206 Reverse Linked List
經典Linked List反轉 - LeetCode#61 Rotate List
利用反轉實現旋轉操作 - LeetCode#114 Flatten Binary Tree to Linked List
將二叉樹轉換為單向Linked List
使用時機4. 排序與合併
處理Linked List的合併、重排和排序問題
- LeetCode#21 Merge Two Sorted Lists
合併兩個排序Linked List - LeetCode#328 Odd Even Linked List
根據奇偶節點位置重排 - LeetCode#143 Reorder List
合併已翻轉和未翻轉的Linked List
使用時機5. 特殊指標處理
處理Linked List中帶有隨機指針或其他特殊結構的問題
- LeetCode#138 Copy List with Random Pointer
深拷貝帶隨機指針的Linked List - LeetCode#2 Add Two Numbers
處理帶進位的Linked List
使用時機6. 刪除重複節點
刪除Linked List中多餘或重複的節點
- LeetCode#83 Remove Duplicates from Sorted List
刪除排序Linked List中的重複節點
Tree
Tree 的建構
1 |
|
BFS
又稱 Level-Order Traversal
1 | void BT::bfs(TreeNode *head){ |
DFS
前序(Preorder)
中序(Inorder)
後序(Postorder)
Graph
Graph 主要由節點(vertex, node)跟邊(edge)構成,基本上有分成有向跟無向圖,還有一些特性像是是否是連接(connected) 以及是否有環(circle) 存在。 圖的題目類型通常會是給定一個2D陣列要你去求數量或是面積還有路徑,或者有些會給 Adjacency Matrix (List) 來告訴你節點的連接關係。目前學到的走訪方式有: DFS, BFS (後續好像還有 Topological Sort)
DFS | BFS | |
---|---|---|
適用範圍 | 有向圖、無向圖 | 有向圖、無向圖 |
用途 | 找路徑(不一定最短) | 找最短路徑 |
無向圖
(1) 找出陣列中的 Connected Components
可使用 DFS
, BFS
在走訪過程中透過 counter 來記錄數量,這種題目也會有其他變形題目
- LeetCode#200. Number of Islands 這題就是單純紀錄 connected components 數量
- LeetCode#695. Max Area of Island 這題就是單純紀錄 connected components 數量,還需要累加大小並做比較
- LeetCode#133. Clone Graph 這題為了deep copy 一個 graph,會需要先走訪tree並記錄到 hash table
- LeetCode#130. Surrounded Regions 這題比較特別,他會是從矩陣邊緣開始先找搜尋起點,再去走訪,並且搜尋起點只會在邊緣。
- 417. Pacific Atlantic Water Flow 這題的策略也是需要先從矩陣邊緣作為起點開始走訪,並且會進行兩次 BFS 來去取交集找出座標位置。
(2) 找出最短路徑
- LeetCode#286. Walls and Gates 這題需要透過 BFS 走訪網格,並且直接將距離更新到網格中,這題的bfs 會優先將搜尋起點(閘門)推入Queue中去解
Recursion
使用情境1: 排列組合
使用情境2: 子集
使用情境3: 迴文
Binary Search
題目可能會給陣列或字串,然後要做的事就是要先定義出左右兩側邊界以及中間值,
框架會像是下面這樣
1 | int left = <MIN_VALUE>; |
常見題目
- LeetCode#875. Koko Eating Bananas: 這題主要是透過先定義出最大跟最小吃香蕉的速度,然後透過 Binary Search 來去挑選吃相較的速度值,再去透過其他函數驗證這個值是否能夠在時間內吃完香蕉。接著會去收窄範圍,最終得出的吃香蕉速度就會是最小的。
Heap
Dynamic Programming
DP 筆記整理 整理了Dynamic Programming 的解題邏輯,跟問題背景會是怎樣的, DP會是最佳化解答的好工具!
判斷:1. 大量重複子問題, 2. 解決所有子問題,可以得到整體問題的最最佳化答案 這時候就可以先找出題目的遞迴關係式(暴力解),接著再透過 Memoization進行最佳化,或者如果透過Iteration 的方式直接提出最佳解。
使用情境1: 選與不選的問題
對於每個元素,都可以選或不選,藉由多種選或不選的組合,可以找出正確結果,但包含了大量的重複計算
- LeetCode#70. Climbing Stairs
- LeetCode#322. Coin Change
- LeetCode#139. Word Break
- LeetCode#120. Triangle
其中還包括許多變形,像是股票交易的題目
使用情境2: 迴文系列問題
這類題目通常在遞迴函數之外還需要額外定義的 **用於檢查回文的函數 checkPalindrome
**,通常會將最佳化的 dp
陣列用於這個函數,來避免大量重複計算,如果直接用 reverse()
會是較高的複雜度,通常會透過 Two Pointer 的方式來去實踐回文檢查
Example
1 | while(left < right){ |