Leetcode 算法之双指针 —— Java 题解

285 阅读8分钟

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

88. 合并两个有序数组 - 简单

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

示例

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3

输出:[1,2,2,3,5,6]

解释:需要合并 [1,2,3] 和 [2,5,6] 。

合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。

题解:

我们可以声明三个指针:

  1. pos 指向 nums1 数组的最后位置,即合并后数组的最后一个位置;
  2. m 指向 nums1 数组元素的最后一个位置,如例子中指向元素 3
  3. n 指向 nums2 数组元素的最后一个位置,如例子中指向元素 6

比较 mn 所指向元素的大小,将较大者放置到 pos 指针所指向的位置,同时移动 pos 指针和值较大者的指针

例如:当 m = 2,n = 2 时,nums1[m] = 3 < nums2[n] = 6,令 nums1[pos] = 6 ,同时指针 posn 指针同时向前移动。

❗ 需要注意,如果 nums1 提前遍历结束,我们仍需要遍历 nums2 数组;但是如果 nums2 提前遍历结束,我们就不需要继续遍历 nums1 数组了。

代码:

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int pos = m-- + n-- -1;
        while (m >= 0 && n >= 0) {
            nums1[pos--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];
        }
        while (n >= 0) {
            nums1[pos--] = nums2[n--];
        }
    }
}

633. 平方数之和 - 简单

给定一个非负整数 c ,你要判断是否存在两个整数 ab,使得 a2+b2=c a^2 + b^2 = c

示例:

输入:c = 5

输出:true

解释:1 * 1 + 2 * 2 = 5

题解:

❗❗ 这道题我们需要注意使用 long 类型,因为在计算的过程中,使用到了乘法以及加法,如果使用 int 可能会有整形溢出的问题。

我们令 a=0a = 0, 令 b=cb = \sqrt{c}, 然后根据 a2+b2a^2 + b^2 cc 比较,决定是令 a 增加还是令 b 减少,最终确定是否有两个整数满足要求。

代码:

 class Solution {
     public boolean judgeSquareSum(int c) {
         long a = 0, b = (long) Math.sqrt(c);
         while (a <= b) {
             long sum = a * a + b * b;
             if (sum == c) {
                 return true;
             }else if(sum < c) {
                 a++;
             }else {
                 b--;
             }
         }
         return false;
     }
 }

680. 验证回文字符串 Ⅱ - 简单

给定一个非空字符串 s最多删除一个字符。判断是否能成为回文字符串。

示例:

输入: s = "aba"

输出: true

输入: s = "abca"

输出: true

解释: 你可以删除c字符。

题解:

我们可以定义两个指针 lr 分别指向字符串 s 的左边和右边。

如果指针 l 和指针 r 所指向的字符相等,同时移动指针 lr

如果不相等,看看将这个不相等的字符删除掉后是否是回文串,即将 l 移动一个位置。

如果依然不是回文串,再看看右边删除掉一个字符后是否为回文串。

代码:

class Solution {
    public boolean validPalindrome(String s) {

        int l = 0, r = s.length()-1;
        while (l <= r) {
            if (s.charAt(l) == s.charAt(r)) {
                l++;
                r--;
            }else {
                return validPalindrome(s, l+1, r) || validPalindrome(s, l, r-1);
            }
        }
        return true;
    }
    public boolean validPalindrome(String s, int l, int r) {

        while (l <= r) {
            if (s.charAt(l) != s.charAt(r)) {
                return false;
            }
            l++;
            r--;
        }

        return true;

    }
}

167. 两数之和 II - 输入有序数组 - 中等

给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。

以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。

你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。

你所设计的解决方案必须只使用常量级的额外空间。

示例:

输入:numbers = [2,7,11,15], target = 9

输出:[1,2]

解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。

题解:

这道题还是相对简单的,由于数组有序,我们可以定义两个指针:

  1. 指针 l 指向数组左边界
  2. 指针 r 指向数组右边界

如果两个指针所指元素之和大于 target,表示 r 所指元素太大了,我们移动 r 指针;反之,如果元素之和小于 target,我们移动左指针 l;这样子,让元素之和不断接近 target,直到找到对应的元素或者左指针大于等于右指针(表示数组中没有满足要求的元素)。

代码:

class Solution {
    public int[] twoSum(int[] numbers, int target) {

        int l = 0, r = numbers.length-1;
        while (l < r) {
            int sum = numbers[l] + numbers[r];
            if (sum == target) {
                return new int[]{l+1,r+1};
            }else if (sum < target) {
                l++;
            }else {
                r--;
            }
        }
        // 数组中没有满足要求的元素
        return null;
    }
}

524. 通过删除字母匹配到字典里最长单词 - 中等

给你一个字符串 s 和一个字符串数组 dictionary ,找出并返回 dictionary 中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。

如果答案不止一个,返回长度最长且字母序最小的字符串。如果答案不存在,则返回空字符串。

示例:

输入:s = "abpcplea", dictionary = ["ale","apple","monkey","plea"]

输出:"apple"

题解:

我们可以遍历 dictionary 中的每个单词 word,在遍历过程中,我们定义两个指针:

  1. 指针 i 指向字符串 s,指向第一个字符
  2. 指针 j 指向单词 word

如果指针 ij 所指字符相同,那么同时移动两个指针,否则,只移动指针 i,表示字符串 s 需要删除字符。

如果指针 j 能够遍历完单词 word,则表示字符串 s 可以通过删除字符匹配该单词 word

❗❗ 但是需要注意,先前找到的单词长度是否比该单词小,如果是则更新为当前单词;如果长度相等,则比较字母序。

