算法 : Rabin-Karp 字符串编码

先看一个题目

「快乐前缀」是在原字符串中既是 非空 前缀也是后缀(不包括原字符串自身)的字符串。
给你一个字符串 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..

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

The End

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

你可能感兴趣的:(算法 : Rabin-Karp 字符串编码)