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

算法魅力-双指针之滑动窗口的叛逆

#1024程序员节#征文

216fe205150546fda849a555c57559a6.png

目录

1.滑动窗口的定义

2.算法实战

2.1 长度最小的子数组

算法思路

2.2 无重复字符的最长子串

算法思路

2.3 最大连续 1 的个数 III

算法思路

哈希表的简要补充

结束语

祝大家1024程序节快乐!!! 


1.滑动窗口的定义

 滑动窗口是一种双指针算法的特例,主要用于处理连续区间的问题,特别是在字符串或数组上寻找满足某些条件的连续子区间。在滑动窗口中,通常有个指针,分别称为“窗口的起始指针”和“窗口的结束指针”,它们一起构成一个“窗口”,在数组或字符串上移动。

滑动窗口的几个关键特点
1.动态调整大小:窗口的大小和位置会根据问题的需求动态地调整。
2.连续性:窗口内的元素是连续的。
3.方向性:窗口通常从数组的左端开始,向右移动。

滑动窗口算法的步骤
- 初始化两个指针,通常称为`left`和`right`,它们分别表示窗口的起始和结束位置。
- 移动`right`指针来扩展窗口,直到窗口中的元素满足某个条件。
- 当窗口满足条件后,尝试通过移动`left`指针来缩小窗口,同时保持窗口中的元素满足条件。
- 在移动指针的过程中,更新所需的答案(例如最大或最小长度)。

简而言之,可以理解成两个指针的同向移动。

滑动窗口算法常用于以下典型问题:
- 最长不含重复字符的子字符串:在字符串中找到一个最长子串,该子串不包含任何重复字符。
- 最小覆盖子串:在字符串S中找到一个包含字符串T中所有字符的最短连续子串。

2.算法实战

2.1 长度最小的子数组

209. 长度最小的子数组 - 力扣(LeetCode)

e9abf9ac38124e4981f05462f84aa430.png

算法思路

1.暴力枚举(超时)

从前往后枚举数组中的任意一个元素,把它当成起始位置。然后从这个起始位置开始,然
后寻找一段最短的区间,使得这段区间的和「大于等于」目标值。
将所有元素作为起始位置所得的结果中,找到最短的数组即可。

class Solution {
public:
 int minSubArrayLen(int target, vector<int>& nums) {
 // 记录结果
 int ret = INT_MAX;
 int n = nums.size();
 
 // 枚举开始位置
 for (int start = 0; start < n; start++)
 {
 int sum = 0; // 记录从这个位置开始的连续数组的和
 // 寻找结束位置
 for (int end = start; end < n; end++)
 {
 sum += nums[end]; // 将当前位置加上
 
 if (sum >= target) // 当这段区间内的和满⾜条件时
 {
 // 更新结果,start 开头的最短区间已经找到
 ret = min(ret, end - start + 1);
 break;
 }
 }
 }
 // 返回最后结果
 return ret == INT_MAX ? 0 : ret;
 }
};

补充:将ret赋值无穷大,即=INT_MAX

2.滑动窗口解法

让滑动窗口满足:从 i 位置开始,窗口内所有元素的和小于 target (当窗口内元素之和
第一次大于等于目标值的时候,就是 i 位置开始,满足条件的最小长度)。
做法:将右端元素划入窗口中,统计出此时窗口内元素的和,如果窗口内元素之和大于等于 target, 更新结果,并且将左端元素划出去的同时继续判 断是否满足条件并更新结果(因为左端元素可能很小,划出去之后依旧满足条件)
如果窗口内元素之和不满足条件: right++ ,另下一个元素进入窗口。
那么为何滑动窗口可以解决问题,并且时间复杂度更低?
这个窗口寻找的是:以当前窗口最左侧元素(记为 left1 )为基准,符合条件的情况。也
就是在这道题中,从 left1 开始,满足区间和 sum >= target 时的最右侧(记为
right1 )能到哪里。
既然已经找到从 left1 开始的最优的区间,那么就可以舍去 left1 。但是如果继续像方法一一样,重新开始统计第二个元素( left2 )往后的和,势必会有大量重复的计算(因为在求第一段区间的时候,已经算出很多元素的和了,这些和是可以在计算下次区间和的时候用上的)。
此时, rigth1 的作用就体现出来了,我们只需将 left1 这个值从 sum 中剔除。从right1 这个元素开始,往后找满足 left2 元素的区间(此时 right1 也有可能是满足的,因为 left1 可能很小。 sum 剔除掉 left1 之后,依旧满足大于等于target )。这样我们就能省掉大量重复的计算。
时间复杂度:虽然代码是两层循环,但是我们的 left 指针和 right 指针都是不回退的,两者
最多都往后移动 n 次。因此时间复杂度是O(n).

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
      //sort(nums.begin(),nums.end());
        int len=INT_MAX,n=nums.size();
        int sum=0;
       // int left=0;
        for(int left=0, right=0;right<n;right++){
           
           sum += nums[right];
           while(sum>=target){
             len=min(len,right-left+1);
             sum-=nums[left++];
           }    
        }
        return len==INT_MAX?0:len;
    }
};

