LeetCode438 找到字符串中所有字母异位词

本文详细介绍了如何使用滑动窗口和双指针解决LeetCode438题——找到字符串中所有字母异位词。通过比较字符频数,确保找到的子串与目标字符串长度和字符数量相同。文章讨论了不同实现策略的时间和空间复杂度,并指出在处理过程中应注意的细节。

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

LeetCode438 找到字符串中所有字母异位词

题目

在这里插入图片描述

解题:滑动窗口 + 双指针

LeetCode76 最小覆盖子串 同样使用滑动窗口 + 双指针来解题。下面一版的代码也与其类似,两道问题的不同之处在于:76题返回的最短字符串里面包含 p 的异位词,长度可以比 p 长,可以包含 p 以外的字母等,只要从中选取的一组字符可以构成 p 的异位词就成。

438题找的是异位词,长度和每个字符的个数都要相同。

sFreq 只记录在 p 中出现的字符的频数。 从提交结果来看,用数组来实现 sFreq 效率更高。

用 sFreq.clear(); / sFreq.fill(0); 的写法不能用一个哈希表来代替 sFreq 和 pFreq,因为在清除时,会把 p 中字符频数的信息一起抹去。

// javascript
var findAnagrams = function(s, p) {
    const sFreq = new Map(), pFreq = new Map();
    const sLen = s.length, pLen = p.length;
    // pFreq 记录 p 所有字符频数
    for (const pEle of p) {
        pFreq.set(pEle, (pFreq.get(pEle) || 0) + 1);
    }
    const res = [];
    // 左右指针 left 和 right, matchSuccess 记录有多少个字符频数 >= pFreq
    let left = 0, right = 0, matchSuccess = 0;
    while (right < sLen) {
        if (pFreq.has(s[right]) === true) {
            sFreq.set(s[right], (sFreq.get(s[right]) || 0) + 1);
            if (sFreq.get(s[right]) === pFreq.get(s[right])) {
                matchSuccess += 1;
            }          
        } else {
        	// [left, right]里如果包含不在 p 中的字符,那必定不是 p 的异位词
        	// left 指向 s[right] 的后一位,并且将 matchSuccess 和 sFreq 记录清除
        	// 新一轮的扫描时 left = right,并且该值还未处理,相当于窗口里没有字符
            left = right + 1;
            matchSuccess = 0;
            sFreq.clear();
        }
        // s 涵盖 p 所有字符且 left <= right 时:开始收缩窗口
        while (matchSuccess === pFreq.size && left <= right) {
        	// s 涵盖 p 所有字符且 [left, right] 长度与 p 一样:找到异位词
            if (right - left + 1 === pLen) {
                res.push(left);
            }
            // 下一步要移除 left,如果 pFreq.has(s[left]) 是 true,sFreq 对应的字符频数要减少
            if (pFreq.has(s[left]) === true) {
                sFreq.set(s[left], sFreq.get(s[left]) - 1);
                if (sFreq.get(s[left]) < pFreq.get(s[left])) {
                    matchSuccess -= 1;
                }
            }
            // 移动窗口的左边界
            left++;
        }
        // 移动窗口的右边界
        right++;
    }
    return res;
};

上面的写法是等到 s[left, right] 包含了 p 的所有字符后再移动左指针来收缩窗口,看能不能找到一个 s[left, right] 包含了 p 的所有字符并且长度正好等于 pLen。

另一种写法是保证在 s[left, right] 中,sFreq 的每个频数都 <= pFreq:一旦发现例外就移动左指针,直到满足条件为止。

// javascript
var findAnagrams = function(s, p) {
    const sFreq = new Map(), pFreq = new Map();
    const sLen = s.length, pLen = p.length;
    for (const pEle of p) {
        pFreq.set(pEle, (pFreq.get(pEle) || 0) + 1);
    }
    const res = [];
    let left = 0, right = 0;
    while (right < sLen) {
        if (pFreq.has(s[right]) === true) {
            sFreq.set(s[right], (sFreq.get(s[right]) || 0) + 1);
            // sFreq 中 s[right] 的频数多于 pFreq,移动左指针收缩窗口直到相等
            while (sFreq.get(s[right]) > pFreq.get(s[right])) {
                if (pFreq.has(s[left]) === true) {
                    sFreq.set(s[left], sFreq.get(s[left]) - 1);
                }
                left++;
            } 
        } else {
        	// 跳过没有在 p 中出现的字符,清空 sFreq
            left = right + 1;
            sFreq.clear();
        }
        // s[left, right] 中所有字符频数 <= p,但是长度等于 p:真相是异位词找到了!
        if (right - left + 1 === pLen) {
            res.push(left);
        }
        right++;
    }
    return res;
};

题目里表明了 s 和 p 仅包含小写字母,可以用数组来代替哈希表

// javascript
var findAnagrams = function(s, p) {
    const sLen = s.length, pLen = p.length;
    const sFreq = new Array(26).fill(0), pFreq = new Array(26).fill(0);
    const codeA = "a".charCodeAt();
    for (let i = 0; i < pLen; i++) pFreq[p.charCodeAt(i) - codeA]++;
    const res = [];
    let left = 0, right = 0;
    while (right < sLen) {
        const idxRight = s.charCodeAt(right) - codeA;
        if (pFreq[idxRight] > 0) {  // > 0 代表该字符在 p 中出现过
            sFreq[idxRight]++;
            while (sFreq[idxRight] > pFreq[idxRight]) {
                const idxLeft = s.charCodeAt(left) - codeA;
                if (pFreq[idxLeft] > 0) {
                    sFreq[idxLeft]--;
                }
                left++;
            }          
        } else {
            left = right + 1;
            sFreq.fill(0);
        }
        if (right - left + 1 === pLen) {
            res.push(left);
        }
        right++;
    }
    return res;
};

假设 sFreq 记录所有在 s 中出现的字符的频数:即如果 s 中有 p 中没有出现过的字符,也进行记录。 这会导致该字符的频数大于 p 中该字符的频数(p 中没出现过,频数是 0)。触发了 left 指针移动到 right + 1 的位置,并且 sFreq 也同步被清空了:不用再写 else 来单独处理,两种情况可以合并处理。

// javascript
var findAnagrams = function(s, p) {
    const sLen = s.length, pLen = p.length;
    const sFreq = new Array(26).fill(0), pFreq = new Array(26).fill(0);
    const codeA = "a".charCodeAt();
    for (let i = 0; i < pLen; i++) pFreq[p.charCodeAt(i) - codeA]++;
    const res = [];
    let left = 0, right = 0;
    while (right < sLen) {
        const idxRight = s.charCodeAt(right) - codeA;
        sFreq[idxRight]++;
        while (sFreq[idxRight] > pFreq[idxRight]) {
            const idxLeft = s.charCodeAt(left) - codeA;
            sFreq[idxLeft]--;
            left++;        
        }
        if (right - left + 1 === pLen) {
            res.push(left);
        }
        right++;
    }
    return res;
};

时间复杂度:最坏情况下左右指针对 s s s 的每个元素各遍历一遍, t t t 的每个元素也会被遍历一遍, O ( ∣ s ∣ + ∣ t ∣ ) O(∣s∣+∣t∣) O(s+t)

空间复杂度:这里用了两张哈希表 / 数组作为辅助空间,每张哈希表最多不会存放超过字符集大小的键值对,因为仅包含小写字母,字符集大小 C C C 为 26,空间复杂度为 O ( 1 ) O(1) O(1)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值