滑动窗口算法

滑动窗口算法主要用于在特定长度的字符串或者数组上操作,从而避免重复多次地反复操作整个字符串或者数组实现了降低时间复杂度的效果。下面将用两道题目作为例子来陈述对于滑动窗口算法在不同情况下的具体应用。

1.无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

 在本题中, 我们使用滑动窗口来操作不含重复字符的最长子串。

在最开始的时候滑动窗口的大小为1,且在滑动窗口内部没有重复的字符时,滑动窗口的大小+1,直至找到重复的字符。找到重复的字符后,整个滑动窗口向右移动,直至遇到比原来滑动窗口大小还要大的新的不含重复字符的子串。此时滑动窗口大小继续扩大,循环以上步骤直至滑动窗口滑到主串结束。

接下来我们以示例来举例:

我们设置两个指针,一个指向滑动窗口的开始位置,一个指向滑动窗口的结束位置。在最开始的时候滑动窗口的大小为1,滑动窗口的开始位置和结束位置相同。

此时滑动窗口内没有重复的元素, 滑动窗口的大小+1,滑动窗口结束指针右移一位。

此时的滑动窗口大小为2,依然没有重复元素,滑动窗口大小+1,滑动窗口结束指针继续右移1位。 

滑动窗口大小为3,滑动窗口大小+1,重复上述操作。

 此时滑动窗口大小为4,滑动窗口内有重复的元素a。当出现重复元素的时候,滑动窗口的大小不再增加,滑动窗口的起始指针向右移动一位。

此时的滑动窗口大小为3,重复上述操作,直至滑动窗口的结束指针遍历完整个字符串,此时整个字符串比对结束,滑动窗口的大小即为我们想要求得的无重复字符的最长子串的长度。

但是以上算法的效率并不算高,因为在每次滑动窗口移动的时候,我们都需要重复地比较滑动窗口内的所有元素查看是否相同,在已知有元素不相同的情况下依然重复比较了多次,浪费了大量的时间。

例如下图的例子:

 在"abcadcbb"这个字符串中,我们比较到上图所示的情况时可知,滑动窗口中有两个重复的"c"字符,此时我们按照之前得出的算法将滑动窗口的起始位置右移一位。

现在的情况是我们在上次比较时,我们已经通过比对得知滑动窗口内有两个重复的"c"字符,但是滑动窗口移动后我们依然需要比较这两个"c"字符是否相同。其实就是这样的无用功降低了我们算法的效率。

所以我们提出了对如上算法的改进。我们用变量max 来记录我们扫描至当前位置无重复字符子串的最大长度(也就是滑动窗口的最大大小),然后我们每次比较只需要比较滑动窗口结束位置的元素和滑动窗口内其他位置的元素是否重复,若重复,我们便将滑动窗口的起始位置移动到重复元素的下一个位置,从而避免重复比较的问题。

所以在使用了如上的改进算法后,我们查找到重复的字符"c"时,我们的滑动窗口应该做如下改变:

 如此循环往复,直至滑动窗口的结束位置遍历到字符串末尾位置时,变量max的值便是我们相求得的字符串中无重复字符子串的最大长度了。

具体实现代码如下所示:

int lengthOfLongestSubstring(char * s)
{
    char *p,*q,*cmp;
    int max = 0;
    int same = 0;
    p = s;
    q = s;
    cmp = p;
    while(*q != '\0')
    {
        if(p < q)
        {
        same = 0;
        for(int i = 0;i < q - p;i++)
        {
            cmp = p + i;
            if(*cmp == *q)
            {
                same = 1;
                break;
            }
        }
        if(same == 1)
        {
            p += cmp - p + 1;
            q--;
        }
        }
        max = max<(q - p + 1)?(q - p + 1):max;
        q++;
    }
    return max;

}

2.字符串的排列

给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。

换句话说,s1 的排列之一是 s2 的 子串 。

示例:

输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").

对于本题笔者的解题方式为设立两个长度为26的数组用来存放两个串中不同字母对应的个数。

 先把数组1的所以元素全部置0,再遍历s1,将s1中字母对应下标的元素+1。

例如在示例中s1的为ab,指针先扫描到字母'a',将'a'对应的下标0的元素+1,指针再扫描到字母‘b’,再将'b'对应下标1的元素+1。至此,数组1处理完毕,数组1中存放的即为s1串中不同字母出现的个数。

对于s2,我们先从s2的起始位置创建一个大小和s1相同的滑动窗口,当前滑动窗口中的元素为字符'e'和'i'。所以我们对数组2的4号下标和8号下标+1。

此时,我们得到了两个数组,数组中存放的既是s1和s2第一个子串中每个字母出现的个数,那么只需要比较两个数组是否相同就可以得知s2的第一个子串是不是s1的排列了。 

由比较得知,s2的第一个子串并不是s1的排列,那么滑动窗口右移一位,比较s2的下一个子串是否为s1的子串。

 滑动窗口右移之后,字母'e'离开滑动窗口,字母'd'进入滑动窗口,此时我们只需要将数组2清空,然后将滑动窗口内所有的元素对应的下标+1,此时数组2中的元素依然表示的是此时滑动窗口中不同字母出现的个数。

至此,循环往复,直至能够找到两个数组完全相同的情况,此时s2当前的子串就是s1的排列。若直至滑动窗口滑动到s2的结束位置都没有找到两个数组完全相同的情况,则s2中没有子串是s1的排列。

具体实现代码如下:

