數據結構和算法-雙指針法


參考:

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救生艇問題為例

由於本題只要求計算出最小船數,所以原數組是否被改變,和元素索引位置都不考慮在內,所以可以先對於給定數組進行排序,再從數組兩側向中間遍歷。所以解題思路如下:

  1. 對給定數組進行升序排序
  2. 初始化左右指針
  3. 每次都用一個”最重的“和一個”最輕的“進行配對,如果二人重量小於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)
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM