Родительский индекс может быть найден в O (log * n) время и O (1) место.
Здесь журнал * п означает iterated logarithm: число раз функция логарифм должна быть итеративно применяется до результата меньше или равна 1.
На самом деле, что может быть сделано даже быстрее - в O (1) раз, если бы мы могли позволить себе пространство O (n) для большой таблицы поиска (хранения родительского индекса для каждого узла в дереве).
Ниже я набросаю несколько алгоритмов, которые не нуждаются в дополнительном пространстве и дают результат в O (log n) наихудшем случае, O (log log n) ожидаемое время, O (log log n) наихудшее время, и O (log * n) наихудший случай времени. Они основаны на следующие свойства индексов после заказа для идеального бинарного дерева:
- Всех индексы на крайнем левом пути дерева, равны 2 я -1.
- Индексы каждого правого дочернего узла узла на самом левом пути равны 2 i -2.
- Любой узел на самом левом пути и его правое поддерево обозначены индексами с самым значительным ненулевым битом в той же позиции:
i
.
- Левое поддерево любого узла на самом левом пути содержит 2 i -1 узлы. (Это означает, что после вычитания 2 i -1 мы получим узел, который расположен таким же образом относительно его родителя, имеет ту же глубину, но ближе к «специальным» узлам, удовлетворяющим свойствам # 1 и # 2).
Свойство # 1 и # 2 дают простые алгоритмы, чтобы получить родительский узел для некоторых узлов дерева: для индексов вида 2 я -1, умножить на 2
и добавить 1
; для индексов формы 2 i -2, просто добавьте 1
. Для других узлов мы могли многократно использовать свойство # 4 для перехода к узлу, удовлетворяющему свойству # 1 или # 2 (путем вычитания размеров нескольких левых поддеревьев), затем найдите некоторый родительский узел, расположенный на самом левом пути, затем добавьте обратно все ранее вычитаемые значения. И свойство № 3 позволяет быстро найти размер, который следует вычесть под деревьями. Таким образом, мы по следующему алгоритму:
- While
((x+1) & x) != 0
и ((x+2) & (x+1)) != 0
повторите шаг 2.
- Очистить наиболее значащий ненулевая битном и добавить
1
. Накопите разницу.
- Если
((x+1) & x) == 0
, умнож. На 2
и добавить 1
; в противном случае, если ((x+2) & (x+1)) == 0
, добавьте 1
.
- Добавьте все отличия, накопленные на шаге 2.
Например, 12
(в двоичной форме 0b1100
) преобразуется на этапе # 2 к 0b0101
, затем 0b0010
(или 2
в десятичной системе). Накопленная разница составляет 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 log n).
Вместо разделения битов индекса на две равные наборы мы могли бы использовать другую стратегию:
- Compute количество населения индекса и добавить его к значению индекса. Самый значительный бит, который изменился с
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
; в противном случае, если ((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);
}
Если нам нужно найти родитель только для листа узла, этот алгоритм и код могут быть упрощен. (Ideone).
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);
}
Является ли ваше дерево бинарное дерево поиска или просто Binary Tree? Приведенный пример не имеет свойства поиска, но простое двоичное дерево не может найти элемент в O (log n), так что это? – Simon
@DAnstahr u can not даже сделать это в O (logn) в дереве примеров, поскольку это не BST –
@ Danstahr Я считаю, что ваша структура данных - это максимальная куча, а для максимальной кучи - O (N), чтобы найти элемент или его родитель. –