Как мне определить хороший хэш-код для кругового связанного списка в Java? - PullRequest
10 голосов
/ 19 сентября 2010

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

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

Поэтому я переопределяю equals() при сравнении двух списков, и я прочитал, что всякий раз, когда вам нужно переопределить equals(), вы должны также переопределить 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;
 }
}

Ответы [ 7 ]

15 голосов
/ 19 сентября 2010

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

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

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

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

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

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

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

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

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

5 голосов
/ 20 сентября 2010

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

public int hashCode() {
    return 5;
}

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

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

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

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

3 голосов
/ 20 сентября 2010
int hashcode() {
    int hash = 0;
    for (c in list) {
        hash += c * c;
    }
    return hash;
}

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

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

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

0 голосов
/ 20 сентября 2010

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

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

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

0 голосов
/ 20 сентября 2010
  1. определить equals() и hashCode() для Letter. Сделайте это, используя только поле char.
  2. Для CircularWord реализовать hashCode() путем итерации от head до tail XOR'а соответствующих значений 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.

0 голосов
/ 19 сентября 2010

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

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

0 голосов
/ 19 сентября 2010

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

Что-то вроде

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