Как можно реализовать std:: make_heap при максимальном сравнении 3N?

Я просмотрел стандарт С++ 0x и нашел требование, чтобы make_heap выполнял не более 3 * N сравнений.

т.е. heapify неупорядоченная коллекция может быть выполнена в O (N)

   /*  @brief  Construct a heap over a range using comparison functor.

Почему это?

Источник не дает мне никаких подсказок (g++ 4.4.3)

В то время как (true) + __parent == 0 не являются подсказками, а скорее предположением для поведения O (N)

template<typename _RandomAccessIterator, typename _Compare>
void
make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
          _Compare __comp)
{

  const _DistanceType __len = __last - __first;
  _DistanceType __parent = (__len - 2) / 2;
  while (true)
    {
      _ValueType __value = _GLIBCXX_MOVE(*(__first + __parent));
      std::__adjust_heap(__first, __parent, __len, _GLIBCXX_MOVE(__value),
                 __comp);
      if (__parent == 0)
        return;
      __parent--;
    }
}

__ adjust_heap выглядит как метод журнала N:

while ( __secondChild < (__len - 1) / 2)
{
    __secondChild = 2 * (__secondChild + 1);

Является ли стандартным журналом Nog для меня.

  template<typename _RandomAccessIterator, typename _Distance,
       typename _Tp, typename _Compare>
    void
    __adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex,
          _Distance __len, _Tp __value, _Compare __comp)
    {
      const _Distance __topIndex = __holeIndex;
      _Distance __secondChild = __holeIndex;
      while (__secondChild < (__len - 1) / 2)
      {
        __secondChild = 2 * (__secondChild + 1);
          if (__comp(*(__first + __secondChild),
             *(__first + (__secondChild - 1))))
          __secondChild--;
          *(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first + __secondChild));
          __holeIndex = __secondChild;
      }
      if ((__len & 1) == 0 && __secondChild == (__len - 2) / 2)
      {
        __secondChild = 2 * (__secondChild + 1);
        *(__first + __holeIndex) = _GLIBCXX_MOVE(*(__first
                             + (__secondChild - 1)));
        __holeIndex = __secondChild - 1;
      }
      std::__push_heap(__first, __holeIndex, __topIndex, 
               _GLIBCXX_MOVE(__value), __comp);      
      }

Любые подсказки, почему это O <= 3N, будут оценены. EDIT:

Экспериментальные результаты:

В этой фактической реализации используется

  • < 2N сравнение для heapifying heaps
  • < 1,5N для heapifying кучи в обратном порядке.

Ответ 1

@templatetypedef уже дал хороший ответ, почему асимптотическое время выполнения build_heap равно O (n). В главе 6 также содержится доказательство CLRS, второе издание.

Что касается того, почему стандарт С++ требует, чтобы использовалось не более 3n сравнений:

Из моих экспериментов (см. код ниже), похоже, что на самом деле требуется менее 2-х сравнений. Фактически, эти примечания к лекции содержат доказательство того, что build_heap использует только 2 (n-⌈log n⌉) сравнения.

Оценка стандарта кажется более щедрым, чем требуется.


def parent(i):
    return i/2

def left(i):
    return 2*i

def right(i):
    return 2*i+1

def heapify_cost(n, i):
    most = 0
    if left(i) <= n:
        most = 1 + heapify_cost(n, left(i))
    if right(i) <= n:
        most = 1 + max(most, heapify_cost(n, right(i)))
    return most

def build_heap_cost(n):
    return sum(heapify_cost(n, i) for i in xrange(n/2, 1, -1))

Некоторые результаты:

n                     10  20  50  100  1000  10000
build_heap_cost(n)     9  26  83  180  1967  19960

Ответ 2

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

Алгоритм работает следующим образом. Начните с того, что возьмите половину узлов и обработайте их как одиночные max-heaps - так как есть только один элемент, дерево, содержащее только этот элемент, должно автоматически быть максимальной кучей. Теперь возьмите эти деревья и соедините их друг с другом. Для каждой пары деревьев возьмите одно из значений, которые вы еще не использовали, и выполните следующий алгоритм:

  • Создайте новый node корень кучи, указав его левые и правые указатели для детей на две max-heaps.

  • В то время как этот node имеет дочерний объект, который больше, чем он, замените его дочерним по отношению к его более крупному дочернему элементу.

Моя претензия заключается в том, что эта процедура заканчивается созданием новой максимальной кучи, содержащей элементы двух входных макс-кучек, и делает это во времени O (h), где h - высота двух куч. Доказательство является индукцией по высоте куч. В качестве базового случая, если подповерхности имеют нулевой размер, тогда алгоритм немедленно завершается с помощью singleton max-heap, и он делает это в O (1) раз. Для индуктивного шага предположим, что для некоторого h эта процедура работает на любых подпунктах размера h и учитывает, что происходит, когда вы выполняете ее на двух кучах размера h + 1. Когда мы добавляем новый корень для объединения двух поддеревьев размера h + 1, существует три возможности:

  • Новый корень больше корней обоих поддеревьев. Тогда в этом случае мы имеем новую max-кучу, так как корень больше любого из узлов в любом поддереве (по транзитивности)

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

  • Новый корень меньше, чем его дети. Затем, используя слегка модифицированную версию вышеупомянутого анализа, мы можем показать, что полученное дерево действительно является кучей.

Кроме того, поскольку на каждом шаге высоты кучи ребенка уменьшаются на единицу, общая продолжительность выполнения этого алгоритма должна быть O (h).


В этот момент мы имеем простой алгоритм для создания кучи:

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

Так как на каждом шаге мы знаем, что кучи, которые мы имеем до сих пор, являются допустимыми max-heaps, в конечном итоге это дает допустимую общую максимальную кучу. Если мы умны с тем, как мы выбираем, сколько одиночных кучек нужно сделать, это также приведет к созданию полного двоичного дерева.

Однако, похоже, что это должно выполняться в O (n lg n), так как мы делаем O (n), каждый из которых работает в O (h), а в худшем случае высота деревьев we "Объединение - это O (lg n). Но эта оценка не является жесткой, и мы можем сделать намного лучше, уточнив ее анализ.

В частности, подумайте о том, насколько глубоки все деревья, с которыми мы сливаемся. Около половины куч имеет глубину нуля, затем половина того, что осталось, имеет глубину 1, затем половина того, что осталось, имеет глубину два и т.д. Если подвести итог, получим сумму

0 * n/2 + 1 * n/4 + 2 * n/8 +... + nk/(2 k) = & Sigma; k = 0 & lceil; log n & rceil; (nk/2 k) = n & Sigma; k = 0 & lceil; log n & rceil; (k/2 k + 1)

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

n & Sigma; k = 0 (k/2 k)

Суммирование здесь представляет собой суммирование 0/2 0 + 1/2 1 + 2/2 2 + 3/2 3 +.... Это известное суммирование, которое можно оценить несколькими способами. Один из способов оценить это: в этих слайдах слайдов, слайдов 45-47. Он заканчивается ровно на 2n, а это означает, что количество сравнений, которые заканчиваются созданием, конечно ограничено сверху на 3n.

Надеюсь, это поможет!