玩转双指针

简介: 双指针即用两个不同速度或不同方向的指针对数组或对象进行访问,通过两个不同指针的碰撞从而达到特定的目的。

双指针


概念:

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。


Two Sum

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

在这里插入图片描述
题解:

因为数组已经排好序,我们可以采用方向相反的双指针来寻找这两个数字,一个初始指向最小的元素,即数组最左边,向右遍历;一个初始指向最大的元素,即数组最右边,向左遍历。
如果两个指针指向元素的和等于给定值,那么它们就是我们要的结果。如果两个指针指向元素的和小于给定值,我们把左边的指针右移一位,使得当前的和增加一点。如果两个指针指向元素的和大于给定值,我们把右边的指针左移一位,使得当前的和减少一点。
可以证明,对于排好序且有解的数组,双指针一定能遍历到最优解。证明方法如下:假设最优解的两个数的位置分别是 l 和 r。我们假设在左指针在 l 左边的时候,右指针已经移动到了 r;此时两个指针指向值的和小于给定值,因此左指针会一直右移直到到达 l。同理,如果我们假设在右指针在 r 右边的时候,左指针已经移动到了 l;此时两个指针指向值的和大于给定值,因此右指针会一直左移直到到达 r。所以双指针在任何时候都不可能处于 (l,r) 之间,又因为不满足条件时指针必须移动一个,所以最终一定会收敛在 l 和 r。
    public int[] twoSum(int[] numbers, int target) {
        int l=0;
        int r=numbers.length-1;

        while(l<r){
            if(numbers[l]+numbers[r]==target) break;
            while(numbers[l]+numbers[r]>target) r--;
            while(numbers[l]+numbers[r]<target) l++;
        }

        int[] res =new int[2];
        res[0]=l+1;
        res[1]=r+1;
        return res;
    }



归并两个有序数组

88.合并两个有序数组
在这里插入图片描述
题解:

因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即 nums1 的m − 1 位和 nums2 的 n − 1 位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。因为我们也要定位 nums1 的末尾,所以我们还需要第三个指针,以便复制。
在以下的代码里,我们直接利用 m 和 n 当作两个数组的指针,再额外创立一个 pos 指针,起始位置为 m +n−1。每次向前移动 m 或 n 的时候,也要向前移动 pos。这里需要注意,如果 nums1的数字已经复制完,不要忘记把 nums2 的数字继续复制;如果 nums2 的数字已经复制完,剩余nums1 的数字不需要改变,因为它们已经被排好序。

注意 这里我们使用了 ++ 和- -的小技巧:a++ 和 ++a 都是将 a 加 1,但是 a++ 返回值为 a,而++a 返回值为 a+1。如果只是希望增加 a 的值,而不需要返回值,则推荐使用 ++a,其运行速度会略快一些。

    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int pos = --m + --n + 1;
        while(n>=0){
            if(m<0){
                while(n>=0) nums1[n]=nums2[n--];
                break;
            }
            
            if(nums1[m]>=nums2[n]) nums1[pos--]=nums1[m--];
            else nums1[pos--]=nums2[n--];
        }
    }



快慢指针

142.环形链表 II

在这里插入图片描述
在这里插入图片描述
题解:

对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd 判圈)。给定两个指针,分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步,slow 前进一步。如果 fast可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        while(slow!=null&&fast!=null&&fast.next!=null){
            slow=slow.next;
            fast=fast.next.next;
            if(fast==slow){
                //指定到这里说明有环
                fast=head;
                while(fast!=slow){
                    slow=slow.next;
                    fast=fast.next; 
                }
                return slow;
            }
        }
        return null;
    }



滑动窗口

76.最小覆盖子串
在这里插入图片描述
题解:

