在許多數組和鏈表的題中,都需要用到雙指針的思想來優化,本文總結歸納了幾種常見的雙指針和對應的應用案例,通過針對的性的刷題希望能熟練的掌握雙指針的運用
文章目錄
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;
}
}