双指针算法基本原理和实践
原文作者:huansky
原文地址:雙指針算法基本原理和實踐
?
什么是雙指針
雙指針,指的是在遍歷對象的過程中,不是普通的使用單個指針進行訪問,而是使用兩個相同方向(快慢指針)或者相反方向(對撞指針)的指針進行掃描,從而達到相應的目的。換言之,雙指針法充分使用了數組有序這一特征,從而在某些情況下能夠簡化一些運算。在?LeetCode?題庫中,關于雙指針的問題還是挺多的。雙指針
?
截圖來之 LeetCode 中文官網
對撞指針
對撞指針是指在數組中,將指向最左側的索引定義為左指針(left),最右側的定義為右指針(right),然后從兩頭向中間進行數組遍歷。
對撞數組適用于連續數組和字符串,也就是說當你遇到題目給定連續數組和字符床時,應該第一時間想到用對撞指針解題。偽代碼大致如下:
?
public void find (int[] list) {var left = 0;var right = list.length - 1;//遍歷數組while (left <= right) {left++;// 一些條件判斷 和處理... ...right--;} }?
算法實例
344. 反轉字符串
編寫一個函數,其作用是將輸入的字符串反轉過來。輸入字符串以字符數組 char[] 的形式給出。不要給另外的數組分配額外的空間,你必須原地修改輸入數組、使用 O(1) 的額外空間解決這一問題。你可以假設數組中的所有字符都是 ASCII 碼表中的可打印字符。
示例 1:
輸入:["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 在后,巧妙解決一些鏈表中的問題。
1、判定鏈表中是否含有環
這應該屬于鏈表最基本的操作了,如果讀者已經知道這個技巧,可以跳過。
單鏈表的特點是每個節點只知道下一個節點,所以一個指針的話無法判斷鏈表中是否含有環的。
如果鏈表中不包含環,那么這個指針最終會遇到空指針 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; }2、已知鏈表中含有環,返回這個環的起始位置
這個問題其實不困難,有點類似腦筋急轉彎,先直接看代碼:
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; }可以看到,當快慢指針相遇時,讓其中任一個指針重新指向頭節點,然后讓它倆以相同速度前進,再次相遇時所在的節點位置就是環開始的位置。
3、尋找鏈表的中點
類似上面的思路,我們還可以讓快指針一次前進兩步,慢指針一次前進一步,當快指針到達鏈表盡頭時,慢指針就處于鏈表的中間位置。
ListNode slow, fast; slow = fast = head; while (fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next; } // slow 就在中間位置 return slow;當鏈表的長度是奇數時,slow 恰巧停在中點位置;如果長度是偶數,slow 最終的位置是中間偏右:
尋找鏈表中點的一個重要作用是對鏈表進行歸并排序。
回想數組的歸并排序:求中點索引遞歸地把數組二分,最后合并兩個有序數組。對于鏈表,合并兩個有序鏈表是很簡單的,難點就在于二分。
但是現在你學會了找到鏈表的中點,就能實現鏈表的二分了。關于歸并排序的具體內容本文就不具體展開了。具體可看文章
4、尋找鏈表的倒數第 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;滑動窗口算法
這也許是雙指針技巧的最高境界了,如果掌握了此算法,可以解決一大類子字符串匹配的問題,不過「滑動窗口」算法比上述的這些算法稍微復雜些。
具體原理和實踐可以詳見文章:滑動窗口算法基本原理與實踐
參考文章:?
https://www.cnblogs.com/kyoner/p/11087755.html
https://zhuanlan.zhihu.com/p/71643340
總結
以上是生活随笔為你收集整理的双指针算法基本原理和实践的全部內容,希望文章能夠幫你解決所遇到的問題。