Как с учетом заранее определенного набора ключей переупорядочить ключи так, чтобы при вставке в B-дерево использовалось минимальное количество узлов?

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

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

Итак, вопрос в том, как переупорядочить ключи так, чтобы после создания BTree использовалось наименьшее количество узлов. Вот пример:

Пример

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

Я также наткнулся на 2 исследовательские работы, которые так близки к решению моего вопроса, но не совсем там! Оптимизация по времени и пространству в B-деревьях и Оптимальные 2,3-деревья (где я фактически взял изображение выше) обсуждают и количественно определяют различия между оптимальным пространством и пробел пессимальные BTrees, но не заходите так далеко, чтобы описывать, как разработать порядок вставки, насколько я могу видеть.

Любая помощь по этому поводу будет принята с благодарностью.

Спасибо

Научные статьи можно найти по адресу:

http://www.uqac.ca/rebaine/8INF805/Automne2007/Sujets2007Automne/p174-rosenberg.pdf

http://scholarship.claremont.edu/cgi/viewcontent.cgi?article=1143&context=hmc_fac_pub

РЕДАКТИРОВАТЬ :: Я закончил заполнение скелета b-дерева, построенного, как описано в вышеупомянутых документах, с помощью алгоритма FILLORDER. Как упоминалось ранее, я надеялся избежать этого, но в итоге реализовал его до того, как были опубликованы 2 отличных ответа!


person Squimmy    schedule 28.07.2014    source источник
comment
Вы можете довольно легко создать пакетное дерево b-дерева, отсортировав элементы и построив дерево с этим отсортированным списком. Их даже не нужно вставлять по одному.   -  person usr    schedule 28.07.2014
comment
Боюсь, я не совсем понимаю. Вставка элементов в отсортированном порядке определенно не приводит к оптимальному пространству BTree, насколько я могу видеть?   -  person Squimmy    schedule 28.07.2014
comment
Алгоритм не работает при вставке элементов. Вы сортируете список, а затем строите листовые узлы из этого списка. Теперь у вас есть идеальные листовые узлы. Затем вы строите дополнительные слои поверх этих листовых узлов .; Что делают РСУБД, так это то, что у них есть специальное правило для вставок в конце. Они не разделяют страницы равномерно, а создают новую пустую страницу. Не воспринимайте алгоритмы b-дерева (из Википедии) слишком буквально. С ними можно играть.   -  person usr    schedule 28.07.2014
comment
О, я вижу. Я очень неохотно строю дерево из листьев, хотя тогда появилась возможность появления какой-то специфической ошибки один из миллиона, которую мы не предвидели, и в конечном итоге создавалось некое недопустимое неисследуемое B-дерево, которое быть супер, супер проблематично. На самом деле я не беспокоюсь о вставках послесловий. После того, как он построен, нам действительно не нужно делать никаких вставок на нем.   -  person Squimmy    schedule 28.07.2014
comment
А как насчет использования библиотеки b-tree? Вставляйте в последовательном порядке. Я бы сказал, что хорошая библиотека оптимизирует для этих случаев, потому что это очень распространено.   -  person usr    schedule 28.07.2014
comment
Если оптимальное пространство является вашим основным драйвером, уверены ли вы, что b-дерево является наиболее подходящей структурой данных? Простой отсортированный массив (вектор) обеспечивает лучшую локализацию кеша и сохраняет возможность поиска со сложностью log (n).   -  person Chad    schedule 30.07.2014
comment
Это как-то отличается от стандартного самобалансирующегося двоичного дерева, такого как красно-черное дерево? Вы просто ищете гарантии, что высота равна log2 (n), верно?   -  person Moby Disk    schedule 30.07.2014
comment
красно-черные деревья не имеют гарантии log2 (n). Гарантия составляет всего O (log2 (n)) и фактически может достигать 2log2 (n). И я думаю, что отчасти в этом и заключается суть вопроса. Можете ли вы вставить в определенном порядке, который дает лучшие характеристики высоты, чем гарантирует алгоритм сам по себе. Смотрите мой следующий комментарий (слишком длинный)!   -  person Jeremy West    schedule 31.07.2014
comment
На этот вопрос будет сложно ответить, не зная точно, как работает операция вставки. Я предполагаю, что у вас есть какая-то реализация черного ящика, которая позволяет вам вставлять элемент, а затем как-то помещать его в дерево? Тогда ответ будет зависеть от того, когда алгоритм решает разделить блоки и как он разбивает их, когда это происходит.   -  person Jeremy West    schedule 31.07.2014


Ответы (5)


