在许多数组和链表的题中,都需要用到双指针的思想来优化,本文总结归纳了几种常见的双指针和对应的应用案例,通过针对的性的刷题希望能熟练的掌握双指针的运用
167.两数之和II-输入有序数组(头尾指针)
定义两个指针,一个是头指针i,一个是尾指针j
判断 numbers[i]+numbers[j]与target的值大大小
如果相等,就返回下标+1的数组
如果target更大,说明数小了,i向后移
如果target更小,说明数大了,j向前移
public class Solution { public int[] twoSum(int[] numbers, int target) { int i = 0; int j = numbers.length - 1; int sum; while (i < j) { sum = numbers[i] + numbers[j]; if (sum == target) return new int[]{i + 1, j + 1}; else if (sum > target) j--; else i++; } return new int[0]; } }
633.平方数之和(头尾指针)
a和b的取值在0
到(int)Math.sqrt(c)
之间
所以就以这两个值为上下界进行双指针遍历
public class Solution { public boolean judgeSquareSum(int c) { int i = 0; int j = (int) Math.sqrt(c); while (i <= j) { int tmp = i * i + j * j; if (tmp == c) return true; else if (tmp > c) j--; else i++; } return false; } }
345.反转字符串中的元音字母(头尾指针)
题目的意思是将字符串的正数第n个元音字母
和倒数第n个元音字母
交换
定义头指针i和尾指针j
定义一个char类型的数组
从头尾同时遍历,如果不是元音就直接加入新数组,如果是元音就等到两端都是元音了然后交换写入数组
public class Solution { public String reverseVowels(String s) { int i = 0; int j = s.length() - 1; char news[] = new char[s.length()]; Set<Character> set = new HashSet<>(Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U')); while (i <= j) { char ci = s.charAt(i); char cj = s.charAt(j); if (!set.contains(ci)) news[i++] = ci; else if (!set.contains(cj)) news[j--] = cj; else { news[i++] = cj; news[j--] = ci; } } return new String(news); } }
680.验证回文字符串Ⅱ(头尾指针)
定义头指针i
和尾指针j
从头尾开始遍历,如果相等同时向中间缩进
如果发现不等,尝试删去i
所在数和j
所在数
删除后继续遍历,如果有任意一种情况能走完就符合回文字符串
public class Solution { public boolean validPalindrome(String s) { int i = 0; int j = s.length() - 1; while (i < j) { if (s.charAt(i) == s.charAt(j)) { i++; j--; } else { return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j); } } return true; } private boolean isPalindrome(String s, int i, int j) { while (i < j) { if (s.charAt(i++) != s.charAt(j--)) return false; } return true; } }
88.合并两个有序数组(异步指针)
从数组的尾部开始
定义一个指针k从新数组的最后一个位置开始
指针m,n分别从两个数组的最后一个数的位置开始
比较两个数组最后一个数的大小,大的放在新数字的指针处
如果有个数组指针为0了,判断是否是n的,如果是m就直接有序不需要处理,如果是n就依次填入新数组的位置
public class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { int k = m-- + n-- - 1; while (m >= 0 && n >= 0) { nums1[k--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--]; } while (n >= 0) { nums1[k--] = nums2[n--]; } } }
21.合并两个有序链表(异步指针)
从头部开始
定义前驱指针pre
定义迭代的指针node = pre
判断连个指针l1和l2的值,把小的赋值给node.next
当迭代到有一个为空时,判断一下,把不为空的接在最后
public class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { ListNode pre = new ListNode(-1); ListNode node = pre; while (l1 != null && l2 != null) { if (l1.val < l2.val) { node.next = l1; l1 = l1.next; node = node.next; } else { node.next = l2; l2 = l2.next; node = node.next; } } if (l1 == null) node.next = l2; else node.next = l1; return pre.next; } }
141.环形链表(快慢指针)
定义快指针fast和慢指针slow
fast一次走两步,slow一次走一步
关于为什么将fast设置为head.next:其实快慢指针中fast是head
或者head.next
都可以找到环,因为我们这里判断的是fast==slow
,所以为了简化代码就保证他们初始就不一致,一旦一致就是有环
public class Solution { public boolean hasCycle(ListNode head) { if (head == null || head.next == null) return false; ListNode slow = head; ListNode fast = head.next; while (slow != fast) { if (fast == null || fast.next == null) return false; slow = slow.next; fast = fast.next.next; } return true; } }
fast = head
版本
public class Solution { public boolean hasCycle(ListNode head) { if (head == null || head.next == null) return false; ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) return true; } return false; } }
142.环形链表II(快慢指针)
思路:
- 先和环形链表I一样判断有没有环,有才进行第二步
- 从同一起点开始,fast一次走两步,slow一次走一步,当他们相遇之后,fast回到原点一次走一步,第二次相遇节点就是环连接点
- 我的理解:
- 假设第二圈发生第一次相遇,第二圈走完了就第二次相遇了
- 第一次相遇时fast走了F+(a+b)+a
- slow走了F+a
- 因为fast是slow的两倍速度,所以第二次相遇时应该满足fast=2*slow
- 设slow还需要走 x远第二次相遇,可得方程
(F+2a+b+2x)=2(F+a+x)
,,解得F=b - 所以就假设fast以一倍的速度陪slow走完b,另一倍速从F出发,当fast与slow相遇时,相当于fast走过了b+F = 2b = 2倍slow的距离
- 此时满足第二次相遇的情况
public class Solution { public ListNode detectCycle(ListNode head) { ListNode fast = head; ListNode slow = head; while (true) { if (fast == null || fast.next == null) return null; fast = fast.next.next; slow = slow.next; if (fast == slow) break;//找到第一次相遇 } fast = head;//将fast移动到head while (fast != slow) { fast = fast.next; slow = slow.next; }//第二次相遇 return fast; } }
19.删除链表的倒数第N个节点(快慢指针)
本题也运用到了快慢指针,说明快慢指针在链表题中的地位
本题思路:
- 移动快指针n步,使快慢指针步差为n
- 如果此时快指针为null,说明要删除的节点是头结点,直接返回head.next;
- 如果快指针不是null,那么就通过快慢指针查找到倒数第k+1个节点
- 然后删除掉倒数第k个节点
public class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { if (head == null || head.next == null) return null; ListNode fast = head; ListNode slow = head; //将fast和slow的步调差调成n,当fast到达末尾了,slow到达指定节点 while (n-- > 0) { fast = fast.next; } //倒数的数就是头结点的情况,此时fast是null if (fast == null){ return head.next; } //倒数的数不是头结点,按顺序移动fast和slow节点,直到倒数第二个,测试slow节点到达删除节点的上一个 while (fast.next != null) { fast = fast.next; slow = slow.next; } //slow是待删除节点上一个节点,删除slow.next节点 slow.next = slow.next.next; return head; } }
234.回文链表(快慢指针)
O(n) 时间复杂度和 O(1) 空间复杂度解法:
- 快慢指针找到中点
- 将链表分割为两部分
- 选择后一半进行翻转
- 翻转后再次比较
public class Solution { /** * 快慢指针找到中点 * 选择一半进行翻转 * 翻转后再次比较 */ public boolean isPalindrome(ListNode head) { if (head == null || head.next == null) return true; ListNode slow = head; ListNode fast = head.next; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } if (fast != null) slow = slow.next;//偶数个结点,让slow指向下一个,作为后半段的开头 //将链表分为以head和slow开头的等长的两段 cut(head, slow); return isEqual(head, reverse(slow)); } //翻转链表 private ListNode reverse(ListNode head) { ListNode newHead = null; while (head != null) { ListNode tmp = head.next; head.next = newHead; newHead = head; head = tmp; } return newHead; } //判定两个链表是否相等 private boolean isEqual(ListNode head1, ListNode head2) { while (head1 != null && head2 != null) { if (head1.val != head2.val) return false; head1 = head1.next; head2 = head2.next; } return true; } //分割链表 private void cut(ListNode head, ListNode slow) { while (head.next != slow) { head = head.next; } head.next = null; } }