11

我已经建立了一个表示一个单词的循环链表数据结构,列表中的每个元素都是单词中的一个字母。我的问题的底部是列表的类定义和列表的元素。

列表数据结构的目的是能够比较循环字。所以...“picture”和“turepic”是同一个循环词,所以这两个列表是相等的。

所以我equals()在比较两个列表时会覆盖,而且我已经读过,每当你必须覆盖时equals(),你也必须覆盖hashCode()。但是,我真的不知道如何做到这一点。

我应该如何为我设置的内容定义一个好的 hashCode?我应该考虑什么?在 "picture" 和 "turepic" 的例子中,两个列表是相等的,所以它们的 hashCode 需要相同。有任何想法吗?

谢谢, 赫里斯托

public class Letter {
 char value;
 Letter theNextNode;

 /**
  * Default constructor for an element of the list.
  * 
  * @param theCharacter - the value for this node.
  */
 Letter(char theCharacter) {
  this.value = theCharacter;
 }
}


public class CircularWord {

 /*
  * Class Variables
  */
 Letter head;
 Letter tail;
 Letter theCurrentNode;

 int iNumberOfElements;


 /**
  * Default Constructor. All characters that make up 'theWord' are stored in a 
  * circular linked list structure where the tail's NEXT is the head. 
  */
 public CircularWord(String theWord) {

  char[] theCharacters = theWord.toCharArray();

  for (int iIndex = 0; iIndex < theCharacters.length; iIndex++) {
   this.addElement(theCharacters[iIndex]);
  }

  this.theCurrentNode = head;
  this.iNumberOfElements = theCharacters.length;
 }
}
4

7 回答 7

15

因此,您需要一个哈希码计算,它为“图片”和“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',所以任务到此结束)。所以你有两个相同的字典最小变体。但是由于它们是相同的,它们显然会产生相同的哈希码。

更新:最后,这一切都归结为您实际上需要多少哈希码 - 即您是否打算将循环列表放入一个关联集合中,例如Setor Map。如果没有,您可以使用简单的,甚至是微不足道的哈希方法。但是,如果您大量使用一些关联集合,那么简单的哈希实现会给您带来很多冲突,从而导致性能欠佳。在这种情况下,值得尝试实现此哈希方法并衡量它是否在性能上为自己付出代价。

更新 3:示例代码

Letter基本上和上面一样,我只做了 fields private,重命名theNextNodenext,并根据需要添加了 getter/setter 。

CircularWord我做了一些更多的改变:去掉tailand 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基本上就是我上面描述的:比较并选择每次旋转的字典顺序最小的第一个、第二个、第三个等字母,丢弃较大的字母直到只剩下一个候选者,或者你翻转这个词。由于列表是循环的,我使用计数器来避免无限循环。

最后,如果我只剩下一个候选,它可能在单词的中间,我需要得到最小单词轮换的开始。但是,由于这是一个单链表,因此在其中后退一步会很尴尬。幸运的是,计数器很好地帮助了我:它记录了到目前为止我比较了多少个字母,但在一个循环列表中,这相当于我在滚动之前可以向前移动多少个字母。因此,我知道要向前移动多少个字母才能再次回到我正在寻找的最小单词轮换的开头。

希望这对某人有所帮助-至少写起来很有趣:-)

于 2010-09-19T19:52:31.547 回答
5

你真的需要使用你的 hashCodes 吗?如果您不打算将对象成员放在任何类型的哈希结构中,则可以忽略该问题:

public int hashCode() {
    return 5;
}

这满足了相等的实例具有相等的哈希码的要求。除非我知道我需要更好的散列分布,否则这可能足以满足我自己的需要。

但我想我可能有一个想法,可以更好地分布散列。伪代码:

hash = 0
for each rotation
    hash += hash(permutation)
end
hash %= MAX_HASH

既然hash()很可能是O(n),那么这个算法就是O(n^2),有点慢,但是hash反映了等价测试的方法,hash码的分布大概还算不错。任何其他可交换的内核(prod、xor)都将与本示例中使用的 sum 一样工作。

于 2010-09-19T20:47:06.753 回答
3
int hashcode() {
    int hash = 0;
    for (c in list) {
        hash += c * c;
    }
    return hash;
}

由于 + 是可交换的,相同的单词将具有相同的哈希码。哈希码不是很区分(所有字母排列都得到相同的哈希码),但它应该可以解决问题,除非您通常将许多排列放入 HashSet。

注意:我添加c * c而不是简单地添加c是为了减少不同字母的冲突。

注 2:具有相同哈希码的不等列表违反哈希码合约。应该避免这种“冲突”,因为它们会降低性能,但不会威胁到程序的正确性。一般来说,冲突是无法避免的,尽管在我的回答中肯定可以避免它们,但这样做会使哈希码的计算成本更高,这可能会超过任何性能提升。

于 2010-09-19T20:02:01.990 回答
0

列表中所有元素的哈希码总和如何,每个元素乘以任意值?

就像是

hashCode = 1;
for (char c : myChars) {
    hashCode += 31 * c;
}
于 2010-09-19T19:48:17.533 回答
0

我误读了您的问题-我以为您想要“图片”和“ turepic”的不同哈希值;我认为在这种情况下,您可以从以下事实中得到提示:两个相等的对象必须具有相同的哈希码,但具有相同哈希码的两个对象可能不一定相等。

所以你可以使用 Vivien 的解决方案,它可以保证“picture”和“turepic”具有相同的哈希码。但是,这也意味着“picture”和“pitcure”也将具有相同的哈希码。在这种情况下,您的equals方法必须更智能,并且必须弄清楚这两个字母列表是否实际上代表同一个单词。本质上,您的 equals 方法有助于解决您可以从“picture”/“turepic”和“pitcure”获得的冲突。

于 2010-09-19T19:54:36.893 回答
0
  1. 定义equals()hashCode()Letterchar仅使用该字段执行此操作。
  2. 对于CircularWordhashCode()通过迭代从headtailXOR'ing 的各个值来实现Letter.hashCode。最后将结果与某个常数进行异或。

另一种方法是规范化循环词,将它们表示为:

public class CircularWord {

    private static Set<String> canonicalWords = new HashSet<String>();
    private String canonicalWord;
    private int offset;

    public CircularWord(String word) {
        // Looks for an equal cirular word in the set (according to our definition)
        // If found, set canonicalWord to it and calculate the offset.
        // If not found, put the word in the set, set canonical word to our argument and set offset to 0.
    }
    // Implementation of CircularWord methods using
    // canonicalWord and offset
}

然后,您将实施equals()hashCode()委托String实施。

于 2010-09-19T20:04:07.053 回答
0

请记住,哈希码不是唯一的。两个不同的对象可以散列到完全相同的值。因此哈希码不足以确定相等性;您必须在 equals() 中进行实际比较。[推测性评论已删除。我的天啊]

hashcode() 在所有情况下都可以只返回一个常量。这可能会影响性能,但它是完全有效的。完成其他所有工作后,您可以使用更有效的 hashcode() 算法。

这是一篇好文章。请注意“惰性哈希码”部分。

于 2010-09-19T20:40:25.373 回答