双指针算法

简介: 双指针,顾名思义,就是用两个指针来操作数据结构,通常是数组或链表。这两个指针可以有不同的角色,比如一个快、一个慢,或者一个从左往右走,一个从右往左走。这种技术的核心思想是通过两个指针的协作来解决问题,避免暴力枚举,从而提高效率。

双指针算法思想:

双指针嘛,顾名思义,就是用两个指针来操作数据结构,通常是数组或链表。这两个指针可以有不同的角色,比如一个快、一个慢,或者一个从左往右走,一个从右往左走。这种技术的核心思想是通过两个指针的协作来解决问题,避免暴力枚举,从而提高效率。


实战

注意,为锻炼双指针思想,一定要用双指针的方法解题!!!

一、对撞指针:

对撞指针——这个很好理解,两个指针从数组的两端开始,逐步向中间移动。比如在排序数组中找两个数的和等于目标值,这种情况下用对撞指针特别方便。一个指针指向左边,一个指针指向右边,根据当前的和调整指针的位置。下面用一道题来举例。

题目:巧克力解析

二、快慢指针:

这个我挺熟悉的,通常是一个指针走得快另一个走得慢。比如链表问题里,快指针每次走两步,慢指针每次走一步,这样就能用来找链表的中点或者判断是否有环。

题目:环形链表指针运用-Java)(解析

三、滑动窗口:

滑动窗口——这个稍微复杂一点,但本质上还是两个指针在操作一个窗口的左右边界。比如在数组中找满足某种条件的最大子数组,滑动窗口可以动态调整窗口的大小,找到最优解。

题目:长度最小的子数组解析

挑战者系列:日志统计解析

( •̀ ω •́ )✧点击这里,继续学习其他模块吧!


题目汇集

一、对撞指针:

对撞指针——这个很好理解,两个指针从数组的两端开始,逐步向中间移动。比如在排序数组中找两个数的和等于目标值,这种情况下用对撞指针特别方便。一个指针指向左边,一个指针指向右边,根据当前的和调整指针的位置。下面用一道题来举例。

题目: 巧克力

问题描述

由于 Setsuna 和 myworld 两位同学在队内测试中 akak 了比赛,所以 Wiki 老师决定奖励他们。但是,这两位同学作为校内顶级 acmersacmers,Wiki 老师觉得奖励也必须要和算法挂钩。

现在已知 Wiki 老师有 nn 个盒子,已知每个盒子里面有 aiai 块巧克力,盒子按照 1,2,3...n1,2,3...n 的编号顺序从左往右依次摆放在 301301 训练基地的桌子上。

Setsuna 可以从左往右选择 kk 个盒子(编号 11 到编号 kk),myworld 可以从右往左选 mm 个盒子(编号 n−m+1n−m+1 到编号 nn),然后需要保证 Setsuna 选的前 kk 个盒子里面的巧克力块数之和必须等于 myworld 选的后 mm 个盒子里面的巧克力块数之和 ,即:

image.gif 编辑

请问,在满足上述要求的前提下,Setsuna 和 myworld 每个人最多可以拿走多少块巧克力?当然,如果选不到符合要求的盒子,那所有巧克力只能被 Wiki 老师独享啦~

输入格式

输入第 11 行包含一个正整数 nn,表示盒子的总数。

输入第 22 行包含 nn 个正整数 aiai,表示每个盒子里面巧克力数量。

输出格式

输出一行,这一行包含一个整数,表示答案。

样例输入1

3
6 3 6
image.gif

样例输出1

6
image.gif

样例输入2

4
2 3 1 4
image.gif

样例输出2

5
image.gif

样例输入3

5
1 2 3 4 5
image.gif

样例输出3

0
image.gif

说明/提示

对于所有测试数据,1≤n≤2×105,1≤ai≤1091≤n≤2×105,1≤ai≤109。、

Java版:
import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        // 创建 Scanner 对象用于读取输入
        Scanner scanner = new Scanner(System.in);
        // 读取数组的长度
        int n = scanner.nextInt();
        // 创建一个长度为 n 的长整型数组
        long[] arr = new long[n];
        // 循环读取数组的每个元素
        for (int i = 0; i < n; i++) {
            arr[i] = scanner.nextLong();
        }
        // 初始化左指针
        int left = 0;
        // 初始化右指针
        int right = n - 1;
        // 初始化最大相等和为 0
        long maxNum = 0;
        // 初始化左部分的和为左指针指向元素的值
        long leftSum = arr[left];
        // 初始化右部分的和为右指针指向元素的值
        long rightSum = arr[right];
        // 双指针循环,只要左指针小于右指针就继续循环
        while (left < right) {
            if (leftSum > rightSum) {
                // 左部分和大于右部分和,右指针左移
                right--;
                // 更新右部分的和
                rightSum += arr[right];
            } else if (leftSum < rightSum) {
                // 左部分和小于右部分和,左指针右移
                left++;
                // 更新左部分的和
                leftSum += arr[left];
            } else {
                // 左右部分和相等,更新最大相等和
                maxNum = leftSum;
                // 左指针右移
                left++;
                // 更新左部分的和
                leftSum += arr[left];
            }
        }
        // 输出最大相等和
        System.out.println(maxNum);
        // 关闭 Scanner 对象
        scanner.close();
    }
}

