Можно решить её наивным способом. Обходим большее дерево (назовем его T) и для каждого его узла пытаемся сопоставить поддерево с меньшим деревом (назовем его t). Выглядит это примерно следующим образом:
public static <E> boolean isSubtree(final Node<E> tree, final Node<E> subtree) { return isEquals(tree, subtree) || (tree.left != null && isSubtree(tree.left, subtree)) || (tree.right != null && isSubtree(tree.right, subtree)); } private static <E> boolean isEquals(final Node<E> tree, final Node<E> subtree) { if (tree == null && subtree == null) return true; if (tree == null || subtree == null) return false; return tree.value == subtree.value && isEquals(tree.left, subtree.left) && isEquals(tree.right, subtree.right); }
В секции ответов книжки дано именно такое "наивное" решение. В худшем случае каждый шаг сопоставления будет выполняться O(|t|) (за |t| обозначим количество вершин в дереве). И таких шагов будет |T|. В итоге, время работы будет O(|t| * |T|).
Есть ощущение, что можно лучше. Эта задача напомнила мне задачу поиска подстроки в строке. Там наивный алгоритм выглядел примерно также. Но в нашей задаче структура объектов посложнее. При задании дерева в виде связанных ссылками узлов, мы не можем делать по нему произвольные скачки за O(1). Но один из подходов применить можно. Это алгоритм Рабина-Карпа. Вкратце напомню, как он работает. Нам даны исходная строка s, и подстрока p. Считаем хэш функцию образца p (за O(|p|)). Затем для строки s строим просчитываем значениямя хэш-функции всех подстрок строки s длины |p|. Считая хэш-функцию инкрементально, выполняем этот шаг за O(|s|). На каждом шаге сравниваем значение с подсчитанным ранее значением хэш-функции p. При равенстве, есть очень большая вероятность, что мы нашли подстроку. Таким образом при "хорошей" хэш-функции алгоритм работает за O(|s| + |p|).
Также можно поступить и в нашей задаче. Для начала посчитаем значение хеш-функции для каждого поддерева исходного дерева. Получаем отображение из узлов дерева в значение хеш-функции поддерева с корнем в этом узле.
Итак:
1. Рассчитываем значение хеш-функции для каждого поддерева дерева T.
2. Затем рассчитываем хеш-функцию для второго дерева t. В данном случае нам нужно значение только для корня.
3. И наконец обходим дерево T и проверяем равны ли значения хеш-функции для поддерева T и дерева t.
public static <E> boolean isSubtree(final Node<E> tree, final Node<E> subtree) { final Map<Node<E>, Long> nodeToHash = new HashMap<>(); calculateNodesHashes(tree, nodeToHash); final long subtreeHash = calculateNodesHashes(subtree, new HashMap<>()); return isSubtree(tree, subtree, subtreeHash, nodeToHash); } private static <E> boolean isSubtree(final Node<E> tree, final Node<E> subtree, final long subtreeHash, final Map<Node<E>, Long> nodeToHash) { if (tree == null) return false; if (nodeToHash.get(tree) == subtreeHash) { if (checkSubtree(tree, subtree)) { return true; } } return isSubtree(tree.left, subtree, subtreeHash, nodeToHash) || isSubtree(tree.right, subtree, subtreeHash, nodeToHash); } private static <E> long calculateNodesHashes(final Node<E> n, final Map<Node<E>, Long> nodeToHash) { final long hash = calculateHash(n.value, n.left==null ? 0 : calculateNodesHashes(n.left, nodeToHash), n.right==null ? 0 : calculateNodesHashes(n.right, nodeToHash) ); nodeToHash.put(n, hash); return hash; } private static long calculateHash(final Object value, final long leftNodeHash, final long rightNodeHash) { return value.hashCode() + 17*(leftNodeHash + 17 * rightNodeHash); }
Оценим производительность нашего алгоритма. Рассчитывая значение хэш-функции на основе значений хэш-функций правого и левого поддеревьев получаем асимптотику расчета хеш-функций за размер дерева (O(|t|)). Расчет значений хеш-функции для каждого поддерева дерева T (1) происходит за O(|T|). Расчет значения хеш-функцию для второго дерева t (2) за O(|t|). Ну и на обход дерева (3) занимает O(|T|) (в принципе, если сначала рассчитать (2), то шаги (1) и (3) можно совместить). Итого: O(|T|) + O(|t|) + O(|T|) -> O(|T| + |t|). Расчет был сделан при отсутствии коллизий. Конечно, если хэш-функция будет плохой, то из-за коллизий, производительность алгоритма деградирует до O(|T| * |t|). Но на практике вероятность большого количества коллизий очень низка.
Комментариев нет:
Отправить комментарий