Какой самый эффективный способ заселить красное черное дерево? - PullRequest
0 голосов
/ 21 мая 2018

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

Или в контексте популярного std::set/mapреализации (на основе «красного черного дерева») - какой самый эффективный способ заполнить мой std::set вышеупомянутым набором данных?

Прежде чем ответить, рассмотрите следующее:

  • afaik, красное черное дерево имеет дешевую вставку O (1) (правильно подсказанную) ... если глубина дерева не нарушает определенный предел, в этом случае оно будет перебалансировано (со стоимостью O (log N)) - точно так же, какв случае std::vector::push_back() мы в конечном итоге получим амортизированную постоянную сложность

  • например, если набор данных представляет собой список значений [0,999], должна существовать последовательность подсказок, которые никогда не вызывают перебалансировку (т. е.сохраняет каждую вставку O (1)).

Очень тривиальный пример (нужно выяснить, как выбрать эти значения YYY / ZZZ):

std::set<int> s;
std::vector< std::set<int>::iterator > helper(1000);

helper[0] = s.insert(0);
helper[1] = s.insert(helper[0], 1);
//...
helper[500] = s.insert(helper[YYY], 500);
//...
helper[999] = s.insert(helper[ZZZ], 999);

Что яищу:

  1. a aАлгоритм, который позволил бы мне заполнить (на основе «красного черного дерева») std::set (специально) подготовленной (произвольно длинной) последовательностью, где каждая вставка гарантированно O (1)

  2. должен быть способ уменьшить дополнительные требования к памяти (т.е. размер helper) или в идеале исключить необходимость в этом

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

  4. и целью бонуса являетсяполучить ответы на вопросы 1-3 для "дерева AVL" std::set

Спасибо

Ответы [ 2 ]

0 голосов
/ 25 августа 2018

Найден алгоритм, который не требует дополнительной памяти и работает для любого бинарного дерева поиска (красно-черный / AVL / и т. Д.):

  1. массив входящих данных для представления «уплощенного»"двоичное дерево (root в [0], корневые дочерние элементы в [1] и [2], дочерние узлы в левом узле в [3] и [4], дочерние узлы в правом узле в [5] и [6] и т. д.).Хитрость заключается в том, чтобы выбрать корни каждого поддерева таким образом, чтобы в полученном двоичном дереве каждый lvl (но последний) был заполнен, а на последнем уровне все узлы образовывали «непрерывную» линию.Например:

         N11
       /     \
     N21      N22
     / \      /
    N31 N32 N33
    

См. Код ниже о том, как преобразовать отсортированный массив в такую ​​последовательность.Я полагаю, что для любой последовательности есть только один возможный способ упорядочить ее в бинарном дереве поиска, например, таким образом, - здесь вы получите какую-то гарантию «стабильности» (для данной длины последовательности мы точно знаем, где будет находиться каждый элемент).в дереве).

затем вы выполняете один проход над вашими данными и заполняете дерево по уровням.На каждом уровне мы точно знаем, сколько элементов нужно потянуть (2 ^ (lvl-1)), прежде чем перейти к следующему lvl (или исчерпать данные).В начале каждой итерации мы сбрасываем нашу позицию на крайний левый элемент (std::set<T>::begin()) и после вставки левого и правого потомков мы переходим на следующий лист на текущем уровне (удваивается ++it от результата последнего вызова insert()).

Примечания:

  • с std::set<int> выигрышем в производительности (по сравнению с подсказкой вставки отсортированной последовательности 5-10%)

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

  • . Преимущество этого подхода было бы значительно выше, если бы он был реализован внутри (без использования открытого интерфейса std::set).как функция, которая ожидает, что данные будут соответствовать требованиям и объявляет «неопределенное поведение», если это не так ...

  • ... в этом случае еще лучший алгоритм будет заполнять глубину дерева.сначала и потребует, чтобы входные данные были переставлены по-другому ([N11, N21, N31, N32, N22, N33] в примере выше).В конечном итоге мы также проведем только один обход дерева ... Увы, мы не можем реализовать этот подход с использованием std::set открытого интерфейса, хотя - он будет приводить в действие инвариант красно-черного дерева на каждом этапе построения, вызывая ненужную перебалансировку

Код: (MSVC 2015, простите за качество картофеля - он был написан на колене в течение часа)

#include <set>
#include <cassert>
#include <vector>
#include <utility>
#include <chrono>
#include <cstdio>


using namespace std;


unsigned hibit(size_t n)
{
    unsigned long l;
    auto r = _BitScanReverse(&l, n);
    assert(r);
    return l;
}


