【LeetCode】雙指針題型解法總結

什麼是雙指針

雙指針,指的是在遍歷對象的過程中,不是普通的使用單個指針進行訪問,而是使用兩個相同方向(快慢指針:一個在前一個在後,同方向遍歷)或者相反方向(對撞指針:一個從頭向尾,一個從尾向頭,反方向遍歷)的指針進行掃描,從而達到相應的目的,多用於有序數組題目中,部分鏈表相關題目也有出現,善加利用能極大提升算法的效率。

快慢指針(數組)

兩個指針從同一側開始遍歷數組,將這兩個指針分別定義爲 快指針(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};
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章