先看一个题目
「快乐前缀」是在原字符串中既是 非空
前缀
也是后缀
(不包括原字符串自身)的字符串。
给你一个字符串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原文
网友评论