今天在做牛客网的摸底考试题的时候,遇到一道可能很经典的字符串算法题。
如果一个字符串可以通过移动字符的位置获得另外一个字符串,则称这两个字符串属于同一类。现在输入N个字符串,请判断里面有多少类字符串。
输入格式:第一行输入N,后面紧接着N行分别是N个字符串。每条字符串不超过50个字符。 输入示例:
4
abcd
bcda
accb
abdd
输出示例:
3
移动一条字符串,并不会改变一条字符串字符的数量。于是,问题就很好解了,算出一个字符串,分别有什么字符,和字符的数量,一条字符串相当于有了自己的一个签名,或者说是散列值。有着相同散列值的字符串就是同一类的字符串了。
import java.util.HashMap;
import java.util.Scanner;
public class Main{
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int numOfString = in.nextInt();
in.nextLine();
String[] strings = new String[numOfString];
for (int j = 0; j < numOfString; j++) {
strings[j] = in.nextLine();
}
System.out.println(fenlei(strings));
}
public static int fenlei(String[] strings) {
HashMap<Integer, Integer> hashMap = new HashMap<>();
for (int i = 0; i < strings.length; i++) {
String s = strings[i];//取出当前字符串
int[] map = new int[26];//存着26个小写字母频率的数组
//计算频率
for (int j = 0; j < strings[i].length(); j++) {
char c = s.charAt(j);
map[c - 'a']++;
}
//笨方法,重新合成一条字符串,然后用String.hashCode()。真的超级笨的
StringBuilder builder = new StringBuilder();
for (int j = 0; j < 26; j++) {
for (int k = 0; k < map[j]; k++) {
builder.append(((char) ('a' + j)));
}
}
int hashCode = builder.toString().hashCode();
//把刚才计算的值放进去HashMap
if (hashMap.containsKey(hashCode)) {
Integer remove = hashMap.remove(hashCode);
hashMap.put(hashCode, remove + 1);
} else {
hashMap.put(hashCode, 1);
}
}
return hashMap.size();//这个散列表的大小就是多少类了
}
里面用了一个最笨的办法,居然又重新合成一条字符串。。。
我之前看到过一个方法,利用质数来算签名,也就是他这个类的标识符。
因为一个数可以分为多个质数的乘积,也就是质因数分解。质数本身无法被其他数整除。因为一个数的质因数分解是唯一的。因此,质数乘积能够保证唯一,只与这条字符串有关,只要改变一个字符串,乘积就会变。我们赋给一个字符唯一一个对应的质数,比如’a’是2,’b’是3,’c’是5。若有一个字符串是acbb,则对应的乘积2*5*3*3=90
,有一个字符串是bbca的话,乘积=3*3*5*2=90
。算出的乘积一样,就是同一类了。
那好,26个小写字母对应的质数依次是,2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101。
说到质数,看到过一篇算N以内质数的算法,有空写。
最后一个居然是101了。恩恩,那么,问题来了。
如果用int型来储存的话,老大难的溢出问题!如果long呢?一样!因为字符串最长50,如果50个z,想想100的50次方是多少。
那么,我用double型?我把所有的质数除以10或者100再算,可能可行吧,还没试过。但是也要注意浮点数的精度问题。字符串长了,运算多了,可能就会失去精度。
因为我想到,对int的乘法,溢出是溢出,但是其实只是没有了高位而已,低位的32个bit仍然保留着原本结果低位的32个bit。int的范围有-2147483648~2147483647这么多的数,遇到散列冲突的问题可能有,但是几率不大。
担心的话,直接上long。甚至BigDecimal。
不过这个算法还是很简陋,不够MD5,SHA-1这种算法冲突少。
给出一个简单的实现。
public static int[] ZHISHU = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101};
public static int fenlei2(String[] strings) {
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < strings.length; i++) {
String s = strings[i];
//一个一个字符,取出来,然后乘以对应的质数
int sig = ZHISHU[s.charAt(0) - 'a'];
for (int j = 1; j < s.length(); j++) {
sig *= ZHISHU[s.charAt(j) - 'a'];
}
//放到哈希表里面去,相同签名的会放到同一格,
//如果已经存在这个签名,取出直接加一存回去,
//JDK1.8以后可以用replace方法。如果没有这个签名,则新建一个。
if (map.containsKey(sig)) {
Integer remove = map.remove(sig);
map.put(sig, remove + 1);
} else
map.put(sig, 1);
}
return map.size();
}
类似问题还有:
- 给出两条字符串,求出其中一条是否包含另外一条的所有字符。
- 字符串旋转问题:给出若干条字符串,其中如果能够通过字符串旋转变成另外一条字符串,则两条字符串为一类,求有多少类字符串
(文中的推理都是基于我的认识而来,并没有做验证。如果有错,欢迎指出)