解題算法之——雙指針

這是我參與更文挑戰的第7天,活動詳情查看:更文挑戰java

引言:什麼是雙指針呢?故名思義,就是兩個指針。這麼說可能有點欠揍,經過問題來講明應該更直觀。衆所周知,鏈表有一種數據結構爲環形鏈表,即鏈表中有某個節點,能夠經過連續跟蹤 next 指針再次到達,則鏈表中存在環。那麼給你一個鏈表,你怎麼判斷其中有環呢?其中一種思路是,選定兩個指針,同時從好比 a 節點出發,一個指針一次走一步,a.nex,一個指針一次走兩步,a.next.next,若是它倆某一次又重合到了同一節點,就說明鏈表中是有環的。這樣對於雙指針你就應該有了更直觀的認識。git

上面提到的雙指針,又被稱做「快慢指針」,另外,雙指針還有「對撞指針」,即從鏈表或者數組兩端往中間走。包括「滑動窗口」也是雙指針的思想。今天咱們就來看看「雙指針」的應用。github

一、快慢指針

就從引言中的問題來看,如何判斷一個列表是環形鏈表呢?首先給出列表節點的數據結構:算法

public class ListNode {
  int val;
  ListNode next;
  
  ListNode(int x) {
    val = x;
    next = null;
  }
}
複製代碼

節點的數據結構包含本節點的值,以及 next 節點。按照快慢指針的思路,咱們就能夠寫出下面的判斷代碼:數組

/** * 判斷鏈表是否有環。使用快慢指針便可解決 * * @param head 鏈表 * @return 是否有環 */
    public boolean hasCycle(ListNode head) {
      // 快慢指針都是從頭部節點出發
      ListNode fast = head;
      ListNode low = head;
      while (fast != null && fast.next != null) {
        // 快指針一次走兩步
        fast = fast.next.next;
        // 慢節點一次走一步
        low = low.next;
        // 當兩個指針走到同一位置時,代表鏈表中存在環形結構,即環形鏈表
        if (fast == low) {
          return true;
        }
      }
      return false;
    }
複製代碼

二、對撞指針

所謂對撞指針,就是說咱們能夠將指向最左側的索引定義爲左指針(left),最右側的定義爲右指針(right),兩個指針從兩端往中間靠攏,最終到達相同位置,即撞在一塊兒。這種方法通常適用於有序列表或者數組,能夠更好地肯定位置。二分查找也是能夠用這種思路來解決的,首先肯定中間位置的值,若是比目標值小,則左指針從中間位置向右移,不然,右指針從中間位置向左移。能夠簡單看下代碼:markdown

/** * 二分查找非遞歸寫法 * * @param nums 數組 * @param target 目標值 * @param left 左指針 * @param right 右指針 * @return 目標值位置 */
    public static int binarySearch(int[] nums, int target, int left, int right) {
        // 這裏須要注意,循環條件
        while (left <= right) {
            // 這裏須要注意,計算mid
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                // 左指針向右移動
                left = mid + 1;
            } else if (nums[mid] > target) {
                // 右指針向左移動
                right = mid - 1;
            }
        }
        // 沒有找到該元素,返回 -1
        return -1;
    }
複製代碼

LeetCode 上有一個比較典型的問題,救生艇問題:數據結構

#881. 救生艇
第 i 我的的體重爲 people[i],每艘船能夠承載的最大重量爲 limit。
每艘船最多可同時載兩人,但條件是這些人的重量之和最多爲 limit。
返回載到每個人所需的最小船數。(保證每一個人都能被船載)。

示例 1:
輸入:people = [1,2], limit = 3
輸出:1
解釋:1 艘船載 (1, 2)

示例 2:
輸入:people = [3,2,2,1], limit = 3
輸出:3
解釋:3 艘船分別載 (1, 2), (2) 和 (3)

示例 3:
輸入:people = [3,5,3,4], limit = 5
輸出:4
解釋:4 艘船分別載 (3), (3), (4), (5)
複製代碼

這時候咱們就能夠有這麼一種思路,既然是求所需的救生艇的最小數量,那麼就須要每條船上承載更多重量(數量上已經限定了最多兩人)。因此,首先是對重量進行排序,若是最重的人能夠與最輕的人共用一艘船,那麼就這樣安排;不然,最重的人沒法與任何人配對,那麼他們將本身獨自乘一艘船。而後,最輕的人繼續檢查是否和次重的共用一條船,若是能夠,第二輕的人繼續和第三重的人進行檢查是否能夠共用,以此類推……代碼以下:oop

/** * 思路:雙指針 * 若是最重的人能夠與最輕的人共用一艘船,那麼就這樣安排。不然,最重的人沒法與任何人配對,那麼他們將本身獨自乘一艘船。 * * @param people * @param limit * @return 所需最小船數量 */
    public int solution(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;
    }
複製代碼

三、滑動窗口

這個名稱可能你們就比較熟悉了,就是說有兩個指針,一前一後組成滑動窗口,窗口的寬度能夠是固定的,算法題相關的話大部分都是不固定寬度的,而後計算滑動窗口中元素的值。post

咱們能夠經過下面這道題更爲直觀的瞭解。spa

#209. 長度最小的子數組
給定一個含有 n 個正整數的數組和一個正整數 target 。
找出該數組中知足其和 ≥ target 的長度最小的 連續子數組 [numsl, numsl+1, ..., numsr-1, numsr] ,並返回其長度。若是不存在符合條件的子數組,返回 0 。

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

示例 2:
輸入:target = 4, nums = [1,4,4]
輸出:1

示例 3:
輸入:target = 11, nums = [1,1,1,1,1,1,1,1]
輸出:0

提示:
 1 <= target <= 109
 1 <= nums.length <= 105
 1 <= nums[i] <= 105
複製代碼

下面咱們來看一下這道題目的作題思路,其實原理也很簡單,咱們建立兩個指針,一個指針負責在前面探路,並不斷累加遍歷過的元素的值,當和大於等於咱們的目標值時,後指針開始進行移動,判斷去除當前值時,是否仍能知足咱們的要求,直到不知足時後指針中止,前面指針繼續移動,直到遍歷結束。前指針和後指針之間的元素個數就是咱們的滑動窗口的窗口大小,即最小子數組長度。

/** * 滑動窗口:就是經過不斷調節子數組的起始位置和終止位置,進而獲得咱們想要的結果,滑動窗口也是雙指針的一種。 * * @param target 目標值 * @param nums 數組 * @return len 最小長度 */
    public int minSubArrayLen(int target, int[] nums) {
        int len = nums.length;
        int windowLen = Integer.MAX_VALUE;
        // i 後指針
        int i = 0;
        int sum = 0;
        for (int j = 0; j < len; ++j) {
            // j 爲前指針
            sum += nums[j];
            while (sum >= target) {
                // 比較求最小值
                windowLen = Math.min(windowLen, j - i + 1);
                // 移除當前值
                sum -= nums[i];
                // 移動後指針
                i++;
            }
        }
        return windowLen == Integer.MAX_VALUE ? 0 : windowLen;
    }
複製代碼

總結

對於查找某個重複值,判斷是否有環,兩個鏈表是否有相交節點等等這類的題目,咱們均可以首先使用 快慢指針 來解決;對於有序的列表或者數組,咱們能夠優先考慮一下 對撞指針 來解決;滑動窗口 則很適用於求數組某個範圍內元素的計算結果。

連接

相關文章
相關標籤/搜索