双指针,指的是在遍历对象的过程当中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。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--; } }
编写一个函数,其做用是将输入的字符串反转过来。输入字符串以字符数组 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 ; } }
给定一个含有 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 步,而后快慢指针开始同速前进。这样当快指针走到链表末尾 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;
这也许是双指针技巧的最高境界了,若是掌握了此算法,能够解决一大类子字符串匹配的问题,不过「滑动窗口」算法比上述的这些算法稍微复杂些。
具体原理和实践能够详见文章:滑动窗口算法基本原理与实践