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

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

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) {  // 如果当前元素不等于val
                nums[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.kler.cn/a/558158.html

相关文章:

  • 若依按照时间段查询
  • Django的初步使用
  • Python基于Django的广州、北京、杭州二手房房价可视化分析系统(附源码)
  • 【再谈设计模式】迭代器模式~遍历集合元素的利器
  • relief=tk.RAISED详细介绍 relief是指定控件的边框样式
  • 【练习】【回溯:组合:一个集合 元素可重复】力扣 39. 组合总和
  • 敏捷开发07:敏捷项目可视化管理-ScrumBoard(Scrum板)使用介绍
  • 论文略读:Uncovering Hidden Representations in Language Models
  • 分类解析决策模型
  • 快速定位并优化CPU 与 JVM 内存性能瓶颈
  • 学习Linux准备2
  • figure机器人技术架构的演进初探——Helix人形机器人控制的革新
  • PL/SQL 异常处理
  • 无人机挂载5G通信基站网络恢复技术详解
  • WordPress自定义排序插件:Simple Custom Post Order完全指南(SEO优化版)
  • 前端面试-JavaScript 数据类型详解
  • asp.net mvc、webform 跨域问题
  • 手机怎样玩电脑游戏?
  • 搭建一个基于Django框架的WebApi项目
  • [Android]如何查看APK是使用的什么签名方案