【字符串】 优雅的暴力——字符串下的哈希判重问题

  在刷题/比赛时经常会遇到判重的问题,那么这次就来讲一讲字符串上的判重问题。

 

▎哈希是什么

  判重我们通常会想到什么?小编首先想到的是桶排序,这种排序正是用了哈希的方法,其实把哈希理解为一堆桶更合适。

  比如说现在给你一堆数字,让你判断一共有几种数字(也就是重复出现的不算): 1 5 4 1 1 3 5 6 。以哈希的思想来解决就是这样的:

  

  放若干个桶,每个桶代表一种数,遇到相应的数字就放进去,判断几种数字就转换成了判断有几个有东西的桶就可以了。

  那么,接下来思考一个问题:怎么存这些桶?要存这些桶只要用绝对不可能重复出现的量来代表桶的序号,例如……数组下标!我们可以利用数组下标来当做桶,每个桶里面东西的个数就是对应数组元素的值。比如说用一个叫做a的数组来存这些桶,当遇到数字3时,只要将a[ 3 ] ++;就可以了。

  其实这就是哈希,所以说理解成一堆桶更形象。

 

▎字符串下的哈希

  看到这里,你一定会想,字符串哈希有什么好讲的?不也是一个道理吗?当然不行!仔细想想,数组下标怎么存储成字符串呢?数组下标都是整数的啊!

  此时出路就很敞亮了,我们可以把字符串转换成整数处理!

  还记得吗?在最开始学习时还学过ASCII码,我们可以通过强制转换替换成整数。

  可是问题又来了,如何用ASCII表示字符串?例如AB,其中A的ASCII码是65,B的ASCII码是66。

  1)用加的:AB表示为65+66=131。反例:BA表示为66+65=131,可AB和BA不一样;

  2)用减的、乘的、除的,似乎都同上,表示出的值都不唯一;

  3)放在一起:AB表示为6566,这样的确举不出什么反例了,但是数字的值变大了,同时也区分不回去了,6566究竟是6和566呢?还是6,56和6呢?似乎不知道原来的字符串长什么样了。

  自然而然,我们便想到了转进制,这样不易发生问题。那么取什么样的进制会不发生或少发生问题呢?我们往往会取27,233,19260817等等,具体会视情况而定。(稍后会有例题讲解)。

  有时会超出unsigned long long的范围,那该怎么办呢?那么我们就要用取模的方法了,通常会模一个很大的质数,模多少可以看看题后的数据规模是多大。

  有些时候会发生一些情况,比如3%2=1,5%2=1(打个比方,一般模数不会这么小),所以两个数取模后当成了一个数来处理,这便叫做哈希冲突,在做题时要减少这种冲突的产生。

 

▎例题——【模板】字符串哈希

题目描述

如题,给定N个字符串(第i个字符串长度为Mi,字符串内包含数字、大小写字母,大小写敏感),请求出N个字符串中共有多少个不同的字符串。

输入输出格式

输入格式:

 

第一行包含一个整数N,为字符串的个数。

接下来N行每行包含一个字符串,为所提供的字符串。

 

输出格式:

 

输出包含一行,包含一个整数,为不同的字符串个数。

 

输入输出样例

输入样例#1: 
5
abc
aaaa
abc
abcc
12345
输出样例#1: 
4

说明

时空限制:1000ms,128M

数据规模:

对于30%的数据:N<=10,Mi≈6,Mmax<=15;

对于70%的数据:N<=1000,Mi≈100,Mmax<=150

对于100%的数据:N<=10000,Mi≈1000,Mmax<=1500

样例说明:

样例中第一个字符串(abc)和第三个字符串(abc)是一样的,所以所提供字符串的集合为{aaaa,abc,abcc,12345},故共计4个不同的字符串。

   这道题完全是模板题,直接套思路就好了。

 

