LeetCode - 子串匹配类总结(滑动窗口解法)

串联所有单词的子串

给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。

示例 1:
输入:
s = “barfoothefoobarman”,
words = [“foo”,“bar”]
输出:[0,9]
解释:从索引 0 和 9 开始的子串分别是 “barfoo” 和 “foobar” 。输出的顺序不重要, [9,0] 也是有效答案。

示例 2:
输入:
s = “wordgoodgoodgoodbestword”,
words = [“word”,“good”,“best”,“word”]
输出:[]

分析: 最先想起用map先对word中的单词进行全排列 然后去s中找出每一个全排列的子串,但是这种的时间消耗较长。主要是每一个全排列都要在s中从头到尾匹配一遍。后面改进用滑动窗口依次从s中找到目标匹配大小的子串,从子串中依次提取长度固定的字符作为单词去map中查找,如果查找成功该单词数目在map中减一,若匹配不到则该子串不能匹配。时间复杂度o(n*m), n为s的长度,m为words的长度,滑动窗口依次向后移动一位。

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> rst;
        if((s == "") || (words.size() == 0)){
			return rst;
		}
		
		int word_num = words.size();
		int word_one = words[0].size();
		int window_len = word_one * word_num;
        map<string,int> m_map;
        map<string,int>::iterator iter;

        for(auto word :  words){
            m_map[word]++;
		}

        for(int i = 0; i < s.size()-window_len+1; ++i){
			map<string,int> t_map = m_map;
			string subword = s.substr(i,window_len);
			for(int j = 0; j < word_num; ++j){
				string word_item = subword.substr(j*word_one,word_one);
				if((iter=t_map.find(word_item)) != t_map.end()){
					if(iter->second == 1){
						t_map.erase(word_item);
					}else{ 
						--(iter->second);
					}
				}else{
					break;
				}
			}
			if(t_map.empty()){
				rst.push_back(i);
			}
		}

        return rst;
    }
};

滑动窗口(双指针)伪代码

遇到子串问题,首先想到的就是滑动窗口技巧。滑动窗口算法的思路是这样:

  1. 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。

  2. 我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。

  3. 此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。

  4. 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

以总结出滑动窗口算法的抽象思想:

int left = 0, right = 0;

while (right < s.size()) {
    window.add(s[right]);
    while (valid) {
        window.remove(s[left]);
        left++;
    }
    right++;
}


最小覆盖子串

给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。说明:如果 S 中不存这样的子串,则返回空字符串 “”。如果 S 中存在这样的子串,我们保证它是唯一的答案。

示例:
输入: S = “ADOBECODEBANC”, T = “ABC”
输出: “BANC”

分析1: 依次从s中往后查找,若该下标字符在map中匹配,如果查找成功该单词数目在map中减一,若匹配不到依次向后查找直到字符串结束。这种方法在字符串s较长的时候时间消耗大,时间复杂度o(n^2),在leetcode中最后几个测试用例会超时。该算法需改进。

class Solution {
public:
	string minWindow(string s, string t) {
		if (s == ""||t=="")  
			return "";
		int length = t.size();
		map<char, int> m_map;
		map<char, int>::iterator iter;
		bool find = false;
		for (auto word : t) 
			m_map[word]++;
		int min = INT_MAX;
		int indexleft = 0;
		for (int i = 0; i < s.size()-length+1; ++i) {
			map<char, int> t_map = m_map;
			int tlength = length;
			for (int j = i; j < s.size()&&tlength<=s.size()-j+1; ++j) {
				if ((iter = t_map.find(s[j])) != t_map.end()) {
					tlength--;
					if (iter->second == 1)
						t_map.erase(s[j]);
					else
						--(iter->second);
				}
				if (tlength==0) {
					if (j - i + 1 < min) {
						min = j - i + 1;
						indexleft = i;
					}
					find = true;
					break;
				}
			}
			if (find == false)
				break;	
		}
		return min==INT_MAX?"":s.substr(indexleft,min);
	}
};

分析2: 滑动窗口,这个算法的时间复杂度是 O(M + N),M 和 N 分别是字符串 S 和 T 的长度。因为我们先用 for 循环遍历了字符串 T 来初始化 needs,时间 O(N),之后的两个while 循环最多执行2M 次,时间 O(M)。while 执行的次数就是双指针 left 和 right 走的总路程,最多是 2M 。

滑动窗口算法的思路是这样:

  1. 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。

  2. 我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。

  3. 此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。

  4. 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

class Solution {
public:
	string minWindow(string s, string t) {
	    // 记录最短子串的开始位置和长度
	    int start = 0, minLen = INT_MAX;
	    int left = 0, right = 0;
	    
	    unordered_map<char, int> window;
	    unordered_map<char, int> needs;
	    for (char c : t) needs[c]++;
	    
	    int match = 0;
	    
	    while (right < s.size()) {
	        char c1 = s[right];
	        if (needs.count(c1)) {
	            window[c1]++;
	            if (window[c1] == needs[c1]) 
	                match++;
	        }

	        while (match == needs.size()) {
	            if (right - left+1 < minLen) {
	                // 更新最小子串的位置和长度
	                start = left;
	                minLen = right - left+1;
	            }
	            char c2 = s[left];
	            if (needs.count(c2)) {
	                window[c2]--;
	                if (window[c2] < needs[c2])
	                    match--;
	            }
	            left++;
	        }
	        right++;
	    }
	    return minLen == INT_MAX ?"" : s.substr(start, minLen);
	}
};

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

给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。说明:字母异位词指字母相同,但排列不同的字符串。不考虑答案输出的顺序。
示例 :
输入: s: “cbaebabacd” p: “abc”
输出: [0, 6]

解释:起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。

分析: 滑动窗口

vector<int> findAnagrams(string s, string t) {
    // 用数组记录答案
    vector<int> res;
    int left = 0, right = 0;
    unordered_map<char, int> needs;
    unordered_map<char, int> window;
    for (char c : t) needs[c]++;
    int match = 0;
    
    while (right < s.size()) {
        char c1 = s[right];
        if (needs.count(c1)) {
            window[c1]++;
            if (window[c1] == needs[c1])
                match++;
        }

        while (match == needs.size()) {
            // 如果 window 的大小合适
            // 就把起始索引 left 加入结果
            if (right - left+1 == t.size()) {
                res.push_back(left);
            }
            char c2 = s[left];
            if (needs.count(c2)) {
                window[c2]--;
                if (window[c2] < needs[c2])
                    match--;
            }
            left++;
        }
        ++right;
    }
    return res;
}

无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 :输入: “abcabcbb” 输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
分析: 滑动窗口

class Solution {
public:
	int lengthOfLongestSubstring(string s) {
		int left = 0, right = 0;
		unordered_map<char, int> window;
		int res = 0; // 记录最长长度

		while (right < s.size()) {
			char c1 = s[right];
			window[c1]++;
			// 如果 window 中出现重复字符
			// 开始移动 left 缩小窗口
			while (window[c1] > 1) {
				char c2 = s[left];
				window[c2]--;
				left++;
			}
			res = max(res, right - left+1);
			++right;
		}
		return res;
	}
};
发布了76 篇原创文章 · 获赞 6 · 访问量 2772

猜你喜欢

转载自blog.csdn.net/u014618114/article/details/104223048