【算法学习】-【滑动窗口】-【找到字符串中所有字母异位词】

本文介绍如何使用C++和哈希表解决LeetCode中的字符串异位词子串查找问题,涉及滑动窗口算法和哈希表优化,通过数组映射减少数据结构使用,提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LeetCode原题链接:438. 找到字符串中所有字母异位词

下面是题目描述:
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:
输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的异位词。

示例 2:
输入: s = “abab”, p = “ab”
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的异位词。

1、解题思路
前言:如果有第一次学习滑动窗口算法的朋友,可以先阅读一下笔者关于滑动窗口算法的第一篇文章:【算法学习】-【滑动窗口】-【长度最小的子数组】,那里对滑动窗口会有较详细的讲解,下面的解题思路中关于相关算法的步骤就仅进行简单的叙述啦。

由题目描述可得, 本题主要可分为以下两个步骤:
(1)判断一个字符串是否为另一个字符串的异位词
这里需要借助哈希表这个数据结构来进行判断,即将两个字符串中的字符分别放入两个哈希表中,然后对比这两个哈希表,若两个哈希表中的字符及字符个数都一样,则说明是异位词;否则不是。

(2)确定滑动窗口
相较于之前笔者有关滑动窗口算法的文章中的滑动窗口,这里的窗口大小是恒定的,即用于构成窗口大小的两个指针是 “共进退” 的。故此时直接照搬之前控制窗口移动的思路反而会使情况变得复杂。下面介绍一下算法的步骤

  • 先初始化两个哈希表,便于直接进行第一次判断
  • 判断两个哈希表中的内容否相等,若相等,则记录索引(也就是构成窗口的前面的那个指针的值)
  • 接着无论是否相等都需将字符串s对应的哈希表中的第一个字符删除(注意这里要先让数量--,数量为0后才执行删除操作)而进行下一次枚举
  • 删除后,向s对应的哈希表中插入新的字符,然后两个指针都向后移动一位,准备进行下一次的判断。循环执行上述过程。

2、具体代码

 	vector<int> findAnagrams(string s, string p)
    {
        unordered_map<char, int> mapOfp;
        unordered_map<char, int> mapOfs;

		//初始化哈希表
        for (size_t i = 0; i < p.size(); i++)
        {
            mapOfp[p[i]]++;
            mapOfs[s[i]]++;
        }

        vector<int> res;
        size_t cur = p.size();
        size_t begin = 0;
        while (cur <= s.size())
        {
            if (mapOfp == mapOfs)
            {
                res.push_back(begin);
            }

            if( --mapOfs[s[begin]] == 0)
            {
                mapOfs.erase(s[begin]);
            }

            begin++;
            mapOfs[s[cur++]]++;
        }

        if (mapOfp == mapOfs)
        {
            res.push_back(begin);
        }
        return res;
    }

更新日志
进一步学习之后,发现这道题还有不少可优化之处和需要注意的地方,请看:
可优化的地方主要分为两个方面:一个是哈希表;另一个是判断两个哈希表是否相等
(1)关于哈希表
如上面具体的解题代码,笔者直接使用了C++STL库中的unordered_map作为哈希表,虽然也能得到正确答案,但会使整个代码显得比较 “重”;
所以我们可以通过数组下标的直接映射对其进行优化。即:

int hashOfs[26] = {0};
int hashOfp[26] = {0};
  • 非常需要注意的一点,也是笔者踩坑的一点:
    这里的数组类型应该是int而不能是char
    在为字母建立映射时(这里专指用数组进行映射),若仅判断字母 “有没有或在不在” (即对相应下标置一置零问题),那么可以用char类­型数组;但若要涉及到字母数量,则应该用int类­型数组,因为当数据量大时,一个字母的数量可能已超过了char所能表示的数据范围而发生错误。
    错误再现:
    仅想着字母'a'会映射在下标为97这个位置,并没想过这个位置的值只能从0~127

