Поиск последнего элемента двоичной кучи

quoting Wikipedia:

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

Любые идеи о том, как может работать такой алгоритм?

Мне не удалось найти какую-либо информацию об этой проблеме, поскольку большинство двоичных кучек реализованы с использованием массивов.

Любая помощь была оценена.


Недавно я зарегистрировал учетную запись OpenID и не могу редактировать свои начальные сообщения и комментарии. Вот почему я отвечаю через этот ответ. Извините за это.


цитирование Митча Пшеница:

@Yse: это ваш вопрос "Как найти последний элемент двоичной кучи"?

Да, это так. Или, если быть более точным, мой вопрос: "Как найти последний элемент двоичной кучи на основе массива?".

quoting Подавляющий огонь:

Есть ли какой-то контекст, в котором вы задавая этот вопрос? (т.е. существует какая-то конкретная проблема, которую вы пытаетесь решить?)

Как указано выше, я хотел бы знать хороший способ "найти последний элемент двоичной кучи на основе не-массива", который необходим для вставки и удаления узлов.

цитируя Роя:

Мне кажется наиболее понятным просто используйте обычное двоичное дерево структуры (используя pRoot и Nodeопределяется как [данные, pLeftChild, pRightChild]) и добавьте два дополнительных указатели (pInsertionNode и pLastNode). pInsertionNode и pLastNode будет обновляться во время подпрограммы вставки и удаления сохранять их в актуальном состоянии, когда данные внутри структуры изменений. Эта дает O (1) доступ к обоим вводам point и last node структуры.

Да, это должно сработать. Если я не ошибаюсь, было бы немного сложно найти вставку node и последний node, когда их местоположения меняются на другое поддерево из-за удаления/вставки. Но я попробую.

цитируя Зак Скривена:

Как выполнить первую глубину поиск...

Да, это был бы хороший подход. Я тоже попробую.

Тем не менее мне интересно, если есть способ "рассчитать" местоположения последнего node и точки вставки. Высота бинарной кучи с N узлами может быть рассчитана путем взятия логарифма (базы 2) наименьшей степени, равной 2, что больше N. Возможно, также можно вычислить количество узлов на самом глубоком уровне. Тогда было возможно определить, как пройти кучу, чтобы достичь точки вставки или node для удаления.

Ответы

Ответ 1

В основном цитируемый оператор относится к проблеме разрешения местоположения для вставки и удаления элементов данных в кучу и из нее. Чтобы поддерживать "свойство формы" двоичной кучи, минимальный уровень кучи всегда должен быть заполнен слева направо, не оставляя пустых узлов. Чтобы поддерживать среднее время вставки и удаления O (1) для двоичной кучи, вы должны иметь возможность определить местоположение для следующей вставки и местоположение последнего node на нижнем уровне для использования для удаления корня node, как в постоянное время.

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

Для двоичной кучи, хранящейся в древовидной структуре, эта информация не так очевидна, но поскольку она представляет собой полное двоичное дерево, ее можно вычислить. Например, в полном двоичном дереве с 4 элементами точка вставки всегда будет правым дочерним элементом левого дочернего элемента корня node. node, который будет использоваться для удаления, всегда будет левым дочерним элементом левого дочернего элемента корня node. И для любого заданного произвольного размера дерева дерево всегда будет иметь определенную форму с четко определенными точками вставки и удаления. Поскольку дерево является "полным двоичным деревом" с определенной структурой для любого заданного размера, очень возможно рассчитать местоположение вставки/удаления в O (1) раз. Однако улов в том, что даже когда вы знаете, где это структурно, вы не знаете, где будет node в памяти. Таким образом, вам нужно пройти дерево, чтобы перейти к данному node, который является процессом O (log n), делая все вставки и удаления минимум O (log n), нарушая обычно желаемое поведение O (1). Любой поиск ( "глубина-первый" или какой-либо другой) будет по меньшей мере равно O (log n) из-за отмеченной обходной проблемы и обычно O (n) из-за случайного характера полусортированной кучи.

Хитрость заключается в том, чтобы одновременно рассчитывать и ссылаться на эти точки вставки/удаления в постоянное время либо путем увеличения структуры данных ( "потоки" дерева, как упоминание в статье в Википедии), либо с использованием дополнительных указателей.

Реализация, которая кажется мне самой простой для понимания, с низкой памятью и дополнительными издержками для кодирования, заключается в том, чтобы просто использовать обычную простую двоичную древовидную структуру (используя pRoot и node, определенные как [data, pParent, pLeftChild, pRightChild]) и добавьте два дополнительных указателя (pInsert и pLastNode). pInsert и pLastNode будут обновляться во время подпрограмм вставки и удаления, чтобы поддерживать их при изменении данных в структуре. Эта реализация дает O (1) доступ как к точке вставки, так и к последнему node структуры и должна обеспечивать сохранение общего поведения O (1) как при вставке, так и делеции. Стоимость реализации - это два дополнительных указателя и некоторый дополнительный дополнительный код в подпрограммах вставки/удаления (он же минимальный).

