双指针算法(持续更新)

本文深入探讨了Java中双指针技术在处理数组和链表问题上的应用,包括快慢指针、左右指针和滑动窗口策略。通过实例分析167.两数之和II、88.合并两个有序数组和142.环形链表II等题目,阐述了如何巧妙利用双指针解决排序数组的搜索、合并及链表环检测等问题。文章还提及了76.最小覆盖子串问题,展示了滑动窗口在字符串处理中的运用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

理解双指针

在Java中双指针主要用来遍历数组,两个指针指向数组中不同的元素,从而协同任务。也可以延伸到多个数组的多个指针。
快慢指针:两个指针指向同一数组,遍历方向相同,两个指针包围的区域称为滑动窗口,经常用于区间搜索。
左右指针:两个指针指向同一数组,遍历方向相反,可以用来进行搜索,被搜索的数组往往是排好序的。

左右指针

指针的概念结合实例更容易理解,下面结合力扣题目对指针进行说明:

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

题目描述
给出一个由元素从小到大排列的顺序数组numbers,同时给出一个整数target为该数组中两个不同的元素a和b的和,要求返回长度为2的整数数组,数组中元素为a、b在numbers中的下标,numbers数组的下标从1开始。

输入输出样例

Input : numbers = [0, 2, 4, 6, 7, 8], target = 9
Output : [2, 5]

题解
1、题目要求求出两个元素的下标,可以使用双指针定位两个元素;
2、定义左指针left在数组的最左侧,右指针right在数组的最右侧;
3、取数组元素numbers[left]和numbers[right]相加同target比较,若两数之和num恰好等于target,则将left + 1和right + 1放入数组中返回;若num小于target,则将left右移,使结果稍大一点;若num大于target,则将right左移,使结果稍小一点。

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int n = numbers.length;
        int left = 0, right = n - 1;
        while(left < right) {
            int num = numbers[left] + numbers[right];
            if(num == target) {
                break;
            }
            if(num < target) {
                left++;
            }
            if(num > target) {
                right--;
            }
        }
        return new int[] {left + 1, right + 1};
    }
}

此例中双指针是如何避免其中一个指针会超过所求解的下标的呢?

可以证明,对于排好序且有解的数组,双指针一定能遍历到最优解。证明方法如下:假设最优解的两个数的位置分别是l和r。
我们假设在左指针在l左边的时候,右指针已经移动到了r;此时两个指针指向值的和小于给定值,因此左指针会一直右移直
到到达l。同理,如果我们假设在右指针在r右边的时候,左指针已经移动到了l;此时两个指针指向值的和大于给定值,因此
右指针会一直左移直到到达r。所以双指针在任何时候都不可能处于l和r之间,又因为不满足条件时指针必须移动一个,所以
最终一定会收敛在l和r。

88. 合并两个有序数组

题目描述
有两个有序数组nums1和nums2,请将nums2合并到nums1中,使nums1成为一个有序数组。
nums1和nums2的长度分别为m和n, 可以假设nums1的长度为m + n, 这样可以使nums1完全容纳nums2的元素。

输入输出样例

Input : nums1 = [1, 2, 3, 0, 0, 0], nums2 = [2, 4, 6], m = 3, n = 3
Output : nums1 = [1, 2, 2, 3, 4, 6]

题解
1、nums1和nums2是已经排好序的数组,合并数组时只需要比较nums1和nums2中元素的大小即可;
2、因为nums1的长度满足m + n,而且题目给定nums1和nums2的长度m和n, 可以考虑在两个数组的末尾m和n处分别定义指针,从后向前对数组中的元素进行比较;
3、两个数组中元素比较的较大值放到数组nums1的对应位置,这个位置也需要使用一个指针进行记录,初始位置为m + n - 1,也就是nums1数组的末尾;
4、如果nums2先遍历结束,那么nums1剩余元素是已经排好序的且和nums1之前的元素位置相同,不需要继续遍历;如果num1先遍历结束,则需要将nums2中未遍历完的元素放入nums1的剩余位置上;
5、代码中使用了- -操作符,举例来说:- -a的返回值为a - 1,而a- -的返回值为a.

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--];
        }
    }
}

快慢指针

142. 环形链表 II

题目描述
给定一个链表head,判断其是否有环,无环返回null, 有环返回环的入点。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

输入输出示例

Input : head = [3,2,0,-4], pos = 1
Output : 返回索引为 1 的链表节点

