雙指針算法基本原理和實踐

什麼是雙指針

雙指針,指的是在遍歷對象的過程當中,不是普通的使用單個指針進行訪問,而是使用兩個相同方向(快慢指針)或者相反方向(對撞指針)的指針進行掃描,從而達到相應的目的。css

換言之,雙指針法充分使用了數組有序這一特徵,從而在某些狀況下可以簡化一些運算。html

在 LeetCode 題庫中,關於雙指針的問題仍是挺多的。雙指針算法

 

截圖來之 LeetCode 中文官網數組

對撞指針

對撞指針是指在數組中,將指向最左側的索引定義爲左指針(left),最右側的定義爲右指針(right),而後從兩頭向中間進行數組遍歷。函數

對撞數組適用於連續數組和字符串,也就是說當你遇到題目給定連續數組和字符牀時,應該第一時間想到用對撞指針解題。

僞代碼大體以下:post

public void find (int[] list) {
  var left = 0;
  var right = list.length - 1;

  //遍歷數組
  while (left <= right) {
    left++;
    // 一些條件判斷 和處理
    ... ...
    right--;
  }
}

算法實例

344. 反轉字符串

編寫一個函數,其做用是將輸入的字符串反轉過來。輸入字符串以字符數組 char[] 的形式給出。url

不要給另外的數組分配額外的空間,你必須原地修改輸入數組、使用 O(1) 的額外空間解決這一問題。spa

你能夠假設數組中的全部字符都是 ASCII 碼錶中的可打印字符。指針

示例 1:code

輸入:["h","e","l","l","o"]
輸出:["o","l","l","e","h"]

示例 2:

輸入:["H","a","n","n","a","h"]
輸出:["h","a","n","n","a","H"]

解答

能夠套用前面的僞代碼:

class Solution {
    public void reverseString(char[] s) {
        if (s.length == 0 || s.length == 1) return ;
        int left = 0;
        int right = s.length-1;
        while (left <right) {
            char temp = s[left];
            s[left++] = s[right];
            s[right--] = temp;
        }
        return ;
    }
}

209. 長度最小的子數組

給定一個含有 n 個正整數的數組和一個正整數 s ,找出該數組中知足其和 ≥ s 的長度最小的 連續 子數組,並返回其長度。若是不存在符合條件的子數組,返回 0。

示例:

輸入:s = 7, nums = [2,3,1,2,4,3]
輸出:2
解釋:子數組 [4,3] 是該條件下的長度最小的子數組。

解答 

class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int right =0;
        int left=0;
        int sum =0;
        int len =Integer.MAX_VALUE;
        while(right < nums.length) {
            sum+=nums[right];
            while (sum >=s) {
               len = Math.min(right -left+1,len);
               sum -= nums[left]; 
               left++;
            }
            right++;
        }
        if (len == Integer.MAX_VALUE) return 0;
        return len;

    }
}

雖然這道題目也是用的雙指針,可是實際上採用滑動窗口的算法思想,具體能夠看文章:滑動窗口算法基本原理與實踐。 

快慢指針

快慢指針也是雙指針,可是兩個指針從同一側開始遍歷數組,將這兩個指針分別定義爲快指針(fast)慢指針(slow),兩個指針以不一樣的策略移動,直到兩個指針的值相等(或其餘特殊條件)爲止,如 fast 每次增加兩個,slow 每次增加一個。

LeetCode 141.環形鏈表爲例,,判斷給定鏈表中是否存在環,能夠定義快慢兩個指針,快指針每次增加一個,而慢指針每次增加兩個,最後兩個指針指向節點的值相等,則說明有環。就好像一個環形跑道上有一快一慢兩個運動員賽跑,若是時間足夠長,跑地快的運動員必定會遇上慢的運動員。 

算法示例

快慢指針通常都初始化指向鏈表的頭結點 head,前進時快指針 fast 在前,慢指針 slow 在後,巧妙解決一些鏈表中的問題。

