Подтвердить что ты не робот

Хеширование структуры дерева

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

Возьмем, например, следующее дерево:

        O
       / \
      /   \
     O     O
    /|\    |
   / | \   |
  O  O  O  O
          / \
         /   \
        O     O

Где каждый O представляет node дерева, является произвольным объектом, имеет связанную хэш-функцию. Таким образом, проблема сводится к: учитывая хэш-код узлов древовидной структуры и известную структуру, что является достойным алгоритмом для вычисления (относительно) коллизионного хеш-кода для всего дерева?

Несколько замечаний о свойствах хэш-функции:

  • Хэш-функция должна зависеть от хеш-кода каждого node внутри дерева, а также от его позиции.
  • Переупорядочение дочерних элементов node должно отчетливо изменить полученный хеш-код.
  • Отражение любой части дерева должно отчетливо изменять полученный хеш-код

Если это помогает, я использую С# 4.0 здесь, в моем проекте, хотя я в первую очередь ищу теоретическое решение, поэтому псевдокод, описание или код на другом императивном языке будет в порядке.


UPDATE

Ну, вот мое собственное предлагаемое решение. Несколько из этих ответов были очень полезны.

Каждый node (поддерево/лист node) имеет следующую хеш-функцию:

public override int GetHashCode()
{
    int hashCode = unchecked((this.Symbol.GetHashCode() * 31 +
        this.Value.GetHashCode()));
    for (int i = 0; i < this.Children.Count; i++)
        hashCode = unchecked(hashCode * 31 + this.Children[i].GetHashCode());
    return hashCode;
}

Хорошая вещь об этом методе, как я вижу, заключается в том, что хэш-коды могут быть кэшированы и пересчитываться только при изменении node или одного из его потомков. (Спасибо Ватине и Джейсону Орендорфу за это).

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

4b9b3361

Ответ 1

Если бы я сделал это, я бы, вероятно, сделал бы что-то вроде следующего:

Для каждого листа node вычислите конкатенацию 0 и хэш данных node.

Для каждого внутреннего node вычислите конкатенацию 1 и хэш любых локальных данных (NB: может быть неприменим) и хеш дочерних элементов слева направо.

Это приведет к каскаду дерева при каждом изменении чего-либо, но это МОЖЕТ быть достаточно низким, чтобы накладные расходы были полезными. Если изменения относительно нечасты по сравнению с количеством изменений, может оказаться даже целесообразным использовать криптографически безопасный хеш.

Edit1: существует также возможность добавить флаг "hash valid" для каждого node и просто распространять "false" по дереву (или "hash invalid" и распространять "true" ) вверх по дереву на node изменить. Таким образом, может быть возможно избежать полного пересчета, когда требуется хэш хэша, и, возможно, избежать многочисленных вычислений хэша, которые не используются, с риском немного менее прогнозируемого времени для получения хэша, когда это необходимо.

Edit3: хеш-код, предложенный Нолдорином в вопросе, выглядит так, что у него будет вероятность столкновения, если результат GetHashCode может когда-либо равняться 0. По существу, нет возможности различать дерево, состоящее из одного node с хешем символа 30 и "значением хеш" 25 и деревом с двумя символами node, где корень имеет "символьный хеш" 0 и "хэш-значение" из 30, а дочерний элемент node имеет общий хэш 25. Примеры полностью выдуманы, я не знаю, какие ожидаемые диапазоны хеширования я могу лишь прокомментировать, что я вижу в представленном коде.

Использование 31 в качестве мультипликативной константы является хорошим, поскольку оно вызовет любое переполнение на небитовой границе, хотя я думаю, что с достаточным количеством детей и, возможно, состязательным контентом в дереве хэш-вклад от элементов хэширование рано МОЖЕТ доминировать над более поздними хеш-элементами.

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

Edit2: Что касается конкретных алгоритмов и минимальной структуры данных, то что-то вроде следующего (Python, перевод на любой другой язык должен быть относительно простым).

#! /usr/bin/env  python

import Crypto.Hash.SHA

class Node:
    def __init__ (self, parent=None, contents="", children=[]):
        self.valid = False
        self.hash = False
        self.contents = contents
        self.children = children


    def append_child (self, child):
        self.children.append(child)

        self.invalidate()

    def invalidate (self):
        self.valid = False
        if self.parent:
            self.parent.invalidate()

    def gethash (self):
        if self.valid:
            return self.hash

        digester = crypto.hash.SHA.new()

        digester.update(self.contents)

        if self.children:
            for child in self.children:
                digester.update(child.gethash())
            self.hash = "1"+digester.hexdigest()
        else:
            self.hash = "0"+digester.hexdigest()

        return self.hash

    def setcontents (self):
        self.valid = False
        return self.contents

Ответ 2

Хорошо, после вашего редактирования, где вы ввели требование о том, что результат хеширования должен отличаться для разных макетов дерева, вам остается оставить опцию, чтобы пересечь все дерево и записать его структуру в один массив.

Это делается следующим образом: вы пересекаете дерево и выполняете операции, которые вы выполняете. Для исходного дерева, которое могло бы быть (для структуры слева и справа):