Приведенный ниже алгоритм должен работать для B-деревьев с минимальным количеством ключей в node = d и максимальным = 2 * d. Я полагаю, что его можно обобщить для 2 * d + 1 максимальных ключей, если известен способ выбора медианы.

Алгоритм, приведенный ниже, предназначен для минимизации количества узлов, а не только высоты дерева.

Метод основан на идее помещения ключей в любой неполный лист или, если все листья заполнены, чтобы поместить ключ под нижний неполный узел.

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

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

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

Вот почти рабочий (псевдо) код, массив нужно отсортировать:

PushArray(BTree bTree, int d, key[] Array)
{
    List<int> counters = new List<int>{0};
    //skip list will contain numbers of nodes to skip 
    //after filling node of some order in half
    List<int> skip  = new List<int>();
    List<Pair<int,int>> skipList = List<Pair<int,int>>();

    int i = -1;
    while(true)
    {     
       int order = 0;
       while(counters[order] == d) order += 1;
       for(int j = order - 1; j >= 0; j--) counters[j] = 0;

       if (counters.Lenght <= order + 1) counters.Add(0);
       counters[order] += 1;

       if (skip.Count <= order)
              skip.Add(i + 2);    
       if (order > 0)
           skipList.Add({i,order}); //list of skipped parts that will be needed later
       i += skip[order];

       if (i > N) break;

       bTree.Push(Array[i]);
    }

    //now we need to add all skipped keys in correct order
    foreach(Pair<int,int> p in skipList)
    {
        for(int i = p.2; i > 0; i--)
            PushArray(bTree, d, Array.SubArray(p.1 + skip[i - 1], skip[i] -1))
    }
}

Пример:

Вот как должны быть расположены числа и соответствующие ключи счетчиков для d = 2 при первом проходе через массив. Я пометил ключи, которые были вставлены в B-дерево во время первого прохода (до цикла с рекурсией), с помощью «o» и пропустил с помощью «x».

                                                              24
        4         9             14             19                            29 
0 1 2 3   5 6 7 8   10 11 12 13    15 16 17 18    20 21 22 23    25 26 27 28    30 ...
o o x x o o o x x o  o  o  x  x  x  x  x  x  x  x  x  x  x  x  o  o  o  x  x  o  o ...
1 2     0 1 2     0  1  2                                      0  1  2        0  1 ...
0 0     1 1 1     2  2  2                                      0  0  0        1  1 ...
0 0     0 0 0     0  0  0                                      1  1  1        1  1 ...
skip[0] = 1 
skip[1] = 3 
skip[2] = 13

Поскольку мы не перебираем пропущенные ключи, у нас есть временная сложность O (n) без добавления самого B-Tree и для отсортированного массива;

В этой форме может быть неясно, как это работает, когда недостаточно ключей для заполнения второй половины узла после пропущенного блока, но мы также можем избежать пропуска всех ключей skip [order], если общая длина массива меньше ~ i + 2 * skip [order] и skip вместо клавиш skip [order - 1], такая строка может быть добавлена ​​после изменения счетчиков, но до изменения переменной i:

while(order > 0 && i + 2*skip[order] > N) --order;

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

Возможно, есть смысл найти какую-нибудь анимацию и посмотреть, как она работает, вот последовательность, которая должна быть сгенерирована в диапазоне 0..29: 0 1 4 5 6 9 10 11 24 25 26 29 / конец первого прохода < / em> / 2 3 7 8 14 15 16 19 20 21 12 13 17 18 22 23 27 28 введите описание изображения здесь

person Lurr    schedule 03.08.2014

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

Уловка состоит в том, чтобы запускать разделение узлов контролируемым образом. Сначала вы точно заполняете узел, левую половину ключами, которые принадлежат друг другу, а правую половину - другим диапазоном ключей, которые принадлежат друг другу. Наконец, вы вставляете ключ, который находится между этими двумя диапазонами, но не принадлежит ни одному из них; два поддиапазона разделяются на отдельные узлы, и последний вставленный ключ попадает в родительский узел. После разделения таким образом вы можете заполнить оставшуюся часть обоих дочерних узлов, чтобы дерево было как можно более компактным. Это также работает для родительских узлов с более чем двумя дочерними узлами, просто повторяйте трюк с одним из дочерних узлов, пока не будет создано желаемое количество дочерних узлов. Ниже я использую то, что концептуально является крайним правым дочерним узлом, как «раскол» (шаги 5 и 6.1).

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

Подразумеваемое дерево T с максимальным фактором ветвления M.

Верхняя процедура с ключами длиной N:

  1. Отсортируйте ключи.
  2. Задайте для минимальной высоты-дерева значение ceil (log (N +1) / log (M)).
  3. Вызов insert-chunk с chunk = ключами и H = minimal-tree-height .

