PresentationCore.dllの.NET Frameworkには、汎用の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
クラスのコードのバグは正確にどこにありますか?
この動作は、初期化ベクトル[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
そして、最終的に...壊れたヒープ。
正直に言うと、著者が何をしようとしていたのか理解できないので、既存のコードを修正することはできません。せいぜい、私はそれを動作するバージョンと交換することができます( Wikipedia から恥知らずにコピーされます):
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--;
}
Kevin Gosseの答えは問題を特定します。彼のヒープの再バランスは機能しますが、元の削除ループで根本的な問題を修正する場合は必要ありません。
彼が指摘したように、アイデアは、ヒープの最上部にあるアイテムを一番右下のアイテムに置き換え、それを適切な場所にふるい落とすことです。これは、元のループの簡単な変更です。
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);
私の置換コードにはその修正が組み込まれています。