双指针算法
双指针算法思想:
双指针嘛,顾名思义,就是用两个指针来操作数据结构,通常是数组或链表。这两个指针可以有不同的角色,比如一个快、一个慢,或者一个从左往右走,一个从右往左走。这种技术的核心思想是通过两个指针的协作来解决问题,避免暴力枚举,从而提高效率。
实战
注意,为锻炼双指针思想,一定要用双指针的方法解题!!!
一、对撞指针:
对撞指针——这个很好理解,两个指针从数组的两端开始,逐步向中间移动。比如在排序数组中找两个数的和等于目标值,这种情况下用对撞指针特别方便。一个指针指向左边,一个指针指向右边,根据当前的和调整指针的位置。下面用一道题来举例。
题目:巧克力 (解析)
二、快慢指针:
这个我挺熟悉的,通常是一个指针走得快,另一个走得慢。比如链表问题里,快指针每次走两步,慢指针每次走一步,这样就能用来找链表的中点或者判断是否有环。
题目:环形链表 (指针运用-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、蓝桥官网
( •̀ ω •́ )✧点击这里,继续学习其他模块吧!