本题使用滑动窗口求解,即两个指针 l 和 r 都是从最左端向最右端移动,且 l 的位置一定在r 的左边或重合。注意本题虽然在 for 循环里出现了一个 while 循环,但是因为 while 循环负责移动 l 指针,且 l 只会从左到右移动一次,因此总时间复杂度仍然是 O(n)。本题使用了长度为 128的数组来映射字符,也可以用哈希表替代;其中 chars 表示目前每个字符缺少的数量,flag 表示每个字符是否在 T 中存在。
    public String minWindow(String s, String t) {
        int[] arr = new int[128];
        Arrays.fill(arr, -1000000);
        int len = t.length();
        for (int i = 0; i < len; ++i) {
            arr[t.charAt(i)] += arr[t.charAt(i)] == -1000000 ? 1000001 : 1;
        }

        int l = 0, count = len;
        int res = 0x3f3f3f3f, resL = 0, resR = 0;
        for (int r = 0; r < s.length(); ++r) {
             //如果==-1000000说明t中不存在该字符
            if (arr[s.charAt(r)] != -1000000) {
                arr[s.charAt(r)]--;
                if (arr[s.charAt(r)] >= 0) count--;
            }
            //如果s中从l到r中的字符包含t
            while (count <= 0) {
                if (res > r - l + 1) {
                    res = r - l + 1;
                    resL = l;
                    resR = r;
                }
                if (arr[s.charAt(l)] == -1000000) {
                    l++;
                } else if (arr[s.charAt(l)] < 0) {
                    arr[s.charAt(l)]++;
                    l++;
                } else break;
            }
        }
        return count<=0?s.substring(resL, resR + 1):"";
    }



练习

基础难度

633.Sum of Square Numbers (Easy)
Two Sum 题目的变形题之一。

680.Valid Palindrome II (Easy)
Two Sum 题目的变形题之二。

524.Longest Word in Dictionary through Deleting (Medium)
归并两个有序数组的变形题。

进阶难度

340.Longest Substring with At Most K Distinct Characters (Hard)
需要利用其它数据结构方便统计当前的字符状态。
目录
相关文章
|
SQL 前端开发 Oracle
mysql合并查询(多张表) union 和 union all
简介 小序 :今天写首页动态业务的时候,用到了两张表,还需要分页查询,刚开始以为需要关联查询,后来发现关联的话不会放到一个实体,然后我就上网找方法,然后发现了一个我没学过的sql语句union,union all,卧槽 还是得好好学习啊,前端我想学,mysql我想学,真的时间不够用啊,还得给学弟学妹拍趣味编程课看的视频,真的是烦啊! 如果我们需要将两个select语句的结果作为一个整体显示出来,我们就需要用到union或者union all关键字。union(或称为联合)的作用是将多个结果合并在一起显示出来。 UNION 操作符用于合并两个或多个 SELECT 语句的结果集。
1340 0
mysql合并查询(多张表) union 和 union all
|
关系型数据库 MySQL 数据库
【Databend】多表联结,你不会还没有掌握吧!
【Databend】多表联结,你不会还没有掌握吧!
200 2
|
11月前
|
机器学习/深度学习 数据可视化 算法
激活函数与神经网络------带你迅速了解sigmoid,tanh,ReLU等激活函数!!!
激活函数与神经网络------带你迅速了解sigmoid,tanh,ReLU等激活函数!!!
|
网络协议 应用服务中间件 Linux
LVS介绍与配置
LVS介绍与配置
768 8
|
PyTorch 算法框架/工具
pytorch - swa_model模型保存的问题
pytorch - swa_model模型保存的问题
204 0
|
机器学习/深度学习 人工智能 自然语言处理
大模型:人工智能发展的引擎
大模型:人工智能发展的引擎
495 0
|
监控 UED
如何提高ROI?
【5月更文挑战第16天】如何提高ROI?
468 4
|
Rust 编译器
Rust代码组织:Package、Crate、Module
Rust代码组织:Package、Crate、Module
233 0
|
开发者 Python
深入浅出Python协程:提高并发性能的利器
本文旨在深入探讨Python中的协程机制,一种轻量级的并发编程解决方案。与传统的多线程和多进程相比,协程提供了更高效的并发性能,尤其是在I/O密集型应用中。我们将从协程的基本概念入手,解析其工作原理,并通过实例讲解如何在Python中使用协程来优化程序性能。文章还将对比协程与其他并发模型的优缺点,帮助读者全面理解协程在现代软件开发中的应用价值。
210 3
|
数据库
关系代数运算——除法运算
关系代数运算——除法运算
531 0
关系代数运算——除法运算