(2)关于对两个哈希表进行比较
如上面具体的解题代码,笔者也直接用了unordered_map重载的==运算符来进行比较,那么它的底层其实就是遍历表中所有节点来判断是否相等,当数据量大时会有一定的时间消耗;

这里的优化方式是采用一个计数变量(下面命名为count)来统计滑动窗口中有效字符的个数,即当前滑动窗口中的字符在子串(也就是本题的p串)中的个数。然后进出窗口的时候维护这个count,用这个count来完成两个哈希表的比较,当有效字符的个数等于p串的长度时,此时窗口中的字符即为一种p串的异位词。下面是具体过程,请看:
在进窗口前,先对p串的哈希表进行初始化工作,遍历p串,将出现的字符在哈希表中对应位置的数量加1,即:

 //映射字符串p
 for(auto e : p)
 {
     hashOfp[e - 'a']++;
 }

接下来就遍历s串,通过进出窗口来获得问题的解

  • 进窗口
    由于p串中的字符可能重复,这里每次进窗口的字符可分为三种。第一种在p串(p串哈希表)中,而全不在或不全在窗口(s串哈希表)中,此时进窗口的字符为有效字符第二种,是不在p串中,此时为无效字符最后一种也是关键的一种,在p串中又已经全在窗口中,此时的也为无效字符
    无论是哪种情况,都可以直接让哈希表中对应位置上的值++,++后再去判断字符的有效性,当字符有效时才让count++,可通过以下判断条件来维护进窗口阶段的count:
char in = s[cur++];
if(++hashOfs[in - 'a'] <= hashOfp[in - 'a']) 
{
	count++;
}

解释:对于第一种,s串哈希表中对应位置上的值++后会等于或小于p串哈希表中对应位置上的值,此时count++;对于第二种,s串哈希表中对应位置上的值++后会大于p串哈希表中对应位置上的值,此时count不变;对于第三种,s串哈希表中对应位置上的值++后也会大于p串哈希表中对应位置上的值,此时count也不变

  • 出窗口
    和之前的逻辑一样,当窗口的大小等于p串的大小时即可进行出窗口操作。此时可先进行判断:当有效字符的个数等于p串的长度时,此时窗口中的字符为一种p串的异位词,进行记录。
    接着可通过以下判断条件来维护出窗口阶段的count:
char out = s[begin];
if(--hashOfs[out] < hashOfp[out])
{
    count--;
} 
begin++;

解释:对于第一种,s串哈希表中对应位置上的值--后会小于p串哈希表中对应位置上的值,此时count--;对于第二种,s串哈希表中对应位置上的值--不会等于p串哈希表中对应位置上的值,此时count不变;对于第三种,s串哈希表中对应位置上的值--不会小于p串哈希表中对应位置上的值,此时count也不变

下面是优化后的代码,请看:

   vector<int> findAnagrams(string s, string p) 
    {
        int hashOfs[26] = {0};     //注意用char测试用例大了过不了
        int hashOfp[26] = {0};

        //映射字符串p
        for(auto e : p)
        {
            hashOfp[e - 'a']++;
        }

        int begin = 0;
        int cur = 0;
        int count = 0;  //有效字符的个数(判断是否可组成异位词)
        int kinds = p.size();
        vector<int> res;
        while(cur < s.size())
        {
            //进窗口
            char in = s[cur++];
            if(++hashOfs[in - 'a'] <= hashOfp[in - 'a'])  
            {
                count++;
            }
			//出窗口
            if(cur - begin == p.size())
            {
                char out = s[begin];
                if(count == kinds)
                {
                    res.push_back(begin);
                }

                if(--hashOfs[out - 'a'] < hashOfp[out - 'a'])
                {
                    count--;
                } 
                begin++;
            }
        } 
        return res;
    }

看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或看不懂的地方或有可优化的部分还恳请朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值