85 votes

Bug dans la PriorityQueue interne de Microsoft <T> ?

Dans l' .NET Framework dans PresentationCore.dll, il y a un générique PriorityQueue<T> classe dont le code peut être trouvé ici.

J'ai écrit un programme court pour tester le tri, et les résultats n'ont pas très bien:

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 en proportion.

Ai-je fait quelque chose de mal? Si non, où est le bug dans le code de l' PriorityQueue classe situé exactement?

87voto

KooKiz Points 19056

Le problème peut être reproduit en utilisant le 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' Push algorithme est correct. Il construit un min-segment d'une façon simple:

  • Commencer en bas à droite
  • Si la valeur est plus grande que le nœud parent puis de l'insérer et de retour
  • Sinon, mis à la place du parent dans le bas à droite de position, puis essayez d'insérer la valeur à la place des parents (et de garder la permutation de l'arbre jusqu'à ce que la place a été trouvé)

L'arbre résultant est:

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

Le problème avec l' Pop méthode. Il commence par examiner le nœud supérieur comme un "écart" à remplir (depuis que nous avons sauté):

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

Pour le remplir, il recherche pour le plus immédiat de l'enfant (dans ce cas: 1). Il se déplace ensuite la valeur de combler l'écart (et l'enfant est désormais le nouveau gap):

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

Il fait ensuite la même chose avec la nouvelle écart, de sorte que l'écart se déplace vers le bas à nouveau:

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

Lorsque l'écart a atteint le fond, l'algorithme... prend les bas-droite de la valeur de l'arbre et l'utilise pour combler l'écart:

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

Maintenant que l'écart est en bas à droite du nœud, il décrémente _count pour supprimer l'écart de l'arbre:

                 1
               /   \
              /     \
             4       2
           /  \     
          3    5   

Et nous nous retrouvons avec... Une fracture de la tas.

Pour être parfaitement honnête, je ne comprends pas ce que l'auteur a essayé de le faire, donc je ne peux pas corriger le code existant. Tout au plus, je peux l'échanger avec une version de travail (copié sans vergogne de Wikipédia):

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

Problème principal de ce code est récursive de la mise en œuvre, ce qui va casser si le nombre d'éléments est trop importante. Je recommande fortement d'utiliser une optimisation du tiers de la bibliothèque de la place.


Edit: je crois que j'ai trouvé ce qui fait défaut. Après la prise de la bas-nœud le plus à 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--;
}

20voto

Jim Mischel Points 68586

Kevin Gosse de réponse identifie le problème. Bien que sa ré-équilibrage des tas de travail, il n'est pas nécessaire si vous corrigez le problème fondamental à l'origine, la suppression de la boucle.

Comme il l'a souligné, l'idée est de remplacer l'élément au sommet de la pile avec le plus faible, le plus à droite de l'élément, et puis 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 comme l'écrit a 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];

Ne désactivez pas la valeur de _heap[_count - 1]. Si le tas est de stocker des types de référence, les références restent dans le tas et ne peut pas être nettoyée jusqu'à ce que la mémoire du tas de déchets collectés. Je ne sais pas où ce segment est utilisé, mais si c'est important et de vie pour toute quantité importante de temps, il pourrait causer des excès de consommation de mémoire. La réponse est clairement l'élément après reproduit:

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

Mon code de remplacement intègre que correctif.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X