代码:

class Solution {
    public String findLongestWord(String s, List<String> dictionary) {

        String ans = "";

        for (String word : dictionary) {	
            int i = 0, j = 0;
            while (i < s.length() && j < word.length()) {
                if (s.charAt(i) == word.charAt(j)) {
                    j++;
                }
                i++;
            }
            if (j == word.length()) {
                if (word.length() > ans.length()) {
                    ans = word;
                } else if (word.length() == ans.length()) {
                    ans = word.compareTo(ans) > 0 ? ans : word;
                }
            }
        }

        return ans;
    }
}

142. 环形链表 II - 中等

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例:

circularlinkedlist.png

输入:head = [3,2,0,-4], pos = 1

输出:返回索引为 1 的链表节点

解释:链表中有一个环,其尾部连接到第二个节点。

题解:

这道题可以使用快慢指针。

定义两个指针 fast 和 slow,fast 向后移动两步,slow 移动一步。

如果 fast 为空,说明没有环。否则,当 fast 和 slow 相等时,slow 回到链表起点,随后两个指针以相同速度移动,直到两个指针所指元素相同,这个元素即为环的入口。

合理性证明: image-20220811165844518.png

当 fast 和 slow 第一次相遇时,fast 所走过的路程为和 slow 所走过的路程的关系为 fast=a+n(b+c)+b=2(a+b)=slowfast = a+n(b+c)+b = 2 (a+b) = slow

化简,得 a=(n1)(b+c)+ca=(n-1)(b+c)+c

从上面公式可以看出,当 fast 和 slow 相遇后,slow 和 fast 以同样的速度重新走一遍路程 a,就相当于 fast 再走 (n-1) 环(此时又回到第一次相遇的点)后,再走 c 的路程,刚好是环的入口,也刚好和 slow 再次相遇。

💡 本题也可以使用哈希表完成

代码:

class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head, slow = head;
        do {
            // 如果 fast 指针能指向 null,说明链表中不存在环
            if (fast == null || fast.next == null) {
                return null;
            }
            fast = fast.next.next;
            slow = slow.next;
        }while (fast != slow);

        slow = head;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
}

76. 最小覆盖子串 - 困难

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例:

输入:s = "ADOBECODEBANC", t = "ABC"

输出:"BANC"

输入:s = "a", t = "a"

输出:"a"

题解:

这道题可以使用滑动窗口来解决,同样也是双指针。

我们利用滑动窗口寻找覆盖子串,如果找到一个覆盖子串,我们缩小滑动窗口直到该子串不覆盖字符串 t,再继续扩大窗口,如此反复,直到找到最小的覆盖子串

首先,我们定义两个数组:

  1. char 数组 chars,用来记录字符串 t 中各个字符的出现次数
  2. boolean 数组 flag,用来记录字符串 t 中需要哪些字符,即必须字符

随后,遍历字符串 s,我们需要两个指针,指针 l 作为字符串 s 上的窗口的左边界,指针 r 作为窗口的右边界。同时,还需要一个计数器 count,用来记录当前窗口内是否已经覆盖了字符串 t

⭐ 当 count 的值与字符串 t 长度相等时,即窗口已经覆盖了字符串 t

我们移动指针 r,同时维护 chars 数组,将指针 r 所指字符 c 的次数减一。

如果字符 c必须字符,并且窗口内该字符已经满足了最小要求(即 字符串t 中该字符的出现个数),那么 count 加一。

不断移动右边界指针 r

直到 count 的值与字符串 t 长度相等时,我们开始移动左边界 l。同时更新最小覆盖子串的长度 minLen 和该覆盖子串的起始位置 minL

并且指针 l 所指的字符 lc 的需要个数重新加一。

如果该字符 lc 所指字符是必须字符,并且该字符在滑动窗口之外(即左边界外),同时所需个数大于零,那么计数器 count 减一,我们需要重新移动右边界,找出另一个覆盖子串,看是否长度更小并更新。

代码:

public String minWindow(String s, String t) {

    int sLen = s.length(), tLen = t.length();
    // 记录 所需字符的个数
    int[] chars = new int[128];
    // 记录那些字母是必须的
    boolean[] flag = new boolean[128];

    // 初始化 chars 和 flag
    for (int i = 0; i < tLen; ++i) {
        char c = t.charAt(i);
        chars[c]++;
        flag[c] = true;
    }
    // count 用来记录子串是否已经满足要求, minL 记录最小子串的起始索引
    int count = 0, l = 0, minL = 0, minLen = s.length() + 1;

    // 移动右窗口
    for (int r = 0; r < sLen; ++r) {
        // 窗口向右移动,所需字符个数 减 1,这里对比上面的解法,对于不需要的字符也会多了这步操作
        char c = s.charAt(r);
        chars[c]--;
        // 如果 字母是必须的,并且所需字符仍为满足,为0 表示刚好满足个数要求
        if (flag[c] && chars[c] >= 0) {
            count++;
        }

        // 如果当前窗口已经 满足要求
        while (count == tLen) {
            // 更新最小子串长度
            if (r-l+1 < minLen) {
                minLen = r-l+1;
                minL = l;
            }
            // 左窗口向右移动,字母个数需要增加
            char lc = s.charAt(l);
            chars[lc]++;
            // 如果字符是必须要的,此时移动了左窗口,该字符个数需要增加,如果 > 0 子串将不满足要求,count -1
            if (flag[lc] && chars[lc] > 0) {
                count--;
            }
            l++;
        }
    }

    return minLen > sLen ? "" : s.substring(minL, minL + minLen);
}