2.2 无重复字符的最长子串

3. 无重复字符的最长子串 - 力扣(LeetCode)

2cde9d8b105e48308ba4a01c2c3896bb.png

算法思路

1.暴力枚举

首先在暴力枚举的过程中,可以通过哈希表来记录字符出现的次数,简单来说,就是定义一个数组,每个字符对应的ASCII值是数组的相应位置。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n=s.size(),ret=0,i,j;
        for( i=0;i<n;i++){
            int hash[128]={0};
            for( j=i;j<n;j++){
                hash[s[j]]++;
               if(hash[s[j]]>1)
                break;
            }
            ret=max(ret,j-i);
        }
        return ret;
    }    
};

当字符字数大于一的时候,就跳出内循环,更新结果,更改数组首元素。

2.滑动窗口

在暴力枚举的过程中,我们发现当进行新的一层外循环时,只要还在遇到重复字母的范围内,就会重复遍历相同的子串子集,并且长度是减小的。

所以我们可以省略这些操作,让滑动窗口满足:窗口内所有元素都是不重复的。

做法:右端元素 ch 进入窗口的时候,哈希表统计这个字符的频次:
如果这个字符出现的频次超过 1 ,说明窗口内有重复元素,那么就从左侧开始划出窗口, 直到 ch 这个元素的频次变为 1 ,然后再更新结果。
如果没有超过 1 ,说明当前窗口没有重复元素,可以直接更新结果
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n=s.size();
        int left=0,right=0,ret=0;
        int hash[128]={0};
        while(right<n){
            hash[s[right]]++;//进入窗口
            while(hash[s[right]]>1){//判断
                hash[s[left]]--;
                left++;}//出窗口
                //ret=max(ret,(right-1)-(left-1)+1);
                ret=max(ret,right-left+1);//更新结果
            right++;//下一个元素进入窗口
        }
        return ret;
    }
};

2.3 最大连续 1 的个数 III

1004. 最大连续1的个数 III - 力扣(LeetCode)

0febcc46c0a44bc798b41c8eaa8c156b.png

算法思路

1.暴力枚举(超时)

用两个循环嵌套,固定首数组元素,然后遍历,枚举出所有情况。定义一个count来储存0的数量。

class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
       int n=nums.size();
       int i,j,len=0;
       for(i=0;i<n;i++){
        int count=0;
        for(j=i;j<n;j++){
            if(nums[j]==0)
            count++;
            if(count>k){
               break;
            }
            len=max(len,j-i+1);
        }
       }
       return len;
    }
};

2.滑动窗口

可以把问题转化成:求数组中一段最长的连续区间,要求这段区间内 0 的个数不超过 k 个。 既然是连续区间,可以考虑使用「滑动窗口」来解决问题。
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int len=0,zero=0;
        for(int left=0,right=0;right<nums.size();right++){
            if(nums[right]==0) zero++;
            while(zero>k){
                if(nums[left++]==0){
                    zero--;
                }
            }
            len=max(len,right-left+1);
        }
        return len;
    }
};

算法采用双指针技术来维护一个窗口,该窗口包含最多 k个0。通过调整窗口的左右边界,我们可以找到包含最多1的子数组。
算法步骤
1. 初始化变量:
   - len:记录当前找到的最长子数组的长度。
   - zero:记录当前窗口中0的数量。
   - left和 right:分别表示滑动窗口的左右边界。