EDIT: добавлен псевдокод для вставки O (1)()

Вот псевдокод для подпрограммы вставки, которая является O (1), в среднем:

define Node = [T data, *pParent, *pLeft, *pRight]

void insert(T data)
{
    do_insertion( data );   // do insertion, update count of data items in tree

    # assume: pInsert points node location of the tree that where insertion just took place
    #   (aka, either shuffle only data during the insertion or keep pInsert updated during the bubble process)

    int N = this->CountOfDataItems + 1;     # note: CountOfDataItems will always be > 0 (and pRoot != null) after an insertion

    p = new Node( <null>, null, null, null);        // new empty node for the next insertion

    # update pInsert (three cases to handle)
    if ( int(log2(N)) == log2(N) )
        {# #1 - N is an exact power of two
        # O(log2(N))
        # tree is currently a full complete binary tree ("perfect")
        # ... must start a new lower level
        # traverse from pRoot down tree thru each pLeft until empty pLeft is found for insertion
        pInsert = pRoot;
        while (pInsert->pLeft != null) { pInsert = pInsert->pLeft; }    # log2(N) iterations
        p->pParent = pInsert;
        pInsert->pLeft = p;
        }
    else if ( isEven(N) )
        {# #2 - N is even (and NOT a power of 2)
        # O(1)
        p->pParent = pInsert->pParent;
        pInsert->pParent->pRight = p;
        }
    else 
        {# #3 - N is odd
        # O(1)
        p->pParent = pInsert->pParent->pParent->pRight;
        pInsert->pParent->pParent->pRight->pLeft = p;
        }
    pInsert = p;

    // update pLastNode
    // ... [similar process]
}

Таким образом, insert (T) - это O (1) в среднем: точно O (1) во всех случаях, за исключением случаев, когда дерево должно быть увеличено на один уровень, когда оно равно O (log N), которое происходит при каждом входе в log N (при отсутствии исключений). Добавление другого указателя (pLeftmostLeaf) может сделать insert() O (1) для всех случаев и позволяет избежать возможного патологического случая чередующейся вставки и удаления в полном полном двоичном дереве. (Добавление pLeftmost остается в виде упражнения [это довольно легко].)

Ответ 2

Мой первый раз, чтобы участвовать в переполнении стека.

Да, вышеупомянутый ответ Зак Скривена (бог, я не знаю, как правильно обращаться к другим людям, извините) прав. То, что я хочу добавить, является упрощенным способом, если нам присваивается количество узлов.

Основная идея:

Учитывая количество N узлов в этом полном двоичном дереве, выполните вычисления "N% 2" и вытащите результаты в стек. Продолжите вычисление до N == 1. Затем выложите результаты. Результат равен 1, а 0 - влево. Последовательность представляет собой маршрут от корня до целевой позиции.

Пример:

Теперь дерево имеет 10 узлов, я хочу вставить еще один node в позицию 11. Как его проложить?

11 % 2 = 1  --> right    (the quotient is 5, and push right into stack)
 5 % 2 = 1  --> right    (the quotient is 2, and push right into stack)
 2 % 2 = 0  --> left     (the quotient is 1, and push left into stack. End)

Затем поместите стек: влево → вправо → вправо. Это путь от корня.

Ответ 3

Вы можете использовать двоичное представление размера двоичной кучи, чтобы найти местоположение последнего node в O (log N). Размер может быть сохранен и увеличен, что займет время O (1). Основной концепцией этого является структура двоичного дерева.

Предположим, что размер кучи равен 7. Бинарное представление 7 равно "111". Теперь не забудьте всегда опустить первый бит. Итак, теперь мы остаемся с "11". Читайте слева направо. Бит равен "1", поэтому перейдите в правый дочерний элемент корня node. Затем строка остается "1", первый бит - "1". Итак, снова перейдите к правильному ребенку текущего node, на котором вы находитесь. Поскольку у вас больше нет бит для обработки, это означает, что вы достигли последнего node. Таким образом, сырая обработка процесса заключается в том, что преобразование размера кучи в биты. Опустите первый бит. В соответствии с самым левым битом перейдите в правый дочерний элемент текущего node, если он равен "1", и левому ребру текущего node, если он равен "0".

Как всегда, до самого конца двоичного дерева эта операция всегда занимает время O (log N). Это простая и точная процедура для поиска последнего node.

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

Надеюсь, мой ответ помог вам, если да, дайте мне знать...! ☺

Ответ 4

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


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

Пусть N - общее число узлов в дереве, а H - высота дерева.

Некоторые значения (N, H) являются (1,0), (2,1), (3,1), (4,2),..., (7,2), (8, 3). Общая формула, связывающая два, H= ceil [log2 ( N +1)] - 1. Теперь, учитывая только N, мы хотим перейти от корня к позиции для нового node, наименьшее количество шагов, т.е. Без какого-либо "обратного отслеживания". Сначала мы вычисляем общее число узлов M в идеальном бинарном дереве высотой H= ceil [log2 ( N +1)] - 1, которая M= 2 ^ ( H +1) - 1.

