LeetCode 热题 100之子串

1.和为k的子数组


思路分析:利用前缀和和哈希表来高效地解决

1.什么是前缀和?

  • 假设 nums = [1, 2, 3, 4],那么前缀和就是从数组开始到每个位置的累加和;
  • 对于一个位置 j 的前缀和 s[j],可以表示从第一个元素到 j 位置的总和。这样的话,如果我们想知道从位置 i 到 j 的元素的和,只需要 s[j] - s[i - 1]

2.问题转化为找特定的前缀和差值

  • 我们希望找到一个子数组,使得该子数组的和为k。假设当前位置的前缀和为s[j],我们需要寻找一个之前的位置i的前缀和s[i],使得s[j]-s[i] = k,这相当于:s[i] = s[j]- k;
  • 因此,如果我们当前的前缀和s[j]-k的值已经出现在之前的前缀和中,那么我们就找到了一个满足为k的子数组。

3.使用哈希表记录前缀和

  • 为快速查找s[i]是否出现过,使用一个哈希表s来记录每个前缀和出现的次数
    • 遍历数组,计算当前位置的前缀和currentSum;
    • 检查currentSum - k 是否已经在哈希表中:
      • 如果在,说明从某个之前的位置到当前的位置的子数组和为k,把这个数量累加到结果中;
    • 将currentSum的值存入哈希表,以便后续元素使用
  • 我们在哈希表中初始化s[0] = 1,这样可以处理前缀和本身等于 k 的情况

具体实现代码(详解版):

cpp 复制代码
class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        // 哈希表用于记录前缀和出现的次数
        unordered_map<int, int> s;
        s[0] = 1;  // 初始化,处理前缀和本身等于 k 的情况
        
        int currentSum = 0;
        int res = 0;
        
        for (int num : nums) {
            currentSum += num;  // 累加当前元素
            
            // 检查是否存在一个前缀和满足 currentSum - k
            if (s.find(currentSum - k) != s.end()) {
                res += s[currentSum - k];
            }
            
            // 更新哈希表中的前缀和次数
            s[currentSum]++;
        }
        
        return res;
    }
};

2.滑动窗口的最大值

思路分析:这个我们可以使用双端队列来高效的解决。双端队列可以帮助我们在每次移动窗口时快速获得最大值。

  • 使用双端队列存储候选元素的下标
    • 需要一个双端队列dq来存储滑动窗口内看你的最大元素的下标,双端队列中的元素此时始终保持从大到小的顺序。
  • 遍历数组,并维护双端队列的状态
    • 移除窗口外的元素:当滑动窗口向右移动时,队首元素可能会出滑出窗口范围,因此需要将其移除
    • 保持队列元素递减:我们只保留可能成为最大值的元素下标,并且他们按递减顺序排列。对于一个新的元素,我们会依次踏出队尾小于该元素的所有元素,因为这些元素在当前滑动窗口中不可能成为最大值。
    • 加入新元素:将当前元素下标加入队尾
  • 获取当前窗口的最大值
    • 在每次移动窗口后,队首的元素就是当前窗口的最大值。

具体实现代码(详解版):

cpp 复制代码
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
      deque<int> dq;//双端队列,存储下标
      vector<int> result;

      for(int i = 0 ; i < nums.size() ; i ++){
        //移除滑出窗口的元素
        if(!dq.empty() && dq.front() == i - k){
            dq.pop_front();
        }

        //移除队列中所有比当前元素小的元素
        while(!dq.empty() && nums[dq.back()] < nums[i]){
            dq.pop_back();
        }

        //将当前元素下标加入队列
        dq.push_back(i);

        //当窗口大小达到k时,将当前窗口的最大值添加到结果
        if(i >= k - 1){
            result.push_back(nums[dq.front()]);
        }
      }
      return result;

    }
};

3.最小覆盖子串

这个题目确实是挺难的(需要花时间去理解和写代码)