image.gif

C++版:
#include <iostream>
#include <vector>
using namespace std;
// 当然本题,用双指针就能解决,接下来,我要用双指针解决
int main()
{
    int n;
    cin>>n;
    vector<long long> arr(n,0);
    for(int i=0; i<n; ++i) cin>>arr[i];
    int left=0,right=n-1;
    long long max_num = 0;
    long long left_sum=arr[left],right_sum=arr[right];
    while(left<right){
        if(left_sum>right_sum){
            right--;
            right_sum+=arr[right];
        }else if(left_sum<right_sum){
            left++;
            left_sum+=arr[left];
        }else {
            max_num = left_sum;
            left++;
            left_sum+=arr[left];
        }
    }
    cout<<max_num<<endl;
    return 0;
}

image.gif

二、快慢指针:

这个我挺熟悉的,通常是一个指针走得快另一个走得慢。比如链表问题里,快指针每次走两步,慢指针每次走一步,这样就能用来找链表的中点或者判断是否有环。

题目:环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

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

如果链表中存在环 ,则返回 true 。 否则,返回 false

示例 1:

image.gif 编辑

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

输出:true

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


示例 2:

image.gif 编辑

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

输出:true

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


示例 3:

image.gif 编辑

输入:head = [1], pos = -1

输出:false

解释:链表中没有环。


提示:

  • 链表中节点的数目范围是 [0, 104]
  • -105 <= Node.val <= 105
  • pos-1 或者链表中的一个 有效索引
Java版:
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
         // 如果头节点为空,说明链表为空,不存在环
        if (head == null) return false;
        // 慢指针,初始指向头节点
        ListNode slow = head;
        // 快指针,初始指向头节点
        ListNode fast = head;
        // 当快指针的下一个节点和下下个节点都不为空时,继续循环
        while (fast.next != null && fast.next.next != null) {
            // 快指针每次移动两步
            fast = fast.next.next;
            // 慢指针每次移动一步
            slow = slow.next;
            // 如果快指针和慢指针相遇,说明链表存在环
            if (fast == slow) return true;
        }
        // 循环结束,说明没有环
        return false;
    }
}

image.gif

C++版:
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool hasCycle(ListNode *head) {
        if(head==nullptr) return false;
        ListNode* slow=head;  
        ListNode* fast=head;
        while(fast->next!=nullptr&&fast->next->next!=nullptr){
            fast = fast->next->next;
            slow = slow->next;
            if(fast==slow) return true;
        }
        return false;
    }
};

image.gif

三、滑动窗口:

滑动窗口——这个稍微复杂一点,但本质上还是两个指针在操作一个窗口的左右边界。比如在数组中找满足某种条件的最大子数组,滑动窗口可以动态调整窗口的大小,找到最优解。

题目:长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度如果不存在符合条件的子数组,返回 0

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]

输出:2

解释:子数组 [4,3] 是该条件下的长度最小的子数组。


示例 2:

输入:target = 4, nums = [1,4,4]

输出:1


示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]

输出:0


提示:

  • 1 <= target <= 109
  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 104
Java版:
class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        // 初始化最小子数组长度为 Integer.MAX_VALUE
        int minNum = Integer.MAX_VALUE;
        // 滑动窗口的左指针
        int left = 0;
        // 滑动窗口内元素的和
        int sum = 0;
        // 遍历数组,i 作为滑动窗口的右指针
        for (int i = 0; i < nums.length; i++) {
            // 将当前元素加入滑动窗口的和
            sum += nums[i];
            // 当滑动窗口内元素的和大于等于目标值时
            while (sum >= target) {
                // 更新最小子数组长度
                minNum = Math.min(minNum, i - left + 1);
                // 从滑动窗口的和中减去左指针指向的元素,并将左指针右移
                sum -= nums[left++];
            }
        }
        // 如果最小子数组长度仍然是 Integer.MAX_VALUE,说明没有找到满足条件的子数组,返回 0;否则返回最小子数组长度
        return minNum != Integer.MAX_VALUE ? minNum : 0;
    }
}

