字符串哈希

news/2024/11/24 11:42:44/

字符串哈希

    • 1.原理
    • 2.实现
    • 3.应用

1.原理

从主串中找目标串,一种思路是枚举所有的子串,判断子串是否与目标串相同,子串长度过长,
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,依次类推

2.实现

#include<iostream>
#include<string>
#include<vector>
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^0for (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。
在这里插入图片描述

3.应用

力扣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更长则更新ansans = 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定义及初始化详解


http://www.ppmy.cn/news/10857.html

相关文章

数据结构课程设计

数据结构课程设计 文章目录数据结构课程设计1.1问题描述1.2需求分析1.3概要设计1.4详细设计1.5调试分析1.6测试结果1.7参考文献1.8源码1.1问题描述 编制一个能演示执行集合的交、并和差运算的程序。 要求&#xff1a; 集合元素用小写英文字母&#xff0c;执行各种操作应以对话…

在CSDN年收入竟达五位数?----大学生技术自媒体成长之路

前言&#xff1a; Hello大家好&#xff0c;我是Dream。 还有不到两周就要过年了&#xff0c;自己也马上迈入了21岁&#xff0c;感慨时间飞快&#xff0c;从19岁开始入驻C站&#xff0c;到现在也已经整整两年了&#xff0c;把自己最好的两年青春时光留在了CSDN&#xff0c;超百万…

Java支付宝沙箱环境支付,官方Demo远程调试【内网穿透】

文章目录1. 下载当面付demo2. 修改配置文件3. 打包成web服务4. 局域网测试5. 内网穿透6. 测试公网访问7. 配置二级子域名8. 测试使用固定二级子域名访问在沙箱环境调试支付SDK的时候&#xff0c;往往沙箱环境部署在本地&#xff0c;局限性大&#xff0c;在沙箱环境中有多种支付…

FPGA与数字IC求职知识准备 - 数字电路知识总结

前言 本文整理了数字电路课程中的相关基本的知识点和较为重要的知识点&#xff0c;用于求职的数电部分的知识准备&#xff0c;差缺补漏。 二进制数的算术运算 无符号二进制数的算术运算 加法&#xff1a;同十进制加法&#xff0c;逢二进一&#xff0c;无符号二进制数的加法…

漏洞挖掘-不安全的HTTP方法

前言&#xff1a; 年关将至&#xff0c;这可能是年前最后一篇文章了。已经有一段时间没有更新文章了&#xff0c;因为最近也没有学到什么新的知识&#xff0c;也就没什么可写的&#xff0c;又不想灌水。最近关注的好兄弟们多了很多&#xff0c;在这里也是十分感谢大家的支持&am…

一个自定义的html5视频播放器

// 功能:// 1.视频的播放与暂停(图标变化)// 2.总时间的显示// 3.当前时间的显示(进度)// 4.进度条的显示// 5.跳跃播放// 6.全屏<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport"…

【2004NOIP普及组】T2.花生采摘 试题解析

【2004NOIP普及组】T2.花生采摘 试题解析 时间限制: 1000 ms 内存限制: 65536 KB 【题目描述】 鲁宾逊先生有一只宠物猴,名叫多多。这天,他们两个正沿着乡间小路散步,突然发现路边的告示牌上贴着一张小小的纸条:“欢迎免费品尝我种的花生!——熊字”。 鲁宾逊先生…

RHCE ansible第二次作业

1)安装和配置ansible以及ansible控制节点server.example.com如下&#xff1a; 2)创建一个名为/home/student/ansible/inventory的静态库存文件如下所示&#xff1a; 2.1)node1 是dev主机组的成员 2.2)node2是test主机组的成员 2.3)node1和node2是prod主机组的成员 2.4)node1是b…