美文网首页程序员
算法 : Rabin-Karp 字符串编码

算法 : Rabin-Karp 字符串编码

作者: 呆木大人 | 来源:发表于2020-04-17 16:48 被阅读0次

    先看一个题目

    「快乐前缀」是在原字符串中既是 非空 前缀也是后缀(不包括原字符串自身)的字符串。
    给你一个字符串 s,请你返回它的 最长快乐前缀
    如果不存在满足题意的前缀,则返回一个空字符串。

    • 示例 1:
      输入:s = "level"
      输出:"l"
      解释:不包括 s 自己,一共有 4 个前缀("l", "le", "lev", "leve")和 4 个后缀("l", "el", "vel", "evel")。最长的既是前缀也是后缀的字符串是 "l" 。

    • 示例 2:
      输入:s = "ababab"
      输出:"abab"
      解释:"abab" 是最长的既是前缀也是后缀的字符串。题目允许前后缀在原字符串中重叠。

    • 提示:
      1 <= s.length <= 10^5
      s 只含有小写英文字母

    看完这个题后 , 觉得挺简单的, 为啥给了难度:困难

    分析一下 :

    从字符串长度-1开始取前缀和后缀,
    判断是否相等,
    然后不相等的话长度再-1,再次取值比较
    当前缀与后缀相同的时候完成

    例如 s = "ababab";
    可以写个循环
    step 1: pre = "ababa"; suf = "babab"; 不相等, next
    step 2: pre = "abab"; suf = "abab" 相等 , 找到答案 , so easy

    用代码实现:

    class Solution {
        func longestPrefix(_ s: String) -> String {
            if s.count <= 1 {
                return "";
            }
            
            for i in 0...s.count-2 {
                let pre = s.prefix(s.count-1-i);
                let suf = s.suffix(s.count-1-i);
                if pre == suf {
                    return String(pre);
                }
            }
            
            return "";
        }
    }
    

    多么的完美, 直接提交 , 结果不出意料的 超时!

    分析一下,感觉逻辑完美, 为什么会超时呢

    据我的分析 pre == suf 这一句当字符串很长的时候,耗时严重,
    当判断两个字符串是否相等的时候, 会把两个字符串每一位每一位的比较,
    当发现有某一位不一样的时候, 就说明字符串不相同, 所以耗时很严重

    怎么解决呢, 请出今天的主角: Rabin-Karp 字符串编码

    背景知识:

    Rabin-Karp 字符串编码是一种将字符串映射成整数的编码方式,可以看成是一种哈希算法。具体地,假设字符串包含的字符种类不超过 ∣Σ∣(其中 Σ 表示字符集),那么我们选一个大于等于 ∣Σ∣ 的整数 base,就可以将字符串看成 base进制 的整数,将其转换成十进制数后,就得到了字符串对应的编码。

    例如: 给定字符串 s = "abca",其包含的字符种类为 3(即 abc三种)。我们取 base = 9,将字符串 s 看成九进制数 (0120)9 ,转换为十进制为 90,也就是说字符串 abca 的编码为 90

    这样做的好处是什么?我们可以发现一个结论:

    两个字符串 s1 和 s2 相等,当且仅当它们的长度相等且编码值相等。

    只考虑小写的情况下, 小写字母26个, 定义>=26进制的编码就可以了.
    例如 "abcdef" = (012345)26

    但是聪明的同学会发现个问题:当字符串长度变长的时候, 这个编码值会非常大, 这样可能就无法用一般语言中的整数类型(例如 int,long long 等)存储编码值了。对此,一般的解决方法是将编码值对一个数 mod 进行取模,使得其保持在整数类型的范围之内。

    但这样就带来了一个问题,两个不相同但长度相等的字符串在取模前会有不同的编码值,在取模后的编码值就有可能相同了。换句话说,我们的编码方式不是单射,这种哈希算法会产生碰撞。然而我们并不需要过分担心这个问题:
    只要我们将模数设置得很大,并且多选择一些模数,Rabin-Karp 字符串编码产生哈希碰撞的概率就微乎其微。一般来说,对于算法题而言,我们只需要选择一个模数即可,并且它最好是一个质数,例如 10^9+7。如有需要,还可以选择第二个模数 10^9+9 。对于前文提到的 base,一般也选择一个质数,例如本题中 ∣Σ∣=26(即所有小写英文字母),我们可以选择 base=31 (>= 26的质数)。

    算法

    对于这道题而言:
    设定:

    • base = 31;
    • mod = 1000000007; // 10^9+7
    • 我们从小到大枚举前缀的长度 i。然后计算 i 对应的前缀编码值后缀编码值。如果这两个编码值相等,我们就可以判定该前缀和后缀相等。
    • 对于前缀而言,每在字符串最后多出一个新的字符,就相当于原编码值乘以 base 再加上新的字符的编码值;

    例如bcd,
    b = (1) 31 = b;
    bc = (12)31 = (1 * 31 + 2)10 = b * 31 + c
    bcd = (123)31 = (1 * 31 * 31 + 2 * 31 + 3) = (b * 31 * 31 + c * 31 + d) = (b * 31 + c) * 31 + d = bc*31+d

    • 对于后缀而言,每在字符串最前多出一个新的字符,就相当于原编码值加上新的字符的编码值乘以 base 的 i-1 次幂。

    例如bcd,
    d = (3) 31 = d;
    cd = (23)31 = (2 * 31 + 3)10 = c * 31 + d
    bcd = (123)31 = (1 * 31 * 31 + 2 * 31 + 3) = (b * 31 * 31 + c * 31 + d) = b * 31 * 31 + cd

    到此就到了最重要的写代码环节了:

    class Solution {
        
        func longestPrefix(_ s: String) -> String {
            
            let base = 31; // 大于取值范围的质数 , 代表 31 进制
            let mod = 1000000007;
            let n = s.count;
            let s = s.cString(using: .utf8) ?? [];
            let a = 97;
            
            var preValue = 0;   // 前缀值
            var sufValue = 0;   // 后缀值
            var mul = 1;        // mul = (base)的 n-1 次方
            var happy = -1;
            
            for i in 0..<n-1 {
                preValue = (preValue * base + Int(s[i])-a)%mod;
                sufValue = (sufValue + (Int(s[n-i-1])-a) * mul)%mod;
                
                if preValue == sufValue {
                    happy = i;
                }
                
                // 每计算一次  mul * base;  mul = base ^ i
                mul = mul * base % mod;
            }
    
            if (happy < 0) {
                return "";
            } else {
                return String(String.init(cString: s).prefix(happy+1));
            }
        }
    }
    

    同样是循环比较每个子串是否相等, 这样的效率会高很多......

    The End

    这是我学习LeetCode上的知识的一个总结 , 并非原版照抄 , 原版十分生硬, 看了好久才看明白...
    赋原文链接: LeetCode原文

    相关文章

      网友评论

        本文标题:算法 : Rabin-Karp 字符串编码

        本文链接:https://www.haomeiwen.com/subject/nwtyvhtx.html