▎Code speaks louder than words!

  话不多说,直接上代码(详见注释)

  

 1 #include<iostream>
 2 #include<algorithm>
 3 using namespace std;
 4 string s;int n;int hash[10000],mod=19270817,k=30,ans=1;
 5 int Hash(string str)
 6 {
 7         int len=str.length();
 8         int value=0;
 9         for(int i=0;i<len;i++)
10         value=value*k+((int)str[i]-96);//转进制
11         return value;//这里其实也可以模一下,不过数据规模没有那么大
12 }
13 int main()
14 {
15         cin>>n;
16         for(int i=1;i<=n;i++)
17         {
18                 cin>>s;
19                 hash[i]=Hash(s);//存储每个字符串转换后的哈希值
20         }
21         sort(hash+1,hash+n+1);//排序,目的是为了排除相同哈希值的字符串
22         for(int i=2;i<=n;i++)
23         if(hash[i]!=hash[i-1]) ans++;//如果哈希值不同,那么两个字符串就不一样
24         cout<<ans;
25         return 0;
26 }

 

▎map是啥?

  说来对于这种题来说还有一大利器——map。简单介绍一下:

  1)头文件:#include<map>

  2)定义:map< 类型 , 类型 > 变量名;

       第一个类型是数组下标的类型,第二个变量是数组值的类型

  3)用处:map定义出来的东西可以理解为数组下标为任意的数组,这恰恰起到了刚才那道题最开始思路的效果

  4)举个栗子:比如说要定义一个数组下标是字符串的整型数组s,可以这么写map< string , int > s;

  怎么解刚才那道题?直接普通哈希就可以了,就不写注释了。

 1 #include<iostream>
 2 #include<map>
 3 using namespace std;
 4 map<string,int>s;string str[100000];int n,ans;
 5 int main()
 6 {
 7     cin>>n;
 8     for(int i=1;i<=n;i++)
 9     {
10         cin>>str[i];
11         s[str[i]]=1;
12     }
13     for(int i=1;i<=n;i++)
14     {
15         if(s[str[i]]==1)
16         {
17             ans++;
18             s[str[i]]=0;
19         }
20     }
21     cout<<ans;
22     return 0;
23 }

 

▎为什么放着map不用而用前一种方法

  map看起来好用,就像数组一样,其实map只是单单的映射,简单来说就是暴力查询,时间复杂度可想而知,这速度很慢,有时是可以AC题目的,但有时是满足不了题目的要求的时间的,所以还是老老实实用字符串下的哈希吧。

 

转载于:https://www.cnblogs.com/TFLS-gzr/p/10927097.html