[1, child, 2, child, 3, sibling, 4, sibling, 5, parent, parent, //we're at root again
 sibling, 6, child, 7, child, 8, sibling, 9, parent, parent]

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

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


Если вы не хотите перемещаться по всему дереву:

Один из алгоритмов, который сразу пришел мне в голову, подобен этому. Выберите большое простое число H (большее, чем максимальное количество детей). Чтобы хэш-дерево, хэш его корень, выберите дочерний номер H mod n, где n - количество дочерних элементов root и рекурсивно хэш-поддерево этого дочернего элемента.

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

Если вы хотите хэш меньше элементов, но пройти через все дерево:

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

    --- O  ------- layer 0, n=1
       / \
      /   \
 --- O --- O ----- layer 1, n=2
    /|\    |
   / | \   |
  /  |  \  |
 O - O - O O------ layer 2, n=4
          / \
         /   \
 ------ O --- O -- layer 3, n=2

A node из слоя выбрано с правилом H mod n.

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

Ответ 3

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

Например, вот хеш-функция для кортежей в Python (взятая из Object/tupleobject.c в источнике Python 2.6):

static long
tuplehash(PyTupleObject *v)
{
    register long x, y;
    register Py_ssize_t len = Py_SIZE(v);
    register PyObject **p;
    long mult = 1000003L;
    x = 0x345678L;
    p = v->ob_item;
    while (--len >= 0) {
        y = PyObject_Hash(*p++);
        if (y == -1)
            return -1;
        x = (x ^ y) * mult;
        /* the cast might truncate len; that doesn't change hash stability */
        mult += (long)(82520L + len + len);
    }
    x += 97531L;
    if (x == -1)
        x = -2;
    return x;
}

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

Один из вариантов учета позиции - это позиция node в походном дереве по умолчанию.

Ответ 4

Каждый раз, когда вы работаете с рекурсией деревьев, приходите на ум:

public override int GetHashCode() {
    int hash = 5381;
    foreach(var node in this.BreadthFirstTraversal()) {
        hash = 33 * hash + node.GetHashCode();
    }
}

Хэш-функция должна зависеть от хеш-кода каждого node внутри дерева, а также от его позиции.

Check. Мы явно используем node.GetHashCode() при вычислении хеш-кода дерева. Кроме того, из-за характера алгоритма позиция node играет роль в конечном хэш-коде дерева.

Переупорядочение дочерних элементов node должно отчетливо изменить полученный хэш-код.

Check. Они будут посещаться в другом порядке в обходном пути, приводящем к другому хэш-коду. (Обратите внимание: если есть два ребенка с одним и тем же хэш-кодом, вы получите тот же хэш-код при замене порядка этих детей.)

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

Check. Опять же, узлы будут посещаться в другом порядке, что приведет к другому хэш-коду. (Обратите внимание, что есть ситуации, когда отражение может привести к одному и тому же хеш-коду, если каждый node отражается в node с тем же хэш-кодом.)

Ответ 5

Свойство без конфликтов для этого будет зависеть от того, насколько беспощадна хэш-функция, используемая для данных node.

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

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

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

Чтобы найти что-то похожее на хэш-код Java String:

Скажем, у вас есть n дочерних узлов.

hash(node) = hash(nodedata) +
             hash(childnode[0]) * 31^(n-1) +
             hash(childnode[1]) * 31^(n-2) +
             <...> +
             hash(childnode[n])

Более подробную информацию о приведенной выше схеме можно найти здесь: http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/

Ответ 6

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

Подстрока, которая будет работать, просто использует синтаксис lisp для размещения скобок вокруг дерева, выпишите идентификатор каждого node в предварительном порядке. Но это вычислительно эквивалентно предварительному сопоставлению дерева, поэтому почему бы просто не сделать это?

Я дал два решения: один для сравнения двух деревьев, когда вы закончили (необходимо для разрешения конфликтов), а другой для вычисления хэш-кода.

СРАВНЕНИЕ ДЕРЕВА:

Наиболее эффективным способом сравнения будет просто рекурсивное перемещение каждого дерева в фиксированном порядке (предварительный порядок прост и не хуже других), сравнивая node на каждом шаге.

  • Итак, просто создайте шаблон посетителя, который последовательно возвращает следующий node в предварительном порядке для дерева. т.е. конструктор может взять корень дерева.

  • Затем просто создайте две вставки посетителя, которые действуют как генераторы для следующего node в preorder. т.е. Vistor v1 = новый посетитель (root1), посетитель v2 = новый посетитель (root2)

  • Напишите функцию сравнения, которая может сравниться с другим node.

  • Затем просто посещайте каждый node деревьев, сравнивая и возвращая false, если сравнение не выполняется. то есть.

Модуль

 Function Compare(Node root1, Node root2)
      Visitor v1 = new Visitor(root1)
      Visitor v2 = new Visitor(root2)

      loop
          Node n1 = v1.next
          Node n2 = v2.next
          if (n1 == null) and (n2 == null) then
                return true
          if (n1 == null) or (n2 == null) then
                return false
          if n1.compare(n2) != 0 then
                return false
      end loop
      // unreachable
 End Function

