30. Substring with Concatenation of All Words

来源:互联网 发布:java系统日志记录 编辑:程序博客网 时间:2024/06/16 00:01

这道题让我们求串联所有单词的子串,就是说给定一个长字符串,再给定几个长度相同的单词,让我们找出串联给定所有单词的子串的起始位置。

这道题我们需要用到两个哈希表,第一个哈希表先把所有的单词存进去,然后从开头开始一个个遍历,停止条件为当剩余字符个数小于单词集里所有字符的长度。这时候我们需要定义第二个哈希表,然后每次找出给定单词长度的子串,看其是否在第一个哈希表里,如果没有,则break,如果有,则加入第二个哈希表,但相同的词只能出现一次,如果多了,也break。如果正好匹配完给定单词集里所有的单词,则把i存入结果。

class Solution {public:    vector<int> findSubstring(string s, vector<string>& words) {        vector<int> res;        if (s.empty() || words.empty()) return res;        int n = words.size(), m = words[0].size();        unordered_map<string, int> m1;        for (auto &a : words) ++m1[a];        for (int i = 0; i <= (int)s.size() - n * m; ++i) {            unordered_map<string, int> m2;            int j = 0;             for (j = 0; j < n; ++j) {                string t = s.substr(i + j * m, m);                if (m1.find(t) == m1.end()) break;                ++m2[t];                if (m2[t] > m1[t]) break;            }            if (j == n) res.push_back(i);        }        return res;    }};
还有一种用滑动窗口的方法,看到别人一个很清楚的讲解:

这道题还有一种O(n)时间复杂度的解法,设计思路非常巧妙,但是感觉很难想出来,博主目测还未到达这种水平。这种方法不再是一个字符一个字符的遍历,而是一个词一个词的遍历,比如根据题目中的例子,字符串s的长度n为18,words数组中有两个单词(cnt=2),每个单词的长度len均为3,那么遍历的顺序为0,3,6,8,12,15,然后偏移一个字符1,4,7,9,13,16,然后再偏移一个字符2,5,8,10,14,17,这样就可以把所有情况都遍历到,我们还是先用一个哈希表m1来记录words里的所有词,然后我们从0开始遍历,用left来记录左边界的位置,count表示当前已经匹配的单词的个数。然后我们一个单词一个单词的遍历,如果当前遍历的到的单词t在m1中存在,那么我们将其加入另一个哈希表m2中,如果在m2中个数小于等于m1中的个数,那么我们count自增1,如果大于了,那么需要做一些处理,比如下面这种情况, s = barfoofoo, words = {bar, foo, abc}, 我们给words中新加了一个abc,目的是为了遍历到barfoo不会停止,那么当遍历到第二foo的时候, m2[foo]=2, 而此时m1[foo]=1,这是后已经不连续了,所以我们要移动左边界left的位置,我们先把第一个词t1=bar取出来,然后将m2[t1]自减1,如果此时m2[t1]<m1[t1]了,说明一个匹配没了,那么对应的count也要自减1,然后左边界加上个len,这样就可以了。如果某个时刻count和cnt相等了,说明我们成功匹配了一个位置,那么将当前左边界left存入结果res中,此时去掉最左边的一个词,同时count自减1,左边界右移len,继续匹配。如果我们匹配到一个不在m1中的词,那么说明跟前面已经断开了,我们重置m2,count为0,左边界left移到j+len,

实现的步骤:

  1. 遍历一遍单词数组L集合,构造总单词表
  2. 以单词长度为步长,遍历目标字符串,如果当前单词在总单词表内,则进入步骤3;反之,则清空当前滑块单词表,将cout置零,将left移动到下一位置
  3. 当前滑块档次表中的相应单词计数加1,检查该单词的计数是否小于等于总单词表中该单词的总数,如果是,则将count计数加1,进入步骤5;反之,进入步骤4
  4. 根据左起点left收缩滑块,直到收缩到与当前单词相同的字符串片段,将其剔除之后,滑块的收缩工作完成
  5. 如果当前count计数等于单词集合长度,记录下left左起点的位置后,将left右移,当前滑块中相应单词计数减1,总计数减1,继续循环

这里解释下步骤4中的收缩滑块,这是因为当前滑块中有单词的出现次数超过了额定的出现次数,那么就是需要收缩滑块来剔除这个单词,相当于是从滑块的左起点开始寻找该单词,找到之后,将该单词的右端点作为滑块新的左起点,这样就保证了滑块中所有单词都是小于等于额定出现次数,这样也保证了count计数的有效性。

遇到总单词表中不存在的单词的情况,在步骤2中已经说明,清空当前数据之后继续循环,也就是保证了滑块中是不会出现不存在单词表中的单词的。

最后,考虑最外圈循环,如果是从0开始作为滑块的初始起点,那么其实并没有遍历字符串中的所有可能子串,因为步长是单词长度,所以移动滑块的时候会跨过很多可能子串,所以要在外圈再加一层循环,这个循环的作用就是移动滑块的初始起点,所以循环次数就是单词的长度。

参见代码如下:

class Solution {public:    vector<int> findSubstring(string s, vector<string>& words) {        if (s.empty() || words.empty()) return {};        vector<int> res;        int n = s.size(), cnt = words.size(), len = words[0].size();        unordered_map<string, int> m1;        for (string w : words) ++m1[w];        for (int i = 0; i < len; ++i) {            int left = i, count = 0;            unordered_map<string, int> m2;            for (int j = i; j <= n - len; j += len) {                string t = s.substr(j, len);                if (m1.count(t)) {  //j~j+len字符串在m1中存在                    ++m2[t];                    if (m2[t] <= m1[t]) {                        ++count;                    }                     else {//这个字符串多了                        while (m2[t] > m1[t]) {                            string t1 = s.substr(left, len);                            --m2[t1];                            if (m2[t1] < m1[t1]) --count;                            left += len;                        }                    }                    if (count == cnt) {                        res.push_back(left);                        --m2[s.substr(left, len)];//把最左边单词拿出去                        --count;                        left += len;                    }                }                 else {                    m2.clear();                    count = 0;                    left = j + len;//往右移动一个单词                }            }        }        return res;    }};


原创粉丝点击