Процедура insert-chunk с chunk длиной L, высотой поддерева H:

  1. If H is equal to 1:
    1. Insert all keys from the chunk into T
    2. Немедленно возвращайся.
  2. Установите идеальный размер подчанка S на pow (M, H - 1).
  3. Установите количество поддеревьев T на ceil ((L + 1) / S).
  4. Установите фактический размер субчанка S ' на ceil ((L + 1) / T).
  5. Рекурсивно вызывать insert-chunk с ключами chunk ' = последний этаж ((S - 1) / 2) chunk и H ' = H - 1.
  6. For each of the ceil(L / S') subchunks (of size S') except for the last with index I:
    1. Recursively call insert-chunk with chunk' = the first ceil((S - 1) / 2) keys of subchunk I and H' = H - 1.
    2. Вставьте последний ключ части I в T (эта вставка целенаправленно запускает разделение).
    3. Рекурсивно вызвать insert-chunk с chunk ' = оставшимися ключами подчанка I (если есть) и H' = H - 1.
  7. Рекурсивно вызвать insert-chunk с chunk ' = оставшимися ключами последнего подчанка и H' = H - 1.

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

person Julian    schedule 31.07.2014

Вот способ, который приведет к минимальной высоте в любом BST (включая дерево b): -

  1. массив сортировки
  2. Скажем, у вас может быть клавиша m в дереве b
  3. Разделите массив рекурсивно на m + 1 равных частей, используя m ключей в родительском элементе.
  4. построить дочернее дерево из n / (m + 1) отсортированных ключей с помощью рекурсии.

пример : -

m = 2 array = [1 2 3 4 5 6 7 8 9 10]

divide array into three parts :-

root = [4,8]

recursively solve :-

child1 = [1 2 3]

root1 = [2]

left1 = [1]

right1 = [3]

similarly for all childs solve recursively.
person Vikram Bhat    schedule 31.07.2014

Так идет ли речь об оптимизации процедуры создания или оптимизации дерева?

Вы можете явно создать максимально эффективное B-дерево, сначала создав полное сбалансированное двоичное дерево, а затем сжимая узлы.

На любом уровне двоичного дерева промежуток в числах между двумя узлами содержит все числа между этими двумя значениями по определению двоичного дерева, и это более или менее определение B-дерева. Вы просто начинаете сжимать подразделения двоичного дерева в узлы B-Tree. Поскольку двоичное дерево сбалансировано по конструкции, промежутки между узлами на одном уровне всегда содержат одинаковое количество узлов (при условии, что дерево заполнено). Таким образом, построенное таким образом BTree гарантированно сбалансировано.

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

=====================================

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

person phil_20686    schedule 06.08.2014
comment
Понятия не имею, возможно ли дерево 3-3? Кто-нибудь знает? - person phil_20686; 06.08.2014
comment
Вы не можете поддерживать ни 3-3, ни 3-4, ни 3-5, используя классический алгоритм B-tree, только 3-6 или 3-7. Я не уверен, что дерево 3-3 вообще невозможно изобрести, но очевидно, что вы не можете хранить n ключей в дереве, если каждый узел содержит 3 ключа, а n не делится на 3. Так что вам понадобится дополнительный буфер. сохранить хотя бы n% 3 ключей. - person Lurr; 06.08.2014
comment
Это не жесткое ограничение, поскольку у вас может быть некоторый нулевой эквивалент листьев node. например добавьте в список некоторое long.minvalue, поскольку они всегда являются самыми низкими узлами в дереве, вы можете легко отслеживать их и удалять их, если вам нужно добавить узлы. 3-6 и 3-7 более удобны, поскольку вы избегаете этого осложнения. Проблема с 3-3 состоит в том, что перебалансировка должна происходить всякий раз, когда узел удаляется. - person phil_20686; 06.08.2014

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

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

Как только вы захотите принять хоть какое-то использование памяти, вы можете переосмыслить свое утверждение «При поиске в BTree оно выгружается по запросу с диска в память», что дает возможность балансировать между оптимизацией чтения и записи. Максимально оптимизированный BTREE - это максимальная оптимизация записи. Я знаю, что в большинстве сценариев доступа к данным вы получаете запись при любых 10–100 чтениях. Это означает, что максимальная оптимизация записи может дать низкую производительность с точки зрения оптимизации доступа к данным. Вот почему базы данных допускают циклы реструктуризации, трату ключевого пространства, несбалансированные btree и тому подобное ...

person Quicker    schedule 05.08.2014