В .NET Framework в PresentationCore.dll есть общий класс PriorityQueue<T>, код которого можно найти здесь.

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

using System;
using System.Collections.Generic;
using System.Diagnostics;
using MS.Internal;

namespace ConsoleTest {
    public static class ConsoleTest {
        public static void Main() {
            PriorityQueue<int> values = new PriorityQueue<int>(6, Comparer<int>.Default);
            Random random = new Random(88);
            for (int i = 0; i < 6; i++)
                values.Push(random.Next(0, 10000000));
            int lastValue = int.MinValue;
            int temp;
            while (values.Count != 0) {
                temp = values.Top;
                values.Pop();
                if (temp >= lastValue)
                    lastValue = temp;
                else
                    Console.WriteLine("found sorting error");
                Console.WriteLine(temp);
            }
            Console.ReadLine();
        }
    }
}

Результаты:

2789658
3411390
4618917
6996709
found sorting error
6381637
9367782

Существует ошибка сортировки, и если размер выборки увеличивается, количество ошибок сортировки увеличивается несколько пропорционально.

Я сделал что-то не так? Если нет, то где именно находится ошибка в коде класса PriorityQueue?

79
MathuSum Mut 27 Май 2017 в 23:44

2 ответа

Лучший ответ

Поведение может быть воспроизведено с использованием вектора инициализации [0, 1, 2, 4, 5, 3]. Результат:

[0, 1, 2, 4, 3, 5]

(мы видим, что 3 неправильно размещен)

Алгоритм Push правильный. Он создает минимальную кучу простым способом:

  • Начните снизу справа
  • Если значение больше, чем родительский узел, вставьте его и верните
  • В противном случае, поместите вместо этого родительский элемент в нижнее правое положение, затем попробуйте вставить значение в родительское место (и продолжайте менять дерево, пока не будет найдено правильное место)

Полученное дерево:

                 0
               /   \
              /     \
             1       2
           /  \     /
          4    5   3

Проблема связана с методом Pop. Он начинается с рассмотрения верхнего узла как «пробела» для заполнения (так как мы добавили его):

                 *
               /   \
              /     \
             1       2
           /  \     /
          4    5   3

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

                 1
               /   \
              /     \
             *       2
           /  \     /
          4    5   3

Затем он делает то же самое с новым разрывом, поэтому разрыв снова уменьшается:

                 1
               /   \
              /     \
             4       2
           /  \     /
          *    5   3

Когда зазор достиг дна, алгоритм ... берет самое нижнее правое значение дерева и использует его, чтобы заполнить зазор:

                 1
               /   \
              /     \
             4       2
           /  \     /
          3    5   *

Теперь, когда разрыв находится в самом нижнем правом узле, он уменьшает _count, чтобы удалить разрыв из дерева:

                 1
               /   \
              /     \
             4       2
           /  \     
          3    5   

И мы в конечном итоге ... сломанная куча.

Честно говоря, я не понимаю, что пытался сделать автор, поэтому я не могу исправить существующий код. Самое большее, я могу поменять его на рабочую версию (беззастенчиво скопированную с Википедии):

internal void Pop2()
{
    if (_count > 0)
    {
        _count--;
        _heap[0] = _heap[_count];

        Heapify(0);
    }
}

internal void Heapify(int i)
{
    int left = (2 * i) + 1;
    int right = left + 1;
    int smallest = i;

    if (left <= _count && _comparer.Compare(_heap[left], _heap[smallest]) < 0)
    {
        smallest = left;
    }

    if (right <= _count && _comparer.Compare(_heap[right], _heap[smallest]) < 0)
    {
        smallest = right;
    }

    if (smallest != i)
    {
        var pivot = _heap[i];
        _heap[i] = _heap[smallest];
        _heap[smallest] = pivot;

        Heapify(smallest);
    }
}

Основная проблема с этим кодом - рекурсивная реализация, которая сломается, если число элементов слишком велико. Я настоятельно рекомендую использовать оптимизированную стороннюю библиотеку.


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

internal void Pop()
{
    Debug.Assert(_count != 0);

    if (_count > 1)
    {
        // Loop invariants:
        //
        //  1.  parent is the index of a gap in the logical tree
        //  2.  leftChild is
        //      (a) the index of parent's left child if it has one, or
        //      (b) a value >= _count if parent is a leaf node
        //
        int parent = 0;
        int leftChild = HeapLeftChild(parent);

        while (leftChild < _count)
        {
            int rightChild = HeapRightFromLeft(leftChild);
            int bestChild =
                (rightChild < _count && _comparer.Compare(_heap[rightChild], _heap[leftChild]) < 0) ?
                    rightChild : leftChild;

            // Promote bestChild to fill the gap left by parent.
            _heap[parent] = _heap[bestChild];

            // Restore invariants, i.e., let parent point to the gap.
            parent = bestChild;
            leftChild = HeapLeftChild(parent);
        }

        // Fill the last gap by moving the last (i.e., bottom-rightmost) node.
        _heap[parent] = _heap[_count - 1];

        // FIX: Rebalance the heap
        int index = parent;
        var value = _heap[parent];

        while (index > 0)
        {
            int parentIndex = HeapParent(index);
            if (_comparer.Compare(value, _heap[parentIndex]) < 0)
            {
                // value is a better match than the parent node so exchange
                // places to preserve the "heap" property.
                var pivot = _heap[index];
                _heap[index] = _heap[parentIndex];
                _heap[parentIndex] = pivot;
                index = parentIndex;
            }
            else
            {
                // Heap is balanced
                break;
            }
        }
    }

    _count--;
}
82
Kevin Gosse 27 Май 2017 в 23:23

Ответ Кевина Госсе определяет проблему. Хотя его перебалансировка кучи будет работать, в этом нет необходимости, если вы исправите фундаментальную проблему в исходном цикле удаления.

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

internal void Pop()
{
    Debug.Assert(_count != 0);

    if (_count > 0)
    {
        --_count;
        // Logically, we're moving the last item (lowest, right-most)
        // to the root and then sifting it down.
        int ix = 0;
        while (ix < _count/2)
        {
            // find the smallest child
            int smallestChild = HeapLeftChild(ix);
            int rightChild = HeapRightFromLeft(smallestChild);
            if (rightChild < _count-1 && _comparer.Compare(_heap[rightChild], _heap[smallestChild]) < 0)
            {
                smallestChild = rightChild;
            }

            // If the item is less than or equal to the smallest child item,
            // then we're done.
            if (_comparer.Compare(_heap[_count], _heap[smallestChild]) <= 0)
            {
                break;
            }

            // Otherwise, move the child up
            _heap[ix] = _heap[smallestChild];

            // and adjust the index
            ix = smallestChild;
        }
        // Place the item where it belongs
        _heap[ix] = _heap[_count];
        // and clear the position it used to occupy
        _heap[_count] = default(T);
    }
}

Также обратите внимание, что написанный код имеет утечку памяти. Этот бит кода:

        // Fill the last gap by moving the last (i.e., bottom-rightmost) node.
        _heap[parent] = _heap[_count - 1];

Не очищает значение из _heap[_count - 1]. Если в куче хранятся ссылочные типы, то ссылки остаются в куче и не могут быть собраны сборщиком мусора, пока память для кучки не будет собрана сборщиком мусора. Я не знаю, где используется эта куча, но если она велика и живет какое-то значительное время, это может привести к избыточному потреблению памяти. Ответ - очистить элемент после его копирования:

_heap[_count - 1] = default(T);

Мой код замены содержит это исправление.

20
Nicholas Petersen 7 Дек 2017 в 23:54