Как определить хороший хэш-код для кругового связанного списка в Java?

Я создал структуру данных циклического связанного списка, которая представляет слово, и каждый элемент в списке является буквой от слова. В нижней части моего вопроса находятся определения класса списка и элемента списка.

Цель структуры данных списка состоит в том, чтобы иметь возможность сравнивать циклические слова. Итак... "picture" и "turepic" - это одно и то же циклическое слово, поэтому оба списка будут равны.

Итак, я переопределяю equals() при сравнении двух списков, и я прочитал, что всякий раз, когда вам нужно переопределить equals(), вы также должны переопределить hashCode(). Тем не менее, я действительно не очень хорошо понимаю, как это сделать.

Как мне определить хороший хэш-код для того, что я создал? Что я должен учитывать? В примере "picture" и "turepic" оба списка равны, поэтому их хэш-код должен быть одинаковым. Любые идеи?

Спасибо, Христо

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 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;
 }
}

Ответ 1

Как насчет суммы хэш-кодов всех элементов внутри вашего списка, каждая из которых умножается на произвольное значение?

Что-то вроде

hashCode = 1;
for (char c : myChars) {
    hashCode += 31 * c;
}

Ответ 2

Итак, вам нужен расчет hashcode, который дает равные результаты для "изображения" и "turepic", но (предпочтительно) отличается от хэш-кода, например. "Eruptic". Таким образом, недостаточно просто добавить хэш-коды букв, содержащихся в слове, - вам также нужно иметь некоторую информацию о позиции, но все же она не должна зависеть от фактической перестановки слова. Вам нужно определить "классы эквивалентности" и всегда вычислять один и тот же хэш-код для каждого члена класса.

Самый простой способ добиться этого - выбрать конкретный член класса эквивалентности и всегда использовать хэш-код этого варианта для всех эквивалентных слов. Например. выберите первый вариант в алфавитном порядке (спасибо @Michael за его краткое изложение). Для "картинки" и др. Это будет "cturepi". И "изображение", и "turepic" (и все другие эквивалентные варианты) должны возвращать хэш-код "cturepi". Этот хэш-код может быть рассчитан стандартным методом LinkedList или любым другим предпочтительным способом.

Можно сказать, что этот расчет очень дорог. Правда, однако можно кэшировать результат, так что только первый расчет будет дорогостоящим. И я предполагаю, что выбор первого алфавитного варианта может быть достаточно оптимизирован в общем случае (по сравнению с тривиальным решением генерации всех перестановок в конкретном классе эквивалентности, затем сортировка их и выбор первого).

например. во многих словах первая буква в алфавитном порядке уникальна ( "картина" - одна из них - ее первая буква в алфавитном порядке - "c" , и в ней есть только один "c" ). Поэтому вам нужно только найти его, а затем вычислить хэш-код, начиная с этого момента. Если это не уникально, вам нужно сравнить вторую, третью и т.д. Буквы после этого, пока не найдете разницу (или переверните).

Обновить 2 - примеры

  • "abracadabra" содержит 5 'a. 2-й символ после "a" - "b", "c" , "d", "b" и "a" соответственно. Поэтому во втором раунде сравнения вы можете заключить, что лексикографически наименьшая вариация - "абракадабр".
  • "abab" содержит 2 'a и a' b 'после каждого (и затем вы перекатываетесь, снова достигаете "a" , поэтому квест заканчивается). Таким образом, у вас есть два одинаковых лексикографически небольших варианта. Но поскольку они идентичны, они, очевидно, производят один и тот же хэш-код.

Обновление:. В конце концов, все это сводится к тому, насколько вам действительно нужен хэш-код, то есть планируете ли вы поместить свои круговые списки в ассоциативную коллекцию, например, Set или Map. Если нет, вы можете сделать простой или даже тривиальный хэш-метод. Но если вы используете какую-либо ассоциативную коллекцию в значительной степени, тривиальная реализация хэширования дает вам много коллизий, таким образом, субоптимальную производительность. В этом случае стоит попробовать реализовать этот хэш-метод и измерить, платит ли он себя за производительность.

Обновление 3: пример кода

Letter в основном оставлен таким же, как и выше, я сделал только поля private, переименовал theNextNode в next и добавил по мере необходимости нужные геттеры/сеттеры.