题解
找环形链表入口的问题是经典的快慢指针的问题,分为以下步:
1、定义fast和slow两个指针,都从链表头部开始;
2、快指针一次走两步,慢指针一次走一步。如果链表存在环,两个指针都不可能结束,并且在某个时刻相遇;如果链表不存在环,快指针先结束;
3、两个指针相遇时,将快指针移动到链表头部,两个指针每次都走一步,再次相遇时即为链表环的入点,关于此处的证明大家可以看力扣官方题解的第二种快慢指针解法,之后我这里也会更新详细一点的解法。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        if(head == null) {
            return null;
        }
        ListNode fast = head, slow = head;
        do {
            if(fast.next == null || fast.next.next == null) {
                return null;
            }
            fast = fast.next.next;
            slow = slow.next;
        } while(fast != slow);

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

滑动窗口

76. 最小覆盖子串

题目描述
给定一个字符串s,一个字符串t,返回s中包含t的最小子字符串,如果s中没有包含t的子字符串,返回空字符串"".

输入输出示例

input : s = "ADOBECODEBANC", t = "ABC"
output : "BANC"

题解
此种问题可以使用“滑动窗口”求解
滑动窗口的基本思路是,定义左右两个指针,首先将右指针右移,同时判断从左指针到右指针的字符串是否包含t中所有字符,不包含,右指针继续右移,左指针不动,如果包含,则右指针停止右移,左指针右移,缩小左右指针中间包含的字符串的范围,寻找最小子字符串(同一时间内只有一个指针会进行移动)。

具体的做法为:
1、定义哈希表ori,用于存储t字符串中所有字符及其出现次数;定义哈希表cnt,用于动态存储左右指针之间的字符串所包含的字符及其出现的次数;
2、右移右指针,当右指针未到达右边界且ori包含右指针所标记的字符时,将该字符放到cnt并更新cnt;
3、当cnt包含所有ori中的字符且左指针left小于右指针right时,更新当前left到right的长度len,并更新当前左右指针的值到变量ansL和ansR中;
4、判断cnt中是否包含left所在的字符,如果包含,将该字符从cnt中去除,保证下一次循环中判断cnt包含所有ori中字符的准确性;
5、最后,如果ansL不是初始值-1,说明左右指针都进行过移动,s中有包含t中所有字符的子字符串,返回s.substring(ansL, ansR),否则返回空字符串"".

class Solution {
    Map<Character, Integer> ori = new HashMap<>();
    Map<Character, Integer> cnt = new HashMap<>();
    public String minWindow(String s, String t) {
        int tLength = t.length();
        for(int i = 0; i < tLength; ++i) {
            char c = t.charAt(i);
            ori.put(c, ori.getOrDefault(c, 0) + 1);
        }

        int left = 0, right = -1, sLength = s.length(), ansL = 0, ansR = 0, len = Integer.MAX_VALUE;
        while(right < sLength) {
            ++right;
            // right指针右移,如果有包含t中的字符就将该字符放到动态的cnt中
            if(right < sLength && ori.containsKey(s.charAt(right))) {
                cnt.put(s.charAt(right), cnt.getOrDefault(s.charAt(right), 0) + 1);
            }
            // 当cnt包含t中所有字符时,进行left指针左移的操作
            // check()方法避免了当cnt不再包含t中所有字符时left指针继续左移的情况
            while(check() && left <= right) {
                // 此处判断避免了当right指针继续右移的时候会取到比之前的len更长的子字符串的情况,保护了ansL和ansR的值
                if(right - left < len) {
                    len = right - left + 1;
                    ansL = left;
                    ansR = left + len;
                }
                if(cnt.containsKey(s.charAt(left))) {
                    cnt.put(s.charAt(left), cnt.getOrDefault(s.charAt(left), 0) - 1);
                }
                ++left;
            }
        }
            return ansL == -1 ? "" : s.substring(ansL, ansR);
    }

    /**
        检查cnt中是否包含所有ori中的key值(也就是t中包含的所有字符)
     */
    private boolean check() {
        Iterator iter = ori.entrySet().iterator();
        while(iter.hasNext()) {
            Map.Entry entry = (Map.Entry)iter.next();
            Character key = (Character)entry.getKey();
            Integer value = (Integer)entry.getValue();
            if(cnt.getOrDefault(key, 0) < value) {
                return false;
            }
        }
        return true;
    }
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值