什麼是雙指針
雙指針,指的是在遍歷對象的過程中,不是普通的使用單個指針進行訪問,而是使用兩個相同方向(快慢指針:一個在前一個在後,同方向遍歷)或者相反方向(對撞指針:一個從頭向尾,一個從尾向頭,反方向遍歷)的指針進行掃描,從而達到相應的目的,多用於有序數組題目中,部分鏈表相關題目也有出現,善加利用能極大提升算法的效率。
快慢指針(數組)
兩個指針從同一側開始遍歷數組,將這兩個指針分別定義爲 快指針(fast) 和 慢指針(slow),兩個指針以不同的策略移動,直到滿足條件爲止(兩個指針的值相等或其他特殊條件)爲止,如fast每次增長兩個,slow每次增長一個。 快慢指針典型題目爲LeetCode No.26 刪除有序數組中的重複項, 初見該題目很容易想到的是兩層循環遍歷每一個元素,如果有相同的元素則標記爲一個值(如Integer.MAX_VALUE),後續甩到隊尾(Arrays.sort()),這種解法的時間複雜度爲O(n^2),相比快慢指針方法O(n)性能非常底下(300ms VS 1ms)。 那麼如何使用雙指針法來解該題,代碼如下:
public int removeDuplicates(int[] nums) {
// 題目規定nums.length >= 0, 不需要考慮nums爲0的情況
int slow = 0;
int fast = slow + 1;
while (fast < nums.length) {
// slow指向的值和fast指向的值相等, 移動fast繼續尋找不同值
if (nums[slow] == nums[fast]) {
++fast;
} else {
// slow指向的值和fast指向的值不相等
// 將第一個出現的不相等值移動到slow指向值的後面
// 移動向後移動slow指針到新的值, 並移動fast指針到下一位
nums[slow + 1] = nums[fast];
++fast;
++slow;
}
return slow + 1;
}
}
分析上述代碼,可以得到在有序數組題型中,快慢指針的模板代碼:
快慢指針(鏈表)
鏈表中任然可以使用快慢指針,一種使用場景是判斷鏈表是否有環142. 環形鏈表 II,另一種場景是兩條鏈表相交的場景面試題 02.07. 鏈表相交,都是判斷鏈表是否有相交的場景,只不過環是首尾相交而已。
鏈表是否存在環可以分別定義 fast 和 slow指針,從頭結點出發,fast指針每次移動兩個節點,slow指針每次移動一個節點,如果 fast 和 slow指針在途中相遇 ,說明這個鏈表有環。
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
// 遍歷鏈表, 尋找是否存在環
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
// slow == fast, 證明有環, 尋找相交的節點
if (slow == fast) {
ListNode idx1 = fast;
ListNode idx2 = head;
while (idx1 != idx2) {
idx1 = idx1.next;
idx2 = idx2.next;
}
return idx1;
}
}
return null;
}
}
兩條鏈表是否相交,需要先將兩條長度不等的鏈表在尾部“對齊”,完成後在根據是否指向相同的節點判斷是否有交點。
/**
*
* @param headA
* @param headB
* @return
*/
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 遍歷兩個鏈表, 需要知道對齊所需移動的長度
ListNode currA = headA;
int lengthA = 0;
while (currA != null) {
++lengthA;
currA = currA.next;
}
ListNode currB = headB;
int lengthB = 0;
while (currB != null) {
++lengthB;
currB = currB.next;
}
// 重置一下鏈表的頭
currA = headA;
currB = headB;
int inter = Math.abs(lengthA - lengthB);
// 長的向後移, 對齊短的
if (lengthA > lengthB) {
while (inter-- > 0) {
currA = currA.next;
}
} else {
while (inter-- > 0) {
currB = currB.next;
}
}
while (currA != null) {
if (currA == currB) {
return currA;
}
currA = currA.next;
currB = currB.next;
}
return null;
}
}
對撞指針
對撞指針是指在有序數組中,將指向最左側的索引定義爲左指針(left),最右側的定義爲右指針(right),然後從兩頭向中間進行數組遍歷。 對撞指針的典型題目爲LeetCode No.167 兩數之和 II - 輸入有序數組,初見該題目也很容易與上述的快慢指針案例一樣,使用雙循環解題,在外層循環計算目標值與當前值差值,內層循環遍歷所有元素是否存在該差值,由於題目規定有且僅有一組解,外層循環中當前元素大於目標值的就不用計算了,因爲本身的值已經大於了目標值。 使用對撞指針解題的代碼如下:
public int[] twoSum(int[] numbers, int target) {
// 由於題目規定numbers.length >= 2, 故無需判斷邊界
int left = 0;
int right = numbers.length;
// left < right保證兩個指針不會“擦肩而過”
while (left < right) {
int sum = numbers[left] + numbers[right];
// 題目規定有且只有一組解,這裏找到的就是唯一解
if (sum == target) {
return new int[]{left + 1, right + 1};
} else if (sum < target) {
// 兩數之和小於target,需要左指針右移
++left;
} else {
// 兩數之和大於target,需要右指針左移
--right;
}
}
return new int[]{left + 1, right + 1};
}