В CircularWord я сделал несколько изменений: упал tail и 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, чтобы найти лексикографически наименьшее вращение слова, в основном, я опишу выше: сравните и выберите лексикографически наименьшие 1, 2, 3 и т.д. буквы каждого поворота, опустив большие буквы, пока остается только один кандидат, или вы переверните слово. Поскольку список является круглым, я использую счетчик, чтобы избежать бесконечного цикла.

В конце концов, если у меня останется единственный кандидат, он может быть посредине слова, и мне нужно получить начало наименьшего поворота слов. Однако, поскольку это односвязный список, неудобно делать шаг назад. К счастью, счетчик помогает мне: он записал, сколько букв я сравнил до сих пор, но в круговом списке это эквивалентно тому, сколько букв я могу продвинуться вперед, прежде чем перевернуться. Таким образом, я знаю, сколько писем нужно двигаться вперед, чтобы вернуться к началу минимального поворота слова, которое я ищу.

Надеюсь, это поможет кому-то - по крайней мере, было весело писать: -)

Ответ 3

Вам действительно нужно использовать ваши хэш-коды? Если вы не намерены размещать элементы объекта в какой-либо структуре хеша, вы можете просто игнорировать проблему:

public int hashCode() {
    return 5;
}

это удовлетворяет требованиям, которые равны экземплярам, ​​имеют одинаковые хэш-коды. Если бы я не знал, что мне нужно лучшее распределение хэша, это, вероятно, будет работать достаточно хорошо для моих собственных потребностей.

Но я думаю, что у меня есть идея, которая дает лучшее распределение хешей. код psuedo:

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

Так как hash(), вероятно, будет O (n), то этот алгоритм O (n ^ 2), который немного медленный, но хеши отражают метод, используемый для тестирования эквивалентности, распределение хеш-кодов, вероятно, довольно приличный. любое другое ядро ​​(prod, xor), которое является коммутативным, будет работать так же, как и сумма, используемая в этом примере.

Ответ 4

int hashcode() {
    int hash = 0;
    for (c in list) {
        hash += c * c;
    }
    return hash;
}

Так как + является коммутативным, равные слова будут иметь одинаковые хэш-коды. Хэш-код не очень различается (все перестановки букв получают один и тот же хэш-код), но он должен делать трюк, если вы обычно не помещаете много перестановок в HashSet.

Примечание. Я добавляю c * c, а не просто c, чтобы получить меньше столкновений для разных букв.

Примечание 2: Неравные списки с равными хэш-кодами не нарушают контракт для хэш-кода. Такие "столкновения" следует избегать, поскольку они снижают производительность, но они не угрожают правильности программы. В общем, столкновения нельзя избежать, хотя, конечно, можно избежать их больше, чем в моем ответе, но это делает хэш-код более дорогостоящим для вычисления, что может привести к увеличению производительности.

Ответ 5

Я неправильно понял ваш вопрос - я думал, что вы хотите разные хешеды для "картинки" и "turepic" ; Я думаю, в этом случае вы можете получить намек на то, что два одинаковых объекта должны иметь один и тот же хэш-код, но два объекта с одним и тем же хэш-кодом могут не обязательно быть равными.

Итак, вы можете использовать решение Vivien, которое гарантирует, что "картинка" и "turepic" будут иметь один и тот же хэш-код. Однако это также означает, что "картинка" и "яма" будут иметь одинаковые хеш-коды. В этом случае ваш метод equals должен быть более умным и должен будет выяснить, действительно ли два списка букв представляют одно и то же слово. По сути ваш метод equals помогает разрешить столкновение, которое вы можете получить от "картинки" / "turepic" и "pitcure".

Ответ 6

  • определить equals() и hashCode() для Letter. Сделайте это, используя только поле char.
  • Для CircularWord, реализуйте hashCode(), итерируя от head до tail XOR'ing соответствующие значения Letter.hashCode. Наконец, XOR результат с некоторой константой.

Другим способом было бы канонизировать слова в виде отдельных слов, представляя их как нечто вроде:

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.

Ответ 7

Имейте в виду, что хэш-коды не уникальны. Два разных объекта могут хешировать точно так же. Таким образом, hashcode недостаточно для определения равенства; вы должны сделать фактическое сравнение в equals(). [СПЕЦИАЛЬНЫЙ КОММЕНТАРИЙ УДАЛЕН. OMG]

hashcode() может просто возвращать константу во всех случаях. Это может повлиять на производительность, но это абсолютно справедливо. Как только вы сделаете все остальное, вы можете работать с более эффективным алгоритмом hashcode().

Это хорошая статья. Обратите внимание на раздел "ленивый хэш".