面试题,是纸上写的,发现了些错误,回来改进了下。写纸上和写计算机里并编译成功完全是两个效果。 开始没太多字符串操作,很繁琐、难点也多,后逐渐改进。 典型问题1: sizeof()局限于栈数组 char a[] = "asd213123123"; 形式,并且这种不能用'\0'断是否结束(这种断方式能很方便加在while条件中用于断越界——b != '\0')。 如果是字符串常量: char *b = "dasadafasdf"; 这种情况,sizeof()就废掉了! 总之: 对号入座,前者sizeof、后者strlen~!不过sizeof(a)和strlen(b)还有另外一个区别,strlen不计算'\0',而sizeof要计算(前提是sizeof()不针对char指针) 典型问题2: 用什么来暂存并输出结果?还是只是记录下来相关位置——这是我底下未完成版本1想到的思路——用一个count[sizeof(A)]数组记录下A每个位置作为起点所能和B达到的最大合,最后断查找数组中最大值,此时目标子字符串的起点下标(i)和 i 对应的长度(counter[i])都有了。 这是针对不知道字符串大小并且不占用额外空间的做法,需要非常繁琐的操作,要加很多标记,越界断也会有些麻烦(结合优势么,用字符串常量而不是栈空间中的字符数组,有'\0'——就好断了!) (关于空间的占用,如果要用一个和字符串a一样长的数组counter来计录a中各起点对应与b最大合子字符串,这个数组也要和a一样长,空间上也不合适,除非情形很特殊,a短b长,不然不如直接malloc()一个堆空间来储存当前最长“子字符串”,并实时更新) 先放一个改完编译测试成功的。 release1 //题目:要求比较A字符串(例如“abcdef"),B字符串(例如(bdcda)。找出合度最大的子字符串,输出(根据OJ经验,输>出结果对即可) #include #include #include main(){ char *A = "abcderfghi"; char *B = "aderkkkkkabcd"; int i,j,c = 0,count = 0; unsigned int maxSeg = 0; int max = strlen(A) > strlen(B) ? strlen(A) : strlen(B); char* final = (char*)malloc(sizeof(char) * (max + 1)); final[max] = '\0'; for(i = 0;A[i] != '\0';i++){ for(j = 0;B[j] != '\0';j++){ while(A[i + c] == B[j] && A[i+c] != '\0' && B[j] != '\0'){ count++; c++; j++; }                         if(count > maxSeg){                                 strncpy(final,(A + i),count);                                 maxSeg = count;                         } count = 0; c = 0; } } printf("%s\n",final); free(final); } 这是能将就用的第一个版本~!关于结束符'\0'能否影响free()的使用,觉得是完全不用操心的,因为malloc的大小是系统来保存的,删除时候系统来接手就完了,而'\0'结束符只是针对一些常规字符串操作,比如printf()用%s控制输出时~! 新难点:找到的子字符串同时一样长怎么办?那我这只能叫做”第一个最长的字符串“用两块空间来存储?三段等长怎么办? 如: "abclbcdlcdel" "kabckbcdkcde" abc长3,bcd长3,cde长3。。。 未完成版本1:这段是错误示范,初期定位模糊思路乱,有些函数和功能不把握,又在纸上写。 思路乱的一个后果就是前期想用i和j简单断越界问题,后期又弄了i+c之类的下标, 修改思路: 把字符串换成“字符串常量”——带'\0'的,这样在小while中用 != '\0'就能断出界问题。 把字符串变成字符串常量以后的另一个问题是sizeof不能用了,引入string.h,用strlen()替代即可。 //题目:要求比较A字符串(例如“abcdef"),B字符串(例如(bdcda)。找出合度最大的子字符串,输出(根据OJ经验,输出结果对即可) //遗忘,未使用string.h相关函数。 #include main(){ char A[] = "asdasd"; char B[] = "asdasd"; /*本版本处理方式为最通用的针对字符串大小未知情况的遍历——比如“字符串常量”——此时可用strlen()代替sizeof(),并引入即可。 *但是因在纸上做题,在条件上做了简化————使用了sizeof()可确定大小的字符数组而非“字符串常量”。具体用sizeof()还是strlen()。这些小问题请读者自行区分。 *如果可用sizeof()确定大小,就可以用malloc()创建一个临时字符串来存储并输出最大字符子段,代码会简化很多~! *不过如果用malloc()保存最大子段,随着最大子段变化,需要不停的free()再新malloc(),要注意 */ int i,j,flag,c = 0,temp = 0,max = 0,count[sizeof(A)]; for(i = 0;i < sizeof(A);i++){//以i为A中“子字符串”首位,遍历B,看B中与A[i]起的子字符串最大匹配数量是多少,记为count[i],每个count[i]对应A中一个字符 for(j = 0;j count[i])//找出B中匹配度最高子段,不用记录下标,只需记录匹配的字符数量,A[i]是固定的起点,加上偏移量,就是这段 count[i] = temp; } //清零,准备面对新的起点j~!以j为起点再找匹配的一段字符串 //j不用恢复~!恢复原样的话,算上j++是移动了一位,不会死循环~!但是,因为这一段本来就是连续的,abcd都连续了,bcd和cd不用看了。 temp = 0; c = 0; } } //比较count数组,看哪个i对应子段越大 //temp = 0;//节省空间的考虑(虽然只有4B),怕不适应就改叫max,去声明一个max变量。 for(i = 0;i max){//找出最大的一个计数器~~~~并记录i!!! max = count[i];//这句可以精简掉,可能?不可以,作为“下标”可以被精简,因为有了flag~!但作为max不能少,做比较用——叫max比较好理解。 flag = i;//用flag记录相应最大子段的起始偏移量 } //输出该子段 for(i = flag;i < flag + count[flag];i++){//temp来源于前一个for循环,意为最大偏移量。 printf("%c",A[i]); } printf("\n"); } ———————————————— 版权声明:本文为优快云博主「秦伟H」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.youkuaiyun.com/huqinwei987/article/details/25316699
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值