B-деревья

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

Древовидные структуры и деревья всё еще удобны и для внешнего поиска. Надо только выбрать способ правильно представить дерева и максимально эффективно использовать оперативную память.

Давайте рассмотрим довольно простую идею, которая открывает нам практически безграничные просторы внешнего поиска. Место нашего поиска буду дальше называть диском. Вот пусть для поиска в диске мы используем бинарное дерево. Записей у нас 10000001\,000\,000. Если наше дерево идеально сбалансировано, то при поиске нам понадобится сделать log2100000020\log_2 1\,000\,000 \approx 20 обращений к диску.

Разделим наши данные в древесной структуре на страницы фиксированного размера, как на рисунке. Будем из диска запрашивать не по одному значению, а сразу по страницам. Тогда наш поиск ускорится в log2(высота страницы)\log_2 (\text{высота страницы}) раз!

...

B-деревья

B-дерево порядка mm — сильноветвящиеся дерево, у которого

  • у каждого узла максимум mm детей
  • у каждого узла, кроме корня и листов, минимум m/2\lceil m/2 \rceil детей
  • корневой узел, если не является листом, имеет минимум 22 ребенка
  • все листья находятся на одном уровне
  • любой узел с kk детьми содержит k1k-1 ключ

B-деревьев порядка 11 не бывает, а B-деревья порядка 22 — это простые бинарные деревья. Поэтому будем рассматривать только B-деревья порядка 33 и больше, m3m \ge 3.

Каждый узел B-дерева можно представить как массив из 2m12m-1 элементов. На нечётных позициях стоят ссылки на детей. На чётных позициях находятся ключи.

(P1K1P2K2P3K3Pm1Km1Pm)\Big( P_1 \quad K_1 \quad P_2 \quad K_2 \quad P_3 \quad K_3 \quad \cdots \quad P_{m-1} \quad K_{m-1} \quad P_m \Big)

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

В узлах ключи упорядочены: K1<K2<<Km1K_1 < K_2 < \dotsb < K_{m-1}, а каждая ссылка PiP_i указывает на узел, в котором все ключи находятся между ключами Ki1K_{i-1} и KiK_i родительского узла.

Высота B-дерева

Ключевой характеристикой для B-дерева является высота. От нее зависит и временная сложность операций, и количество обращений к диску. Пусть в B-дереве хранится nn ключей. Оценим высоту дерева — hh.

В корне минимум 11 ключ и минимум 22 ребёнка. В остальных узлах минимум m/21\lceil m/2 \rceil - 1 ключей и минимум m/2\lceil m/2 \rceil детей. Получаем формулу для подсчёта минимального количества ключей в B-дереве высоты hh:

nmin(h)=1+2nmin(h1)    nmin(h)=2m/2h1n_{\mathrm{min}}(h) = 1 + 2 \cdot n_{\mathrm{min}}(h-1) \implies n_{\mathrm{min}}(h) = 2 \cdot \lceil m/2 \rceil^h - 1

Аналогично, для максимального количества ключей

nmax(h)=(m1)+mnmax(h)    nmax(h)=mh+11n_{\mathrm{max}}(h) = (m-1) + m \cdot n_{\mathrm{max}}(h) \implies n_{\mathrm{max}}(h) = m^{h+1} - 1

Тогда из общего неравенства

2m/2h1nmh+112 \cdot \lceil m/2 \rceil^h - 1 \le n \le m^{h+1} - 1

можно получить неравенство для высоты дерева

logmn1hlogm/2n+12\lfloor \log_m n \rfloor - 1 \le h \le \left\lceil \log_{\lceil m/2 \rceil} \frac{n+1}{2} \right\rceil

Поиск

В целом операция поиска в B-дереве аналогична операции поиска в бинарном дереве.

Пусть нам нужно найти ключ KK в B-дереве. Обращаемся к диску, получаем узел и сохраняем его в оперативную память. Ищем в этом узле ключ KK, или ссылку PiP_i, где Ki1<K<KiK_{i-1} < K < K_i. Переходим по этой ссылке, получаем новый узел, и продолжаем наш поиск. Если мы дошли до листа и не нашли узел KK, значит его в B-дереве нет.

function search(node root, key) -> node: ...

В целом нам потребуется максимум