В информатике B -дерево — это самобалансирующаяся древовидная структура данных , которая поддерживает отсортированные данные и позволяет выполнять поиск, последовательный доступ, вставки и удаления за логарифмическое время . B-дерево обобщает двоичное дерево поиска , допуская узлы с более чем двумя потомками. [2] В отличие от других самобалансирующихся двоичных деревьев поиска , B-дерево хорошо подходит для систем хранения, которые считывают и записывают относительно большие блоки данных , таких как базы данных и файловые системы .
B-деревья были изобретены Рудольфом Байером и Эдвардом М. МакКрейтом во время работы в Boeing Research Labs с целью эффективного управления страницами индексов для больших файлов с произвольным доступом. Основное предположение состояло в том, что индексы будут настолько объемными, что только небольшие фрагменты дерева смогут поместиться в основной памяти. Статья Байера и МакКрейта Организация и обслуживание больших упорядоченных индексов [1] была впервые распространена в июле 1970 года и позже опубликована в Acta Informatica . [3]
Bayer и McCreight никогда не объясняли, что означает буква B , если она вообще что-то означает; предлагались Boeing , balance , between , broad , bushy и Bayer . [4] [5] [6] McCreight сказал, что «чем больше вы думаете о том, что означает буква B в B-деревьях, тем лучше вы понимаете B-деревья». [5]
Согласно определению Кнута , B-дерево порядка m — это дерево, которое удовлетворяет следующим свойствам: [7]
Ключи каждого внутреннего узла действуют как разделительные значения, которые разделяют его поддеревья. Например, если внутренний узел имеет 3 дочерних узла (или поддерева), то он должен иметь 2 ключа: a 1 и a 2 . Все значения в самом левом поддереве будут меньше a 1 , все значения в среднем поддереве будут между a 1 и a 2 , а все значения в самом правом поддереве будут больше a 2 .
B-дерево глубины n +1 может содержать примерно в U раз больше элементов, чем B-дерево глубины n , но стоимость операций поиска, вставки и удаления растет с глубиной дерева. Как и в случае любого сбалансированного дерева, стоимость растет гораздо медленнее, чем количество элементов.
Некоторые сбалансированные деревья хранят значения только в листовых узлах и используют разные типы узлов для листовых узлов и внутренних узлов. B-деревья хранят значения в каждом узле дерева, за исключением листовых узлов.
Литература по B-деревьям неоднородна в терминологии. [8]
Bayer и McCreight (1972), [3] Comer (1979), [2] и другие определяют порядок B-дерева как минимальное количество ключей в некорневом узле. Folk и Zoellick [9] указывают, что терминология неоднозначна, поскольку максимальное количество ключей неясно. B-дерево порядка 3 может содержать максимум 6 ключей или максимум 7 ключей. Knuth (1998) избегает этой проблемы, определяя порядок как максимальное количество потомков (которое на один больше максимального количества ключей). [7]
Термин «лист» также непоследователен. Байер и МакКрейт (1972) [3] считали, что уровень листьев — это самый низкий уровень ключей, но Кнут считал, что уровень листьев находится на один уровень ниже самых низких ключей. [9] Существует много возможных вариантов реализации. В некоторых проектах листья могут содержать всю запись данных; в других проектах листья могут содержать только указатели на запись данных. Эти варианты не являются основополагающими для идеи B-дерева. [10]
Для простоты большинство авторов предполагают, что существует фиксированное количество ключей, которые помещаются в узел. Основное предположение заключается в том, что размер ключа и размер узла фиксированы. На практике могут использоваться ключи переменной длины. [11]
Как и другие деревья, B-деревья можно представить в виде набора из трех типов узлов: корневых , внутренних (иначе говоря, внутренних) и листовых .
Обратите внимание на следующие определения переменных:
В B-деревьях для этих узлов поддерживаются следующие свойства:
Каждый внутренний узел в B-дереве имеет следующий формат:
Каждый конечный узел в B-дереве имеет следующий формат:
Границы узлов приведены в таблице ниже:
Для сохранения предопределенного диапазона дочерних узлов внутренние узлы могут быть объединены или разделены.
Обычно количество ключей выбирается в диапазоне от до , где — минимальное количество ключей, а — минимальная степень или фактор ветвления дерева. Фактор 2 гарантирует, что узлы можно разделить или объединить.
Если внутренний узел имеет ключи, то добавление ключа к этому узлу может быть выполнено путем разделения гипотетического ключевого узла на два ключевых узла и перемещения ключа, который был бы в середине, в родительский узел. Каждый разделенный узел имеет требуемое минимальное количество ключей. Аналогично, если внутренний узел и его сосед имеют ключи, то ключ может быть удален из внутреннего узла путем объединения его с соседом. Удаление ключа приведет к тому, что внутренний узел будет иметь ключи; присоединение к соседу добавит ключи и еще один ключ, взятый из родителя соседа. Результатом будет полностью полный узел ключей.
B-дерево сохраняется сбалансированным после вставки путем разделения потенциально переполненного узла ключей на два родственных узла -key и вставки ключа со средним значением в родительский узел. Глубина увеличивается только при разделении корня, что сохраняет баланс. Аналогично, B-дерево сохраняется сбалансированным после удаления путем слияния или перераспределения ключей между родственными узлами для поддержания минимума -key для некорневых узлов. Слияние уменьшает количество ключей в родительском узле, потенциально заставляя его сливать или перераспределять ключи со своими родственными узлами и т. д. Единственное изменение глубины происходит, когда у корня есть два потомка, ключей и (транзиционно) , в этом случае два родственных узла и родительский узел объединяются, уменьшая глубину на один.
Эта глубина будет медленно увеличиваться по мере добавления элементов в дерево, но увеличение общей глубины происходит нечасто и приводит к тому, что все конечные узлы оказываются на один узел дальше от корня.
Поскольку разрешен диапазон дочерних узлов, B-деревья не нуждаются в повторной балансировке так часто, как другие самобалансирующиеся деревья поиска, но могут тратить некоторое пространство впустую, поскольку узлы не полностью заполнены.
B-деревья имеют существенные преимущества по сравнению с альтернативными реализациями, когда время доступа к данным узла значительно превышает время, затрачиваемое на обработку этих данных, поскольку тогда стоимость доступа к узлу может быть амортизирована за счет нескольких операций внутри узла. Обычно это происходит, когда данные узла находятся во вторичном хранилище, таком как дисковые накопители . Максимизируя количество ключей в каждом внутреннем узле , высота дерева уменьшается, а количество дорогостоящих доступов к узлам сокращается. Кроме того, перебалансировка дерева происходит реже. Максимальное количество дочерних узлов зависит от информации, которая должна храниться для каждого дочернего узла, и размера полного дискового блока или аналогичного размера во вторичном хранилище. Хотя 2–3 B-дерева проще объяснить, практические B-деревья, использующие вторичное хранилище, требуют большого количества дочерних узлов для повышения производительности.
Термин B-дерево может относиться к конкретной конструкции или к общему классу конструкций. В узком смысле B-дерево хранит ключи во внутренних узлах, но не обязано хранить эти ключи в записях на листьях. Общий класс включает такие вариации, как дерево B+ , дерево B * и дерево B *+ .
Алгоритмы сортировки и поиска можно охарактеризовать числом операций сравнения, которые необходимо выполнить с использованием порядковой нотации . Например, двоичный поиск отсортированной таблицы с N записями может быть выполнен примерно за ⌈ log 2 N ⌉ сравнений. Если бы в таблице было 1 000 000 записей, то конкретная запись могла бы быть найдена максимум за 20 сравнений: ⌈ log 2 (1 000 000) ⌉ = 20 .
Большие базы данных исторически хранились на дисках. Время чтения записи на диске намного превышает время, необходимое для сравнения ключей после того, как запись становится доступной из-за времени поиска и задержки вращения. Время поиска может составлять от 0 до 20 или более миллисекунд, а задержка вращения в среднем составляет около половины периода вращения. Для диска со скоростью вращения 7200 об/мин период вращения составляет 8,33 миллисекунды. Для такого диска, как Seagate ST3500320NS, время поиска от дорожки к дорожке составляет 0,8 миллисекунды, а среднее время поиска чтения — 8,5 миллисекунды. [17] Для простоты предположим, что чтение с диска занимает около 10 миллисекунд.
Время поиска одной записи из миллиона в приведенном выше примере составит 20 операций чтения с диска по 10 миллисекунд на операцию чтения с диска, что составляет 0,2 секунды.
Время поиска сокращается, поскольку отдельные записи группируются в дисковый блок . Дисковый блок может быть размером 16 килобайт. Если каждая запись имеет размер 160 байт, то в каждом блоке можно сохранить 100 записей. Приведенное выше время чтения с диска фактически относится к целому блоку. После того, как головка диска находится в нужном положении, один или несколько дисковых блоков могут быть прочитаны с небольшой задержкой. При наличии 100 записей на блок последние 6 или около того сравнений не требуют выполнения каких-либо чтений с диска — все сравнения находятся в пределах последнего прочитанного дискового блока.
Чтобы еще больше ускорить поиск, необходимо сократить время выполнения первых 13–14 сравнений (каждое из которых требует доступа к диску).
Индекс B-дерева может использоваться для повышения производительности. Индекс B-дерева создает многоуровневую древовидную структуру, которая разбивает базу данных на блоки или страницы фиксированного размера. Каждый уровень этого дерева может использоваться для связывания этих страниц через адресное местоположение, позволяя одной странице (известной как узел или внутренняя страница) ссылаться на другую с листовыми страницами на самом низком уровне. Одна страница обычно является начальной точкой дерева или «корнем». Именно здесь начинается поиск определенного ключа, проходящий по пути, который заканчивается листом. Большинство страниц в этой структуре будут листовыми страницами, которые ссылаются на определенные строки таблицы.
Поскольку каждый узел (или внутренняя страница) может иметь более двух дочерних элементов, индекс B-дерева обычно будет иметь меньшую высоту (расстояние от корня до самого дальнего листа), чем двоичное дерево поиска. В приведенном выше примере начальные чтения с диска сузили диапазон поиска в два раза. Это можно улучшить, создав вспомогательный индекс, содержащий первую запись в каждом блоке диска (иногда называемый разреженным индексом ). Этот вспомогательный индекс будет составлять 1% от размера исходной базы данных, но по нему можно быстро выполнить поиск. Нахождение записи во вспомогательном индексе скажет нам, в каком блоке искать в основной базе данных; после поиска по вспомогательному индексу нам придется искать только в этом одном блоке основной базы данных — за счет еще одного чтения с диска.
В приведенном выше примере индекс будет содержать 10 000 записей и потребует максимум 14 сравнений для возврата результата. Как и в основной базе данных, последние шесть или около того сравнений во вспомогательном индексе будут находиться в одном и том же блоке диска. Поиск по индексу можно будет выполнить примерно за восемь чтений с диска, а доступ к нужной записи можно будет получить за 9 чтений с диска.
Создание вспомогательного индекса можно повторить, чтобы создать вспомогательный индекс для вспомогательного индекса. Это создаст индекс aux-aux, которому понадобится всего 100 записей и который поместится в один дисковый блок.
Вместо чтения 14 блоков диска для поиска нужной записи нам нужно прочитать только 3 блока. Эта блокировка является основной идеей создания B-дерева, где блоки диска заполняют иерархию уровней для составления индекса. Чтение и поиск первого (и единственного) блока индекса aux-aux, который является корнем дерева, идентифицирует соответствующий блок в aux-index на уровне ниже. Чтение и поиск этого блока aux-index идентифицирует соответствующий блок для чтения, пока последний уровень, известный как уровень листьев, не идентифицирует запись в основной базе данных. Вместо 150 миллисекунд нам нужно всего 30 миллисекунд, чтобы получить запись.
Вспомогательные индексы превратили задачу поиска из двоичного поиска, требующего примерно log 2 N чтений с диска, в задачу, требующую всего лишь log b N чтений с диска, где b — фактор блокировки (количество записей на блок: b = 100 записей на блок в нашем примере; log 100 1 000 000 = 3 чтения).
На практике, если основная база данных часто просматривается, индекс aux-aux и большая часть индекса aux могут находиться в дисковом кэше , поэтому они не будут вызывать чтение с диска. B-дерево остается стандартной реализацией индекса почти во всех реляционных базах данных , и многие нереляционные базы данных также используют их. [18]
Если база данных не меняется, то компиляция индекса проста, и индекс никогда не нужно менять. Если есть изменения, управление базой данных и ее индексом требует дополнительных вычислений.
Удаление записей из базы данных относительно просто. Индекс может остаться прежним, а запись может быть просто помечена как удаленная. База данных остается в отсортированном порядке. Если есть большое количество ленивых удалений , то поиск и хранение становятся менее эффективными. [19]
Вставки могут быть очень медленными в сортированном последовательном файле, поскольку должно быть выделено место для вставленной записи. Вставка записи перед первой записью требует сдвига всех записей вниз на одну. Такая операция слишком затратна, чтобы быть практичной. Одним из решений является оставление некоторых пробелов. Вместо того чтобы плотно упаковывать все записи в блок, блок может иметь некоторое свободное пространство для последующих вставок. Эти пробелы будут помечены как «удаленные» записи.
И вставки, и удаления выполняются быстро, пока в блоке есть место. Если вставка не помещается в блок, то необходимо найти свободное место в соседнем блоке и скорректировать вспомогательные индексы. В лучшем случае поблизости должно быть достаточно места, чтобы можно было минимизировать объем реорганизации блока. В качестве альтернативы можно использовать некоторые несоответствующие последовательности блоки диска. [18]
B-дерево использует все идеи, описанные выше. В частности, B-дерево:
Кроме того, B-дерево минимизирует отходы, гарантируя, что внутренние узлы заполнены по крайней мере наполовину. B-дерево может обрабатывать произвольное количество вставок и удалений. [18]
Пусть h ≥ –1 будет высотой классического B-дерева (см. Дерево (структура данных) § Терминология для определения высоты дерева). Пусть n ≥ 0 будет числом записей в дереве. Пусть m будет максимальным числом потомков, которое может иметь узел. Каждый узел может иметь не более m −1 ключей.
Можно показать (например, методом индукции), что B-дерево высотой h со всеми полностью заполненными узлами имеет n = m· h +1 –1 записей. Следовательно, наилучшая высота (т.е. минимальная высота) B-дерева равна:
Пусть будет минимальным числом потомков, которое должен иметь внутренний (некорневой) узел. Для обычного B-дерева,
Комер (1979) и Кормен и др. (2001) приводят худшую высоту (максимальную высоту) B-дерева как [20]
Поиск похож на поиск в бинарном дереве поиска. Начиная с корня, дерево рекурсивно обходит сверху вниз. На каждом уровне поиск сужает поле зрения до указателя-потомка (поддерева), диапазон которого включает искомое значение. Диапазон поддерева определяется значениями или ключами, содержащимися в его родительском узле. Эти ограничивающие значения также известны как разделительные значения.
Двоичный поиск обычно (но не обязательно) используется внутри узлов для поиска значений разделения и интересующего дочернего дерева.
Все вставки начинаются с листового узла. Чтобы вставить новый элемент, выполните поиск по дереву, чтобы найти листовой узел, куда следует добавить новый элемент. Вставьте новый элемент в этот узел, выполнив следующие шаги:
Если разделение идет до самого корня, оно создает новый корень с одним разделительным значением и двумя дочерними узлами, поэтому нижняя граница размера внутренних узлов не применяется к корню. Максимальное количество элементов на узел равно U −1. Когда узел разделяется, один элемент перемещается в родительский узел, но один элемент добавляется. Таким образом, должно быть возможно разделить максимальное количество U −1 элементов на два допустимых узла. Если это число нечетное, то U =2 L и один из новых узлов содержит ( U −2)/2 = L −1 элементов, и, следовательно, является допустимым узлом, а другой содержит еще один элемент, и, следовательно, он также допустим. Если U −1 четное, то U =2 L −1, поэтому в узле 2 L −2 элементов. Половина этого числа равна L −1, что является минимальным количеством элементов, разрешенным на узел.
Альтернативный алгоритм поддерживает один проход вниз по дереву от корня до узла, где будет происходить вставка, предварительно разделяя любые полные узлы, встречающиеся на пути. Это устраняет необходимость вызывать родительские узлы в память, что может быть затратно, если узлы находятся на вторичном хранилище. Однако, чтобы использовать этот алгоритм, мы должны иметь возможность отправить один элемент родительскому элементу и разделить оставшиеся U −2 элементов на два законных узла, не добавляя новый элемент. Для этого требуется U = 2 L, а не U = 2 L −1, что объясняет, почему некоторые [ which? ] учебники налагают это требование при определении B-деревьев.
Существуют две популярные стратегии удаления из B-дерева.
Приведенный ниже алгоритм использует предыдущую стратегию.
При удалении элемента следует учитывать два особых случая:
Процедуры для этих случаев изложены ниже.
Каждый элемент во внутреннем узле действует как разделительное значение для двух поддеревьев, поэтому нам нужно найти замену для разделения. Обратите внимание, что наибольший элемент в левом поддереве все еще меньше разделителя. Аналогично, наименьший элемент в правом поддереве все еще больше разделителя. Оба этих элемента находятся в листовых узлах, и любой из них может быть новым разделителем для двух поддеревьев. Алгоритмически описано ниже:
Перебалансировка начинается с листа и продолжается по направлению к корню, пока дерево не будет сбалансировано. Если удаление элемента из узла привело к тому, что он стал меньше минимального размера, то некоторые элементы должны быть перераспределены, чтобы довести все узлы до минимума. Обычно перераспределение включает перемещение элемента из родственного узла, который имеет больше минимального количества узлов. Эта операция перераспределения называется вращением . Если ни один родственный узел не может выделить элемент, то недостающий узел должен быть объединен с родственным. Объединение приводит к тому, что родительский узел теряет элемент-разделитель, поэтому родительский узел может стать недостающим и нуждаться в перебалансировке. Объединение и перебалансировка могут продолжаться вплоть до корня. Поскольку минимальное количество элементов не применяется к корню, сделать корень единственным недостающим узлом не проблема. Алгоритм перебалансировки дерева следующий:
Хотя недавно загруженные базы данных, как правило, имеют хорошее последовательное поведение, поддерживать такое поведение становится все сложнее по мере роста базы данных, что приводит к увеличению числа случайных операций ввода-вывода и проблемам с производительностью. [22]
Распространенным особым случаем является добавление большого количества предварительно отсортированных данных в изначально пустое B-дерево. Хотя вполне возможно просто выполнить ряд последовательных вставок, вставка отсортированных данных приводит к дереву, состоящему почти полностью из полузаполненных узлов. Вместо этого можно использовать специальный алгоритм «массовой загрузки» для создания более эффективного дерева с более высоким коэффициентом ветвления.
Когда вход сортируется, все вставки находятся на самом правом краю дерева, и в частности, всякий раз, когда узел разделяется, мы гарантированно не будем вставлять больше в левую половину. При массовой загрузке мы пользуемся этим и вместо того, чтобы равномерно разделять переполненные узлы, разделяем их как можно неравномернее : оставляем левый узел полностью заполненным и создаем правый узел с нулевыми ключами и одним потомком (нарушая обычные правила B-дерева).
В конце массовой загрузки дерево состоит почти полностью из полностью заполненных узлов; только самый правый узел на каждом уровне может быть заполнен меньше, чем наполовину. Поскольку эти узлы также могут быть заполнены меньше, чем наполовину , чтобы восстановить обычные правила B-дерева, объедините такие узлы с их (гарантированно заполненными) левыми братьями и сестрами и разделите ключи, чтобы получить два узла, заполненных как минимум наполовину. Единственный узел, у которого нет полного левого брата, — это корень, которому разрешено быть заполненным меньше, чем наполовину.
Помимо использования в базах данных, B-дерево (или § Variants) также используется в файловых системах для обеспечения быстрого случайного доступа к произвольному блоку в определенном файле. Основная проблема заключается в превращении адреса блока файла в адрес блока диска.
Некоторые операционные системы требуют, чтобы пользователь выделял максимальный размер файла при его создании. Затем файл может быть выделен в виде смежных дисковых блоков. В этом случае, чтобы преобразовать адрес блока файла в адрес блока диска, операционная система просто добавляет адрес блока файла к адресу первого дискового блока, составляющего файл. Схема проста, но файл не может превышать свой созданный размер.
Другие операционные системы позволяют файлу расти. Получающиеся блоки диска могут не быть смежными, поэтому сопоставление логических блоков с физическими блоками более сложно.
MS-DOS , например, использовала простую таблицу размещения файлов (FAT). FAT имеет запись для каждого блока на диске, [примечание 1], и эта запись определяет, используется ли его блок файлом, и если да, то какой блок (если есть) является следующим блоком на диске того же файла. Таким образом, размещение каждого файла представлено в таблице в виде связанного списка . Чтобы найти адрес на диске блока файла , операционная система (или дисковая утилита) должна последовательно следовать связанному списку файла в FAT. Хуже того, чтобы найти свободный блок на диске, она должна последовательно сканировать FAT. Для MS-DOS это не было большим штрафом, потому что диски и файлы были небольшими, а в FAT было мало записей и относительно короткие цепочки файлов. В файловой системе FAT12 (использовавшейся на гибких дисках и ранних жестких дисках) было не более 4080 [примечание 2] записей, и FAT обычно находилась в памяти. По мере того, как диски становились больше, архитектура FAT начала сталкиваться с недостатками. На большом диске, использующем FAT, может потребоваться выполнить чтение с диска, чтобы узнать местоположение на диске блока файла, который необходимо прочитать или записать.
TOPS-20 (и, возможно, TENEX ) использовали дерево уровней от 0 до 2, которое имеет сходство с B-деревом [ требуется цитата ] . Дисковый блок состоял из 512 36-битных слов. Если файл помещался в блок из 512 (2 9 ) слов, то каталог файла указывал бы на этот физический дисковый блок. Если файл помещался в 2 18 слов, то каталог указывал бы на вспомогательный индекс; 512 слов этого индекса были бы либо NULL (блок не выделен), либо указывали бы на физический адрес блока. Если файл помещался в 2 27 слов, то каталог указывал бы на блок, содержащий вспомогательный индекс; каждая запись была бы либо NULL, либо указывала бы на вспомогательный индекс. Следовательно, физический дисковый блок для файла из 2 27 слов мог бы быть расположен за два чтения с диска и прочитан на третьем.
Файловые системы Apple HFS+ и APFS , Microsoft NTFS , [23] AIX (jfs2) и некоторые файловые системы Linux , такие как Bcachefs , Btrfs и ext4 , используют B-деревья.
B * -деревья используются в файловых системах HFS и Reiser4 .
Файловая система HAMMER в DragonFly BSD использует модифицированное дерево B+. [24]
B-дерево растет медленнее с ростом объема данных, чем линейность связанного списка. По сравнению со списком пропусков обе структуры имеют одинаковую производительность, но B-дерево лучше масштабируется при росте n . T-дерево для систем баз данных основной памяти похоже, но более компактно.
Lehman и Yao [25] показали, что всех блокировок чтения можно избежать (и, таким образом, значительно улучшить параллельный доступ), связав блоки дерева на каждом уровне вместе указателем «следующий». Это приводит к древовидной структуре, в которой как операции вставки, так и операции поиска спускаются от корня к листу. Блокировки записи требуются только при изменении блока дерева. Это максимизирует параллельный доступ нескольких пользователей, что является важным соображением для баз данных и/или других методов хранения ISAM на основе B-дерева . Стоимость, связанная с этим улучшением, заключается в том, что пустые страницы не могут быть удалены из B-дерева во время обычных операций. (Однако см. [26] для различных стратегий реализации слияния узлов и исходный код в [27] )
Патент США 5283894, выданный в 1994 году, по-видимому, показывает способ использования «метода метадоступа» [28], чтобы разрешить одновременный доступ к дереву B+ и его модификацию без блокировок. Методика обращается к дереву «вверх» как для поиска, так и для обновления с помощью дополнительных индексов в памяти, которые указывают на блоки на каждом уровне в кэше блоков. Не требуется реорганизации для удалений, и нет указателей «следующий» в каждом блоке, как в Lehman и Yao.
Поскольку B-деревья по своей структуре похожи на красно-черные деревья , параллельные алгоритмы для красно-черных деревьев могут быть применены и к B-деревьям.
Maple tree — это B-дерево, разработанное для использования в ядре Linux с целью снижения числа конфликтов при блокировке в управлении виртуальной памятью. [29] [30] [31]
Массовая загрузка