双指针也一般称为快慢指针,主要用于处理链表和数组等线性数据结构。这种技巧主要涉及到两个指针,一个快指针(通常每次移动两步)和一个慢指针(通常每次移动一步)。快指针可以起到’探路‘的作用,给慢指针修改。
适用范围:
- 一般适用于字符串/数组/链表。
- 对数组/字符串/链表进行更改(反转、删除、增添)。
27 移除元素
题目链接
对于数组的移动、删除、插入,都可以使用双指针来处理。数组不像是链表或者以链表为底层设计的数据结构,可以从中间移除元素(例如python的list或者cpp的vector),因此如果每次都要使用暴力解法(双重for循环,每次把后面的元素进行前移),那就会导致时间复杂度为O(n^2),非常低效。而双指针(或者叫快慢指针)可以有效的利用一快一慢的特点,用快指针去”探路“,查看是否需要更改内容,而慢指针则用来配合快指针进行数组的修改,非常好用。
对于本题,我们的目标是将给定的val进行覆盖,因此可以快慢指针,快指针和慢指针开始都在同一位置,如果没有需要val,则两个指针同时前进。而如果遇到了需要覆盖的元素,就将快指针后移,直到其指向的值不为val,也就可以在下一次循环中覆盖慢指针的值,也就是val。
class Solution {
public int removeElement(int[] nums, int val) {
int fast=0,slow=0;
if(nums.length == 0){
return 0;
}
while(fast<nums.length){
if(nums[fast] == val){
fast++;
}else{
nums[slow] = nums[fast];
slow++;
fast++;
}
}
return slow;
}
}
344.反转字符串
题目链接
本题就是双指针比较简单的用法,一头一尾进行交换即可。左右指针不断缩紧直到左指针大于等于右指针为主。
class Solution {
public void reverseString(char[] s) {
int left=0,right=s.length-1;
while(left<right){
char tmp = s[left];
s[left++] = s[right];
s[right--] = tmp;
}
}
}
151 反转字符串中的单词
题目链接
在java里,字符串是不可变的,即无法修改。使用split方法可以将字符串转化为字符串数组。用trim和split一起处理字符串的格式(防止错误的空格),然后只需要对字符串数组进行双指针的操作即可。
res是经过处理的字符串数组,例如s是”hello world",则res[0]是hello,res[1]为world。
class Solution {
public String reverseWords(String s) {
String res[] = s.trim().split("\\s+");
int slow=0,fast=res.length-1;
while(slow<fast){
String tmp=res[fast];
res[fast--] = res[slow];
res[slow++] = tmp;
}
return String.join(" ",res);
}
}
上述的内容都是针对数组,而双指针在链表里也及其常用。
206 反转链表
题目链接
如果单独定义一个链表会导致空间的浪费,而如果暴力做会导致时间复杂度为O(n^2),因此可以定义一个空指针,并且将整个链表倒转。比如我们一开始可以理解是这样:
可以申请一个空指针prev,在1的左边,让1的指针指向prev,也就是只改变指针的方向即可。代码如下:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
ListNode temp = null;
while(cur!=null){
temp = cur.next;
cur.next = prev;
prev = cur;
cur = temp;
}
return prev;
}
}
19. 删除链表的倒数第 N 个结点
题目链接
如果不止一趟遍历来做这个题也是可以的,但是很麻烦,这里可以用快慢指针来做:先让快指针走n次,再让快慢指针同时走,最后去掉slow后的结点即可。
但对于链表的题目,我们最好准备一个虚拟头节点,因为会涉及到很多删除头节点的情况。比如我们的链表只有1个节点,或者两个节点但是要删除头节点,加入特判语句会让代码不那么优雅。
同时返回的时候,也不能返回head,如果只有一个节点,那么我们返回的就是被删除的节点了(或者两个节点但是要删除头节点也是一样的)。代码如下:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy;
for(int i=0; i<n; i++){
fast = fast.next;
}
while(fast.next!=null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
}
面试题 02.07. 链表相交
题目链接
这题只是稍稍麻烦了点,但是思路还是很简单的。先进行a和b的遍历,计算出a和b链表的长度,再让两个指针在同一起跑线上,即是双指针的方法。例如a的长度更长,那么就可以让a先走(cnt_a - cnt_b)步,这样指针就在同一起跑线,然后进行节点的对比即可。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int cnt_a=0,cnt_b=0;
ListNode a = headA;
ListNode b = headB;
// a或者b为空
//
while(a!=null){
a = a.next;
cnt_a++;
}
while(b!=null){
b = b.next;
cnt_b++;
}
// 对齐
a = headA;
b = headB;
if (cnt_a > cnt_b) {
for (int i = 0; i < cnt_a - cnt_b; i++) {
a = a.next;
}
} else if (cnt_a < cnt_b) {
for (int i = 0; i < cnt_b - cnt_a; i++) {
b = b.next;
}
}
// 找交点
while(a!=null&&b!=null){
if(a == b){
return a;
}else{
a = a.next;
b = b.next;
}
}
return null;
}
}
142 环形链表 II
题目链接
顺带一提,如果判断链表是否成环,也可以用双指针进行判断,代码如下:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null) {
if (slow == fast) {
return true; // 快指针和慢指针相遇,说明存在环
}
slow = slow.next; // 慢指针每次移动一步
fast = fast.next.next; // 快指针每次移动两步
}
return false; // 快指针到达链表末尾,说明不存在环
}
}
对于本题,需要动笔画一画:
我们将整个链表分为x y z三部分,而快指针和慢指针一开始都在head。slow每次走一步,而fast每次是两步。这样一定会有一个相遇点p(图中的y下面的点)。对走过的路径分析,slow走过了x+y,而fast是x+y+n*(y+z),而快指针走的长度是slow的二倍(可以自己试一试),因此可以计算得到x的值。
观察得到的表达式,也就是如果有一个新指针再从head开始出发,每次走一步,而slow从p点出发,那么两指针交点则为环入口。因此可以写代码如下:
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast!=null&&fast.next!=null){
slow = slow.next;
fast = fast.next.next;
if(slow==fast){
ListNode res = head;
while(slow!=res){
slow = slow.next;
res = res.next;
}
return res;
}
}
return null;
}
}
15 三数之和
题目链接