因此,您需要一个哈希码计算,它为“图片”和“turepic”提供相同的结果,但(最好)不同于例如“eruptic”的哈希码。因此,简单地将单词中包含的字母的哈希码相加是不够的——您还需要一些位置信息,但它仍然应该独立于单词的实际排列。您需要定义“等价类”,并始终为类的每个成员计算相同的哈希码。
实现这一点的最简单方法是选择等价类的特定成员,并始终对所有等价词使用该变体的哈希码。例如,按字母顺序选择第一个变体(感谢@Michael 简明扼要地总结它)。对于“picture”等人,那将是“cturepi”。“picture”和“turepic”(以及所有其他等效变体)都应该返回“cturepi”的哈希码。该哈希码可以通过标准 LinkedList 方法或任何其他首选方式计算。
有人可能会说这种计算非常昂贵。没错,但是可以缓存结果,因此只有第一次计算会很昂贵。而且我猜想在常见情况下,第一个字母变体的选择可以得到相当大的优化(与在特定等价类中生成所有排列,然后对它们进行排序并选择第一个的简单解决方案相比)。
例如,在许多单词中,按字母顺序排列的第一个字母是唯一的(“图片”就是其中之一——按字母顺序排列的第一个字母是“c”,其中只有一个“c”)。所以你只需要找到它,然后从那里开始计算哈希码。如果它不是唯一的,则需要比较之后的第二个,第三个等字母,直到找到差异(或翻转)。
更新 2 - 示例
- “abracadabra”包含 5 个“a”。'a' 之后的第二个字符分别是 'b'、'c'、'd'、'b' 和 'a'。因此,在第二轮比较中,您可以得出结论,词典上最小的变化是“aabracadabr”。
- “abab”包含 2 个 'a',每个之后都有一个 'b'(然后你翻身,再次到达一个 'a',所以任务到此结束)。所以你有两个相同的字典最小变体。但是由于它们是相同的,它们显然会产生相同的哈希码。
更新:最后,这一切都归结为您实际上需要多少哈希码 - 即您是否打算将循环列表放入一个关联集合中,例如Set
or Map
。如果没有,您可以使用简单的,甚至是微不足道的哈希方法。但是,如果您大量使用一些关联集合,那么简单的哈希实现会给您带来很多冲突,从而导致性能欠佳。在这种情况下,值得尝试实现此哈希方法并衡量它是否在性能上为自己付出代价。
更新 3:示例代码
Letter
基本上和上面一样,我只做了 fields private
,重命名theNextNode
为next
,并根据需要添加了 getter/setter 。
在CircularWord
我做了一些更多的改变:去掉tail
and theCurrentNode
,并使这个词真正循环(即last.next == head
)。构造函数,toString
并且equals
与计算哈希码无关,因此为简单起见将其省略。
public class CircularWord {
private final Letter head;
private final int numberOfElements;
// constructor, toString(), equals() omitted
@Override
public int hashCode() {
return hashCodeStartingFrom(getStartOfSmallestRotation());
}
private Letter getStartOfSmallestRotation() {
if (head == null) {
return null;
}
Set<Letter> candidates = allLetters();
int counter = numberOfElements;
while (candidates.size() > 1 && counter > 0) {
candidates = selectSmallestSuccessors(candidates);
counter--;
}
return rollOverToStart(counter, candidates.iterator().next());
}
private Set<Letter> allLetters() {
Set<Letter> letters = new LinkedHashSet<Letter>();
Letter letter = head;
for (int i = 0; i < numberOfElements; i++) {
letters.add(letter);
letter = letter.getNext();
}
return letters;
}
private Set<Letter> selectSmallestSuccessors(Set<Letter> candidates) {
Set<Letter> smallestSuccessors = new LinkedHashSet<Letter>();
char min = Character.MAX_VALUE;
for (Letter letter : candidates) {
Letter nextLetter = letter.getNext();
if (nextLetter.getValue() < min) {
min = nextLetter.getValue();
smallestSuccessors.clear();
}
if (nextLetter.getValue() == min) {
smallestSuccessors.add(nextLetter);
}
}
return smallestSuccessors;
}
private Letter rollOverToStart(int counter, Letter lastCandidate) {
for (; counter >= 0; counter--) {
lastCandidate = lastCandidate.getNext();
}
return lastCandidate;
}
private int hashCodeStartingFrom(Letter startFrom) {
int hash = 0;
Letter letter = startFrom;
for (int i = 0; i < numberOfElements; i++) {
hash = 31 * hash + letter.getValue();
letter = letter.getNext();
}
return hash;
}
}
找到单词的字典顺序最小旋转的算法getStartOfSmallestRotation
基本上就是我上面描述的:比较并选择每次旋转的字典顺序最小的第一个、第二个、第三个等字母,丢弃较大的字母直到只剩下一个候选者,或者你翻转这个词。由于列表是循环的,我使用计数器来避免无限循环。
最后,如果我只剩下一个候选,它可能在单词的中间,我需要得到最小单词轮换的开始。但是,由于这是一个单链表,因此在其中后退一步会很尴尬。幸运的是,计数器很好地帮助了我:它记录了到目前为止我比较了多少个字母,但在一个循环列表中,这相当于我在滚动之前可以向前移动多少个字母。因此,我知道要向前移动多少个字母才能再次回到我正在寻找的最小单词轮换的开头。
希望这对某人有所帮助-至少写起来很有趣:-)