一、斷定鏈表中是否含有環

這應該屬於鏈表最基本的操做了,若是讀者已經知道這個技巧,能夠跳過。

單鏈表的特色是每一個節點只知道下一個節點,因此一個指針的話沒法判斷鏈表中是否含有環的。

若是鏈表中不包含環,那麼這個指針最終會遇到空指針 null 表示鏈表到頭了,這還好說,能夠判斷該鏈表不含環。

boolean hasCycle(ListNode head) {
    while (head != null)
        head = head.next;
    return false;
}

可是若是鏈表中含有環,那麼這個指針就會陷入死循環,由於環形數組中沒有 null 指針做爲尾部節點。

經典解法就是用兩個指針,一個每次前進兩步,一個每次前進一步。若是不含有環,跑得快的那個指針最終會遇到 null,說明鏈表不含環;若是含有環,快指針最終會和慢指針相遇,說明鏈表含有環。

就好像一個環形跑道上有一快一慢兩個運動員賽跑,若是時間足夠長,跑地快的運動員必定會遇上慢的運動員。 

boolean hasCycle(ListNode head) {
    ListNode fast, slow;
    fast = slow = head;
    while(fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow)
            return true;
    }
    return false;
}

二、已知鏈表中含有環,返回這個環的起始位置

這個問題其實不困難,有點相似腦筋急轉彎,先直接看代碼:

ListNode detectCycle(ListNode head) {
    ListNode fast, slow;
    fast = slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow)
            break;
    }
    
    slow = head;
    while (slow != fast) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}

能夠看到,當快慢指針相遇時,讓其中任一個指針從新指向頭節點,而後讓它倆以相同速度前進,再次相遇時所在的節點位置就是環開始的位置。

三、尋找鏈表的中點

相似上面的思路,咱們還可讓快指針一次前進兩步,慢指針一次前進一步,當快指針到達鏈表盡頭時,慢指針就處於鏈表的中間位置。

ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
    fast = fast.next.next;
    slow = slow.next;
}
// slow 就在中間位置
return slow;

當鏈表的長度是奇數時,slow 恰巧停在中點位置;若是長度是偶數,slow 最終的位置是中間偏右:

尋找鏈表中點的一個重要做用是對鏈表進行歸併排序。

回想數組的歸併排序:求中點索引遞歸地把數組二分,最後合併兩個有序數組。對於鏈表,合併兩個有序鏈表是很簡單的,難點就在於二分。

可是如今你學會了找到鏈表的中點,就能實現鏈表的二分了。關於歸併排序的具體內容本文就不具體展開了。具體可看文章

四、尋找鏈表的倒數第 k 個元素

咱們的思路仍是使用快慢指針,讓快指針先走 k 步,而後快慢指針開始同速前進。這樣當快指針走到鏈表末尾 null 時,慢指針所在的位置就是倒數第 k 個鏈表節點(爲了簡化,假設 k 不會超過鏈表長度):

ListNode slow, fast;
slow = fast = head;
while (k-- > 0) 
    fast = fast.next;

while (fast != null) {
    slow = slow.next;
    fast = fast.next;
}
return slow;

滑動窗口算法

這也許是雙指針技巧的最高境界了,若是掌握了此算法,能夠解決一大類子字符串匹配的問題,不過「滑動窗口」算法比上述的這些算法稍微複雜些。

具體原理和實踐能夠詳見文章:滑動窗口算法基本原理與實踐

 

算法系列文章

滑動窗口算法基本原理與實踐

廣度優先搜索原理與實踐

深度優先搜索原理與實踐

雙指針算法基本原理和實踐

分治算法基本原理和實踐

動態規劃算法原理與實踐

算法筆記

 

參考文章: 

http://www.javashuo.com/article/p-dgwugtex-kv.html

https://zhuanlan.zhihu.com/p/71643340

相關文章
相關標籤/搜索