Печать преемника и предшественника в BST

Моя проблема заключается в следующем:

У меня есть бинарное дерево поиска с ключами: a1<a2<...<an, проблема состоит в том, чтобы напечатать все пары (a_i, a_i+1) в дереве (где i={1,2,3,...}) с использованием рекурсивного алгоритма в O(n) времени без какой-либо глобальной переменной и с использованием O(1) дополнительного пространства. Пример: Пусть ключи будут: 1,2, ..., 5 Пары, которые будут напечатаны: (1,2) (2,3) (3, 4) (4, 5)

Таким образом, вы не можете выполнить неупорядоченный обход дерева и найти преемника/предшественника для каждого узла. Потому что это займет O(nh) времени, и если дерево сбалансировано, оно будет O(nlgn) для всего дерева.


person systemsfault    schedule 06.01.2013    source источник
comment
Обход по порядку не O (nlgn)...   -  person Oliver Charlesworth    schedule 06.01.2013
comment
да, неупорядоченный обход - это O (n), но в среднем функция преемника - это O (h), а в худшем случае - O (n), поэтому, если вы вызываете преемника или предшественника для каждого узла, в среднем это будет O (nlgn), но в в худшем случае она квадратична.   -  person systemsfault    schedule 06.01.2013
comment
Это не правда. При полном обходе каждый узел посещается максимум 3 раза (один раз при спуске и дважды при возвращении).   -  person Oliver Charlesworth    schedule 06.01.2013
comment
На самом деле общее количество посещений узлов равно 2n; каждый узел входит один раз (сверху) и выходит один раз (сверху).   -  person Oliver Charlesworth    schedule 06.01.2013
comment
так что в большой нотации это будет O (2n) = O (n), или я что-то упустил?   -  person systemsfault    schedule 06.01.2013
comment
Нет, вы ничего не потеряли ;)   -  person Oliver Charlesworth    schedule 06.01.2013
comment
Мой ответ сработал для вас?   -  person staafl    schedule 08.01.2013
comment
Привет, персонал, извините, у меня были другие дела, и я не мог проверить эту ветку. Что касается вашего вопроса, я не смог заставить ваш алгоритм работать.   -  person systemsfault    schedule 08.01.2013


Ответы (2)


Хотя вы правы в том, что поиск последовательного преемника или предшественника может занять O(h) времени, оказывается, что если вы начинаете с наименьшего значения в BST и неоднократно находите его преемника, общий объем проделанной работы в конечном итоге составит до O(n) независимо от формы дерева.

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

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

В псевдокоде логика выглядела бы так:

  1. Найдите наименьшее значение в дереве, начиная с корня и многократно следуя за левым дочерним указателем, пока такой указатель не исчезнет.
  2. Until all nodes are visited, remember the current node and find its successor as follows:
    1. If the current node has a right child:
      1. Go right.
      2. Идите налево, пока не останется детей.
      3. Выведите узел, с которого вы начали, затем этот узел. 2: В противном случае:
      4. Подойдите к родительскому узлу, пока не обнаружите, что узел, с которого вы начали, был левым дочерним элементом своего родителя.
      5. Если вы попали в корень и так и не обнаружили, что перешли от левого дочернего элемента вверх, все готово.
      6. В противном случае выведите узел, который вы помните, а затем текущий узел.

Надеюсь это поможет!

person templatetypedef    schedule 06.01.2013
comment
Осмелюсь предположить, что это та же логика, что и в моем ответе :-) - person staafl; 08.01.2013
comment
привет @templatetypedef Я поверил, что этот алгоритм O(n), когда увидел его доказательство :) (personal.kent.edu/~rmuhamma/Algorithms/MyAlgorithms/). Да, этот алгоритм работает, но его рекурсивная реализация довольно сложна. У меня есть интуиция, что у него менее сложная рекурсивная реализация с обычным неупорядоченным обходом. - person systemsfault; 08.01.2013

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

Простым решением было бы пройтись по дереву, используя обход по порядку, отслеживая последний раз, когда вы взяли ребро, указывающее вправо (например, используя переменную с именем last_right_ancestor_seen, чтобы указать на его родителя). узел) и последний просмотренный вами листовой узел (скажем, в last_leaf_seen (фактически любой узел без правого дочернего элемента). Каждый раз, когда вы обрабатываете листовой узел, его предшественником является last_right_ancestor< /strong>, и каждый раз, когда вы сталкиваетесь с неконечным узлом, его предшественником является last_leaf_seen, и вы просто печатаете два O(n) времени, O(1) пространства.

Надеюсь, это достаточно ясно, я могу нарисовать вам схему, если нет.

Изменить: это не проверено, но, вероятно, правильно:

walk(node* current, node* last_right_ancestor_seen, node* last_leaf_seen) {

    if(current->left != null) {
        walk(current->left, last_right_ancestor_seen, last_leaf_seen);
    }

    if(current->is_leaf()) {
            if(last_right_ancestor_seen != null)
                print(last_right_ancestor_seen->value, current->value);
    }
    else {
        print(last_leaf_seen->value, current->value);
    }

    if(current->right != null) {
        *last_right_ancestor_seen = *current;
        walk(current->right, last_right_ancestor_seen, last_leaf_seen);
    }
    else {
        *last_leaf_seen = *current;
    }

}
person staafl    schedule 06.01.2013
comment
обратите внимание, что рекурсивный алгоритм неявно использует память для стека вызовов, в данном случае O(h), но постановка задачи, вероятно, игнорирует это. - person staafl; 06.01.2013
comment
но как сохранить эти переменные без использования глобальных переменных для last_right_ancesstor_seen и last_leaf_seen? Я могу неправильно интерпретировать. а можно псевдокод? Стек-пространство не имеет значения. - person systemsfault; 06.01.2013
comment
Вы можете передавать их в качестве параметров каждому рекурсивному вызову :-) - person staafl; 06.01.2013
comment
извините, глупый вопрос :). Попробую ваш алгоритм, спасибо. - person systemsfault; 06.01.2013
comment
похоже, что это не работает в строке с if(current->is_leaf()) { print(last_right_ancestor_seen->value, current->value);}. Я получил значение last_right_ancestor_seen-›null, что имеет смысл, потому что вы устанавливаете last_right_ancestor_seen только при повороте направо, но если дерево наклонено влево, у него может никогда не быть правого предка. - person systemsfault; 06.01.2013
comment
... и у листа нет предшественника. Хороший улов, обновил псевдокод. - person staafl; 06.01.2013
comment
почему вы пытаетесь найти предшественника каждого узла вместо преемника. Поиск преемника каждого узла и печать с предшественником кажутся мне более интуитивными. - person systemsfault; 08.01.2013
comment
Я не пытаюсь найти предшественника каждого узла по отдельности, а просто просматриваю дерево и распечатываю каждый раз, когда устанавливается пара предшественник/преемник, используя свойства BST. Это не совсем интуитивно понятно, но умные алгоритмы редко бывают :-) - person staafl; 09.01.2013