【算法】双指针总结

206 阅读1分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情

一、前言

双指针技巧,可分为两类:

  1. “快、慢指针”: 常见于链表中,是否有环、环的起始位置等。
  2. “左、右指针”: 常见于数组(字符串)中,比如二分搜索、滑动窗口。

(1)快、慢指针

  1. 判断单链表是否有环

链表-2022-08-0209-20-00.png

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null) return false;
        ListNode fast = head, slow = head;
        while(fast != null && fast.next != null) {
            fast = fast.next.next;         // 快指针每次走 2 步
            slow = slow.next;              // 慢指针每次走 1 步
            if (fast == slow) return true; // 最终是否相遇
        }
        return false;
    }
}
  1. 单链表中圆环的开始节点

链表-2022-08-0209-36-59.png

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if (head == null) return null;
        ListNode fast = head, slow = head;
        while(fast != null && fast.next != null) {
            fast = fast.next.next;  // 快指针每次走 2 步
            slow = slow.next;       // 慢指针每次走 1 步
            if (fast == slow) { // 1. 首次相遇
                // 2. 从开始节点开始走,再次相遇则为圆环的开始节点
                for (ListNode p = head; p != slow; p = p.next, slow = slow.next);
                return slow;
            }
        }
        return null;
    }
}

(2)左、右指针

  1. 二分搜索模板:
int binarySearch(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while(left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] < target) {
            left = mid + 1;
        }
    }
    return -1;
}
  1. 反转数组
void reverse(int [] nums) {
    int left = 0;
    int right = nums.length - 1;
    while (left < right) {
        // 交换 nums[left] 和 nums[right]
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
        ++left;
        --right;
    }
}

二、题目

(1)旋转字符串(易)

LeetCode 796

题干分析

这个题目说的是,给你两个字符串 A 和 B,你要判断字符串 A 是否可以通过将左边的若干字符旋转到右边,来得到字符串 B。如果可以就返回 true,否则返回 false。

# 比如说,给你的两个字符串是:
A = "abcde"
B = "cdeab"
​
# 你可以先将 "abcde" 最左边的 a 旋转到右边,然后再把 b 旋转到右边,就可以把字符串 A 变成字符串 B。因此返回 true# 再比如说,给你的两个字符串是:
A = "abcde"
B = "abced"
​
# 那么 A 就无法通过将左边的若干字符旋转到右边,来得到字符串 B。因此你要返回 false

思路解法

思路有二: 取巧法 和 双指针法

方法一:取巧法

  • 所有变换的结果都是:原字符串 + 原字符串后的子串
// Time: O(n^2), Space: O(1), Faster: 100.00%
public boolean rotateString(String s, String goal) {
    return s.length() == goal.length() && (s + s).contains(goal);
}

方法二:双指针法

// 方法二: 双指针法
// Time: O(n^2), Space: O(1)
public boolean rotateStringStrStr(String A, String B) {
    if (A.length() != B.length()) return false;
    String AA = A + A;
    int n = A.length(), nn = 2 * n;
    for (int start = 0; start <= n; ++start) {
        int i = start, j = 0;
        while (i < nn && j < n && AA.charAt(i) == B.charAt(j)) {
            ++i; ++j;
        }
        if (j == n) return true;
    }
    return false;
}

(2)移除数组中指定数字(易)

LeetCode 27

题干分析

这个题目说的是,给你一个整数数组和一个数字,你要就地(in place) 移除数组中等于给定数字的所有元素。然后返回移除指定数字后的子数组长度。

# 比如说,给你的整数数组是:
[1, 4, 2, 0, 2, 8]
​
# 你要移除的数字是 2。# 在这个数组中就地移除数字 2 后,得到:
[1, 4, 0, 8, _, _]
​
# 因此,你要返回这个子数组的长度 4。# 原数组的长度是 6,所以子数组 1, 4, 0, 8 后面实际上还有两个位置。但这两个位置上放什么数字都无所谓。因为根据返回的长度 4,就可以确定目标子数组的边界。

思路解法

// Time: O(n), Space: O(1), Faster: 100.00%
public int removeElement(int[] nums, int val) {
​
    if (null == nums || nums.length == 0) return 0;
    int slow = 0, faster = 0;
    while (faster < nums.length) {
​
        if (nums[faster] != val) {
​
            nums[slow ++] = nums[faster];
        }
        ++faster;
    }
    return slow;
}

(3)最小覆盖子串(难)

LeetCode 76

题干分析

这个题目说的是,给你两个字符串 s 和 t,你要在 s 中找到一个最短子串,它包含 t 中所有的字符。如果找不到满足条件的子串,就返回空字符串。

# 比如说,给你的字符串 s 和 t 分别是:
s: adbcacab
t: aab
​
在字符串 s 中,包含 a,a,b 这三个字符的最短子串是 acab,于是你要返回它。

思路解法

思路:滑动窗口 ,不断去改变窗口的左右边界,在这个过程中记录下满足最短子串的开始下标和长度。

实现需要解决的问题:

  1. 判断滑动窗口中是否完整已经包含 t 中的全部字符: 存储在 哈希表 中。
  2. 还需要一个记录 count 记录需要的总字符数

滑动窗口-2022-08-0812-56-15.png

// Time: o(n), Space: o(n), Faster: 96.14%
public String minWindow(String s, String t) {
    if (s == null || t == null) return "";
    // 1. 哈希表:用于记录需要的字符
    int [] required = new int[256];
    for (int i = 0; i < t.length(); ++i) {
        ++required[t.charAt(i)];
    }
    // 2. 记录结果。 count: 记录需要的总字符数
    int start = 0, len = Integer.MAX_VALUE, count = t.length();
​
    // 3. 滑动窗口移动
    int left = 0, right = 0;
    while (right < s.length()) {
        char r = s.charAt(right);
        if (required[r] > 0) --count; // 命中需要的字符,总字符数 -1
        --required[r];
​
        // 判断左侧窗口是否要收缩:已经获得所有字符
        while (count == 0) {
            // 更新答案
            if (right - left + 1 < len) {
                start = left;
                len = right - left + 1;
            }
            char l = s.charAt(left);
            ++required[l];
            if (required[l] > 0) ++count;
            ++left;
        }
        ++right;
    }
    return len == Integer.MAX_VALUE ? "": s.substring(start, start + len);
}