更新時間:2025-03-29
- LeetCode題解專欄:實戰算法解題 (專欄)
- 技術博客總目錄:計算機技術系列目錄頁
優先整理熱門100及面試150,不定期持續更新,歡迎關注!
21. 合并兩個有序鏈表
將兩個升序鏈表合并為一個新的 升序 鏈表并返回。新鏈表是通過拼接給定的兩個鏈表的所有節點組成的。
示例 1:
輸入:l1 = [1,2,4], l2 = [1,3,4]
輸出:[1,1,2,3,4,4]
示例 2:
輸入:l1 = [], l2 = []
輸出:[]
示例 3:
輸入:l1 = [], l2 = [0]
輸出:[0]
提示:
兩個鏈表的節點數目范圍是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非遞減順序 排列
方法一:迭代法
使用迭代法合并兩個有序鏈表,通過創建一個 啞節點(dummy node) 作為新鏈表的起始點,逐步比較兩個鏈表的當前節點值,將較小的節點連接到新鏈表中,直到其中一個鏈表遍歷完畢,然后將剩余鏈表直接鏈接到新鏈表的尾部。
- 初始化:創建啞節點
dummy
和當前指針curr
,初始時curr
指向dummy
。 - 遍歷比較:
- 當兩個鏈表均不為空時,比較它們的當前節點值。
- 將較小的節點鏈接到
curr.next
,并移動對應的鏈表指針。 - 移動
curr
到下一個位置。
- 處理剩余節點:將非空鏈表直接鏈接到
curr.next
。 - 返回結果:返回
dummy.next
作為合并后的鏈表頭節點。
代碼實現(Java):
class Solution {public ListNode mergeTwoLists(ListNode list1, ListNode list2) {ListNode dummy = new ListNode(-1);ListNode curr = dummy;while (list1 != null && list2 != null) {if (list1.val <= list2.val) {curr.next = list1;list1 = list1.next;} else {curr.next = list2;list2 = list2.next;}curr = curr.next;}curr.next = list1 != null ? list1 : list2;return dummy.next;}
}
復雜度分析:
- 時間復雜度:
O(m + n)
,其中m
和n
分別為兩個鏈表的長度。 - 空間復雜度:
O(1)
,僅使用固定大小的額外空間。
方法二:遞歸法
遞歸法通過比較兩個鏈表頭節點的值,將較小節點的 next
指針指向剩余鏈表合并的結果,遞歸終止條件為其中一個鏈表為空時直接返回另一個鏈表。
- 終止條件:
- 若
list1
為空,返回list2
。 - 若
list2
為空,返回list1
。
- 若
- 遞歸合并:
- 比較兩個鏈表頭節點的值,較小節點的
next
指向遞歸合并后的結果。 - 返回較小的節點作為當前子鏈表的頭節點。
- 比較兩個鏈表頭節點的值,較小節點的
代碼實現(Java):
class Solution {public ListNode mergeTwoLists(ListNode list1, ListNode list2) {if (list1 == null) return list2;if (list2 == null) return list1;if (list1.val <= list2.val) {list1.next = mergeTwoLists(list1.next, list2);return list1;} else {list2.next = mergeTwoLists(list1, list2.next);return list2;}}
}
復雜度分析:
- 時間復雜度:
O(m + n)
,遞歸遍歷所有節點。 - 空間復雜度:
O(m + n)
,遞歸調用棧的深度最大為兩鏈表長度之和。
對比總結
方法 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
迭代法 | 空間復雜度低,無遞歸開銷 | 代碼相對較長 | 處理大規模鏈表時更優 |
遞歸法 | 代碼簡潔,邏輯清晰 | 空間復雜度高,可能棧溢出 | 鏈表較短或對代碼簡潔性要求高時 |
22. 括號生成
數字 n
代表生成括號的對數,請你設計一個函數,用于能夠生成所有可能的并且 有效的 括號組合。
示例 1:
輸入:n = 3
輸出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
輸入:n = 1
輸出:["()"]
提示:
1 <= n <= 8
方法一:回溯法(深度優先搜索)
通過遞歸生成所有可能的有效括號組合,優先添加左括號并在條件允許時添加右括號,確保右括號數量不超過左括號。
代碼實現(Java):
public class Solution {public List<String> generateParenthesis(int n) {List<String> res = new ArrayList<>();backtrack(res, new StringBuilder(), 0, 0, n);return res;}private void backtrack(List<String> res, StringBuilder current, int left, int right, int n) {if (current.length() == 2 * n) {res.add(current.toString());return;}if (left < n) {current.append('(');backtrack(res, current, left + 1, right, n);current.deleteCharAt(current.length() - 1); // 回溯}if (right < left) {current.append(')');backtrack(res, current, left, right + 1, n);current.deleteCharAt(current.length() - 1); // 回溯}}
}
復雜度分析:
- 時間復雜度:
O(4^n / √n)
,由卡塔蘭數公式決定,每個組合的生成時間為O(1)
均攤。 - 空間復雜度:
O(n)
,遞歸棧深度最大為2n
,字符串長度最多為2n
。
方法二:動態規劃(遞推構造)
利用已知較小規模的結果遞推生成較大規模的結果,將問題分解為內部和外部括號組合的拼接。
代碼實現(Java):
public class Solution {public List<String> generateParenthesis(int n) {List<List<String>> dp = new ArrayList<>();dp.add(List.of("")); // dp[0] 初始化為空字符串for (int i = 1; i <= n; i++) {List<String> current = new ArrayList<>();for (int k = 0; k < i; k++) {// 內部 k 對括號,外部 i-1-k 對括號for (String inner : dp.get(k)) {for (String outer : dp.get(i - 1 - k)) {current.add("(" + inner + ")" + outer);}}}dp.add(current);}return dp.get(n);}
}
復雜度分析:
- 時間復雜度:
O(n^2 * C(n))
,C(n)
為卡塔蘭數,需要多層嵌套循環。 - 空間復雜度:
O(n * C(n))
,存儲所有中間結果。
方法三:迭代法(廣度優先搜索)
用隊列保存中間狀態,逐步擴展每個可能的括號組合,直到達到目標長度。
代碼實現(Java):
public class Solution {static class Node {String str;int left;int right;public Node(String s, int l, int r) {str = s;left = l;right = r;}}public List<String> generateParenthesis(int n) {List<String> res = new ArrayList<>();Queue<Node> queue = new LinkedList<>();queue.offer(new Node("", 0, 0));while (!queue.isEmpty()) {Node node = queue.poll();if (node.str.length() == 2 * n) {res.add(node.str);continue;}if (node.left < n) {queue.offer(new Node(node.str + "(", node.left + 1, node.right));}if (node.right < node.left) {queue.offer(new Node(node.str + ")", node.left, node.right + 1));}}return res;}
}
復雜度分析:
- 時間復雜度:
O(4^n / √n)
,與回溯法相同。 - 空間復雜度:
O(4^n / √n)
,隊列中存儲所有中間狀態的字符串。
對比總結
- 回溯法 是最高效的實現,直接通過剪枝避免無效路徑,代碼簡潔。
- 動態規劃 思路巧妙,但空間占用較大,適合研究問題分解的規律。
- 迭代法(BFS) 無遞歸棧溢出風險,適合需要迭代實現的場景。
23. 合并 K 個升序鏈表
給你一個鏈表數組,每個鏈表都已經按升序排列。
請你將所有鏈表合并到一個升序鏈表中,返回合并后的鏈表。
示例 1:
輸入:lists = [[1,4,5],[1,3,4],[2,6]]
輸出:[1,1,2,3,4,4,5,6]
解釋:鏈表數組如下,
[1->4->5, 1->3->4, 2->6]
將它們合并到一個有序鏈表中得到。
1->1->2->3->4->4->5->6
示例 2:
輸入:lists = []
輸出:[]
示例 3:
輸入:lists = [[]]
輸出:[]
提示:
k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i] 按 升序 排列
lists[i].length 的總和不超過 10^4
方法一:分治合并(遞歸)
將鏈表數組遞歸地分成兩半,分別合并左右兩半,再將結果合并。利用分治策略降低時間復雜度,每次合并兩個有序鏈表。
- 遞歸終止:當鏈表數組為空或只有一個鏈表時返回。
- 分治處理:找到中間位置,遞歸合并左右兩半。
- 合并結果:將遞歸得到的兩個有序鏈表合并。
代碼實現(Java):
class Solution {public ListNode mergeKLists(ListNode[] lists) {if (lists == null || lists.length == 0) return null;return merge(lists, 0, lists.length - 1);}private ListNode merge(ListNode[] lists, int start, int end) {if (start == end) return lists[start];int mid = start + (end - start) / 2;ListNode left = merge(lists, start, mid);ListNode right = merge(lists, mid + 1, end);return mergeTwoLists(left, right);}private ListNode mergeTwoLists(ListNode l1, ListNode l2) {ListNode dummy = new ListNode(0);ListNode curr = dummy;while (l1 != null && l2 != null) {if (l1.val < l2.val) {curr.next = l1;l1 = l1.next;} else {curr.next = l2;l2 = l2.next;}curr = curr.next;}curr.next = (l1 != null) ? l1 : l2;return dummy.next;}
}
復雜度分析:
- 時間復雜度:
O(nk logk)
,其中n
是平均鏈表長度,k
是鏈表數量。 - 空間復雜度:
O(logk)
,遞歸調用棧的深度。
方法二:分治合并(迭代)
通過迭代方式逐步合并相鄰鏈表,避免遞歸棧開銷。每次將鏈表兩兩合并,直到只剩一個鏈表。
- 初始化處理:直接處理鏈表數組。
- 逐步合并:每次合并相鄰兩個鏈表,縮小數組范圍。
- 最終合并:循環直到數組只剩一個鏈表。
代碼實現(Java):
class Solution {public ListNode mergeKLists(ListNode[] lists) {if (lists == null || lists.length == 0) return null;int k = lists.length;while (k > 1) {int idx = 0;for (int i = 0; i < k; i += 2) {ListNode l1 = lists[i];ListNode l2 = (i + 1 < k) ? lists[i + 1] : null;lists[idx++] = mergeTwoLists(l1, l2);}k = idx;}return lists[0];}private ListNode mergeTwoLists(ListNode l1, ListNode l2) {ListNode dummy = new ListNode(0);ListNode curr = dummy;while (l1 != null && l2 != null) {if (l1.val < l2.val) {curr.next = l1;l1 = l1.next;} else {curr.next = l2;l2 = l2.next;}curr = curr.next;}curr.next = (l1 != null) ? l1 : l2;return dummy.next;}
}
復雜度分析:
- 時間復雜度:
O(nk logk)
。 - 空間復雜度:
O(1)
,無額外遞歸棧空間。
方法三:優先隊列(最小堆)
利用最小堆維護當前所有鏈表的最小節點。每次取出堆頂節點,將其下一節點加入堆中,直到堆為空。
- 初始化堆:將所有非空鏈表頭節點加入堆。
- 構建結果:不斷取出堆頂節點,鏈接到結果鏈表。
- 維護堆:將取出節點的下一節點入堆。
代碼實現(Java):
class Solution {public ListNode mergeKLists(ListNode[] lists) {if (lists == null || lists.length == 0) return null;PriorityQueue<ListNode> heap = new PriorityQueue<>((a, b) -> a.val - b.val);for (ListNode node : lists) {if (node != null) heap.offer(node);}ListNode dummy = new ListNode(0);ListNode curr = dummy;while (!heap.isEmpty()) {ListNode minNode = heap.poll();curr.next = minNode;curr = curr.next;if (minNode.next != null) {heap.offer(minNode.next);}}return dummy.next;}
}
復雜度分析:
- 時間復雜度:
O(nk logk)
。 - 空間復雜度:
O(k)
,堆存儲最多k
個節點。
對比總結
方法 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
分治合并(遞歸) | 代碼簡潔,邏輯清晰 | 遞歸棧空間O(logk) | 常規場景,k較小 |
分治合并(迭代) | 無棧溢出風險,空間最優 | 修改原數組結構 | k較大的場景,空間敏感 |
優先隊列 | 實現簡單,直觀 | 堆空間O(k) | k較小的場景,鏈表較短 |
24. 兩兩交換鏈表中的節點
給你一個鏈表,兩兩交換其中相鄰的節點,并返回交換后鏈表的頭節點。你必須在不修改節點內部的值的情況下完成本題(即,只能進行節點交換)。
示例 1:
輸入:head = [1,2,3,4]
輸出:[2,1,4,3]
示例 2:
輸入:head = []
輸出:[]
示例 3:
輸入:head = [1]
輸出:[1]
提示:
鏈表中節點的數目在范圍 [0, 100] 內
0 <= Node.val <= 100
方法:迭代法
通過迭代遍歷鏈表,每次交換相鄰兩個節點。使用啞節點簡化頭節點處理,維護前驅指針current
,每次交換current
后的兩個節點,并更新current
的位置。
- 初始化啞節點:避免處理頭節點交換的特殊情況。
- 遍歷條件:當前節點后存在兩個可交換節點。
- 節點交換:
- 保存當前兩個節點及后續節點。
- 調整指針指向完成交換。
- 移動前驅指針:前驅指針移動到已交換對的第二個節點,繼續后續操作。
代碼實現(Java):
class Solution {public ListNode swapPairs(ListNode head) {ListNode dummy = new ListNode(-1);dummy.next = head;ListNode current = dummy;while (current.next != null && current.next.next != null) {// 獲取要交換的兩個節點ListNode first = current.next;ListNode second = first.next;ListNode next = second.next;// 交換節點current.next = second;second.next = first;first.next = next;// 移動current指針到已交換對的第二個節點,作為下一輪的前驅current = first;}return dummy.next;}
}
復雜度分析
- 時間復雜度:
O(n)
,每個節點僅遍歷一次。 - 空間復雜度:
O(1)
,僅使用常量額外空間。
25. K 個一組翻轉鏈表
給你鏈表的頭節點 head
,每 k
個節點一組進行翻轉,請你返回修改后的鏈表。
k
是一個正整數,它的值小于或等于鏈表的長度。如果節點總數不是 k
的整數倍,那么請將最后剩余的節點保持原有順序。
你不能只是單純的改變節點內部的值,而是需要實際進行節點交換。
示例 1:
輸入:head = [1,2,3,4,5], k = 2
輸出:[2,1,4,3,5]
示例 2:
輸入:head = [1,2,3,4,5], k = 3
輸出:[3,2,1,4,5]
提示:
鏈表中的節點數目為 n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
方法一:迭代法
通過迭代遍歷鏈表,每次處理k個節點組成的子鏈表。使用啞節點簡化頭節點處理,維護前驅指針pre
,每次找到當前組的首尾節點后進行反轉,并重新連接鏈表。
- 初始化啞節點:避免處理頭節點反轉的特殊情況。
- 尋找當前組的尾節點:通過循環k次定位當前組的結束位置。
- 反轉子鏈表:將當前組的k個節點反轉,返回新的頭和尾。
- 重新連接鏈表:將前驅節點連接到反轉后的頭,反轉后的尾連接到下一組的頭。
- 更新指針:將前驅指針移動到當前組的尾,繼續處理后續節點。
代碼實現(Java):
class Solution {public ListNode reverseKGroup(ListNode head, int k) {ListNode dummy = new ListNode(0);dummy.next = head;ListNode pre = dummy;ListNode end = dummy;while (end.next != null) {// 定位當前組的尾節點for (int i = 0; i < k; i++) {end = end.next;if (end == null) return dummy.next; // 不足k個直接返回}// 記錄關鍵節點ListNode start = pre.next;ListNode nextGroup = end.next;end.next = null; // 斷開當前組// 反轉當前組并連接pre.next = reverse(start);start.next = nextGroup; // 原start變為當前組的尾// 更新指針pre = start;end = pre;}return dummy.next;}// 反轉鏈表并返回新頭節點private ListNode reverse(ListNode head) {ListNode prev = null;ListNode curr = head;while (curr != null) {ListNode next = curr.next;curr.next = prev;prev = curr;curr = next;}return prev;}
}
復雜度分析:
- 時間復雜度:
O(n)
,每個節點被處理兩次(遍歷和反轉)。 - 空間復雜度:
O(1)
,僅使用常量額外空間。
方法二:遞歸法
遞歸處理每個分組,先判斷剩余節點是否足夠k個,若足夠則反轉前k個節點,遞歸處理后續鏈表,并將當前尾節點與后續結果連接。
- 檢查剩余長度:遍歷
k
次判斷是否足夠反轉。 - 反轉當前組:反轉前
k
個節點。 - 遞歸后續鏈表:將當前尾節點的
next
指向遞歸處理后的結果。 - 返回新頭節點:當前組的頭節點變為反轉后的首節點。
代碼實現(Java):
class Solution {public ListNode reverseKGroup(ListNode head, int k) {ListNode curr = head;int count = 0;// 檢查是否有足夠k個節點while (curr != null && count < k) {curr = curr.next;count++;}if (count == k) { // 足夠k個則反轉ListNode reversedHead = reverse(head, k);head.next = reverseKGroup(curr, k); // 原head變為當前組的尾return reversedHead;}return head; // 不足k個直接返回}// 反轉前k個節點private ListNode reverse(ListNode head, int k) {ListNode prev = null;ListNode curr = head;while (k-- > 0) {ListNode next = curr.next;curr.next = prev;prev = curr;curr = next;}return prev;}
}
復雜度分析:
- 時間復雜度:
O(n)
,每個節點被處理一次。 - 空間復雜度:
O(n/k)
,遞歸棧深度為分組數。
26. 刪除有序數組中的重復項
給你一個 非嚴格遞增排列 的數組 nums
,請你 原地 刪除重復出現的元素,使每個元素 只出現一次 ,返回刪除后數組的新長度。元素的 相對順序 應該保持 一致 。然后返回 nums
中唯一元素的個數。
考慮 nums
的唯一元素的數量為 k
,你需要做以下事情確保你的題解可以被通過:
- 更改數組
nums
,使nums
的前k
個元素包含唯一元素,并按照它們最初在nums
中出現的順序排列。nums
的其余元素與nums
的大小不重要。 - 返回
k
。
系統會用下面的代碼來測試你的題解:
int[] nums = [...]; // 輸入數組
int[] expectedNums = [...]; // 長度正確的期望答案
int k = removeDuplicates(nums); // 調用
assert k == expectedNums.length;
for (int i = 0; i < k; i++) {assert nums[i] == expectedNums[i];
}
如果所有斷言都通過,那么您的題解將被 通過。
示例 1:
輸入:nums = [1,1,2]
輸出:2, nums = [1,2,_]
解釋:函數應該返回新的長度 2 ,并且原數組 nums 的前兩個元素被修改為 1, 2 。不需要考慮數組中超出新長度后面的元素。
示例 2:
輸入:nums = [0,0,1,1,1,2,2,3,3,4]
輸出:5, nums = [0,1,2,3,4]
解釋:函數應該返回新的長度 5 , 并且原數組 nums 的前五個元素被修改為 0, 1, 2, 3, 4 。不需要考慮數組中超出新長度后面的元素。
提示:
1 <= nums.length <= 3 * 10^4
-10^4 <= nums[i] <= 10^4
nums 已按
非嚴格遞增
排列
方法:雙指針法
使用快慢雙指針,快指針遍歷數組,慢指針記錄不重復元素的位置。當遇到不重復元素時,將其移至慢指針的下一個位置,并更新慢指針。由于數組已排序,只需比較相鄰元素即可。
- 處理空數組的特殊情況。
- 初始化慢指針
slow
為0。 - 快指針
fast
從1開始遍歷數組:- 當
nums[fast] != nums[slow]
,移動慢指針并更新其值。
- 當
- 返回
slow + 1
作為唯一元素的個數。
代碼實現(Java):
class Solution {public int removeDuplicates(int[] nums) {if (nums.length == 0) return 0;int slow = 0;for (int fast = 1; fast < nums.length; fast++) {if (nums[fast] != nums[slow]) {slow++;nums[slow] = nums[fast];}}return slow + 1;}
}
復雜度分析:
- 時間復雜度:
O(n)
,只需一次遍歷數組。 - 空間復雜度:
O(1)
,原地修改,僅使用常數空間。
27. 移除元素
給你一個數組 nums
和一個值 val
,你需要 原地 移除所有數值等于 val
的元素。元素的順序可能發生改變。然后返回 nums
中與 val
不同的元素的數量。
假設 nums
中不等于 val
的元素數量為 k
,要通過此題,您需要執行以下操作:
更改 nums
數組,使 nums
的前 k
個元素包含不等于 val
的元素。nums
的其余元素和 nums
的大小并不重要。
返回 k
。
評測機將使用以下代碼測試您的解決方案:
int[] nums = [...]; // 輸入數組
int val = ...; // 要移除的值
int[] expectedNums = [...]; // 長度正確的預期答案。
int k = removeElement(nums, val); // 調用你的實現
assert k == expectedNums.length;
sort(nums, 0, k); // 排序 nums 的前 k 個元素
for (int i = 0; i < actualLength; i++) {assert nums[i] == expectedNums[i];
}
如果所有的斷言都通過,你的解決方案將會 通過。
示例 1:
輸入:nums = [3,2,2,3], val = 3
輸出:2, nums = [2,2,_,_]
解釋:你的函數函數應該返回 k = 2, 并且 nums 中的前兩個元素均為 2。
你在返回的 k 個元素之外留下了什么并不重要(因此它們并不計入評測)。
示例 2:
輸入:nums = [0,1,2,2,3,0,4,2], val = 2
輸出:5, nums = [0,1,4,0,3,_,_,_]
解釋:你的函數應該返回 k = 5,并且 nums 中的前五個元素為 0,0,1,3,4。
注意這五個元素可以任意順序返回。
你在返回的 k 個元素之外留下了什么并不重要(因此它們并不計入評測)。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
方法:快慢雙指針法
使用快慢指針遍歷數組,快指針檢查每個元素是否等于目標值。當遇到不等于目標值的元素時,將其復制到慢指針位置,并移動慢指針。最終慢指針的位置即為新數組長度。
- 初始化慢指針:
index
記錄有效元素位置,初始為0。 - 快指針遍歷:快指針
i
遍歷數組,遇到非目標值時,復制到index
位置并移動慢指針。 - 返回結果:慢指針位置即為新長度。
代碼實現(Java):
class Solution {public int removeElement(int[] nums, int val) {int index = 0;for (int i = 0; i < nums.length; i++) {if (nums[i] != val) {nums[index++] = nums[i];}}return index;}
}
復雜度分析:
- 時間復雜度:
O(n)
,僅需一次遍歷。 - 空間復雜度:
O(1)
,原地修改數組,無需額外空間。
聲明
- 本文版權歸
CSDN
用戶Allen Wurlitzer
所有,遵循CC-BY-SA
協議發布,轉載請注明出處。- 本文題目來源
力扣-LeetCode
,著作權歸領扣網絡
所有。商業轉載請聯系官方授權,非商業轉載請注明出處。