美文网首页程序员
算法 : 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原文

相关文章

  • 数据结构与算法--Boyer-Moore和Rabin-Karp子

    数据结构与算法--Boyer-Moore和Rabin-Karp子字符串查找 Boyer-Moore字符串查找算法 ...

  • 算法 : Rabin-Karp 字符串编码

    先看一个题目 「快乐前缀」是在原字符串中既是 非空 前缀也是后缀(不包括原字符串自身)的字符串。给你一个字符串 s...

  • Rabin-Karp算法在go的实现

    原文链接 github 简介 Rabin-Karp字符串快速查找算法和FNV hash算法是golang中stri...

  • 子字符串查找(4)——Rabin-Karp算法

    一、定义 Rabin-Karp算法,是由M.O.Rabin和R.A.Karp发明的一种基于散列的字符串查找算法。通...

  • 霍夫曼编码

    问题: 请设计一个算法,给一个字符串进行二进制编码,使得编码后字符串的长度最短 思路:使用霍夫曼编码构造字符串编码...

  • 二十五、哈夫曼树

    哈夫曼编码(Huffman Coding) 哈夫曼编码,又称为霍夫曼编码,它是现代压缩算法的基础 假设要把字符串【...

  • Rabin-Karp字符串匹配算法

    Rabin-Karp字符串匹配算法是对每一个字符进行比较,把每个字符进行对应进制数并取模运算,然后比较每个字符的函...

  • 18_哈夫曼树

    哈夫曼编码的用途 哈夫曼编码,又称霍夫曼编码,它是现代压缩算法的基础 假设要把字符串【ABBBBCCCCCCCCD...

  • 271. Encode and Decode Strings

    设计一种算法,将字符串列表编码为字符串。 编码后的字符串然后通过网络发送并解码回原始字符串列表。 时间复杂度O(n...

  • 赫夫曼编码

    赫夫曼编码 赫夫曼编码在数据压缩领域有着广泛的应用,压缩率在20%-90%,是一种重要的算法 算法思想(以字符串压...

网友评论

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

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