思路分析:使用滑动窗口的技巧来解决。具体的思路是用两个指针来表示一个窗口,不断调整窗口的大小和位置,直到找到复合要求的最小窗口。

  • 使用两个哈希表记录字符频率

    • 用一个哈希表need来存储字符串t中每个字符的需求次数
    • 用一个哈希表window来记录当前窗口中包含的每个字符的次数
  • 初始化双指针和计数器

    • 用左右指针left和right来表示滑动窗口,初始时都指向s的开始位置;
    • 用一个变量valid记录当前窗口中满足need条件的字符个数
  • 扩大右边界形成有效窗口

    • 我们将 right 指针向右移动,扩大窗口
    • 每当加入一个字符时,如果该字符是t中需要的,就更新window中的字符数量,且如果该字符的数量达到了need中的要求,就将valid增加1
  • 缩小左边界寻找最小窗口

    • 当 valid 的值等于 need 中的字符种类数时,表示当前窗口已包含了 t 中所有字符。
    • 这时,我们尝试缩小窗口,通过移动 left 指针来去掉窗口左侧的无效字符,直到窗口刚好满足条件。
    • 每次找到一个符合条件的窗口时,更新最小子串的起始位置和长度。
  • 返回结果

    • 最后,返回记录的最小窗口。如果没有找到满足条件的子串,返回空字符串 ""

具体实现代码(详解版):

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char, int> need, window;
        for (char c : t) need[c]++;
        
        int left = 0, right = 0;
        int valid = 0;  // 用于记录窗口中满足 need 条件的字符数
        int start = 0, minLen = INT_MAX;
        
        while (right < s.size()) {
            char c = s[right];  // 即将移入窗口的字符
            right++;  // 扩大窗口
            
            // 更新 window 中的字符计数
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c])
                    valid++;
            }
            
            // 判断左侧窗口是否需要收缩
            while (valid == need.size()) {
                // 更新最小覆盖子串
                if (right - left < minLen) {
                    start = left;
                    minLen = right - left;
                }
                
                char d = s[left];  // 即将移出窗口的字符
                left++;  // 收缩窗口
                
                // 更新 window 中的字符计数
                if (need.count(d)) {
                    if (window[d] == need[d])
                        valid--;
                    window[d]--;
                }
            }
        }
        
        // 返回最小覆盖子串
        return minLen == INT_MAX ? "" : s.substr(start, minLen);
    }
};

再补充一种解法,思路类似。

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        string ans = "";
        unordered_map<char, int> hash;
        for (auto& c : t)
            ++hash[c];  // 记录 t 中每个字符的需求频率
        size_t need = hash.size();  // 记录不同字符的数量
        
        // 双指针初始化
        for (int i = 0, j = 0; s[j]; ++j) {
            if (--hash[s[j]] == 0)  // 若当前字符 s[j] 满足了需求,更新 need
                need--;
            
            // 缩小窗口,去除多余字符
            while (hash[s[i]] < 0)
                ++hash[s[i++]];  // 左指针右移,并恢复 hash 中的字符频率
            
            // 更新最小覆盖子串
            if (need == 0 && (ans.empty() || ans.size() > j - i + 1))
                ans = move(s.substr(i, j - i + 1));  // 更新答案为更短的符合条件的子串
        }
        return ans;
    }
};

后两个题目难度都挺大的,标记一下需要多刷~

相关推荐
sjsjs112 分钟前
【多维DP】力扣3122. 使矩阵满足条件的最少操作次数
算法·leetcode·矩阵
哲学之窗4 分钟前
齐次矩阵包含平移和旋转
线性代数·算法·矩阵
Sudo_Wang30 分钟前
力扣150题
算法·leetcode·职场和发展
qystca40 分钟前
洛谷 P1595 信封问题 C语言dp
算法
芳菲菲其弥章1 小时前
数据结构经典算法总复习(下卷)
数据结构·算法
我是一只来自东方的鸭.1 小时前
1. K11504 天平[Not so Mobile,UVa839]
数据结构·b树·算法
星语心愿.2 小时前
D4——贪心练习
c++·算法·贪心算法
光头man2 小时前
【八大排序(二)】希尔排序
算法·排序算法
武昌库里写JAVA2 小时前
使用React Strict DOM改善React生态系统
数据结构·vue.js·spring boot·算法·课程设计
创意锦囊2 小时前
随时随地编码,高效算法学习工具—E时代IDE
ide·学习·算法