力扣困难算法精解:串联所有单词的子串与最小覆盖子串

上篇文章:滑动窗口算法精解:水果成篮与字母异位词


目录

1.串联所有单词的子串

理解题意

算法原理

2.最小覆盖子串

理解题意

算法原理


1.串联所有单词的子串

https://leetcode.cn/problems/substring-with-concatenation-of-all-words/

理解题意

本题与上篇文章中的题目有异曲同工之处:

不同点在于:本题需要使用hash<string, int>进行字符串的存储,并且words中每个字符串长度相同,所以每次left与right移动距离都为单个字符串的长度。并且滑动窗口总共执行len次。

算法原理

核心思想是多起点的固定大小滑动窗口结合哈希表。

1.构建目标哈希表:首先遍历 words 数组,将所有单词及其频次记录在 hash~words 中。获取单词长度 len 和单词总数m

2.多起点遍历:由于单词长度为 len,以 s 中不同索引开始的划分方式共有len 种(即从 0,1,...,len- 1 开始)。因此外层循环执行 len 次。

3.维护窗口哈希表与count:在每次内层滑动窗口中:

维护一个当前窗口的哈希表hash,以及一个记录"有效单词数量"的变量count。

进窗口:每次截取 right 处长度为 len 的单词 in 加入 hash。如果加入后该单词的频次仍然<=hash~words [in],说明这是一个"有效"单词,count++。

判断与出窗口:当窗口的跨度 right-left + 1 〉len *m 时,说明窗口内包含了超过 m 个单词,需要将左端点的单词 out 移出窗口。出窗口时,如果移出前该单词是"有效"的(hash[out] 〈= hash~words[out] ),则count--,随后更新 hash left 右移 len 步。

更新结果:如果在某一时刻,窗口内有效单词数 count == m,说明刚好凑齐了所有单词,将此时的 left 加入结果数组ret。

优化点解析:代码中通过引l入count变量来记录当前窗口内的"有效单词数"。这样每次窗口滑动时,只需O(1)的时间去更新和判断count是否等于m,而不需要每次都去遍历对比两个哈希表是否完全相等,大大降低了时间复杂度。

复制代码
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> ret;

        unordered_map<string, int> hash_words; // 保存words中所有单词的频次
        int len = words[0].size(),
            m = words.size(); // words中一个单词的长度和words中字符串个数
        
        for (auto& str_word : words)
            hash_words[str_word]++; // 遍历words所有字符并放入hash表

        for (int i = 0; i < len; i++) // 执行len次
        {
            unordered_map<string, int> hash; // 维护窗口内字符串的频次
            for (int left = i, right = i, count = 0; right + len <= s.size(); right += len)
            // 细节:每次right+len,最后一个单词剩下的位数会遗漏,所以为right + len
            {
                // 进窗口 + 维护count
                string in = s.substr(right, len);
                if(++hash[in] <= hash_words[in])
                    count++;
                // 判断
                if (right - left + 1 > (len * m)) {
                    // 出窗口 + 维护count
                    string out = s.substr(left, len);
                    if (hash[out]-- <= hash_words[out])
                        count--;
                    left += len;
                }
                // 更新结果
                if (count == m)
                    ret.push_back(left);
            }
        }
        return ret;
    }
};

优化:

2.最小覆盖子串

https://leetcode.cn/problems/minimum-window-substring/

理解题意

1.子串中的字符顺序无所谓,只要字符的种类和对应的数量都不小于(t)中的要求即可(即"覆盖")。

2.我们要找的是满足条件的最短子串。

算法原理

核心思想是变长滑动窗口结合数组模拟哈希。

1.数组模拟哈希表:因为题目说明是由英文字母构成(或包含ASCII字符),直接使用两个大小为128的数组 hasht 和 hash 分别代替哈希表来统计字符频次,这比使用unordered_map 会极大提升访问速度。

  1. 统计目标信息:遍历目标串t,填充 hasht。同时用 kinds 变量记录 t 中有效字符的种类总数。

3.进窗口:利用双指针 left 和 right 划定窗口。right 指针主动向右遍历,将新字符 in 放入当前窗口哈希表 hash 中。

  1. 维护有效字符种类count :当且仅当加入字符后 hash[in] == hasht[in] 时,说明某个字符的数量刚好达到了t)中的要求,此时当前窗口满足条件的字符种类数count 增加 1。

5.判断与出窗口(核心收缩逻辑):

当count == kinds 时,说明当前窗口已经包含了t 的所有字符,是一个"覆盖子串"。

此时,首先更新结果(比较并记录最小长度minLen 和起点 begin )。

然后尝试优化(缩小)窗口:将left 指向的字符out 移出窗口。如果移出后hash[out]<hasht[out],说明移除了不可或缺的字符,此时count--,窗口不再合法,退出循环继续移动right;否则说明移出的是冗余字符,left 继续向右收缩寻找更短的有效子串。

6.返回结果:遍历完成后,如果begin 未被修改过说明没找到合法子串,返回空字符串;否则使用substr 截取并返回。

复制代码
class Solution {
public:
    string minWindow(string s, string t) 
    {
        int hasht[128] = { 0 }; // 统计字符串t中字符出现的频次
        int kinds = 0; // 统计有效字符种类
        for(auto str : t)
            if(hasht[str]++ == 0) kinds++;   
        int hash[128] = { 0 }; // 统计窗口内字符出现的频次
        int minLen = INT_MAX, begin = -1;
        for(int right = 0, left = 0, count = 0; right < s.size(); right++)
        {
            // 进窗口 + 维护count
           char in = s[right];
            if(++hash[in] == hasht[in]) count++;
            // 判断
            while(count == kinds)
            {
                // 更新结果
                if(right - left + 1 < minLen) 
                {
                    minLen = right - left + 1;
                    begin = left;
                }
                // 出窗口
                char out = s[left++];
                if(hash[out]-- == hasht[out]) count--;
            }
        }
        if(begin == -1) return "";
        else return s.substr(begin, minLen);
    }
};

本章完。

相关推荐
嘉琪0011 小时前
前端数组核心方法(高级视角 + 场景 + 精简)——————2026 0309
开发语言·前端·javascript
挠头猴子1 小时前
一个数组去重,两个数组找不同或相同
数据结构·算法
big_rabbit05022 小时前
[算法][力扣167]Two Sum II
算法·leetcode·职场和发展
会编程的土豆2 小时前
Set 深度解析:去重、唯一性与你的智能抽屉
java·开发语言·后端·数据结构与算法
颜酱2 小时前
二分图核心原理与判定算法
javascript·后端·算法
java1234_小锋2 小时前
Java高频面试题:JVM内存为什么要分代?
java·开发语言·jvm
筱砚.2 小时前
C++——lambda
开发语言·c++·算法
两个人的幸福online2 小时前
php开发者 需要 协程吗
android·开发语言·php
linux修理工2 小时前
下载亚马逊Corretto 17的方法(OpenJDK 17发行版)
java·运维·服务器