參考:
https://zhuanlan.zhihu.com/p/71643340
https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/
雙指針問題
什么是雙指針(對撞指針、快慢指針)
雙指針,指的是在遍歷對象的過程中,不是普通的使用單個指針進行訪問,而是使用兩個相同方向(快慢指針)或者相反方向(對撞指針)的指針進行掃描,從而達到相應的目的。
換言之,雙指針法充分使用了數組有序這一特征,從而在某些情況下能夠簡化一些運算。
在LeetCode
題庫中,關於雙指針的問題還是挺多的。雙指針
截圖來之LeetCode中文官網
用法
對撞指針
對撞指針是指在有序數組中,將指向最左側的索引定義為左指針(left)
,最右側的定義為右指針(right)
,然后從兩頭向中間進行數組遍歷。
對撞數組適用於有序數組,也就是說當你遇到題目給定有序數組時,應該第一時間想到用對撞指針解題。
偽代碼大致如下:
function fn (list) { var left = 0; var right = list.length - 1; //遍歷數組 while (left <= right) { left++; // 一些條件判斷 和處理 ... ... right--; } }
舉個LeetCode上的例子:
以LeetCode 881救生艇問題為例
由於本題只要求計算出最小船數
,所以原數組是否被改變,和元素索引位置都不考慮在內,所以可以先對於給定數組進行排序,再從數組兩側向中間遍歷。所以解題思路如下:
- 對給定數組進行升序排序
- 初始化左右指針
- 每次都用一個”最重的“和一個”最輕的“進行配對,如果二人重量小於
Limit
,則此時的”最輕的“上船,即(left++
)。不管”最輕的“是否上船,”最重的“都要上船,即(right--
)並且所需船數量加一,即(num++
)
代碼如下:
var numRescueBoats = function(people, limit) { people.sort((a, b) => (a - b)); var num = 0 let left = 0 let right = people.length - 1 while (left <= right) { if ((people[left] + people[right]) <= limit) { left++ } right-- num++ } return num };
題解:https://leetcode-cn.com/problems/boats-to-save-people/solution/jiu-sheng-ting-by-leetcode/
方法:貪心(雙指針)
思路
如果最重的人可以與最輕的人共用一艘船,那么就這樣安排。否則,最重的人無法與任何人配對,那么他們將自己獨自乘一艘船。
這么做的原因是,如果最輕的人可以與任何人配對,那么他們也可以與最重的人配對。
算法
令 people[i] 指向當前最輕的人,而 people[j] 指向最重的那位。
然后,如上所述,如果最重的人可以與最輕的人共用一條船(即 people[j] + people[i] <= limit),那么就這樣做;否則,最重的人自己獨自坐在船上。
class Solution { public int numRescueBoats(int[] people, int limit) { Arrays.sort(people); int i = 0, j = people.length - 1; int ans = 0; while (i <= j) { ans++; if (people[i] + people[j] <= limit) i++; j--; } return ans; } } 作者:LeetCode 鏈接:https://leetcode-cn.com/problems/boats-to-save-people/solution/jiu-sheng-ting-by-leetcode/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
復雜度分析
-
時間復雜度:O(NlogN),其中 N 是
people
的長度。 -
空間復雜度:O(N)
快慢指針
快慢指針也是雙指針,但是兩個指針從同一側開始遍歷數組,將這兩個指針分別定義為快指針(fast)
和慢指針(slow)
,兩個指針以不同的策略移動,直到兩個指針的值相等(或其他特殊條件)為止,如fast每次增長兩個,slow每次增長一個。
以LeetCode 141.環形鏈表為例,,判斷給定鏈表中是否存在環,可以定義快慢兩個指針,快指針每次增長一個,而慢指針每次增長兩個,最后兩個指針指向節點的值相等,則說明有環。就好像一個環形跑道上有一快一慢兩個運動員賽跑,如果時間足夠長,跑地快的運動員一定會趕上慢的運動員。
解題代碼如下:
/** * Definition for singly-linked list. * function ListNode(val) { * this.val = val; * this.next = null; * } */ /** * @param {ListNode} head * @return {boolean} */ var hasCycle = function(head) { if (head === null || head.next === null) { return false } let slow = head let fast = head.next while (slow !== fast) { if (fast === null || fast.next === null) { return false } slow = slow.next fast = fast.next.next } return true };
再比如LeetCode 26 刪除排序數組中的重復項,這里還是定義快慢兩個指針。快指針每次增長一個,慢指針只有當快慢指針上的值不同時,才增長一個(由於是有序數組,快慢指針值不等說明找到了新值)。
真實代碼:
var removeDuplicates = function (nums) { if (nums.length === 0) { return 0; } let slow = 0; for (let fast = 0; fast < nums.length; fast++) { if (nums[fast] !== nums[slow]) { slow++; nums[slow] = nums[fast]; } } return slow + 1; };
題解:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/solution/shan-chu-pai-xu-shu-zu-zhong-de-zhong-fu-xiang-by-/
public int removeDuplicates(int[] nums) { if (nums.length == 0) return 0; int i = 0; for (int j = 1; j < nums.length; j++) { if (nums[j] != nums[i]) { i++; nums[i] = nums[j]; } } return i + 1; }
復雜度分析
-
時間復雜度:O(n),假設數組的長度是 n,那么 i 和 j 分別最多遍歷 n 步。
-
空間復雜度:O(1)。
總結
當遇到有序數組時,應該優先想到雙指針
來解決問題,因兩個指針的同時遍歷會減少空間復雜度和時間復雜度。
leetcode典型題目
160. 相交鏈表
注意:
如果兩個鏈表沒有交點,返回 null.
在返回結果后,兩個鏈表仍須保持原有的結構。
可假定整個鏈表結構中沒有循環。
程序盡量滿足 O(n) 時間復雜度,且僅用 O(1) 內存。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists
著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。
題解:https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/tu-jie-xiang-jiao-lian-biao-by-user7208t/
空間復雜度 O(1) 時間復雜度為 O(n)
這里使用圖解的方式,解釋比較巧妙的一種實現。
根據題目意思
如果兩個鏈表相交,那么相交點之后的長度是相同的
我們需要做的事情是,讓兩個鏈表從同距離末尾同等距離的位置開始遍歷。這個位置只能是較短鏈表的頭結點位置。
為此,我們必須消除兩個鏈表的長度差
指針 pA 指向 A 鏈表,指針 pB 指向 B 鏈表,依次往后遍歷
如果 pA 到了末尾,則 pA = headB 繼續遍歷
如果 pB 到了末尾,則 pB = headA 繼續遍歷
比較長的鏈表指針指向較短鏈表head時,長度差就消除了
如此,只需要將最短鏈表遍歷兩次即可找到位置
聽着可能有點繞,看圖最直觀,鏈表的題目最適合看圖了
public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if (headA == null || headB == null) return null; ListNode pA = headA, pB = headB; while (pA != pB) { pA = pA == null ? headB : pA.next; pB = pB == null ? headA : pB.next; } return pA; } 作者:reals 鏈接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/tu-jie-xiang-jiao-lian-biao-by-user7208t/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
19. 刪除鏈表的倒數第N個節點
給定一個鏈表,刪除鏈表的倒數第 n 個節點,並且返回鏈表的頭結點。
示例:
給定一個鏈表: 1->2->3->4->5, 和 n = 2.
當刪除了倒數第二個節點后,鏈表變為 1->2->3->5.
說明:
給定的 n 保證是有效的。
進階:
你能嘗試使用一趟掃描實現嗎?
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list
著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。
題解:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/hua-jie-suan-fa-19-shan-chu-lian-biao-de-dao-shu-d/
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode pre = new ListNode(0); pre.next = head; ListNode start = pre, end = pre; while(n != 0) { start = start.next; n--; } while(start.next != null) { start = start.next; end = end.next; } end.next = end.next.next; return pre.next; } } 作者:guanpengchn 鏈接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/hua-jie-suan-fa-19-shan-chu-lian-biao-de-dao-shu-d/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
141. 環形鏈表
給定一個鏈表,判斷鏈表中是否有環。
如果鏈表中有某個節點,可以通過連續跟蹤 next 指針再次到達,則鏈表中存在環。 為了表示給定鏈表中的環,我們使用整數 pos 來表示鏈表尾連接到鏈表中的位置(索引從 0 開始)。 如果 pos 是 -1,則在該鏈表中沒有環。注意:pos 不作為參數進行傳遞,僅僅是為了標識鏈表的實際情況。
如果鏈表中存在環,則返回 true 。 否則,返回 false 。
進階:
你能用 O(1)(即,常量)內存解決此問題嗎?
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/linked-list-cycle
著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。
題解:https://leetcode-cn.com/problems/linked-list-cycle/solution/3chong-jie-jue-fang-shi-liang-chong-ji-bai-liao-10/
1,快慢指針解決
判斷鏈表是否有環應該是老生常談的一個話題了,最簡單的一種方式就是快慢指針,慢指針針每次走一步,快指針每次走兩步,如果相遇就說明有環,如果有一個為空說明沒有環。代碼比較簡單
public boolean hasCycle(ListNode head) { if (head == null) return false; //快慢兩個指針 ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { //慢指針每次走一步 slow = slow.next; //快指針每次走兩步 fast = fast.next.next; //如果相遇,說明有環,直接返回true if (slow == fast) return true; } //否則就是沒環 return false; } 作者:sdwwld 鏈接:https://leetcode-cn.com/problems/linked-list-cycle/solution/3chong-jie-jue-fang-shi-liang-chong-ji-bai-liao-10/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
到這里問題好像並沒有結束,為什么快慢指針就一定能判斷是否有環。我們可以這樣來思考一下,假如有環,那么快慢指針最終都會走到環上,假如環的長度是m,快慢指針最近的間距是n,如下圖中所示
快指針每次走兩步,慢指針每次走一步,所以每走一次快慢指針的間距就要縮小一步,在圖一中當走n次的時候就會相遇,在圖二中當走m-n次的時候就會相遇。
2,存放到集合中
這題還可以把節點存放到集合set中,每次存放的時候判斷當前節點是否存在,如果存在,說明有環,直接返回true,比較容易理解
public boolean hasCycle(ListNode head) { Set<ListNode> set = new HashSet<>(); while (head != null) { //如果重復出現說明有環 if (set.contains(head)) return true; //否則就把當前節點加入到集合中 set.add(head); head = head.next; } return false; }
234. 回文鏈表
請判斷一個鏈表是否為回文鏈表。
示例 1:
輸入: 1->2
輸出: false
示例 2:
輸入: 1->2->2->1
輸出: true
進階:
你能否用 O(n) 時間復雜度和 O(1) 空間復雜度解決此題?
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/palindrome-linked-list
著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。
題解:https://leetcode-cn.com/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode-solution/
代碼
class Solution { public boolean isPalindrome(ListNode head) { List<Integer> vals = new ArrayList<Integer>(); // 將鏈表的值復制到數組中 ListNode currentNode = head; while (currentNode != null) { vals.add(currentNode.val); currentNode = currentNode.next; } // 使用雙指針判斷是否回文 int front = 0; int back = vals.size() - 1; while (front < back) { if (!vals.get(front).equals(vals.get(back))) { return false; } front++; back--; } return true; } } 作者:LeetCode-Solution 鏈接:https://leetcode-cn.com/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode-solution/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
復雜度分析
時間復雜度:O(n),其中 n 指的是鏈表的元素個數。
第一步: 遍歷鏈表並將值復制到數組中,O(n)。
第二步:雙指針判斷是否為回文,執行了 O(n/2) 次的判斷,即O(n)。
總的時間復雜度:O(2n) = O(n)。
空間復雜度:O(n),其中 n 指的是鏈表的元素個數,我們使用了一個數組列表存放鏈表的元素值。
作者:LeetCode-Solution
鏈接:https://leetcode-cn.com/problems/palindrome-linked-list/solution/hui-wen-lian-biao-by-leetcode-solution/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
方法三:快慢指針
思路
避免使用 O(n) 額外空間的方法就是改變輸入。
我們可以將鏈表的后半部分反轉(修改鏈表結構),然后將前半部分和后半部分進行比較。比較完成后我們應該將鏈表恢復原樣。雖然不需要恢復也能通過測試用例,但是使用該函數的人通常不希望鏈表結構被更改。
該方法雖然可以將空間復雜度降到 O(1),但是在並發環境下,該方法也有缺點。在並發環境下,函數運行時需要鎖定其他線程或進程對鏈表的訪問,因為在函數執行過程中鏈表會被修改。
算法
整個流程可以分為以下五個步驟:
找到前半部分鏈表的尾節點。
反轉后半部分鏈表。
判斷是否回文。
恢復鏈表。
返回結果。
執行步驟一,我們可以計算鏈表節點的數量,然后遍歷鏈表找到前半部分的尾節點。
我們也可以使用快慢指針在一次遍歷中找到:慢指針一次走一步,快指針一次走兩步,快慢指針同時出發。當快指針移動到鏈表的末尾時,慢指針恰好到鏈表的中間。通過慢指針將鏈表分為兩部分。
若鏈表有奇數個節點,則中間的節點應該看作是前半部分。
步驟二可以使用「206. 反轉鏈表」問題中的解決方法來反轉鏈表的后半部分。
步驟三比較兩個部分的值,當后半部分到達末尾則比較完成,可以忽略計數情況中的中間節點。
步驟四與步驟二使用的函數相同,再反轉一次恢復鏈表本身。
代碼
class Solution { public boolean isPalindrome(ListNode head) { if (head == null) { return true; } // 找到前半部分鏈表的尾節點並反轉后半部分鏈表 ListNode firstHalfEnd = endOfFirstHalf(head); ListNode secondHalfStart = reverseList(firstHalfEnd.next); // 判斷是否回文 ListNode p1 = head; ListNode p2 = secondHalfStart; boolean result = true; while (result && p2 != null) { if (p1.val != p2.val) { result = false; } p1 = p1.next; p2 = p2.next; } // 還原鏈表並返回結果 firstHalfEnd.next = reverseList(secondHalfStart); return result; } private ListNode reverseList(ListNode head) { ListNode prev = null; ListNode curr = head; while (curr != null) { ListNode nextTemp = curr.next; curr.next = prev; prev = curr; curr = nextTemp; } return prev; } private ListNode endOfFirstHalf(ListNode head) { ListNode fast = head; ListNode slow = head; while (fast.next != null && fast.next.next != null) { fast = fast.next.next; slow = slow.next; } return slow; } }
復雜度分析
時間復雜度:O(n),其中 n 指的是鏈表的大小。
空間復雜度:O(1)。我們只會修改原本鏈表中節點的指向,而在堆棧上的堆棧幀不超過 O(1)。
另附上鏈表反轉的代碼
public class NodeReverse { static class Node { int val; Node next; public Node(int val) { this.val = val; } public Node() { } } public static void main(String[] args) { Node head = new Node(1); Node node1 = new Node(2); Node node2 = new Node(3); head.next = node1; node1.next = node2; Node node = head; head.val = 1111; while (head != null) { System.out.println(head.val); head = head.next; } Node result = reverseNode(node); while (result != null) { System.out.println(result.val); result = result.next; } } private static Node reverseNode(Node head) { Node pre = null; Node cur = head; while (cur != null) { Node tmpNode = cur.next; cur.next = pre; pre = cur; cur = tmpNode; } return pre; } }
執行結果:
1111
2
3
3
2
1111
15. 三數之和
給你一個包含 n 個整數的數組 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?請你找出所有滿足條件且不重復的三元組。
注意:答案中不可以包含重復的三元組。
示例:
給定數組 nums = [-1, 0, 1, 2, -1, -4],
滿足要求的三元組集合為:
[
[-1, 0, 1],
[-1, -1, 2]
]
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/3sum
著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。
最簡單的辦法是,每個人都去依次拉上另一個人一起去找第三個人,這個時間復雜度是 O(n3)。
var threeSum = function(nums) { let res = [] for (let i = 0; i < nums.length - 2; i++) { // 每個人 for (let j = i + 1; j < nums.length - 1; j++) { // 依次拉上其他每個人 for (let k = j + 1; k < nums.length; k++) { // 去問剩下的每個人 if (nums[i] + nums[j] + nums[k] === 0) { // 我們是不是可以一起組隊 res.push([nums[i], nums[j], nums[k]]) } } } } return res } 作者:wonderful611 鏈接:https://leetcode-cn.com/problems/3sum/solution/three-sum-ti-jie-by-wonderful611/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
題解:https://leetcode-cn.com/problems/3sum/solution/hua-jie-suan-fa-15-san-shu-zhi-he-by-guanpengchn/
class Solution { public static List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> ans = new ArrayList(); int len = nums.length; if(nums == null || len < 3) return ans; Arrays.sort(nums); // 排序 for (int i = 0; i < len ; i++) { if(nums[i] > 0) break; // 如果當前數字大於0,則三數之和一定大於0,所以結束循環 if(i > 0 && nums[i] == nums[i-1]) continue; // 去重 int L = i+1; int R = len-1; while(L < R){ int sum = nums[i] + nums[L] + nums[R]; if(sum == 0){ ans.add(Arrays.asList(nums[i],nums[L],nums[R])); while (L<R && nums[L] == nums[L+1]) L++; // 去重 while (L<R && nums[R] == nums[R-1]) R--; // 去重 L++; R--; } else if (sum < 0) L++; else if (sum > 0) R--; } } return ans; } } 作者:guanpengchn 鏈接:https://leetcode-cn.com/problems/3sum/solution/hua-jie-suan-fa-15-san-shu-zhi-he-by-guanpengchn/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。