Если N == M, наше дерево идеально, а новый node должен быть добавлен на новый уровень. Это означает, что мы можем просто выполнить DFS (слева направо), пока мы не нажмем первый лист; новый node становится левым дочерним элементом этого листа. Конец истории.

Однако, если N < M, то на последнем уровне нашего дерева все еще есть вакансии, а новый node должен быть добавлен в крайнее левое свободное место. Число узлов, которые уже находятся на последнем уровне нашего дерева, просто ( N - 2 ^ H + 1). Это означает, что новый node занимает точку X= ( N - 2 ^ H + 2) слева, на последнем уровне,

Теперь, чтобы добраться от корня, вам нужно будет сделать правильные повороты (L vs R) на каждом уровне, чтобы вы оказались на месте X на последнем уровне. На практике вы определяете повороты с небольшим вычислением на каждом уровне. Тем не менее, я думаю, что в следующей таблице показана общая картина и соответствующие шаблоны, не погрязшие в арифметике (вы можете признать это как форму арифметическое кодирование для равномерного распределения):

0 0 0 0 0 X 0 0 <--- represents the last level in our tree, X marks the spot!
          ^
L L L L R R R R <--- at level 0, proceed to the R child
L L R R L L R R <--- at level 1, proceed to the L child
L R L R L R L R <--- at level 2, proceed to the R child 
          ^                      (which is the position of the new node)
          this column tells us
          if we should proceed to the L or R child at each level

EDIT: добавлено описание того, как перейти к новому node в кратчайшее количество шагов, предполагая, что мы знаем общее количество узлов в дереве.

Ответ 5

Решение в случае, если у вас нет ссылки на родителя!!! Чтобы найти нужное место для следующего node, у вас есть 3 случая для обработки

  • case (1) Уровень дерева завершен Log2 (N)
  • case (2) Дерево node count даже
  • case (3) Дерево node count нечетно

Вставка:

void Insert(Node root,Node n)
{
Node parent = findRequiredParentToInsertNewNode (root);
if(parent.left == null)
parent.left = n;
else
parent.right = n;
}

Найдите родительский элемент node, чтобы вставить его

void findRequiredParentToInsertNewNode(Node root){

Node last = findLastNode(root);

 //Case 1 
 if(2*Math.Pow(levelNumber) == NodeCount){
     while(root.left != null)
      root=root.left;
  return root;
 }
 //Case 2
 else if(Even(N)){
   Node n =findParentOfLastNode(root ,findParentOfLastNode(root ,last));
 return n.right;
 }
//Case 3
 else if(Odd(N)){
  Node n =findParentOfLastNode(root ,last);
 return n;
 }

}

Чтобы найти последний node, вам необходимо выполнить BFS (поиск по ширине) и получить последний элемент в очереди

 Node findLastNode(Node root)
 {
     if (root.left == nil)
         return root

     Queue q = new Queue();

     q.enqueue(root);
     Node n = null;

     while(!q.isEmpty()){
      n = q.dequeue();
     if ( n.left != null )
         q.enqueue(n.left);
     if ( n.right != null )
         q.enqueue(n.right);
        }
   return n;
}

Найти родителя последнего node, чтобы установить node в null в случае замены корнем в случае удаления

 Node findParentOfLastNode(Node root ,Node lastNode)
{
    if(root == null)
        return root;

    if( root.left == lastNode || root.right == lastNode )
        return root;

    Node n1= findParentOfLastNode(root.left,lastNode);
    Node n2= findParentOfLastNode(root.left,lastNode);

    return n1 != null ? n1 : n2;
}

Ответ 6

Я знаю, что это старый поток, но я искал ответ на тот же вопрос. Но я не мог позволить себе решение o (log n), поскольку мне пришлось найти последние node тысячи раз за несколько секунд. У меня был алгоритм O (log n), но моя программа сканировалась из-за того, сколько раз она выполняла эту операцию. Поэтому после долгих раздумий я наконец нашел исправление для этого. Не уверен, что это интересно.

Это решение O (1) для поиска. Для вставки он определенно меньше O (log n), хотя я не могу сказать, что это O (1).

Просто хотел добавить, что если есть интерес, я также могу предоставить свое решение. Решение состоит в том, чтобы добавить узлы в двоичной куче в очередь. Каждая очередь node имеет указатели спереди и сзади. Мы продолжаем добавлять узлы в конец этой очереди слева направо, пока не достигнем последнего node в двоичной куче. На этом этапе последний node в двоичной куче будет находиться в задней части очереди. Каждый раз, когда нам нужно найти последний node, мы выходим из задней части, а второй-последний теперь становится последним node в дереве. Когда мы хотим вставить, мы ищем назад назад для первого node, где мы можем вставить и поместить его туда. Это не совсем O (1), но значительно сокращает время работы.