Дерево ван Эмде Боаса — различия между версиями
Zemskovk (обсуждение | вклад) м |
м (rollbackEdits.php mass rollback) |
||
(не показано 17 промежуточных версий 6 участников) | |||
Строка 1: | Строка 1: | ||
− | '''Дерево ван Эмде Боаса'''(англ. ''Van Emde Boas tree, vEB tree'') {{---}} структура данных, представляющая собой [[Дерево поиска, наивная реализация|дерево поиска]], позволяющее хранить целые неотрицательные числа в интервале <tex>[0;2^k)</tex> и осуществлять над ними все соответствующие дереву поиска операции. | + | '''Дерево ван Эмде Боаса''' (англ. ''Van Emde Boas tree, vEB tree'') {{---}} структура данных, представляющая собой [[Дерево поиска, наивная реализация|дерево поиска]], позволяющее хранить целые неотрицательные числа в интервале <tex>[0;2^k)</tex> и осуществлять над ними все соответствующие дереву поиска операции. |
− | Проще говоря, данная структура позволяет хранить <tex>k</tex>-битные числа и производить над ними операции <tex>\mathrm{find}</tex>, <tex>\mathrm{insert}</tex>, <tex>\mathrm{remove}</tex>, <tex>\mathrm{next}</tex>, <tex>\mathrm{prev}</tex>, <tex>\mathrm{min}</tex>, <tex>\mathrm{max}</tex> и некоторые другие операции, которые присущи всем деревьям поиска. | + | Проще говоря, данная структура позволяет хранить <tex>k</tex>-битные числа и производить над ними операции <tex>\mathrm{find}</tex>, <tex>\mathrm{insert}</tex>, <tex>\mathrm{remove}</tex>, <tex>\mathrm{next}</tex>, <tex>\mathrm{prev}</tex>, <tex>\mathrm{\min}</tex>, <tex>\mathrm{max}</tex> и некоторые другие операции, которые присущи всем деревьям поиска. |
Особенностью этой структуры является то, что все операции выполняются за <tex>O(\log k)</tex>, что асимптотически лучше, чем <tex>O(\log n)</tex> в большинстве других деревьев поиска, где <tex>n</tex> {{---}} количество элементов в дереве. | Особенностью этой структуры является то, что все операции выполняются за <tex>O(\log k)</tex>, что асимптотически лучше, чем <tex>O(\log n)</tex> в большинстве других деревьев поиска, где <tex>n</tex> {{---}} количество элементов в дереве. | ||
Строка 10: | Строка 10: | ||
Для удобства работы с деревом будем использовать <tex> k </tex>, равные степени двойки. | Для удобства работы с деревом будем использовать <tex> k </tex>, равные степени двойки. | ||
− | Как уже было сказано выше, <tex>k</tex>-дерево хранит числа в интервале <tex>[0;2^k)</tex>. Тогда 1 | + | Как уже было сказано выше, <tex>k</tex>-дерево хранит числа в интервале <tex>[0;2^k)</tex>. Тогда при <tex>k = 1</tex> дерево хранит информацию, содержатся ли в нем <tex>0</tex> и <tex>1</tex>. |
− | Построим <tex>k</tex>-дерево, при <tex>k \neq 1</tex>. В нем будут | + | Построим <tex>k</tex>-дерево, при <tex>k \neq 1</tex>. В нем будут храниться: |
*массив <tex>children</tex>, состоящий из <tex>2^{k/2}</tex> <tex>k/2</tex>-деревьев | *массив <tex>children</tex>, состоящий из <tex>2^{k/2}</tex> <tex>k/2</tex>-деревьев | ||
*вспомогательное <tex>k/2</tex>-дерево, которое назовем <tex>aux</tex> | *вспомогательное <tex>k/2</tex>-дерево, которое назовем <tex>aux</tex> | ||
Строка 25: | Строка 25: | ||
== Операции == | == Операции == | ||
=== empty === | === empty === | ||
− | Чтобы определить, пусто ли дерево, будем изначально инициализировать поле <tex>min</tex> числом, которое не лежит в интервале <tex>[0;2^k)</tex>. Назовем это число <tex>none</tex>. Например, это может быть <tex>-1</tex>, если мы храним в числа в знаковом целочисленном типе, или <tex>2^k</tex>, если в беззнаковом. Тогда проверка на пустоту дерева будет заключаться лишь в сравнении поля <tex>min</tex> с этим числом. | + | Чтобы определить, пусто ли дерево, будем изначально инициализировать поле <tex>\min</tex> числом, которое не лежит в интервале <tex>[0;2^k)</tex>. Назовем это число <tex>none</tex>. Например, это может быть <tex>-1</tex>, если мы храним в числа в знаковом целочисленном типе, или <tex>2^k</tex>, если в беззнаковом. Тогда проверка на пустоту дерева будет заключаться лишь в сравнении поля <tex>\min</tex> с этим числом. |
<code> | <code> | ||
− | '''boolean''' empty(t: '''Tree''') | + | '''boolean''' empty(t: '''Tree'''): |
− | '''if''' t.min == none | + | '''if''' t.min == ''none'' |
− | '''return''' true | + | '''return''' ''true'' |
'''else''' | '''else''' | ||
− | '''return''' false | + | '''return''' ''false'' |
</code> | </code> | ||
=== min и max === | === min и max === | ||
− | Так как мы храним в дереве минимальное и максимальное значения, то данные операции не требуют ничего, кроме вывода значения поля <tex>min</tex> или <tex>max</tex> в соответствии с запросом. Время выполнения данных операций соответственно <tex>O(1)</tex>. | + | Так как мы храним в дереве минимальное и максимальное значения, то данные операции не требуют ничего, кроме вывода значения поля <tex>\min</tex> или <tex>\max</tex> в соответствии с запросом. Время выполнения данных операций соответственно <tex>O(1)</tex>. |
=== find === | === find === | ||
Алгоритм поиска сам напрашивается из выше описанной структуры: | Алгоритм поиска сам напрашивается из выше описанной структуры: | ||
*если дерево пусто, то число не содержится в нашей структуре. | *если дерево пусто, то число не содержится в нашей структуре. | ||
− | *если число равно полю <tex>min</tex> или <tex>max</tex>, то число в дереве есть. | + | *если число равно полю <tex>\min</tex> или <tex>\max</tex>, то число в дереве есть. |
*иначе ищем число <tex>\mathrm{low(x)}</tex> в поддереве <tex>children[\mathrm{high(x)}]</tex>. | *иначе ищем число <tex>\mathrm{low(x)}</tex> в поддереве <tex>children[\mathrm{high(x)}]</tex>. | ||
<code> | <code> | ||
− | '''boolean''' find(t: '''Tree''', x: '''int''') | + | '''boolean''' find(t: '''Tree''', x: '''int'''): |
'''if''' empty(t) | '''if''' empty(t) | ||
− | '''return''' false | + | '''return''' ''false'' |
'''if''' t.min == x '''or''' t.max == x | '''if''' t.min == x '''or''' t.max == x | ||
− | '''return''' true | + | '''return''' ''true'' |
− | '''return''' find(t.children[high(x)], low(x)) | + | '''return''' find(t.children[high(x)], low(x)) |
</code> | </code> | ||
Строка 57: | Строка 57: | ||
Операция вставки элемента <tex>x</tex> состоит из нескольких частей: | Операция вставки элемента <tex>x</tex> состоит из нескольких частей: | ||
− | *если дерево пусто или в нем содержится единственный элемент (<tex> min = max </tex>), то присвоим полям <tex>min</tex> и <tex>max</tex> соответствующие значения. Делать что-то еще бессмысленно, так как информация записанная в <tex>min</tex> и <tex>max</tex> полностью описывает состояние текущего дерева и удовлетворяет структуре нашего дерева. | + | *если дерево пусто или в нем содержится единственный элемент (<tex> \min = \max </tex>), то присвоим полям <tex>\min</tex> и <tex>\max</tex> соответствующие значения. Делать что-то еще бессмысленно, так как информация записанная в <tex>\min</tex> и <tex>\max</tex> полностью описывает состояние текущего дерева и удовлетворяет структуре нашего дерева. |
*иначе: | *иначе: | ||
− | **если элемент <tex>x</tex> больше <tex>max</tex> или меньше <tex>min</tex> текущего дерева, то обновим соответствующее значение минимума или максимума, а старый минимум или максимум добавим в дерево. | + | **если элемент <tex>x</tex> больше <tex>\max</tex> или меньше <tex>\min</tex> текущего дерева, то обновим соответствующее значение минимума или максимума, а старый минимум или максимум добавим в дерево. |
**вставим во вспомогательное дерево <tex>aux</tex> число <tex>\mathrm{high(x)}</tex>, если соответствующее поддерево <tex>children[\mathrm{high(x)}]</tex> до этого было пусто. | **вставим во вспомогательное дерево <tex>aux</tex> число <tex>\mathrm{high(x)}</tex>, если соответствующее поддерево <tex>children[\mathrm{high(x)}]</tex> до этого было пусто. | ||
**вставим число <tex>\mathrm{low(x)}</tex> в поддерево <tex>children[\mathrm{high(x)}]</tex>, за исключением ситуации, когда текущее дерево {{---}} это 1-дерево, и дальнейшая вставка не требуется. | **вставим число <tex>\mathrm{low(x)}</tex> в поддерево <tex>children[\mathrm{high(x)}]</tex>, за исключением ситуации, когда текущее дерево {{---}} это 1-дерево, и дальнейшая вставка не требуется. | ||
<code> | <code> | ||
− | '''function''' insert(t: '''Tree''', x: '''int''') | + | '''function''' insert(t: '''Tree''', x: '''int'''): |
'''if''' empty(t) <span style="color:#008000">// проверка на пустоту текущего дерева</span> | '''if''' empty(t) <span style="color:#008000">// проверка на пустоту текущего дерева</span> | ||
− | t.min = x | + | t.min = x |
− | t.max = x | + | t.max = x |
'''else''' | '''else''' | ||
'''if''' t.min == t.max <span style="color:#008000">// проверка, что в дереве один элемент</span> | '''if''' t.min == t.max <span style="color:#008000">// проверка, что в дереве один элемент</span> | ||
'''if''' T.min < x | '''if''' T.min < x | ||
− | t.max = x | + | t.max = x |
'''else''' | '''else''' | ||
− | t.min = x | + | t.min = x |
'''else''' | '''else''' | ||
'''if''' t.min > x | '''if''' t.min > x | ||
− | swap(t.min, x) | + | swap(t.min, x) <span style="color:#008000">// релаксация минимума</span> |
'''if''' t.max < x | '''if''' t.max < x | ||
− | swap(t.max, x) | + | swap(t.max, x) <span style="color:#008000">// релаксация максимума</span> |
'''if''' t.k != 1 | '''if''' t.k != 1 | ||
'''if''' empty(t.children[high(x)]) | '''if''' empty(t.children[high(x)]) | ||
− | insert(t.aux, high(x)) | + | insert(t.aux, high(x)) <span style="color:#008000">// вставка high(x) во вспомогательно дерево aux</span> |
− | insert(t.children[high(x)], low(x)) | + | insert(t.children[high(x)], low(x)) <span style="color:#008000">// вставка low(x) в поддерево children[high(x)]</span> |
</code> | </code> | ||
− | Нетрудно увидеть, что данная операция работает за время <tex>O(\log k)</tex>. На каждом уровне дерева мы выполняем <tex>O(1)</tex> операций. После этого возможны 2 случая: поддерево <tex>children[\mathrm{high(x)}]</tex> пусто, и мы будем производить дальнейшую вставку и в него, и во вспомогательное дерево <tex>aux</tex>, или же поддерево не пусто, и мы просто спустимся на уровень ниже. Но если поддерево <tex>children[\mathrm{high(x)}]</tex> пусто, то вставка в него будет выполнена за <tex>O(1)</tex>, так как мы всего лишь обновим поля <tex>min</tex> и <tex>max</tex>. Все остальные операции будут выполнятся уже со вспомогательным деревом <tex>aux</tex>, высота которого на 1 уровень меньше, чем высота текущего. Если же поддерево <tex>children[\mathrm{high(x)}]</tex> не пусто, то мы просто перейдем к вставке элемента в это поддерево, высота которого так же на 1 меньше, чем у текущего. В итоге, каждый раз, выполнив <tex>O(1)</tex> операций, мы переходим к дереву, высота которого на 1 меньше, чем у текущего. Следовательно, количество операций пропорционально высоте дерева, которая, как уже было показано, <tex>O(\log k)</tex>. То есть операция вставки займет <tex>O(\log k)</tex> времени. | + | Нетрудно увидеть, что данная операция работает за время <tex>O(\log k)</tex>. На каждом уровне дерева мы выполняем <tex>O(1)</tex> операций. После этого возможны 2 случая: поддерево <tex>children[\mathrm{high(x)}]</tex> пусто, и мы будем производить дальнейшую вставку и в него, и во вспомогательное дерево <tex>aux</tex>, или же поддерево не пусто, и мы просто спустимся на уровень ниже. Но если поддерево <tex>children[\mathrm{high(x)}]</tex> пусто, то вставка в него будет выполнена за <tex>O(1)</tex>, так как мы всего лишь обновим поля <tex>\min</tex> и <tex>\max</tex>. Все остальные операции будут выполнятся уже со вспомогательным деревом <tex>aux</tex>, высота которого на 1 уровень меньше, чем высота текущего. Если же поддерево <tex>children[\mathrm{high(x)}]</tex> не пусто, то мы просто перейдем к вставке элемента в это поддерево, высота которого так же на 1 меньше, чем у текущего. В итоге, каждый раз, выполнив <tex>O(1)</tex> операций, мы переходим к дереву, высота которого на 1 меньше, чем у текущего. Следовательно, количество операций пропорционально высоте дерева, которая, как уже было показано, <tex>O(\log k)</tex>. То есть операция вставки займет <tex>O(\log k)</tex> времени. |
=== remove === | === remove === | ||
Удаление из дерева также делится на несколько подзадач: | Удаление из дерева также делится на несколько подзадач: | ||
− | *если <tex> min = max = x </tex>, значит в дереве один элемент, удалим его и отметим, что дерево пусто. | + | *если <tex> \min = \max = x </tex>, значит в дереве один элемент, удалим его и отметим, что дерево пусто. |
− | *если <tex> x = min </tex>, то мы должны найти следующий минимальный элемент в этом дереве, присвоить <tex>min</tex> значение второго минимального элемента и удалить его из того места, где он хранится. Второй минимум {{---}} это либо <tex> max </tex>, либо <tex> children[aux.min].min </tex> (для случая <tex> x = max </tex> действуем аналогично). | + | *если <tex> x = \min </tex>, то мы должны найти следующий минимальный элемент в этом дереве, присвоить <tex>\min</tex> значение второго минимального элемента и удалить его из того места, где он хранится. Второй минимум {{---}} это либо <tex> \max </tex>, либо <tex> children[aux.min].min </tex> (для случая <tex> x = \max </tex> действуем аналогично). |
− | *если же <tex> x \neq min </tex> и <tex> x \neq max </tex>, то мы должны удалить <tex>\mathrm{low(x)}</tex> из поддерева <tex>children[\mathrm{high(x)}]</tex>. | + | *если же <tex> x \neq \min </tex> и <tex> x \neq \max </tex>, то мы должны удалить <tex>\mathrm{low(x)}</tex> из поддерева <tex>children[\mathrm{high(x)}]</tex>. |
Так как в поддеревьях хранятся не все биты исходных элементов, а только часть их, то для восстановления исходного числа, по имеющимся старшим и младшим битам, будем использовать функцию <tex> merge </tex>. Также нельзя забывать, что если мы удаляем последнее вхождение <tex>x</tex>, то мы должны удалить <tex>\mathrm{high(x)}</tex> из вспомогательного дерева. | Так как в поддеревьях хранятся не все биты исходных элементов, а только часть их, то для восстановления исходного числа, по имеющимся старшим и младшим битам, будем использовать функцию <tex> merge </tex>. Также нельзя забывать, что если мы удаляем последнее вхождение <tex>x</tex>, то мы должны удалить <tex>\mathrm{high(x)}</tex> из вспомогательного дерева. | ||
<code> | <code> | ||
− | '''function''' remove(t: '''Tree''', x: '''int''') | + | '''function''' remove(t: '''Tree''', x: '''int'''): |
'''if''' t.min == x '''and''' t.max == x <span style="color:#008000">// случай, когда в дереве один элемент</span> | '''if''' t.min == x '''and''' t.max == x <span style="color:#008000">// случай, когда в дереве один элемент</span> | ||
− | t.min = none | + | t.min = ''none'' |
− | '''return''' | + | '''return''' |
'''if''' t.min == x | '''if''' t.min == x | ||
'''if''' empty(t.aux) | '''if''' empty(t.aux) | ||
− | t.min = t.max | + | t.min = t.max |
− | '''return''' | + | '''return''' |
− | x = merge(t.aux.min, t.children[t.aux.min].min) | + | x = merge(t.aux.min, t.children[t.aux.min].min) |
− | + | t.min = x | |
'''if''' t.max == x | '''if''' t.max == x | ||
'''if''' empty(t.aux) | '''if''' empty(t.aux) | ||
− | t.max = t.min | + | t.max = t.min |
− | '''return''' | + | '''return''' |
− | + | '''else''' | |
− | x = merge(t.aux.max, t.children[t.aux.max].max) | + | x = merge(t.aux.max, t.children[t.aux.max].max) |
− | t.max = x | + | t.max = x |
'''if''' empty(t.aux) <span style="color:#008000">// случай, когда элемента x нет в дереве</span> | '''if''' empty(t.aux) <span style="color:#008000">// случай, когда элемента x нет в дереве</span> | ||
− | '''return''' | + | '''return''' |
− | remove(t.children[high(x)], low(x)) | + | remove(t.children[high(x)], low(x)) |
'''if''' empty(t.children[high(x)]) <span style="color:#008000">// если мы удалили из поддерева последний элемент</span> | '''if''' empty(t.children[high(x)]) <span style="color:#008000">// если мы удалили из поддерева последний элемент</span> | ||
− | remove(t.aux, high(x)) | + | remove(t.aux, high(x)) <span style="color:#008000">// то удаляем информацию, что это поддерево не пусто</span> |
</code> | </code> | ||
Строка 124: | Строка 124: | ||
Алгоритм нахождения следующего элемента, как и два предыдущих, сводится к рассмотрению случая, когда дерево содержит не более одного элемента, либо к поиску в одном из его поддеревьев: | Алгоритм нахождения следующего элемента, как и два предыдущих, сводится к рассмотрению случая, когда дерево содержит не более одного элемента, либо к поиску в одном из его поддеревьев: | ||
*если дерево пусто, или максимум этого дерева не превосходит <tex> x </tex>, то следующего элемента в этом дереве не существует. | *если дерево пусто, или максимум этого дерева не превосходит <tex> x </tex>, то следующего элемента в этом дереве не существует. | ||
− | *если <tex> x </tex> меньше поля <tex> min </tex>, то искомый элемент и есть <tex> min </tex>. | + | *если <tex> x </tex> меньше поля <tex> \min </tex>, то искомый элемент и есть <tex> \min </tex>. |
− | *если дерево содержит не более двух элементов, и <tex> x < max </tex>, то искомый элемент <tex> max </tex>. | + | *если дерево содержит не более двух элементов, и <tex> x < \max </tex>, то искомый элемент <tex> \max </tex>. |
*если же в дереве более двух элементов, то: | *если же в дереве более двух элементов, то: | ||
**если в дереве есть еще числа, большие <tex> x </tex>, и чьи старшие биты равны <tex>\mathrm{high(x)} </tex>, то продолжим поиск в поддереве <tex> children[\mathrm{high(x)}] </tex>, где будем искать число, следующее после <tex>\mathrm{low(x)} </tex>. | **если в дереве есть еще числа, большие <tex> x </tex>, и чьи старшие биты равны <tex>\mathrm{high(x)} </tex>, то продолжим поиск в поддереве <tex> children[\mathrm{high(x)}] </tex>, где будем искать число, следующее после <tex>\mathrm{low(x)} </tex>. | ||
Строка 133: | Строка 133: | ||
'''int''' next(t: '''Tree''', x: '''int''') | '''int''' next(t: '''Tree''', x: '''int''') | ||
'''if''' empty(t) '''or''' t.max <= x | '''if''' empty(t) '''or''' t.max <= x | ||
− | '''return''' | + | '''return''' ''none''; <span style="color:#008000">// следующего элемента нет</span> |
'''if''' t.min > x | '''if''' t.min > x | ||
'''return''' t.min; | '''return''' t.min; | ||
Строка 143: | Строка 143: | ||
'''else''' <span style="color:#008000">// иначе найдем следующее непустое поддерево</span> | '''else''' <span style="color:#008000">// иначе найдем следующее непустое поддерево</span> | ||
'''int''' nextHigh = next(t.aux, high(x)); | '''int''' nextHigh = next(t.aux, high(x)); | ||
− | '''if''' nextHigh == | + | '''if''' nextHigh == ''none'' |
'''return''' t.max; <span style="color:#008000">// если такого нет, вернем максимум</span> | '''return''' t.max; <span style="color:#008000">// если такого нет, вернем максимум</span> | ||
'''else''' | '''else''' | ||
Строка 159: | Строка 159: | ||
=== Недостатки === | === Недостатки === | ||
− | *существенным недостатком данной структуры является то, что она позволяет хранить лишь целые неотрицательные числа, что существенно | + | *существенным недостатком данной структуры является то, что она позволяет хранить лишь целые неотрицательные числа, что существенно сужает область ее применения, по сравнению с другими деревьями поиска, которые не используют внутреннюю структуру элементов, хранящихся в них. |
− | *другим серьезным недостатком является количество занимаемой памяти. Дерево, хранящее <tex> k </tex>-битные числа, занимает <tex> | + | *другим серьезным недостатком является количество занимаемой памяти. Дерево, хранящее <tex> k </tex>-битные числа, занимает <tex> \Theta(2^k) </tex> памяти, что несложно доказывается индукцией, учитывая, что <tex> S(2^k)=(2^{k/2} + 1) \cdot S(2^{k/2}) + O(2^{k/2})</tex>, где <tex> S(2^i) </tex> {{---}} количество памяти, занимаемое деревом, в котором хранятся <tex> i </tex>-битные числа. Впрочем, можно попытаться частично избежать огромного расхода памяти, создавая необходимые поддеревья «лениво», то есть только тогда, когда они нам потребуются. |
==См. также== | ==См. также== |
Текущая версия на 19:15, 4 сентября 2022
Дерево ван Эмде Боаса (англ. Van Emde Boas tree, vEB tree) — структура данных, представляющая собой дерево поиска, позволяющее хранить целые неотрицательные числа в интервале и осуществлять над ними все соответствующие дереву поиска операции.
Проще говоря, данная структура позволяет хранить
-битные числа и производить над ними операции , , , , , , и некоторые другие операции, которые присущи всем деревьям поиска.Особенностью этой структуры является то, что все операции выполняются за
, что асимптотически лучше, чем в большинстве других деревьев поиска, где — количество элементов в дереве.Содержание
Структура
Для удобства работы с деревом будем использовать
, равные степени двойки.Как уже было сказано выше,
-дерево хранит числа в интервале . Тогда при дерево хранит информацию, содержатся ли в нем и .Построим
-дерево, при . В нем будут храниться:- массив , состоящий из -деревьев
- вспомогательное -дерево, которое назовем
- максимальный и минимальный элементы, хранящиеся в этом дереве (если оно не является пустым), причем дополнительно в массиве эти элементы хранить не будем.
Пусть у нас есть
-битное число . Разобьем это число таким образом, что — число, соответствующее старшим битам числа , а соответствует младшим битам. Тогда информация, хранится ли в данном дереве число , эквивалентна информации, содержится ли в дереве число .Нетрудно увидеть, что высота подобного дерева
, так как каждый следующий уровень дерева содержит числа, количество битов в которых в 2 раза меньше, чем в предыдущем.Во вспомогательном дереве
будем хранить все такие числа , что дерево не пусто.Операции
empty
Чтобы определить, пусто ли дерево, будем изначально инициализировать поле
boolean empty(t: Tree): if t.min == none return true else return false
min и max
Так как мы храним в дереве минимальное и максимальное значения, то данные операции не требуют ничего, кроме вывода значения поля
или в соответствии с запросом. Время выполнения данных операций соответственно .find
Алгоритм поиска сам напрашивается из выше описанной структуры:
- если дерево пусто, то число не содержится в нашей структуре.
- если число равно полю или , то число в дереве есть.
- иначе ищем число в поддереве .
boolean find(t: Tree, x: int): if empty(t) return false if t.min == x or t.max == x return true return find(t.children[high(x)], low(x))
Заметим, что выполняя операцию
, мы либо спускаемся по дереву на один уровень ниже, либо, если нашли нужный нам элемент, выходим из нее. В худшем случае мы спустимся от корня до какого-нибудь 1-дерева, то есть выполним операцию столько раз, какова высота нашего дерева. На каждом уровне мы совершаем операций. Следовательно время работы .insert
Операция вставки элемента
состоит из нескольких частей:- если дерево пусто или в нем содержится единственный элемент ( ), то присвоим полям и соответствующие значения. Делать что-то еще бессмысленно, так как информация записанная в и полностью описывает состояние текущего дерева и удовлетворяет структуре нашего дерева.
- иначе:
- если элемент больше или меньше текущего дерева, то обновим соответствующее значение минимума или максимума, а старый минимум или максимум добавим в дерево.
- вставим во вспомогательное дерево число , если соответствующее поддерево до этого было пусто.
- вставим число в поддерево , за исключением ситуации, когда текущее дерево — это 1-дерево, и дальнейшая вставка не требуется.
function insert(t: Tree, x: int): if empty(t) // проверка на пустоту текущего дерева t.min = x t.max = x else if t.min == t.max // проверка, что в дереве один элемент if T.min < x t.max = x else t.min = x else if t.min > x swap(t.min, x) // релаксация минимума if t.max < x swap(t.max, x) // релаксация максимума if t.k != 1 if empty(t.children[high(x)]) insert(t.aux, high(x)) // вставка high(x) во вспомогательно дерево aux insert(t.children[high(x)], low(x)) // вставка low(x) в поддерево children[high(x)]
Нетрудно увидеть, что данная операция работает за время
. На каждом уровне дерева мы выполняем операций. После этого возможны 2 случая: поддерево пусто, и мы будем производить дальнейшую вставку и в него, и во вспомогательное дерево , или же поддерево не пусто, и мы просто спустимся на уровень ниже. Но если поддерево пусто, то вставка в него будет выполнена за , так как мы всего лишь обновим поля и . Все остальные операции будут выполнятся уже со вспомогательным деревом , высота которого на 1 уровень меньше, чем высота текущего. Если же поддерево не пусто, то мы просто перейдем к вставке элемента в это поддерево, высота которого так же на 1 меньше, чем у текущего. В итоге, каждый раз, выполнив операций, мы переходим к дереву, высота которого на 1 меньше, чем у текущего. Следовательно, количество операций пропорционально высоте дерева, которая, как уже было показано, . То есть операция вставки займет времени.remove
Удаление из дерева также делится на несколько подзадач:
- если , значит в дереве один элемент, удалим его и отметим, что дерево пусто.
- если , то мы должны найти следующий минимальный элемент в этом дереве, присвоить значение второго минимального элемента и удалить его из того места, где он хранится. Второй минимум — это либо , либо (для случая действуем аналогично).
- если же и , то мы должны удалить из поддерева .
Так как в поддеревьях хранятся не все биты исходных элементов, а только часть их, то для восстановления исходного числа, по имеющимся старшим и младшим битам, будем использовать функцию
. Также нельзя забывать, что если мы удаляем последнее вхождение , то мы должны удалить из вспомогательного дерева.
function remove(t: Tree, x: int): if t.min == x and t.max == x // случай, когда в дереве один элемент t.min = none return if t.min == x if empty(t.aux) t.min = t.max return x = merge(t.aux.min, t.children[t.aux.min].min) t.min = x if t.max == x if empty(t.aux) t.max = t.min return else x = merge(t.aux.max, t.children[t.aux.max].max) t.max = x if empty(t.aux) // случай, когда элемента x нет в дереве return remove(t.children[high(x)], low(x)) if empty(t.children[high(x)]) // если мы удалили из поддерева последний элемент remove(t.aux, high(x)) // то удаляем информацию, что это поддерево не пусто
Оценка времени работы операции
такая же, как и у операции . На каждом уровне дерева мы совершаем операций и переходим к удалению элементов максимум в двух деревьях(в одном поддереве и во вспомогательном дереве), чьи высоты на один меньше текущей. Но если мы производим операцию удаления из вспомогательного дерева, значит удаление из поддерева потребовало операций, так как оно содержало всего один элемент. В итоге, количество операций пропорционально высоте дерева, то есть .next и prev
Алгоритм нахождения следующего элемента, как и два предыдущих, сводится к рассмотрению случая, когда дерево содержит не более одного элемента, либо к поиску в одном из его поддеревьев:
- если дерево пусто, или максимум этого дерева не превосходит , то следующего элемента в этом дереве не существует.
- если меньше поля , то искомый элемент и есть .
- если дерево содержит не более двух элементов, и , то искомый элемент .
- если же в дереве более двух элементов, то:
- если в дереве есть еще числа, большие , и чьи старшие биты равны , то продолжим поиск в поддереве , где будем искать число, следующее после .
- иначе искомым элементом является либо минимум следующего непустого поддерева, если такое есть, либо максимум текущего дерева в противном случае.
int next(t: Tree, x: int) if empty(t) or t.max <= x return none; // следующего элемента нет if t.min > x return t.min; if empty(t.aux) return t.max; // в дереве не более двух элементов else if not empty(t.children[high(x)]) and t.childen[high(x)].max > low(x) return merge(high(x), next(t.children[high(x)], low(x))); // случай, когда следующее число начинается с high(x) else // иначе найдем следующее непустое поддерево int nextHigh = next(t.aux, high(x)); if nextHigh == none return t.max; // если такого нет, вернем максимум else return merge(nextHigh, t.children[nextHigh].min); // если есть, вернем минимум найденного поддерева
Время работы, как и всех предыдущих функций, оценивается так же, и равно
. Функция реализуется аналогично.Преимущества и недостатки
Преимущества
Главным преимуществом данной структуры является ее быстродействие. Асимптотически время работы операций дерева ван Эмде Боаса лучше, чем, например, у АВЛ, красно-черных, 2-3, splay и декартовых деревьев уже при небольшом количестве элементов. Конечно, из-за довольно непростой реализации возникают немалые постоянные множители, которые снижают практическую эффективность данной структуры. Но все же, при большом количестве элементов, эффективность дерева ван Эмде Боаса проявляется и на практике, что позволяет нам использовать данную структуру не только как эффективное дерево поиска, но и в других задачах. Например:
- cортировка последовательности из цифровая сортировка, асимптотика которой . чисел. Вставим элементы в дерево, найдем минимум и раз вызовем функцию . Так как все операции занимают не более времени, то итоговая асимптотика алгоритма , что даже лучше, чем
- алгоритм Дейкстры. Данный алгоритм с использованием двоичной кучи для поиска минимума работает за , где — количество вершин в графе, а — количество ребер между ними. Если же вместо кучи использовать дерево ван Эмде Боаса, то релаксация и поиск минимума будут занимать уже не , а , и итоговая асимптотика этого алгоритма снизится до .
Недостатки
- существенным недостатком данной структуры является то, что она позволяет хранить лишь целые неотрицательные числа, что существенно сужает область ее применения, по сравнению с другими деревьями поиска, которые не используют внутреннюю структуру элементов, хранящихся в них.
- другим серьезным недостатком является количество занимаемой памяти. Дерево, хранящее -битные числа, занимает памяти, что несложно доказывается индукцией, учитывая, что , где — количество памяти, занимаемое деревом, в котором хранятся -битные числа. Впрочем, можно попытаться частично избежать огромного расхода памяти, создавая необходимые поддеревья «лениво», то есть только тогда, когда они нам потребуются.