Конечный модуль

ПОКОЛЕНИЕ КОДА ХАРАКТЕРИСТИК:

если вы хотите записать строковое представление дерева, вы можете использовать синтаксис lisp для дерева, а затем образец строки для генерации более короткого хэш-кода.

Модуль

 Function TreeToString(Node n1) : String
        if node == null
            return ""
        String s1 = "(" + n1.toString()
        for each child of n1
            s1 = TreeToString(child)

        return s1 + ")"
 End Function

node.toString() может возвращать уникальный код метки/хэша/что угодно для этого node. Затем вы можете просто выполнить сравнение подстроки со строками, возвращаемыми функцией TreeToString, чтобы определить, эквивалентны ли деревья. Для более короткого хэш-кода просто выберите функцию TreeToString, т.е. Возьмите каждые 5 символов.

Конечный модуль

Ответ 7

Я думаю, вы могли бы сделать это рекурсивно: предположим, что у вас есть хэш-функция h, которая хеширует строки произвольной длины (например, SHA-1). Теперь хэш дерева является хешем строки, созданной как конкатенация хэша текущего элемента (для этого у вас есть собственная функция) и хэшей всех дочерних элементов этого node (из рекурсивных вызовов функции).

Для двоичного дерева вы должны:

Hash( h(node->data) || Hash(node->left) || Hash(node->right) )

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

Ответ 8

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

int hash(Node root) {
  ArrayList<Node> worklist = new ArrayList<Node>();
  worklist.add(root);
  int h = 0;
  int n = 0;
  while (!worklist.isEmpty()) {
    Node x = worklist.remove(worklist.size() - 1);
    worklist.addAll(x.children());
    h ^= place_hash(x.hash(), n);
    n++;
  }
  return h;
}

int place_hash(int hash, int place) {
  return (Integer.toString(hash) + "_" + Integer.toString(place)).hash();
}

Ответ 9

class TreeNode
{
  public static QualityAgainstPerformance = 3; // tune this for your needs
  public static PositionMarkConstan = 23498735; // just anything
  public object TargetObject; // this is a subject of this TreeNode, which has to add it hashcode;

  IEnumerable<TreeNode> GetChildParticipiants()
  {
   yield return this;

   foreach(var child in Children)
   {
    yield return child;

    foreach(var grandchild in child.GetParticipiants() )
     yield return grandchild;
  }
  IEnumerable<TreeNode> GetParentParticipiants()
  {
   TreeNode parent = Parent;
   do
    yield return parent;
   while( ( parent = parent.Parent ) != null );
  }
  public override int GetHashcode()
  {
   int computed = 0;
   var nodesToCombine =
    (Parent != null ? Parent : this).GetChildParticipiants()
     .Take(QualityAgainstPerformance/2)
    .Concat(GetParentParticipiants().Take(QualityAgainstPerformance/2));

   foreach(var node in nodesToCombine)
   {
    if ( node.ReferenceEquals(this) )
      computed = AddToMix(computed, PositionMarkConstant );
    computed = AddToMix(computed, node.GetPositionInParent());
    computed = AddToMix(computed, node.TargetObject.GetHashCode());
   }
   return computed;
  }
}

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

Идея состоит в том, что вам нужно проанализировать некоторую среду node, в зависимости от качества, которое вы хотите достичь.

Ответ 10

Я должен сказать, что ваши требования несколько противоречат всей концепции хэш-кодов.

Сложность вычисления хэш-функции должна быть очень ограниченной.

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

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

Общий принцип, который я бы предложил, заменяет требования MUST с требованиями СЛЕДУЕТ. Таким образом, вы можете найти подходящий и эффективный алгоритм.

Например, рассмотрим создание ограниченной последовательности целых токенов хэш-кодов и добавим то, что вы хотите к этой последовательности, в порядке предпочтения.

Порядок элементов в этой последовательности важен, он влияет на вычисленное значение.

например, для каждого node, который вы хотите вычислить:

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

    //--------5------- ancestor depth 2 and it left sibling;
    //-------/|------- ;
    //------4-3------- ancestor depth 1 and it left sibling;    
    //-------/|------- ;
    //------2-1------- this;
    

    тот факт, что вы добавляете хеш-код прямого исходного объекта, связанного с сайтом, дает свойство позиционирования хэш-функции.

    Если этого недостаточно, добавьте детей: Вы должны добавить каждого ребенка, только некоторые, чтобы дать достойный хэш-код.

  • добавьте первый дочерний элемент и первый ребенок и первый ребенок.. ограничьте глубину некоторой константой и не вычисляйте ничего рекурсивно - только базовый хэш-код node.

    //----- this;
    //-----/--;
    //----6---;
    //---/--;
    //--7---;
    

Таким образом, сложность линейна по отношению к глубине базового дерева, а не к общему количеству элементов.

Теперь у вас есть последовательность, если целые числа, объединить их с известным алгоритмом, как предлагает Эли выше.

1,2,... 7

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

Готов поспорить, что эти 7 чисел будут давать хеш-жертву рядом с совершенством.

Ответ 11

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

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