欢迎来到博主的专栏:算法解析
博主ID:代码小豪
文章目录
- leetcode438——找到字符串中所有字母异位词
- 题目解析
- 算法原理
- 题解代码
- leetcode30——串联所有单词的子串
- 题目解析
- 算法原理
- 题解代码
leetcode438——找到字符串中所有字母异位词
题目解析
异位词是指,有相同的英文字母组成的单词,不要求单词中的字母顺序一致。
以示例1为例:
p中的词组是"abc"。而在s当中有"cba","bac"这两个子串,满足异位词的条件,因此返回这两个子串的起始下标位置。
如果s=“cbaebabacd”,p = “abba”。则s中存在子串"baba"满足异位词的条件。
返回子串"baba"在s当中的起始下标位置4。
算法原理
首先,我们思考一下,如何证明子串和字符串t的是属于字母异位词呢?我们要找到它们的特点,即字母要对应,字母的个数要相同,比如t当中有3个a,1个b,2个c,那么子串也要满足有3个a,1个b,2个c,不能存在更多的字母,比如d,也不能出现不同的个数,比如5个a。
那么我们可以用哈希表来完成字母与个数的映射。我们可以创建哈希表1,用来映射t中的字母。
因为小写字母一共有26位,我们可以用int [26]的数组来作为哈希表,而不是STL中的unordered_map。因为STL容器虽然功能强大,但是效率会比数组低一些。
接下来遍历出整个s中所有大小为4的子串,并且子串中出现的字母映射到另一张哈希表2中。
接下来将hash2出现的结果与hash1挨个作对比,若hash1与hash2一致,则说明该子串是字母异位词。此时将该下标位置记录下来。
因此我们需要找到一个遍历字符串的算法。我们观察一下上面遍历的结果。
由于子串的长度是固定的,可以发现,如果我们从左往右遍历出所有的子串,只需要删除前一个元素,插入后一个元素即可(橙色为删除的元素,蓝色为插入的元素)。因此我们可以采用滑动窗口的遍历方式(见上一篇文章,滑动窗口是一种由暴力枚举优化而来的遍历算法,可以将遍历的复杂度从O(N^2)变成O(N))。
因此我们可以定义出一个left指针,和right指针,使其都指向字符串的起始位置。
由于我们还使用了哈希表来辅助完成对比任务,因此我们要让right当前指向的元素,插入哈希表中。这步操作称为进窗口
。
当子串的长度等于t的长度时,我们要判断一下hash1与hash2是否相等,相等就说明子串与t构成异位词。记录下left的当前下标(子串的起始位置)。
由于子串需要和字符串t的大小一致(异位词的定义)。因此我们要保持right-left+1的大小不超过t的字符串长度,在本例中,字符串t为"abab",即长度为4.因此当right-left+1大于4时,我们要让left指向的元素,在哈希表中被删除,该操作称为出窗口
,完成出窗口操作后,令left++,如下:
无论是进窗口,还是出窗口,都有可能导致子串的长度与t的长度相等,因此无论是进窗口操作完成后,还是出窗口操作完成后,都要判断一下hash1和hash2。
最后就是判断hash1和hash2的方法了,我们可以通过同时遍历hash1和hash2的方式来完成,但是这么做效率其实并不高,博主这里讲解一个优化的算法,优化的方面就是简化哈希表的判断。
我们可以使用count来记录当前子串中的有效字符个数。有效字符指的是当前子串符合构成t的异位词的字符。比如t为"aacb",子串为"cccb
",此时有效字符个数仅为2(因为只有"cb"是符合构成异位词条件的字符),因此不构成异位词。当count与t的个数相等时,此时才符合异位词的条件。
那么如何计算count的值呢?若是hash2中记录的该字符的计数小于等于hash1中该字符的计数。则说明该字符是一个有效字符。若是进窗口的字符是有效字符,则count++,若是出窗口的字符符合有效字符,则count–。
因此整体的运行逻辑如下:
1.进窗口并判断需要更新有效字符
2.判断是否要出窗口,若出窗口则要更新有效字符
3.判断是否构成异位词
4.right++。
题解代码
class Solution {
public:vector<int> findAnagrams(string s, string p) {vector<int> ret;int n=p.size();int hash1[26];for(auto e:p){hash1[e-'a']++;}int hash2[26]={0};for(int left=0,right=0,count=0;right<s.size();right++){//进窗口char in=s[right];hash2[in-'a']++;if(hash2[in-'a']<=hash1[in-'a']) count++;//判断有效字符//出窗口if(right-left+1>n){char out=s[left++];if(hash2[out-'a']<=hash1[out-'a']) count--;//判断有效字符hash2[out-'a']--;}if(count==n) ret.push_back(left);//判断是否为异位词}return ret;}
};
leetcode30——串联所有单词的子串
题目解析
这道题的难度确实符合困难,即使是博主在思路非常明确的情况下做这道题依然被许多细节给困扰一段时间。
算法原理
这道题我们可以看做是找到字符串中所有字母异位词
的plus版,为什么这么说呢?我们以示例1为例:
如果我们让words[0]视为’A’,words[1]视为’b’。未出现在words中的其他字符串等于其他字母,那么s和words会变成下面这样:
有没有发现,这道题的解决思路和异位词的解决思路一模一样?没错,确实如此,我们一样时创建两个哈希表,只不过哈希表中不再是字符与计数的映射关系,而是字符串和计数的映射关系,count从有效字符的个数变成了有效字符串的个数,遍历的方式依然是滑动窗口,这么想这道题的难度是不是从困难变成和异位词一样的一般了?
不过我们要注意几个细节
细节1:字符串s有多种遍历方式
解决方法:分多次从不同的起始位置遍历
细节2:
right和left每次移动都要跳向后多个元素,因为这次滑动窗口要遍历的不是单个字符,而是定长的字符串
而且由于此题涉及较多的字符串操作,因此要求我们对STL库有一定的熟练度。
题解代码
class Solution {
public:vector<int> findSubstring(string s, vector<string>& words) {vector<int> ret;unordered_map<string,int> hash1;int len=words[0].size();for(auto& e:words){hash1[e]++;}for(int i=0;i<len;i++){//多次遍历unordered_map<string,int> hash2;int all=0;//hash2中记录的字符串总个数int cnt=0;//有效字符串总数for(int left=i,right=left;right+len<=s.size();right+=len){//进窗口string sub=s.substr(right,len);hash2[sub]++;all++;if(hash2[sub]<=hash1[sub]) cnt++;//出窗口if(all>words.size()){string out;out=s.substr(left,len);if(hash2[out]<=hash1[out]) cnt--;hash2[out]--;left+=len;all--;}if(cnt==words.size()) ret.push_back(left);}}return ret;}
};