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

Куча представляет собой классическую структуру данных, которая ставит полное двоичное (или d-ary для обобщенной версии) дерево в смежный массив, сохраняя элементы в порядке прохождения по ширине. Таким образом, все элементы с одного уровня дерева сохраняются смежными друг за другом.

Я реализую структуру данных, которая под капотом представляет собой полное сбалансированное дерево фиксированной степени d, и я хочу сохранить дерево в смежной форме, чтобы освободить пространство указателей node. Поэтому я подумал о том, чтобы разместить узлы в первом порядке, используемом в кучах, но затем я беспокоюсь о производительности кеша типичного поиска от корня до листа, поскольку на каждом уровне l я перепрыгиваю через много элементы.

Есть ли способ получить компактное непрерывное представление д-арного полного дерева, основанное на глубине-первом порядке?

Таким образом, узлы, затронутые во время поиска листа, кажутся мне более вероятными, находящимися ближе друг к другу. Проблема в том, как получить индекс родителя и дочерних элементов node, но также мне интересно, какие операции над деревом в целом эффективны в этой настройке.

Я реализую эту вещь на С++, если это имеет значение вообще.

Ответы

Ответ 1

Для простоты я собираюсь ограничить мое обсуждение двоичными деревьями, но то, что я говорю, справедливо и для n-арных деревьев.

Причина, по которой кучи (и деревья вообще) хранятся в массивах по ширине, - это потому, что гораздо проще добавлять и удалять элементы таким образом: расти и сжимать дерево. Если вы сохраняете глубину вначале, то либо дерево должно быть выделено с максимальным ожидаемым размером, либо вам придется делать много движущихся предметов вокруг, когда вы добавляете уровни.

Но если вы знаете, что у вас будет полное, сбалансированное, n-арное дерево, тогда выбор представления BFS или DFS в значительной степени зависит от стиля. Для производительности памяти нет какой-либо особой выгоды для другой. В одном представлении (DFS) вы берете промахи кеша спереди, а в другом случае (BFS) вы берете пропуски кеша в конце.

Рассмотрим двоичное дерево с 20 уровнями (т.е. 2 ^ 20 - 1 элементы), которое содержит числа от 0 до (2 ^ 20 - 1). Каждый node занимает четыре байта (размер целого).

С BFS вы получаете пропущенную кеш, когда получаете первый блок дерева. Но тогда у вас есть первые четыре уровня дерева в кеше. Таким образом, ваши следующие три запроса гарантированы в кеше. После этого вам гарантированно будет пропустить кеш, если индекс node больше 15, потому что левый дочерний элемент находится в x*2 + 1, который будет не менее 16 позиций (64 байта) от родителя.

При использовании DFS вы должны пропустить кеш при чтении первого блока дерева. Пока номер, который вы ищете, находится в левом поддереве текущего node, вы гарантированно не получите пропущенную кеш для первых 15 уровней (т.е. Вы постоянно идите влево). Но любая ветка, которая идет правильно, понесет промахи в кеше, пока вы не опуститесь на три уровня выше листьев. В этот момент все поддерево будет вписываться в кеш, а оставшиеся запросы не будут пропускать кеш.

С BFS количество промахов в кеше прямо пропорционально количеству уровней, которые вы должны искать. С DFS количество промахов в кеше пропорционально пути, пройденному по дереву, и количеству уровней, которые вы должны искать. Но в среднем количество промахов в кэше, которое вы понесете при поиске элемента, будет одинаковым для DFS, как для BFS.

И математика для вычисления позиций node проще для BFS, чем для DFS, особенно если вы хотите найти родителя для определенного node.

Ответ 2

Казалось бы, нужен индикатор is_leaf. Поскольку большинство всего связано с уровнем, нам нужен быстрый способ найти его, который, похоже, зависит от знания, является ли node листом или нет.

В нижеприведенных фрагментах предполагается, что позиция node относительно родителя известна... она не очень и почти бесполезна, поскольку все дело в том, чтобы сэкономить место.

int parent_index(int index, int pos){
  if (pos == LEFT){
    return i-1;
  } else {
    return i - pow(2,num_levels - level(i));
  }
}

int left_child_index(int index){
  return i+1;
}
int right_child_index(int index){
  return i+pow(2,num_levels - level(index))
}

Чтобы получить уровень node, вы можете ходить левыми детьми, пока не попадете в лист.

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

Ответ 3

У меня просто была мысль.

Как насчет порядка infix? Таким образом, все гораздо проще вычислить:

bool is_leaf(unsigned int i){
  return !(i%2);
}

unsigned int left_child(unsigned int i){
  return i - pow(2,num_levels - level(i));
}
unsigned int left_child(unsigned int i){
  return i + pow(2,num_levels - level(i));
}

int level(unsigned int i){
  unsigned int offset = 1;
  unsigned int level_bits = 1;
  int level = 0;
  while ((i - offset)&level_bits == 0){
    level++;
    offset += pow(2,level);
    level_bits = (level_bits << 1) + 1; /* should be a string of trailing 1s */
  }
  return level;
}

Таким образом, вы должны получать большие прыжки только в верхней части большинства узлов. После этого прыжки становятся экспоненциально меньшими. Красота заключается в том, что, поскольку на низких уровнях меньше узлов, вы можете их кэшировать. Если дерево намного более плотное (т.е. Больше сравнений), то прыжки намного меньше.

Откат назад - вставки медленные:

void insert_node(node[] node_array, node new_node){
  for (unsigned int i = num_nodes-1; i >= 0; i--){
    node_array[i*2 + 1] = node_array[i];
    node_array[i] = NULL_NODE_VALUE; /* complete (but not full) trees are discontiguous */
  }
  node_arry[0] = new_node;
}

Этот порядок инфикса, без сомнения, намного лучше, чем префикс (поиск по глубине первого поиска), так как дерево логически и физически "сбалансировано". В порядке префикса левая сторона предпочитается намного больше - поэтому она будет вести себя как несбалансированное дерево. По крайней мере, с помощью infix вы получаете сбалансированный и быстрый поиск среди плотных узлов.

Ответ 4

Двоичные деревья поиска используются для хранения информации, которая впоследствии может быть запрошена и отсортирована эффективно. Левый node любого конкретного node содержит значение, которое меньше, чем значение node, и right node, содержащее большее значение.

Куча - эффективная реализация почти полных бинарных деревьев поиска?

Деревья двоичного поиска нуждаются по крайней мере в двух других указателях (поскольку они также могут быть родительским указателем), кроме значения данных, представленного конкретным node. Структуры на основе кучи преобразуют эти манипуляции с указателями в манипуляции с индексами массива, используя свойство почти полного BST. Мы также знаем, что если конкретный BST не близок к почти полному BST, мы создаем отверстия в представлении массива этого двоичного дерева, чтобы поддерживать связь между родительским и дочерним узлами. Это означает, что это может аннулировать стоимость использования указателей в таких случаях.

Реализовать структуру кучи, основанную на глубине первого порядка обхода дерева?

Путем реализации структуры кучи, подобной структуре для обхода дерева по глубине, мы больше не можем обосновать причину использования кучи на первом месте. Поскольку глубина не фиксирована в отличие от ширины дерева (которая может быть рассчитана на определенном уровне, данное дерево почти завершено BST), мы должны манипулировать сложной взаимосвязью между элементами. И всякий раз, когда добавляется новый элемент, добавленный в/удаляемый из дерева, мы также должны перегруппировать элементы, чтобы они все еще удовлетворяли свойству кучи. Итак, я не думаю, что мы сможем оправдать использование кучи над BST, если это будет реализовано таким образом.