int const* pick_root(int const* begin, int const* end)
{
    assert(begin != end);
    size_t count = end - begin;

    unsigned tree_order = hibit(count);         // tree height minus 1
    size_t max_tail_sz = 1 << tree_order;       // max number of nodes on last tree lvl
    size_t filled_sz = max_tail_sz - 1;         // number of nodes on all but last tree lvl
    size_t tail_sz = count - filled_sz;         // number of nodes on last lvl

    return (tail_sz >= max_tail_sz/2) ?         // left half of tree will be completely filled?
        begin + max_tail_sz - 1                 // pick (2^tree_order)'s element from left
        :
        end - max_tail_sz/2;                    // pick (2^(tree_order - 1))'s element from right
}


vector<int> repack(vector<int> const& v)
{
    vector<int> r; r.reserve(v.size());
    if (!v.empty())
    {
        unsigned tree_order = hibit(v.size());  // tree height minus 1

        vector<pair<int const*, int const*>> ranges(1, make_pair(&v[0], &v[0] + v.size()));
        for(size_t i = 0; i <= tree_order; ++i)
        {
            vector<pair<int const*, int const*>> ranges2; ranges2.reserve(ranges.size()*2);

            for(auto const& range: ranges)
            {
                auto root = pick_root(range.first, range.second);
                r.push_back(*root);

                if (root != range.first)
                {
                    ranges2.push_back(make_pair(range.first, root));

                    if (root + 1 != range.second)
                        ranges2.push_back(make_pair(root + 1, range.second));
                }
            }

            ranges.swap(ranges2);
        }
        assert(ranges.empty());
    }
    return r;
}


set<int> populate_simple(std::vector<int> const& vec)
{
    set<int> r;
    for(auto v: vec) r.insert(v);
    return r;
}


set<int> populate_hinted(std::vector<int> const& vec)
{
    set<int> r;
    for(auto v: vec) r.insert(r.end(), v);
    return r;
}


set<int> populate_optimized(std::vector<int> const& vec)
{
    set<int> r;
    if (vec.empty()) return r;

    int const* p = &vec[0];
    int const* pend = &vec[0] + vec.size();

    r.insert(*p++);                   // take care of root
    if (p == pend) return r;

    for(size_t count = 1; ; count *= 2) // max number of pairs on each tree lvl
    {
        auto pos = r.begin();

        for(size_t i = 1; ; ++i)
        {
            r.insert(pos, *p++);
            if (p == pend) return r;

            //++pos;            // MS implementation supports insertion after hint

            pos = r.insert(pos, *p++);
            if (p == pend) return r;
                            // pos points to rightmost leaf of left subtree of "local" tree
            ++pos;          // pos points to root of "local" tree (or end())

            if (i == count) break;

            ++pos;      // pos points to leftmost leaf of right subtree of "local" tree
        }
    }
}


struct stopwatch
{
    chrono::high_resolution_clock::time_point start_;

    stopwatch() : start_(std::chrono::high_resolution_clock::now()) {}

    auto click()
    {
        auto finish = std::chrono::high_resolution_clock::now();
        auto mks = std::chrono::duration_cast<std::chrono::microseconds>(finish - start_);
        return mks.count();
    }
};


int main()
{
    size_t N = 100000;
    vector<int> v(N, 0);
    for(unsigned i = 0; i < N; ++i) v[i] = i;   // sorted array

    auto rv = repack(v);

    {
        stopwatch w;
        auto s = populate_simple(v);
        printf("simple   : %I64d mks\n", w.click());
    }

    {
        stopwatch w;
        auto s = populate_hinted(v);
        printf("hinted   : %I64d mks\n", w.click());
    }

    {
        stopwatch w;
        auto s = populate_optimized(rv);
        printf("optimized: %I64d mks\n", w.click());
    }

    return 0;
}

Типичные результаты:

simple   : 14904 mks
hinted   : 7885 mks
optimized: 6809 mks

simple   : 15288 mks
hinted   : 7415 mks
optimized: 6947 mks

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

0 голосов
/ 21 мая 2018

Сначала отсортируйте входные данные.

В идеале было бы лучше поместить отсортированный вход в сбалансированное двоичное дерево, но можно просто представить, что он в дереве;это займет немного больше бухгалтерии.На самом деле это не обязательно должна быть настоящая древовидная структура данных;Вы можете использовать массив, где корнем является элемент 0, а дочерние элементы элемента i имеют 2i + 1 и 2i + 2.В любом случае, дерево может быть построено рекурсивно.

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

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

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

Iдумаю, что те же алгоритмы будут работать с деревьями AVL.

...