蓝桥杯 Java B 组之双指针技巧(快慢指针、滑动窗口)

news/2025/2/24 3:07:28/

Day 5:双指针技巧(快慢指针、滑动窗口)

双指针技巧是处理许多算法问题时常用的技巧,尤其在数组或字符串中。双指针可以帮助我们在遍历过程中减少不必要的运算,从而优化时间复杂度。


📖 一、双指针概述

双指针技巧主要分为两种常见方式:

  1. 快慢指针(Floyd's Tortoise and Hare):适用于一些涉及到链表、循环、排序等问题。常见于快慢指针查找问题,如链表环问题、判断回文字符串等。
  2. 滑动窗口:适用于数组或字符串中涉及到子数组、子串问题。通过维护一个可变的窗口范围,避免了频繁的重复计算,从而提高效率。

📖 二、快慢指针(Floyd's Tortoise and Hare)

1. 快慢指针基础

快慢指针技巧最常用于链表问题,主要思想是让两个指针以不同速度进行遍历,快速指针每次移动两步,慢速指针每次移动一步,通常用于判断链表是否有环、链表中环的入口等。

2. 应用示例:判断链表是否有环

问题描述: 给定一个链表,判断该链表是否有环。

  • 快指针每次走两步,慢指针每次走一步。
  • 如果快指针追上慢指针,则链表有环;否则没有环。

代码实现(判断链表是否有环)

public class LinkedListCycle {// 链表节点定义class ListNode {int val;ListNode next;ListNode(int x) { val = x; }}public boolean hasCycle(ListNode head) {if (head == null) {return false;}ListNode slow = head; // 慢指针ListNode fast = head; // 快指针while (fast != null && fast.next != null) {slow = slow.next;          // 慢指针走一步fast = fast.next.next;     // 快指针走两步if (slow == fast) {        // 快慢指针相遇,说明有环return true;}}return false; // 快指针遍历到末尾,说明没有环}public static void main(String[] args) {LinkedListCycle solution = new LinkedListCycle();ListNode head = solution.new ListNode(1);head.next = solution.new ListNode(2);head.next.next = solution.new ListNode(3);head.next.next.next = solution.new ListNode(4);head.next.next.next.next = head.next; // 形成环boolean result = solution.hasCycle(head);System.out.println(result); // 输出 true}
}
3. 代码解析
  • 我们通过定义两个指针,快指针每次移动两步,慢指针每次移动一步。
  • 如果链表有环,快指针和慢指针一定会相遇。
  • 时间复杂度是 O(n),空间复杂度是 O(1),因为我们只使用了常数空间。

📖 三、滑动窗口

1. 滑动窗口简介

滑动窗口技巧常用于解决字符串或数组中的子串、子数组问题。主要思想是通过维护一个动态窗口,在窗口大小固定或变化的情况下,优化对数据的处理。

滑动窗口有两种类型:

  • 固定窗口:窗口大小固定,滑动时,窗口的左右边界会同时变化。
  • 可变窗口:窗口大小可变,滑动时,窗口的左边界和右边界根据某些条件变化。
2. 应用示例:移除元素

问题描述: 给定一个数组 nums 和一个值 val,移除数组中所有等于 val 的元素,并返回移除后的新数组的长度。

思路与分析

  1. 维护一个指针 i,用于遍历数组 nums
  2. 用另一个指针 k 来标记不等于 val 的元素的位置。
  3. 遍历完数组后,返回 k 即为新数组的长度。

代码实现(移除元素)

public class RemoveElement {public int removeElement(int[] nums, int val) {int k = 0;  // 用于标记新的数组长度for (int i = 0; i < nums.length; i++) {if (nums[i] != val) {  // 如果当前元素不等于valnums[k] = nums[i];  // 将当前元素放到新数组的当前位置k++;  // 更新新数组的长度}}return k;  // 返回新数组的长度}public static void main(String[] args) {RemoveElement solution = new RemoveElement();int[] nums = {3, 2, 2, 3, 4, 5, 3};int val = 3;int newLength = solution.removeElement(nums, val);System.out.println("新数组长度: " + newLength); // 输出 4}
}
3. 代码解析
  • 这里使用两个指针:i 用于遍历原数组,k 用于标记新数组的位置。
  • 通过不断检查每个元素是否等于 val,如果不等于 val,就将其保留在新数组中。
  • 时间复杂度是 O(n),空间复杂度是 O(1),因为我们直接在原数组上修改。

4. 应用示例:最小覆盖子串

问题描述: 给定一个字符串 s 和一个字符串 t,返回 s 中包含 t 所有字符的最小子串。如果 s 中没有包含 t 的子串,返回空字符串。

思路与分析

  1. 使用滑动窗口的技巧。
  2. 定义两个指针:左指针和右指针。右指针扩展窗口,左指针收缩窗口。
  3. 窗口内的字符包含 t 的所有字符时,记录当前窗口的最小长度,并尝试收缩窗口。

代码实现(最小覆盖子串)

import java.util.HashMap;
import java.util.Map;public class MinWindowSubstring {public String minWindow(String s, String t) {if (s.length() < t.length()) return "";Map<Character, Integer> tMap = new HashMap<>();for (char c : t.toCharArray()) {tMap.put(c, tMap.getOrDefault(c, 0) + 1);}int left = 0, right = 0, valid = 0, minLength = Integer.MAX_VALUE, start = 0;Map<Character, Integer> window = new HashMap<>();while (right < s.length()) {char c = s.charAt(right);right++;if (tMap.containsKey(c)) {window.put(c, window.getOrDefault(c, 0) + 1);if (window.get(c).equals(tMap.get(c))) {valid++;}}while (valid == tMap.size()) {if (right - left < minLength) {minLength = right - left;start = left;}char d = s.charAt(left);left++;if (tMap.containsKey(d)) {if (window.get(d).equals(tMap.get(d))) {valid--;}window.put(d, window.get(d) - 1);}}}return minLength == Integer.MAX_VALUE ? "" : s.substring(start, start + minLength);}public static void main(String[] args) {MinWindowSubstring solution = new MinWindowSubstring();String s = "ADOBECODEBANC";String t = "ABC";String result = solution.minWindow(s, t);System.out.println("最小覆盖子串: " + result); // 输出 "BANC"}
}
5. 代码解析
  • 我们使用两个指针来维护一个滑动窗口,right 扩展窗口,left 收缩窗口。
  • 使用一个哈希表 tMap 记录目标字符串 t 中每个字符的出现次数,使用另一个哈希表 window 记录当前窗口中的字符计数。
  • valid 记录当前窗口中已包含多少个字符的数量。

时间复杂度

  • O(n),其中 n 是字符串 s 的长度。我们只需要遍历一次 st


📖 四、总结

双指针技巧常见应用:
  1. 快慢指针:用于链表循环检测、回文判断等。
  2. 滑动窗口:用于处理字符串/数组中的子串、子数组问题,特别是在要求优化时间复杂度时非常有效。
常见易错点:
  1. 快慢指针:常常忘记检查快指针是否为空,或者不小心错过了链表是否有环的判断。
  2. 滑动窗口:窗口范围的扩展和收缩条件要精确,容易遗漏边界条件,比如窗口中的字符是否包含目标字符串的所有字符。


http://www.ppmy.cn/news/1574532.html

相关文章

Compose 定制UI视图

Compose 定制UI视图 概述MaterialThemeMaterialTheme与CompositionLocalMaterialThemeCompositionLocal 定制主题方案 概述 新建一个新项目时&#xff0c;Compose会在项目中生成 ui/theme 目录&#xff0c;目录中有四个文件&#xff0c;分别如下 Color.kt&#xff1a;颜色配置…

周末总结(2024/02/22)

工作 人际关系核心实践&#xff1a; 要学会随时回应别人的善意&#xff0c;执行时间控制在5分钟以内 坚持每天早会打招呼 遇到接不住的话题时拉低自己&#xff0c;抬高别人(无阴阳气息) 朋友圈点赞控制在5min以内&#xff0c;职场社交不要放在5min以外 职场的人际关系在面对利…

python用 PythonNet 从 Python 调用 WPF 类库 UI 用XAML

pythonnet 是pythonhe.net通用的神器不多介绍了. 这次这基本上跟python没有关系了. 和winform一样先导包 import clr clr.AddReference("PresentationFramework.Classic, Version3.0.0.0, Cultureneutral, PublicKeyToken31bf3856ad364e35") clr.AddReference(&…

ubuntu部署小笔记-采坑

ubuntu部署小笔记 搭建前端控制端后端前端nginx反向代理使用ubuntu部署nextjs项目问题一 如何访问端口号配置后台运行该进程pm2 问题二 包体过大生产环境下所需文件 问题三 部署在vercel时出现的问题需要魔法访问后端api时&#xff0c;必须使用https协议电脑端访问正常&#xf…

20250220-代码笔记01-class CVRPEnv

文章目录 前言一、def __init__(self, **env_params)&#xff1a;函数功能函数代码 二、use_saved_problems(self, filename, device)函数功能函数代码 三、load_problems(self, batch_size, aug_factor1)函数功能函数代码use_saved_problems 与 load_problems 之间的关系 四、…

【论文笔记】Mamba: Linear-time sequence modeling with selective state spaces

【引用格式】&#xff1a;Gu A, Dao T. Mamba: Linear-time sequence modeling with selective state spaces[J]. arXiv preprint arXiv:2312.00752, 2023. 【网址】&#xff1a;https://arxiv.org/pdf/2312.00752 【开源代码】&#xff1a;https://github.com/state-spaces/…

探索火山引擎 DeepSeek-R1 满血版:流畅、高效的 AI 开发体验

方舟大模型体验中心全新上线&#xff0c;免登录体验满血联网版Deep Seek R1 模型及豆包最新版模型》 https://www.volcengine.com/experience/ark?utm_term202502dsinvite&acDSASUQY5&rcA4K514ZC 大家好&#xff01;作为一名开发者&#xff0c;我一直在寻找能够提升…

Spring Boot 中多线程工具类的配置与使用:基于 YAML 配置文件

文章目录 Spring Boot 中多线程工具类的配置与使用&#xff1a;基于 YAML 配置文件1. 为什么需要多线程工具类&#xff1f;2. 实现步骤2.1 添加依赖2.2 配置线程池参数2.3 创建配置类2.4 创建线程池工具类2.5 使用线程池工具类2.6 测试线程池工具类 3. 配置文件的灵活性4. 总结…