当前位置: 首页 > news >正文

双指针算法

双指针算法思想:

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


实战

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

一、对撞指针:

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

题目:巧克力 (解析)

二、快慢指针:

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

题目:环形链表 (指针运用-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 个盒子里面的巧克力块数之和 ,即:

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

输入格式

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

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

输出格式

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

样例输入1

3
6 3 6

样例输出1

6

样例输入2

4
2 3 1 4

样例输出2

5

样例输入3

5
1 2 3 4 5

样例输出3

0

说明/提示

对于所有测试数据,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();
    }
}
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;
}
二、快慢指针:

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

 题目:环形链表

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

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

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

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入: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;
    }
}
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;
    }
};
三、滑动窗口:

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

 题目:长度最小的子数组

给定一个含有 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;
    }
}
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;
    }
};
四、挑战者系列
题目:日志统计

小明维护着一个程序员论坛。现在他收集了一份"点赞"日志,日志共有 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

输出

1
3

运行限制

  • 最大运行时间: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();
    }
}
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;
}

 


借鉴官网、博客:

1、leetcode

2、蓝桥官网


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

相关文章:

  • 介绍一个能支持高带宽的EDID编辑软件
  • SpringCloud系列教程(十三):Sentinel流量控制
  • python脚本py文件加密 pyarmor
  • Linux - 文件
  • 算法之 前缀和
  • 力扣132. 分割回文串 II
  • 传统工厂转型实录:1套WMS系统如何砍掉40%仓储成本
  • 信奥赛CSP-J复赛集训(DP专题)(16):P1203 [USACO1.1] 坏掉的项链 Broken Necklace
  • 【Qt QML】Loader动态加载组件
  • 【SegRNN 源码理解】图示理解 forward的过程
  • Kanna 与 Swift:结合使用提升网络请求效率
  • vue3,Element Plus中隐藏树el-tree滚动条
  • AIP-160 过滤
  • 采用OllamaSharp实现.NET快速对接deepseek实现聊天、模型管理、流式响应等功能
  • 统计作业提交情况python脚本
  • css错峰布局/瀑布流样式(类似于快手样式)
  • JVM参数调整
  • SAP的错误:General error 339 invalid number
  • 文献学习——考虑混合储能系统选择的基于改进蜂群算法的热电联产微网多目标经济优化调度
  • 攻防世界WEB(新手模式)19-file_include
  • 外贸网站源码怎么建/武汉网络推广网络营销
  • wordpress建站镜像/如何注册网站怎么注册
  • 如何自己做软件网站/营销方案怎么写?
  • 网站开发流程分析/jsurl转码
  • 域名注册的网站/网上销售平台怎么做
  • 工程施工合同协议书范本/seo监控