image.gif

C++版:
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int min_num = INT32_MAX;
        int left=0,right;
        int sum=0; 
        for(int i=0; i<nums.size(); ++i){ // 滑动窗口的右窗口
            sum+=nums[i];
            while(sum>=target){
                min_num = min(min_num,i-left+1);
                sum-=nums[left++];
            }
        }
        return min_num!=INT32_MAX ? min_num : 0;
    }
};

image.gif

四、挑战者系列

题目:日志统计

小明维护着一个程序员论坛。现在他收集了一份"点赞"日志,日志共有 NN 行。其中每一行的格式是:

ts idts id

表示在 tsts 时刻编号 idid 的帖子收到一个"赞"。

现在小明想统计有哪些帖子曾经是"热帖"。如果一个帖子曾在任意一个长度为 DD 的时间段内收到不少于 KK 个赞,小明就认为这个帖子曾是"热帖"。

具体来说,如果存在某个时刻 T 满足该帖在 [T,T+D)[T,T+D) 这段时间内(注意是左闭右开区间)收到不少于 KK 个赞,该帖就曾是"热帖"。

给定日志,请你帮助小明统计出所有曾是"热帖"的帖子编号。

输入描述

输入格式:

第一行包含三个整数 N,D,KN,D,K。

以下 N 行每行一条日志,包含两个整数 ts 和 id。

其中,1≤K≤N≤105,0≤ts≤105,0≤id≤1051≤K≤N≤105,0≤ts≤105,0≤id≤105。

输出描述

按从小到大的顺序输出热帖 idid。每个 idid 一行。

输入输出样例

示例

输入

7 10 2
0 1
0 10
10 10
10 1
9 1
100 3
100 3
image.gif

输出

1
3
image.gif

运行限制

  • 最大运行时间:1s
  • 最大运行内存: 256M
Java版:
import java.util.Arrays;
import java.util.Scanner;
// 定义结构体 node 对应的类
class Node {
    int ts; // ts 代表时间
    int id; // id 代表序号
    // 构造函数,用于初始化 Node 对象
    public Node(int ts, int id) {
        this.ts = ts;
        this.id = id;
    }
}
public class Main {
    static int n, d, k;
    static int[] nowlike = new int[100005]; // 用来存放每个 id 帖子的点赞数
    static Node[] arr = new Node[100005];
    static boolean[] ishot = new boolean[100005]; // 标记每个 id 的帖子是否为热帖
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 从标准输入读取 n, d, k 的值
        n = scanner.nextInt();
        d = scanner.nextInt();
        k = scanner.nextInt();
        // 循环读取每条日志的信息
        for (int i = 1; i <= n; i++) {
            int ts = scanner.nextInt();
            int id = scanner.nextInt();
            arr[i] = new Node(ts, id);
        }
        // 对日志数组按照点赞时刻进行排序
        Arrays.sort(arr, 1, n + 1, (x, y) -> x.ts - y.ts);
        int l = 1; // 设置左边界
        // 遍历排序后的日志数组
        for (int i = 1; i <= n; i++) {
            // 当前帖子的点赞数加 1
            nowlike[arr[i].id]++;
            // 当当前时刻减去左指针指向的时刻大于等于 d 时,说明超出了长度为 d 的时间段
            // 此时需要将左指针指向的帖子的点赞数减 1,并将左指针右移
            while (arr[i].ts >= arr[l].ts + d) {
                nowlike[arr[l].id]--;
                l++;
            }
            // 如果当前帖子在长度为 d 的时间段内的点赞数不少于 k,则将该帖子标记为热帖
            if (nowlike[arr[i].id] >= k) {
                ishot[arr[i].id] = true;
            }
        }
        // 遍历 ishot 数组,输出所有曾经是热帖的帖子编号
        for (int i = 0; i < 100005; i++) {
            if (ishot[i]) {
                System.out.println(i);
            }
        }
        scanner.close();
    }
}

image.gif

