Ответ 1
Родительский индекс можно найти в O (log * n) и O (1).
Здесь log * n означает итерированный логарифм: количество раз, когда функция логарифма должна быть итеративно применена до того, как результат будет меньше или равен 1.
Фактически это можно сделать еще быстрее - в O (1) раз, если бы мы могли позволить O (n) пространство для большой таблицы поиска (хранения родительского индекса для каждого узла в дереве).
Ниже я опишу несколько алгоритмов, которые не нуждаются в дополнительном пространстве и приводят к наихудшему случаю O (log n), ожидаемому времени O (log log n), O (log log n) наихудшего временного времени и O (log * n) наихудшее время. Они основаны на следующих свойствах пост-порядковых индексов для идеального двоичного дерева:
- Все индексы на самом левом пути дерева равны 2 i -1.
- Индексы каждого правого дочернего узла узла на самом левом пути равны 2 i -2.
- Любой узел на самом левом пути и его правое поддерево обозначаются индексами, имеющими самый значительный ненулевой бит в одной и той же позиции:
i
. - Левое поддерево любого узла на самом левом пути содержит 2 i -1 узла. (Это означает, что после вычитания 2 i -1 мы получим узел, который расположен таким же образом относительно его родителя, имеет ту же глубину, но ближе к "специальным" узлам, удовлетворяющим свойствам # 1 и # 2).
Свойства # 1 и # 2 дают простые алгоритмы для получения родительского узла для некоторых узлов дерева: для индексов формы 2 i -1 умножить на 2
и добавить 1
; для индексов формы 2 i -2, просто добавьте 1
. Для других узлов мы могли неоднократно использовать свойство # 4 для перехода к узлу, удовлетворяющему свойству # 1 или # 2 (путем вычитания размеров нескольких левых поддеревьев), затем найдите некоторый родительский узел, расположенный на крайнем левом пути, затем добавьте все ранее вычитаемые значения. И свойство № 3 позволяет быстро найти размер, который следует вычесть под деревьями. Итак, у нас есть следующий алгоритм:
- Хотя
((x+1) & x) != 0
и((x+2) & (x+1)) != 0
повторите шаг 2. - Очистить самый значительный ненулевой бит и добавить
1
. Накопите разницу. - Если
((x+1) & x) == 0
, умножьте на2
и добавьте1
; в противном случае if((x+2) & (x+1)) == 0
, добавьте1
. - Добавьте обратно все отличия, накопленные на шаге 2.
Например, 12
(в двоичной форме 0b1100
) преобразуется на этапе № 2 в 0b0101
, затем в 0b0010
(или 2
в десятичной 0b0010
). Накопленная разница составляет 10
. Шаг № 3 добавляет 1
и шаг 4 добавляет обратно 10
, поэтому результат равен 13
.
Другой пример: 10
(в двоичной форме 0b1010
) преобразуется на этапе № 2 в 0b0011
(или 3
в десятичной форме). Шаг №3 удваивает его (6
), затем добавляет 1
(7
). Шаг 4 добавляет обратно накопленную разницу (7
), поэтому результат равен 14
.
Сложность времени - O (log n) - но только тогда, когда все элементарные операции выполняются в O (1) раз.
Чтобы улучшить временную сложность, мы могли бы выполнять несколько итераций шага № 2 параллельно. Мы могли бы получить n/2
старших бита индекса и вычислить количество населения на них. Если после добавления результата к остальным младшим битам сумма не переполняется в эти старшие разряды, мы могли бы применить этот подход рекурсивно, используя сложность O (log log n). Если у нас есть переполнение, мы можем вернуться к исходному алгоритму (побито). Обратите внимание, что все установленные младшие разряды также должны рассматриваться как переполнение. Таким образом, итоговая сложность - это O (log log n) ожидаемое время.
Вместо того, чтобы возвращаться к побитому, мы могли обрабатывать переполнение с помощью двоичного поиска. Мы могли бы определить, сколько битов высокого порядка (меньше, чем n/2
) должно быть выбрано так, чтобы у нас либо не было переполнения, либо (как для индекса 0b00101111111111
) количество выбранных ненулевых битов высокого порядка равно 1
. Обратите внимание, что нам не нужно применять эту процедуру двоичного поиска несколько раз, потому что второе переполнение не произойдет, в то время как число бит в номере больше, чем O (log log n). Таким образом, возникающая сложность - это O (log log n) наихудшее временное время. Предполагается, что все элементарные операции выполняются в O (1) раз. Если в O (log log n) выполняются некоторые операции (подсчет численности населения, ведущий нулевой подсчет), то наша временная сложность возрастает до O (log 2 log n).
Вместо того, чтобы делить биты индекса на два равных набора, мы могли бы использовать другую стратегию:
- Вычислите подсчет населения индекса и добавьте его в значение индекса. Самый старший бит, который изменился с
0
на1
определяет точку разделения для бит высокого или младшего разряда. - Вычислить количество населения на старших битах, а затем добавить результат к младшим битам.
- Если бит "разделения" отличен от нуля и
((x+1) & x) != 0
и((x+2) & (x+1)) != 0
, продолжайте с шага # 1. - Если установлены все младшие биты и задан младший бит высокого порядка, обработайте этот угловой регистр как правильный ребенок.
- Если
((x+1) & x) != 0
и((x+2) & (x+1)) != 0
, то также обрабатываем это как правый ребенок. - Если
((x+1) & x) == 0
, умножьте на2
и добавьте1
; в противном случае if((x+2) & (x+1)) == 0
, добавьте1
.
Если условие этапа № 3 выполнено, это означает, что добавление на шаге 2 привело к переносу бит разделения. Другие младшие разряды представляют собой некоторое число, которое не может быть больше, чем исходное количество населения. Количество установленных битов в этом числе не может быть больше логарифма подсчета совокупности исходного значения. Это означает, что количество заданных битов после каждой итерации не превышает логарифма числа битов на предыдущей итерации. Поэтому наихудшая временная сложность - O (log * n). Это очень близко к O (1). Например, для 32-битных чисел нам требуется примерно 2 итерации или меньше.
Каждый шаг этого алгоритма должен быть очевидным, за исключением, вероятно, шага №5, правильность которого должна быть доказана. Обратите внимание, что этот шаг выполняется только тогда, когда добавление результатов подсчета количества данных переносится в бит разделения, но добавление количества популяций только старших битов не приводит к этому переносу. Бит "Разделение" соответствует значению 2 i. Разница между численностью населения всех битов и количеством населения только старших бит не более i
. Итак, шаг № 5 имеет значение не менее 2 i -i. Пусть применяется бит-бит-алгоритм к этому значению. 2 i -i достаточно большой, чтобы установить бит i-1
. Очистите этот бит и добавьте 1
к значению в битах 0..i-2
. Это значение не менее 2 i -1 - (i -1), потому что мы просто вычитали 2 i -1 и добавили 1
. Или, если мы переместим индекс на одну позицию вправо, мы снова получим как минимум 2 i -i. Если мы повторим эту процедуру, мы всегда найдем ненулевой бит в позиции i-1
. Каждый шаг постепенно уменьшает разницу между значением в битах 0..i-1
и ближайшей мощностью 2
. Когда это различие приходит к 2
, мы можем остановить этот побитовый алгоритм, потому что текущий узел, очевидно, является правильным ребенком. Поскольку этот побитовый алгоритм всегда приходит к одному и тому же результату, мы можем пропустить его и всегда обрабатывать текущий узел как правильный ребенок.
Вот C++ реализация этого алгоритма. Более подробную информацию и некоторые другие алгоритмы можно найти на ideone.
uint32_t getMSBmask(const uint32_t x)
{ return 1 << getMSBindex(x); }
uint32_t notSimpleCase(const uint32_t x)
{ return ((x+1) & x) && ((x+2) & (x+1)); }
uint32_t parent(const uint32_t node)
{
uint32_t x = node;
uint32_t bit = x;
while ((x & bit) && notSimpleCase(x))
{
const uint32_t y = x + popcnt(x);
bit = getMSBmask(y & ~x);
const uint32_t mask = (bit << 1) - 1;
const uint32_t z = (x & mask) + popcnt(x & ~mask);
if (z == mask && (x & (bit << 1)))
return node + 1;
x = z;
}
if (notSimpleCase(x))
return node + 1;
else
return node + 1 + (((x+1) & x)? 0: x);
}
Если нам нужно найти родителя только для листового узла, этот алгоритм и код могут быть упрощены. (Идеал).
uint32_t parent(const uint32_t node)
{
uint32_t x = node;
while (x > 2)
{
const uint32_t y = x + popcnt(x);
const uint32_t bit = getMSBmask(y & ~x);
const uint32_t mask = (bit << 1) - 1;
x = (x & mask) + popcnt(x & ~mask);
}
return node + 1 + (x == 1);
}