2. 遍历数组:
   - 使用 right指针遍历数组 nums。
   - 如果 `nums[right]` 是0,则增加 zero计数。
3. 调整窗口大小:
   - 当 zero的数量超过 k 时,表示窗口中的0太多,需要通过增加 left 指针来缩小窗口,直到窗口中的0的数量不大于 k。
   - 在增加 left指针的过程中,如果 nums[left] 是0,则减少 zero计数。
4. **更新最长子数组长度**:
   - 在每次调整窗口后,计算当前窗口的长度(`right - left + 1`),并与 len比较,更新 len为最大值。
5. 返回结果:
   - 遍历完成后,返回 len 作为结果,它表示最长的连续1的子数组的长度,同时允许最多 k 个0。
 

哈希表的简要补充

哈希表(Hash table),也被称作散列表,是一种数据结构,用于存储键值对,并能够实现快速查找。哈希表通过一个哈希函数来计算每个键的哈希值,哈希值决定了在表中的位置。

以下是哈希表的一些关键特性:
1. 哈希函数:哈希表使用哈希函数将键映射到表中一个位置来存储对应的值。理想的哈希函数应该容易计算,并且能够将不同的键均匀分布到哈希表中。
2. 冲突解决:由于哈希函数可能会将不同的键映射到同一个位置,这种情况称为“冲突”。解决冲突的方法有多种,比如链地址法(在同一位置存储多个元素,形成一个链表),开放寻址法(寻找下一个空位置)等。
3. 查找效率:在理想情况下,哈希表的查找、插入和删除操作的时间复杂度可以达到O(1)。然而,在最坏的情况下(例如,所有键都映射到同一个位置),这些操作的时间复杂度可能会退化到O(n)。
4. 动态扩容:当哈希表中的元素数量达到一定比例时,哈希表可能会进行扩容,以维持操作的效率。扩容通常涉及创建一个更大的表,并将所有现有元素重新哈希到新表中。
5. 负载因子:负载因子是哈希表中元素数量与哈希表大小的比例。通常,当负载因子超过某个阈值时,哈希表会进行扩容。

结束语

本篇博客就到此结束啦,后面还会更新该算法的相关题目,也会更多有趣的算法陆续产出,感谢友友们一路来的支持!!!

祝大家1024程序节快乐!!! 


http://www.kler.cn/news/363513.html

相关文章:

  • Qt 二进制文件的读写
  • 【C++】动态探索:在C++中实现一个简单的反射系统
  • 【微软商店平台】如何将exe打包上传微软商店
  • AnaTraf | 网络性能监控系统NPM:提升网络性能与业务连续性
  • Notepad++将搜索内容所在行选中,并进行复制等操作
  • 删除本地文件不影响Github
  • 吴恩达深度学习笔记:卷积神经网络(Foundations of Convolutional Neural Networks)3.9-3.10
  • 【vue + mockjs】Mockjs——数据接口模拟
  • git clone卡在Receiving objects
  • matlab生成mipi crc值
  • MySQL 中的连表是怎样实现的?为什么大厂不使用连表查询?
  • Python 文件路径与文件系统操作
  • springboot RedisTemplate支持多个序列化方式
  • MacOS RocketMQ安装
  • 「AIGC」AI设计工具Polymet
  • mac m1 git clone 忽略大小写敏感
  • Linux 部署 Harbor 镜像仓库详解
  • 数据库、数据仓库、数据湖和数据中台有什么区别
  • 如何利用ChatGPT提升SEO内容排名
  • 思迈特助力鸡蛋帮获“24年数据要素x”河北分赛“发展潜力奖”
  • oracle数据恢复—文件损坏导致Oracle数据库打开报错的数据恢复案例
  • Spark 基础概念
  • 编程练习7 5G网络建设
  • AI手机的启明星:从分级标准到智能体手机
  • 【秋招笔试-支持在线评测】10.23花子秋招(已改编)-三语言题解
  • YOLO11 目标检测 | 导出ONNX模型 | ONNX模型推理