[leetcode 面试题 17.17] -- 多次搜索,KMP与字典树

[leetcode 面试题 17.17] -- 多次搜索

  • 题目来源
  • 分析
  • KMP思路
    • 完整代码
  • 字典树
    • 完整代码

题目来源

https://leetcode-cn.com/problems/multi-search-lcci/
[leetcode 面试题 17.17] -- 多次搜索,KMP与字典树_第1张图片

分析

题目就是让从一长串字符中,搜索最多1e5个字符串,找到所有字符串出现的位置。

第一种朴素的做法就是直接查找,但是不能暴力查找,因为暴力搜索时间复杂度是O(n * n),很容易超时。可以使用KMP算法,他的时间复杂度是O(n + m),加上这个题也就是O(n + m) * 1e5,也就是1e8左右,应该是可以的。

第二种方法就是利用字典树,先对所有的子串进行建树,因为子串都是小写字母,所以可以建26个以每个小写字母为根的树,树的每个节点都有最多26个孩子节点。需要注意的是,树的叶子节点一定是一个需要找的子串,但是也可能树的中间的一个节点也是子串,所以我们需要再建立一个额外的数组来保存每个子串出现的位置。
时间复杂度 建树是 O(1e8) + 搜索 O(1e6)也是可以完成的。

KMP思路

KMP算法就是一个跳跃式的寻找前缀的算法。
我们平常的暴力写法是这样的
[leetcode 面试题 17.17] -- 多次搜索,KMP与字典树_第2张图片
这样的搜索复杂度是n * m 了,而KMP算法却可以让这个复杂度变成n + m,因为他们都有一个公共前缀aa,我们每次遍历基本上就可以说是线性的。

每一次我们发现对应位置的字符不相同的时候,就向前移动next数组,这个时候就有两种情况

  • 前面没有与这个前缀相同的字符,那么就从当前位置再重新查找
    [leetcode 面试题 17.17] -- 多次搜索,KMP与字典树_第3张图片
  • 前面有相同的字符,就直接从这个字符开始,向后匹配。
    [leetcode 面试题 17.17] -- 多次搜索,KMP与字典树_第4张图片

在这之前,我们需要维护我们的next数组,这样就可以方便我们遍历的时候更快的找到我们公共前缀
[leetcode 面试题 17.17] -- 多次搜索,KMP与字典树_第5张图片
然后我们再遍历源字符串,从源字符串中查找匹配的情况。

完整代码

class Solution {
public:
    vector<vector<int>> multiSearch(string big, vector<string>& smalls) {
        vector<vector<int> > ans;
        //依次搜索所有的子串
        for(auto& eoch : smalls)
            ans.push_back(kmp(big,eoch));
        return ans;
    }

    vector<int> kmp(string s,string p)
    {
        vector<int> ret;
        
        int lens = s.size();
        int lenp = p.size();
        //如果有一个为空的字符串,说明匹配不到结果
        if(!lens || !lenp) return ret;

        vector<int> next(lenp,0);
        for(int i = 1, j = 0; i < lenp; i++)
        {
            //确保j-1合法  当前位置不相等,就跳到上一个位置
            while(j && p[i] != p[j])
                j = next[j-1];
            
            if(p[i] == p[j])
                j++;
            next[i] = j;
        }

        for(int i = 0, j = 0; i < lens; i++)
        {
            while(j && s[i] != p[j])
                j = next[j-1];
            if(s[i] == p[j])
                j++;
            //成功匹配
            if(j == lenp)
            {
                ret.push_back(i-lenp+1);
                j = next[j-1];
            }
        }
        return ret;
    }
};

字典树

[leetcode 面试题 17.17] -- 多次搜索,KMP与字典树_第6张图片
整个过程分为三步

  • 先把所有待匹配的字符串,依次以他们的首字符为根节点,然后构成一棵树,并在每一次遍历完成之后,统计叶子节点的下标。因为存在,caacaaa,这样的话第二次统计就覆盖了前一次的叶子节点。
  • 第二步就是搜索了,从源字符串的每一个位置开始,每一次都统计出以当前位置开始的待匹配字符串,把他们用一个hash表保存起来。
  • 遍历待匹配字符串,找到他们每一个字符串出现的位置数组。

完整代码

class Solution {
public:
    //字典树
    const static int N = 100010;
    int son[N][26];//字典树
    int idx;
    string key[N];

    vector<vector<int>> multiSearch(string big, vector<string>& smalls) {
       //生成字典树
        for(auto& eoch : smalls)
            insert(eoch);
        
        //查找所有子串出现的情况
        unordered_map<string,vector<int> > hash;
        for(int i = 0; i < big.size(); i++)
            search(big,i,hash);

        vector<vector<int> > ans;
        for(auto& eoch : smalls)
            ans.push_back(hash[eoch]);
        return ans;
    }

    void insert(string& s)
    {
        int p = 0;
        for(int i = 0; i < s.size(); i++)
        {
            int u = s[i] - 'a';
            //如果这个地方之前没有出现过这个字母的路径,就新加一个节点
            if(!son[p][u]) son[p][u] = ++idx;
            p = son[p][u];
        }
        //叶子节点记录这个字串的信息
        key[p] = s;
    }

    void search(string& s,int index,unordered_map<string,vector<int> >& hash)
    {
        int p = 0;
        for(int i = index; i < s.size(); i++)
        {
            int u = s[i] - 'a';
            if(!son[p][u]) return; //说明没有需要查找的子串
            p = son[p][u];
            //key[p] != ""   判断这条路径是不是叶子节点,是叶子节点就添加开始位置到图中
            if(key[p] != "") hash[key[p]].push_back(index);
        }
    }
};

你可能感兴趣的:(题解)