bool equals(int* tmp1, int* tmp2) 
{
    for (int i = 0; i < 26; i++) 
    {
        if (tmp1[i] != tmp2[i]) 
        {

            return false;
        }
    }
    return true;
}

bool checkInclusion(char * s1, char * s2)
{
   int len1 = strlen(s1);
   int len2 = strlen(s2);
   int *tmp1;
   int *tmp2;
   char * p = s1;
   char * q = NULL;
   char * cmp = NULL;

   if(len1 > len2)
   {
       return false;
   }

   tmp1 = (int *)calloc(26,sizeof(int)); 
   tmp2 = (int *)calloc(26,sizeof(int)); 

   while(*p != '\0')
   {
       tmp1[*p - 'a']++;
       p++;
   }
   
   p = s2;
   q = s2 + len1 - 1;
   while(*q != '\0')
   {
       cmp = p;
       while(cmp <= q)
       {
           tmp2[*cmp - 'a']++;
           cmp++;
       }
       if(equals(tmp1,tmp2))
       {
           return true;
       }
       q++;
       p++;
       for(int i = 0;i <26;i++)
       {
           tmp2[i] = 0;
       }
   }
   return false;
}

但是如上算法在滑动窗口每次滑动的时候都重复比较了两个长度为26的数组,导致效率变得不那么高,那么有没有方法能够减少在滑动窗口滑动时的比较次数呢?

 让我们来转换一下思路,既然我们每次滑动窗口滑动时都是在比较两个数组是否完全相同,那么我们只需要设置一个变量diff,用它来表示数组里元素不一样的个数,只要diff为0,那么两个数组的元素就是完全相同的,至此我们便在s2中找到了s1的排列

至此,我们便只需要一个元素全0的数组,先扫描s1和s2的第一个子串,s1中扫描到的字母对应元素+1,s2的子串中扫描到的字母对应元素-1,在扫描完成后该数组中不为0的元素便是两个字符串不同字母的个数(即为diff的个数)。

现在的diff值为4,显然并不符合我们的需求,滑动窗口右移一位。此时字母'e'离开了滑动窗口,字母'd'进入了滑动窗口,如下图所示。

此时我们并不想通过再扫描一遍数组的方式来改变diff的值,其实我们只要修改刚刚退出滑动窗口的字母对应的值与即将进入滑动窗口对应的值便可以知道需要如何修改diff的值了。

在本例中,我们需要先判断进入滑动窗口的字母是否相同,若相同代表原本滑动窗口和s1的字母数量关系并没有变化,若diff不为0,滑动窗口继续向右滑动。字母'e'与字母‘d’并不相等于是我们进行一下的操作。

字母'e'退出了滑动窗口,我们需要先判断'e'对应的数组元素是否为0,若为0代表滑动窗口的移动将导致破坏原本s1与滑动窗口内部该字母数量相等的关系,diff的值+1。此时,元素'e'退出滑动窗口,元素'e'对应的数组元素的值+1。至此,我们还需要判断在'e'对应的数组元素+1后是否为0,若为0代表滑动窗口的移动会导致s1与滑动窗口内该字母的数量重新变得相等,diff的值-1。

同理可得,字母'd'进入了滑动窗口,我们需要判断滑动窗口的移动是否破坏了'd'的相等关系,若破坏,diff的值+1。此时,元素'd'进入滑动窗口,元素'd'对应的数组元素的值-1。此时我们再判断'd'进入滑动窗口后是否达成了新的'd'的相等关系,若达成,diff的值-1。

在做完以上操作后,只需要判断diff的值是否为0,若为0代表滑动窗口中的子串就是s1的排列,若不为0滑动窗口继续右移,直到滑动窗口移动到s2的结束位置,说明s2中并没有s1的排列。

实现代码如下:

bool checkInclusion(char* s1, char* s2) {
    int n = strlen(s1), m = strlen(s2);
    if (n > m) {
        return false;
    }
    int cnt[26];
    memset(cnt, 0, sizeof(cnt));
    for (int i = 0; i < n; ++i) {
        --cnt[s1[i] - 'a'];
        ++cnt[s2[i] - 'a'];
    }
    int diff = 0;
    for (int i = 0; i < 26; ++i) {
        if (cnt[i] != 0) {
            ++diff;
        }
    }
    if (diff == 0) {
        return true;
    }
    for (int i = n; i < m; ++i) {
        int x = s2[i] - 'a', y = s2[i - n] - 'a';
        if (x == y) {
            continue;
        }
        if (cnt[x] == 0) {
            ++diff;
        }
        ++cnt[x];
        if (cnt[x] == 0) {
            --diff;
        }
        if (cnt[y] == 0) {
            ++diff;
        }
        --cnt[y];
        if (cnt[y] == 0) {
            --diff;
        }
        if (diff == 0) {
            return true;
        }
    }
    return false;
}

3.总结

 本文介绍了滑动窗口算法在两种不同情况下的应用。

在例1中,滑动窗口的大小是可以改变的,这是因为我们预先不知道我们需要寻找的子串的长度,需要不断更改滑动窗口的大小来寻找我们的目标串。

在例2中,滑动窗口的大小是不变的,这是因为我们预先知道了我们需要查找的内容,通过不断移动滑动窗口来查看内容是否匹配。

滑动窗口算法在字符串匹配以及计算机网络的拥塞控制部分有着较多的运用,但是由于笔者还需要在该方面进行更加深入的学习,所以本文的分享就在这里告一段落了,希望大家可以变得更强!

同名文章发布于苏嵌教育公众号:滑动窗口算法

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值