假设大多数词对不是字谜,那么您最需要优化的情况就是失败情况。
显然,如果长度不同,则字符串不能是字谜,并且长度是在创建字符串时计算一次的属性,因此作为快速比较进行比较是一件非常有效的事情。
如果您更改字符串类以包含更多这些属性,则可以提高快速输出案例的准确性。而不是以以下方式开始测试功能:
if (s1.length() != s2.length()) return false;
你可以使用:
if (s1.hash != s2.hash) return false;
并且当您创建字符串时(或在修改它之后),您将计算一个值,该值对于hash
具有该组字母的所有字符串以任意顺序是唯一的(或几乎是唯一的)。
您仅在字符串更改时才计算此哈希;并非针对您所做的每一次比较。
计算哈希的一种非常简单的方法是:
long sum = 0;
for (int i = 0; i < s.length(); i++)
sum += s[i];
s.hash = sum;
这可以快速计算,但容易发生冲突。
更高级的方法:
static const unsigned int primetable[256] = { 1, 3, 5, 7, /* ... */ };
unsigned long prod = 1;
for (int i = 0; i < s.length(); i++)
prod *= primetable[s[i]];
s.hash = prod;
请注意,素数列表中省略了两个。这确保prod
始终与 的可能范围互质unsigned long
。这在面对数值溢出时最大化了结果的分布。
如果该表被安排在频繁的字母位置放置小素数,则可以最大限度地减少发生溢出(可能导致哈希冲突)的情况。如果没有溢出,那么您就有了一个完美的哈希值(出于这些目的),并且您可以true
通过false
比较hash
.
因此,像这样计算散列效果要好得多:
static const unsigned int primetable[256] = {
1, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619,
821, 823, 827, 829, 839, 853, 857, 107, 859, 863, 877, 881, 883, 109, 887, 907, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 911, 919, 929, 937, 941, 947,
601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811,
359, 11, 61, 31, 37, 3, 71, 43, 59, 7, 101, 79, 29, 53, 13, 23, 47, 103, 17, 5, 19, 41, 73, 83, 89, 67, 97, 367, 373, 379, 383, 389,
1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399, 1409, 1423, 1427,
953, 967, 971, 977, 983, 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171,
397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599,
173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353
};
unsigned long prod = 1;
boolean overflow = false;
for (int i = 0; i < s.length(); i++)
{
overflow |= (prod > ULONG_MAX / primetable[s[i]]);
prod *= primetable[s[i]];
}
if (overflow)
prod ^= 1;
s.hash = prod;
快速输出:
if (s1.hash != s2.hash) return false;
if ((s1.hash & 1) != 0) return true;
if (s1.length() != s2.length()) return false;
仅当字符编码不是多字节时,才可以安全使用中间行。如果您使用的是多字节编码方案,那么哈希仍然会消除大多数非字谜,但它会产生很多误报,因为某些字节顺序不能被忽略。
破解Mats Petersson 的测试代码以从字典中读取,并在真实的英语字典输入上尝试这个和其他算法,我们看到比下一个最佳算法大约提高了 4 倍(它是 10 倍,但我调整了其他代码):
Functionis_anagram time: 218.9s hits: 93
Functionis_anagram1 time: 200s hits: 93
Functionis_anagram2 time: 40s hits: 93
Functionis_anagram3 time: 7.74s hits: 93
Functionis_anagram4 time: 2.65s hits: 93
Functionis_anagram5 time: 2.3s hits: 166
Functionis_anagram6 time: 0.56s hits: 166 <- This method
(命中数不同,因为最后两种算法都不区分大小写,而且我的字典可能包含与自然词冲突的首字母缩略词)
更新:虽然这不是被问到的,但我没有指出这一点是我的疏忽。我不知道我是否没有发现它,或者我只是厌倦了打字,或者我不想对调用代码做出假设......
对所有单词进行散列处理后,最小化测试次数的一种简单方法是按该散列对列表进行排序。然后,您可以轻松地将比较限制为可能匹配的列表部分(根据哈希)。这也可以使分支预测更有效。
我只是尝试更改我最初测试的 N^2 迭代(我确信这是故意低效的)以迭代排序列表中的邻居。该sort()
调用支配了时间,但比最快的 N^2 测试快 200 倍,并且比较算法的选择不再对性能产生任何有意义的差异。
或者,您可以在收到单词时通过哈希对单词进行分类。