Ошибка во внутреннем PriorityQueue Microsoft?
в .NET Framework в PresentationCore.проблемы, есть универсальный 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
класс находится?
2 ответа:
поведение может быть воспроизведено с помощью вектора инициализации
[0, 1, 2, 4, 5, 3]
. В результате получается:[0, 1, 2, 4, 3, 5]
(мы видим, что 3 неправильно вставлена)
The является правильным. Он строит минимальную кучу простым способом:
- начните с нижнего правого
- если значение больше родительского узла, то вставьте его и верните
- в противном случае, поместите вместо этого родителя в нижнюю правую позицию, а затем попробуйте вставить значение в родительское место (и продолжайте менять дерево до тех пор, пока не будет найдено нужное место)
результирующее дерево:
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); } }
основная проблема с этим кодом-рекурсивная реализация, которая будет нарушена, если количество элементов слишком велико. Я настоятельно рекомендую использовать оптимизированную стороннюю библиотеку вместо.
Edit: я думаю, что я узнал, что отсутствует. Взяв самый нижний правый узел, автор просто забыл перебалансировать кучу:
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--; }
ответ Кевина Госса определяет проблему. Хотя его повторная балансировка кучи будет работать, это не обязательно, если вы исправите фундаментальную проблему в исходном цикле удаления.
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);
мой код замены включает это устанавливать.