C++版:
#include <iostream>
#include <algorithm>
using namespace std;
int n,d,k;
int nowlike[100005]; // 用来存放哪一个id数据
struct node{
    int ts; // ts代表 时间
    int id; // id代表 序号
};
node arr[100005];
bool ishot[100005];
bool cmp(node x,node y)
{
    return x.ts<y.ts;
}
int main(){ // 妈的,这种错位思想,我一定要学到!!
    scanf("%d%d%d",&n,&d,&k);
    for(int i=1;i<=n;i++)
        scanf("%d%d",&arr[i].ts,&arr[i].id);
    sort(arr+1,arr+1+n,cmp);
    int l = 1; // 设置左边界
    for(int i=1; i<=n; ++i)
    {
        nowlike[arr[i].id]++; // 帖子+1
        while(arr[i].ts>=arr[i].ts+d) {
            int l_id = arr[l].id;
            arr[l_id].id--;
            l++;
        }
        if(nowlike[arr[i].id]>=k) ishot[arr[i].id]= true;
    }
    for(int i=0; i<100005; ++i){ // 用户id可不这么确定
        if(ishot[i]){
            cout<<i<<endl;
        }
    }
    return 0;
}

image.gif


借鉴官网、博客:

1、leetcode

2、蓝桥官网


( •̀ ω •́ )✧点击这里,继续学习其他模块吧!

目录
相关文章
|
5月前
|
存储 缓存 安全
一文带你读懂 Go 1.24 map 重构了什么?
本文聚焦 Go 1.24 map 底层重构,解释它如何从旧版 bucket + overflow 方案,演进为 Swiss Table + 局部 split 的新结构,以及它所带来的性能提升。
365 1
一文带你读懂 Go 1.24 map 重构了什么?
|
5月前
|
设计模式 Java Go
Go中的switch的8种使用场景:没有你想的那么简单
在 Go 中灵活使用 switch,可以使代码更清晰、更易维护。 switch 是 Go 中不可或缺的控制结构之一
985 0
|
3月前
|
人工智能 Linux API
全平台零门槛:Win11、Mac、Linux 通用 Hermes Agent 安装教程
Hermes Agent是Nous Research开源的自进化AI助手(MIT协议),越用越懂你。支持多工具并行、自动记忆习惯,Python编写,v0.13.0版。兼容Win/macOS/Linux/Docker,国内用户可配清华镜像快速部署,需API密钥(如Kimi)。
|
5月前
|
网络安全 Go Docker
CI/CD全流程
记录 后端go 算法平台 / python 爬虫网关 / 前端vue项目 CI-CD部署流程
473 8
|
5月前
|
测试技术 微服务
Cookie之domain
本以为我对cookie这个小玩具已经很了解, 后来才发现,只是皮毛罢了,还需要继续学。
629 1
|
6月前
|
数据采集 人工智能 数据可视化
从0开始全面认识高质量数据集建设(2)
本文系统阐述高质量数据集建设的“共建共治”管理模式:以统一标准为前提,推动源头生产、集中监管;以一套标准、一个目录、一套工具为支柱,实现需求精准转化、多源数据质量管控与全生命周期数字化治理,支撑AI场景高效落地。
|
8月前
|
Java Maven 数据安全/隐私保护
06-nexus私仓环境搭建
本文介绍Nexus Repository Manager OSS版的安装与配置,包括JDK环境准备、Nexus部署、用户创建、启动服务及Web界面登录。详细说明如何搭建Maven、Docker私仓,实现Jar包上传、本地仓库迁移、项目配置使用私仓,以及Docker镜像的推送拉取。同时简述npm、helm等其他类型仓库的启用方法,适用于企业级制品管理。
|
网络协议 前端开发 物联网
TCP和UDP区别?
本文首发于微信公众号“前端徐徐”,详细介绍了TCP和UDP两种传输层协议的核心概念、连接性和握手过程、数据传输和可靠性、延迟和效率、应用场景及头部开销。TCP面向连接、可靠、有序,适用于网页浏览、文件传输等;UDP无连接、低延迟、高效,适用于实时音视频传输、在线游戏等。
1484 1
TCP和UDP区别?
|
存储 缓存 算法
优化轮询算法以提高资源分配的效率
【10月更文挑战第13天】通过以上这些优化措施,可以在一定程度上提高轮询算法的资源分配效率,使其更好地适应不同的应用场景和需求。但需要注意的是,优化策略的选择和实施需要根据具体情况进行详细的分析和评估,以确保优化效果的最大化。
|
机器学习/深度学习 PyTorch 编译器
PyTorch 与 TorchScript:模型的序列化与加速
【8月更文第27天】PyTorch 是一个非常流行的深度学习框架,它以其灵活性和易用性而著称。然而,当涉及到模型的部署和性能优化时,PyTorch 的动态计算图可能会带来一些挑战。为了解决这些问题,PyTorch 引入了 TorchScript,这是一个用于序列化和优化 PyTorch 模型的工具。本文将详细介绍如何使用 TorchScript 来序列化 PyTorch 模型以及如何加速模型的执行。
1022 4