web-dev-qa-db-fra.com

Un bug dans PriorityQueue interne de Microsoft <T>?

Dans le .NET Framework dans PresentationCore.dll, il existe une classe générique PriorityQueue<T> Dont le code se trouve ici .

J'ai écrit un petit programme pour tester le tri, et les résultats n'étaient pas excellents:

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();
        }
    }
}

Résultats:

2789658
3411390
4618917
6996709
found sorting error
6381637
9367782

Il y a une erreur de tri et si la taille de l'échantillon augmente, le nombre d'erreurs de tri augmente quelque peu proportionnellement.

Ai-je fait quelque chose de mal? Sinon, où se trouve exactement le bogue dans le code de la classe PriorityQueue?

77
MathuSum Mut

Le comportement peut être reproduit à l'aide du vecteur d'initialisation [0, 1, 2, 4, 5, 3]. Le résultat est:

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

(nous pouvons voir que 3 est mal placé)

L'algorithme Push est correct. Il construit un min-tas d'une manière simple:

  • Commencez en bas à droite
  • Si la valeur est supérieure au nœud parent, insérez-la et retournez
  • Sinon, placez plutôt le parent en bas à droite, puis essayez d'insérer la valeur à l'emplacement parent (et continuez à échanger l'arbre jusqu'à ce que le bon endroit soit trouvé)

L'arbre résultant est:

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

Le problème vient de la méthode Pop. Il commence par considérer le nœud supérieur comme un "espace" à combler (puisque nous l'avons sauté):

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

Pour le remplir, il recherche l'enfant immédiat le plus bas (dans ce cas: 1). Il déplace ensuite la valeur vers le haut pour combler l'écart (et l'enfant est maintenant le nouvel écart):

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

Il fait alors exactement la même chose avec le nouvel écart, donc l'écart redescend:

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

Lorsque l'écart a atteint le bas, l'algorithme ... prend la valeur la plus en bas à droite de l'arbre et l'utilise pour combler l'écart:

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

Maintenant que l'écart se trouve au nœud en bas à droite, il diminue _count pour supprimer l'espace de l'arbre:

                 1
               /   \
              /     \
             4       2
           /  \     
          3    5   

Et nous nous retrouvons avec ... Un tas cassé.

Pour être parfaitement honnête, je ne comprends pas ce que l'auteur essayait de faire, donc je ne peux pas réparer le code existant. Tout au plus, je peux l'échanger avec une version de travail (copiée sans vergogne de 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);
    }
}

Le problème principal avec ce code est l'implémentation récursive, qui se cassera si le nombre d'éléments est trop grand. Je recommande fortement d'utiliser à la place une bibliothèque tierce optimisée.


Edit: Je pense avoir découvert ce qui manque. Après avoir pris le nœud en bas à droite, l'auteur a juste oublié de rééquilibrer le tas:

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--;
}
78
Kevin Gosse

La réponse de Kevin Gosse identifie le problème. Bien que son rééquilibrage du tas fonctionne, ce n'est pas nécessaire si vous corrigez le problème fondamental dans la boucle de suppression d'origine.

Comme il l'a souligné, l'idée est de remplacer l'élément en haut du tas par l'élément le plus bas et le plus à droite, puis de le tamiser à l'emplacement approprié. C'est une simple modification de la boucle d'origine:

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);
    }
}

Notez également que le code tel qu'il est écrit présente une fuite de mémoire. Ce morceau de code:

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

N'efface pas la valeur de _heap[_count - 1]. Si le tas stocke des types de référence, les références restent dans le tas et ne peuvent pas être récupérées jusqu'à ce que la mémoire du tas soit récupérée. Je ne sais pas où ce tas est utilisé, mais s'il est volumineux et vit pendant une période de temps significative, cela pourrait entraîner une consommation de mémoire excessive. La réponse est d'effacer l'élément après sa copie:

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

Mon code de remplacement intègre ce correctif.

17
Jim Mischel