从主串中找目标串,一种思路是枚举所有的子串,判断子串是否与目标串相同,子串长度过长,
substr方法耗时过长;
考虑另外一种方法,字符串哈希。使用前缀和的形式为每个子串进行编码,得到每个子串的hash_code; 例如:s = “abcd”,假设字符串中都是小写字母,字符串可以用26进制表示,s的哈希编码可以表示为:
hashcode = 'a'*26^3 + 'b'*26^2 + 'c'*26^1 + 'd'*26^0
即越左边的字符,位数越高;s="abcd"也可以表示为10进制数
hashcode = ('a'-'a')*10^3 + ('b'-'a')*10^2 + ('c'-'a')*10^1 + ('d'-'a')*10^0
子串s1 = “bcd” 的哈希编码可以用前缀和之差表示, 假设编码函数为hash_code(x)
hash_code("bcd") = hash_code("abcd") - hash_code("a") * 26^(3-1+1)
即"bcd"的哈希编码可由“abcd”的哈希编码减去“a”的哈希编码; 26^(3-1+1)表示,我们要减去的是最高位的“a”,因位越是左边的字符,在表示过程中数位实际是越高的,比如
1234, 1对应的是千位,1*10^3,2是百位,2*10 ^2,依次类推
#include
#include
#include
using namespace std;
typedef unsigned long long ULL;
ULL X = 13331;
vector<ULL> h, x;
void Hash(string &s) {
int n = s.length();
//初始化
h[0] = s[0];
x[0] = 1;
//进位 abc = a*26^2 + b*26^1 + c*26^0
for (int i = 1; i < n; i++) {
h[i] = h[i-1]*X + s[i];
x[i] = x[i-1]*X;
}
}
ULL getHashCode(int left, int right) {
if (!left) {
return h[right];
}
//前缀和求子子串的哈希code
// "abcde" right = 3 'd', left = 1 'b'
// h[right] - h[left-1] = abcd - a = bcd 的哈希
ULL ans = h[right] - h[left-1] * x[right-left+1];
return ans;
// return left ? h[right] - h[left-1] * x[right-left+1] : h[right];
}
int main() {
string s1;
cin >> s1;
h.resize(s1.length());
x.resize(s1.length());
Hash(s1);
string s2;
cin >> s2;
ULL hash2 = 0;
for (int i = 0; i < s2.length(); i++) {
hash2 = hash2 * X + s2[i];
}
for (int i = 0; i < s1.length(); i++) {
int start = i;
int end = min(s1.length()-1,i+s2.length()-1);
cout << getHashCode(start,end) << " ";
}
cout << endl;
cout << hash2 << endl;
return 0;
}
输入 s = “abcdefg”, t = "bc"可以看到s中包含t的编码; 以通过字符串哈希的预处理方式,以o(n)的时间复杂度可以判断主串中是否包含目标字符串t。
力扣1044 最长重复子串
方法:字符串哈希 + 二分
题目要求从字符串中找到最长的重复子串,重复子串是指出现两次或两次以上的子串,例如
输入:s = “banana”
输出:“ana”
这道题分为两步:
第一步:枚举子串;
第二步:判断子串是否重复出现过。这一步可以用hashset存子串,如果子字符串在hashset中出现过,则看其是否为更长的重复子串,于是可以写出第一版代码:
string longestDupSubstring1(string s) {
int n = s.length();
unordered_set<string> st;
int start = 0, maxlen = 0;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
string substr = s.substr(i,j-i+1);
if (st.find(substr) != st.end()) {
if (j-i+1 > maxlen) {
maxlen = j-i+1;
start = i;
}
}
st.insert(substr);
}
}
cout << start << endl;
return s.substr(start,maxlen);
}
但是,上述代码的时间复杂度为 O(n^2) * substr * find; 两重for循环,取子串的函数substr,都有较高的时间复杂度;unordered_set底层实现的数据结构为哈希表,数据插入和查找的时间接近常数,对象在容器中的位置由它们的哈希值决定。
初始化方式:
std::unordered_set<string> things {16}; // 16 buckets
std::unordered_set<string> words {"one", "two", "three", "four"};// Initializer list
std::unordered_set<string> some_words {++std::begin(words), std::end (words)}; // Range
std::unordered_set<string> copy_wrds {words}; // Copy constructor
需要改进时间复杂度。使用二分法枚举子串长度,替代二重for循环;检查每个长度为len的子串中最长的重复子串长度,
从s中找出长度为mid的重复的子串
若s存在长度为mid的重复子串,则移动左指针,mid(子串长度)也进一步增加,判断s中有更长的重复子串;
若s不存在长度为mid的重复的子串,移动右指针,使得子串长度mid缩小。
二分法的过程如下
初始化 left = 0, right = n-1
while (left <= right) {
int mid = (left + right) / 2;
//在s中查找是长度为mid的重复子串
string str = check(s,mid);
if (str.size() != 0 )
{
left = mid + 1;
}
else
{
right = mid - 1;
}
//比较ans和str哪个更长,str更长则更新ans
ans = ans.length() > str.length() ? ans : str;
}
第一步,时间复杂度,二分查找O(log n), 重复子串查找,O(n), 时间复杂度O(nlogn)。
第二步,在s中查找是长度为mid的重复子串。实现check(s,mid)函数,需要用到字符串哈希,用map存子串的hash值及出现次数,如果子串出现次数>=2; 说明重复返回长度为mid的子串。
具体实现,字符串哈希对字符串做预处理,这样在check(s,mid)函数时,check s中长度为mid的子串,可以用前缀和之差形式表示 子串的哈希值。具体实现:
class Solution {
private:
typedef unsigned long long ULL;
vector<ULL> h;
vector<ULL> x;
int X = 13331;
void hash_func(string &s) {
int n = s.length();
h.resize(n);
x.resize(n);
h[0] = s[0];
x[0] = 1;
for (int i = 1; i < n; i++) {
h[i] = h[i-1] * X + s[i];
x[i] = x[i-1] * X;
}
}
public:
string longestDupSubstring(string s) {
int n = s.length();
hash_func(s);
int left = 0, right = n;
string ans = "";
while (left < right) {
int mid = (left+right+1)>>1;
string str = check(s,mid);
if (str.size() != 0) {
left = mid;
} else {
right = mid - 1;
}
ans = ans.length() > str.length() ? ans : str;
}
return ans;
}
string check(string &s, int len){
int n = s.length();
string ans = "";
unordered_map<ULL,int> mp;
for (int i = 0; i + len <= n; i++) {
int j = i + len -1;
ULL hash = (i>0) ? h[j] - h[i-1] * x[j-i+1] : h[j];
if (mp[hash]) {
ans = s.substr(i,len);;
break;
}
++mp[hash];
}
return ans;
}
};
参考:
STUACM-算法讲堂-字符串哈希(hash)
字符串双哈希 + 二分
C++ unordered_set定义及初始化详解