File d'attente prioritaire en .Net [fermée]


216

Je recherche une implémentation .NET d'une file d'attente prioritaire ou d'une structure de données de tas

Les files d'attente prioritaires sont des structures de données qui offrent plus de flexibilité qu'un simple tri, car elles permettent à de nouveaux éléments d'entrer dans un système à des intervalles arbitraires. Il est beaucoup plus rentable d'insérer un nouveau travail dans une file d'attente prioritaire que de tout trier à chaque arrivée.

La file d'attente prioritaire de base prend en charge trois opérations principales:

  • Insérez (Q, x). Étant donné un élément x avec la clé k, insérez-le dans la file d'attente prioritaire Q.
  • Find-Minimum (Q). Renvoie un pointeur sur l'élément dont la valeur de clé est inférieure à toute autre clé de la file d'attente prioritaire Q.
  • Supprimer-Minimum (Q). Supprimer l'élément de la file d'attente prioritaire Q dont la clé est minimale

À moins que je ne regarde au mauvais endroit, il n'y en a pas dans le cadre. Quelqu'un en connaît-il un bon ou dois-je rouler le mien?


34
Pour info, j'ai développé une file d'attente prioritaire C # facile à utiliser et hautement optimisée, que vous pouvez trouver ici . Il a été développé spécifiquement pour les applications d'orientation (A *, etc.), mais devrait également fonctionner parfaitement pour toute autre application. Je
posterais

1
ParallelExtensionsExtras a un code
VoteCoffee

Présentez sans vergogne PriorityQueue , dans le cadre des efforts de portage de l'API java simultanée sur .net pour Spring.Net. C'est à la fois un tas et une file d'attente avec un support générique complet. Le binaire peut être téléchargé ici .
Kenneth Xu

@ BlueRaja-DannyPflughoeft Pourriez-vous ajouter une réponse?
mafu

1
Juste pour résumer. Il n'y a pas de structure de données de tas dans .net, ni dans .net core maintenant. Bien que les utilisateurs d' Array.Sort l' utilisent pour un grand nombre. Des implémentations internes existent.
Artyom

Réponses:


44

J'aime utiliser les classes OrderedBaget OrderedSetdans PowerCollections comme files d'attente prioritaires.


60
OrderedBag / OrderedSet font plus de travail que nécessaire, ils utilisent un arbre rouge-noir au lieu d'un tas.
Dan Berindei

3
@DanBerindei: travail non nécessaire si vous avez besoin de faire un calcul en cours d'exécution (supprimer les anciens éléments), le tas ne prend en charge que la suppression des min ou max
Svisstack

69

Vous pourriez aimer IntervalHeap de la bibliothèque de collections génériques C5 . Pour citer le guide d'utilisation

La classe IntervalHeap<T>implémente l'interface à l' IPriorityQueue<T>aide d'un segment d'intervalle stocké sous forme de tableau de paires. Les opérations FindMin et FindMax, ainsi que l'accesseur get de l'indexeur, prennent le temps O (1). Les opérations DeleteMin, DeleteMax, Add et Update et l'accesseur set de l'indexeur prennent le temps O (log n). Contrairement à une file d'attente prioritaire ordinaire, un segment d'intervalle offre des opérations minimales et maximales avec la même efficacité.

L'API est assez simple

> var heap = new C5.IntervalHeap<int>();
> heap.Add(10);
> heap.Add(5);
> heap.FindMin();
5

Installer depuis Nuget https://www.nuget.org/packages/C5 ou GitHub https://github.com/sestoft/C5/


3
Cela semble être une bibliothèque très solide et elle est livrée avec 1400 tests unitaires.
ECC-Dan

2
J'ai essayé de l'utiliser mais il a de sérieux défauts. IntervalHeap n'a pas de concept de priorité intégré et vous oblige à implémenter IComparable ou IComparer, ce qui en fait une collection triée et non une "priorité". Pire encore, il n'y a aucun moyen direct de mettre à jour la priorité d'une entrée précédente !!!
morteza khosravi

52

Voici ma tentative d'un tas .NET

public abstract class Heap<T> : IEnumerable<T>
{
    private const int InitialCapacity = 0;
    private const int GrowFactor = 2;
    private const int MinGrow = 1;

    private int _capacity = InitialCapacity;
    private T[] _heap = new T[InitialCapacity];
    private int _tail = 0;

    public int Count { get { return _tail; } }
    public int Capacity { get { return _capacity; } }

    protected Comparer<T> Comparer { get; private set; }
    protected abstract bool Dominates(T x, T y);

    protected Heap() : this(Comparer<T>.Default)
    {
    }

    protected Heap(Comparer<T> comparer) : this(Enumerable.Empty<T>(), comparer)
    {
    }

    protected Heap(IEnumerable<T> collection)
        : this(collection, Comparer<T>.Default)
    {
    }

    protected Heap(IEnumerable<T> collection, Comparer<T> comparer)
    {
        if (collection == null) throw new ArgumentNullException("collection");
        if (comparer == null) throw new ArgumentNullException("comparer");

        Comparer = comparer;

        foreach (var item in collection)
        {
            if (Count == Capacity)
                Grow();

            _heap[_tail++] = item;
        }

        for (int i = Parent(_tail - 1); i >= 0; i--)
            BubbleDown(i);
    }

    public void Add(T item)
    {
        if (Count == Capacity)
            Grow();

        _heap[_tail++] = item;
        BubbleUp(_tail - 1);
    }

    private void BubbleUp(int i)
    {
        if (i == 0 || Dominates(_heap[Parent(i)], _heap[i])) 
            return; //correct domination (or root)

        Swap(i, Parent(i));
        BubbleUp(Parent(i));
    }

    public T GetMin()
    {
        if (Count == 0) throw new InvalidOperationException("Heap is empty");
        return _heap[0];
    }

    public T ExtractDominating()
    {
        if (Count == 0) throw new InvalidOperationException("Heap is empty");
        T ret = _heap[0];
        _tail--;
        Swap(_tail, 0);
        BubbleDown(0);
        return ret;
    }

    private void BubbleDown(int i)
    {
        int dominatingNode = Dominating(i);
        if (dominatingNode == i) return;
        Swap(i, dominatingNode);
        BubbleDown(dominatingNode);
    }

    private int Dominating(int i)
    {
        int dominatingNode = i;
        dominatingNode = GetDominating(YoungChild(i), dominatingNode);
        dominatingNode = GetDominating(OldChild(i), dominatingNode);

        return dominatingNode;
    }

    private int GetDominating(int newNode, int dominatingNode)
    {
        if (newNode < _tail && !Dominates(_heap[dominatingNode], _heap[newNode]))
            return newNode;
        else
            return dominatingNode;
    }

    private void Swap(int i, int j)
    {
        T tmp = _heap[i];
        _heap[i] = _heap[j];
        _heap[j] = tmp;
    }

    private static int Parent(int i)
    {
        return (i + 1)/2 - 1;
    }

    private static int YoungChild(int i)
    {
        return (i + 1)*2 - 1;
    }

    private static int OldChild(int i)
    {
        return YoungChild(i) + 1;
    }

    private void Grow()
    {
        int newCapacity = _capacity*GrowFactor + MinGrow;
        var newHeap = new T[newCapacity];
        Array.Copy(_heap, newHeap, _capacity);
        _heap = newHeap;
        _capacity = newCapacity;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _heap.Take(Count).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public class MaxHeap<T> : Heap<T>
{
    public MaxHeap()
        : this(Comparer<T>.Default)
    {
    }

    public MaxHeap(Comparer<T> comparer)
        : base(comparer)
    {
    }

    public MaxHeap(IEnumerable<T> collection, Comparer<T> comparer)
        : base(collection, comparer)
    {
    }

    public MaxHeap(IEnumerable<T> collection) : base(collection)
    {
    }

    protected override bool Dominates(T x, T y)
    {
        return Comparer.Compare(x, y) >= 0;
    }
}

public class MinHeap<T> : Heap<T>
{
    public MinHeap()
        : this(Comparer<T>.Default)
    {
    }

    public MinHeap(Comparer<T> comparer)
        : base(comparer)
    {
    }

    public MinHeap(IEnumerable<T> collection) : base(collection)
    {
    }

    public MinHeap(IEnumerable<T> collection, Comparer<T> comparer)
        : base(collection, comparer)
    {
    }

    protected override bool Dominates(T x, T y)
    {
        return Comparer.Compare(x, y) <= 0;
    }
}

Quelques tests:

[TestClass]
public class HeapTests
{
    [TestMethod]
    public void TestHeapBySorting()
    {
        var minHeap = new MinHeap<int>(new[] {9, 8, 4, 1, 6, 2, 7, 4, 1, 2});
        AssertHeapSort(minHeap, minHeap.OrderBy(i => i).ToArray());

        minHeap = new MinHeap<int> { 7, 5, 1, 6, 3, 2, 4, 1, 2, 1, 3, 4, 7 };
        AssertHeapSort(minHeap, minHeap.OrderBy(i => i).ToArray());

        var maxHeap = new MaxHeap<int>(new[] {1, 5, 3, 2, 7, 56, 3, 1, 23, 5, 2, 1});
        AssertHeapSort(maxHeap, maxHeap.OrderBy(d => -d).ToArray());

        maxHeap = new MaxHeap<int> {2, 6, 1, 3, 56, 1, 4, 7, 8, 23, 4, 5, 7, 34, 1, 4};
        AssertHeapSort(maxHeap, maxHeap.OrderBy(d => -d).ToArray());
    }

    private static void AssertHeapSort(Heap<int> heap, IEnumerable<int> expected)
    {
        var sorted = new List<int>();
        while (heap.Count > 0)
            sorted.Add(heap.ExtractDominating());

        Assert.IsTrue(sorted.SequenceEqual(expected));
    }
}

2
Je recommanderais d'effacer la valeur du tas dans ExtractDominating, afin qu'il ne reste pas sur l'objet référencé plus longtemps que nécessaire (fuite de mémoire potentielle). Pour les types de valeur, cela ne pose évidemment aucun problème.
Wout

5
Bien mais vous ne pouvez pas en supprimer des éléments? C'est une opération importante pour les files d'attente prioritaires.
Tom Larkworthy

Il semble que l'objet sous-jacent soit un tableau. Ne serait-ce pas mieux comme un arbre binaire?
Grunion Shaftoe

1
@OhadSchneider très très cool, je regardais juste le tas min et j'essayais de faire ce que vous faisiez en le rendant générique et le tas min ou max! grand travail
Gilad

1
@Gilad IEqualityComparer<T>ne serait pas suffisant, car cela ne vous dirait que si deux éléments sont égaux, alors que vous devez connaître la relation entre eux (qui est plus petit / plus grand). C'est vrai que j'aurais pu utiliser IComparer<T>cependant ...
Ohad Schneider

23

voici celui que je viens d'écrire, peut-être qu'il n'est pas aussi optimisé (utilise simplement un dictionnaire trié) mais simple à comprendre. vous pouvez insérer des objets de différents types, donc pas de files d'attente génériques.

using System;
using System.Diagnostics;
using System.Collections;
using System.Collections.Generic;

namespace PrioQueue
{
    public class PrioQueue
    {
        int total_size;
        SortedDictionary<int, Queue> storage;

        public PrioQueue ()
        {
            this.storage = new SortedDictionary<int, Queue> ();
            this.total_size = 0;
        }

        public bool IsEmpty ()
        {
            return (total_size == 0);
        }

        public object Dequeue ()
        {
            if (IsEmpty ()) {
                throw new Exception ("Please check that priorityQueue is not empty before dequeing");
            } else
                foreach (Queue q in storage.Values) {
                    // we use a sorted dictionary
                    if (q.Count > 0) {
                        total_size--;
                        return q.Dequeue ();
                    }
                }

                Debug.Assert(false,"not supposed to reach here. problem with changing total_size");

                return null; // not supposed to reach here.
        }

        // same as above, except for peek.

        public object Peek ()
        {
            if (IsEmpty ())
                throw new Exception ("Please check that priorityQueue is not empty before peeking");
            else
                foreach (Queue q in storage.Values) {
                    if (q.Count > 0)
                        return q.Peek ();
                }

                Debug.Assert(false,"not supposed to reach here. problem with changing total_size");

                return null; // not supposed to reach here.
        }

        public object Dequeue (int prio)
        {
            total_size--;
            return storage[prio].Dequeue ();
        }

        public void Enqueue (object item, int prio)
        {
            if (!storage.ContainsKey (prio)) {
                storage.Add (prio, new Queue ());
              }
            storage[prio].Enqueue (item);
            total_size++;

        }
    }
}

cela ne permet-il pas plusieurs entrées avec la même priorité?
Letseatlunch du

2
Cela fait. lorsque vous appelez la méthode Enqueue, il ajoutera l'élément à la file d'attente de cette priorité. (le rôle dans else dans la méthode de mise en file d'attente.)
kobi7

5
Qu'entendez-vous par «ce n'est pas vraiment une file d'attente prioritaire au sens informatique»? Et que pensez-vous que ce n'est pas une file d'attente prioritaire?
Mark Byers

14
-1 pour ne pas utiliser de génériques.
cdiggins

2
L'un des plus grands avantages de Heap / PriorityQueue est la complexité O (1) de l'extraction min / max, c'est-à-dire l'opération Peek. Et ici, cela implique la configuration de l'énumérateur, la boucle for, etc. Pourquoi!? En outre, l'opération "Enqueue" plutôt que d'être O (logN) - une autre caractéristique clé du tas, a un balayage O (longN) en raison de "ContainsKey", un deuxième (encore O (longN)) pour ajouter l'entrée de file d'attente (si nécessaire), un troisième pour récupérer réellement la file d'attente (la ligne de stockage [prio]), et enfin un ajout linéaire à cette file d'attente. C'est vraiment fou à la lumière de la mise en œuvre de l'algorithme de base.
Jonan Georgiev


9

Comme mentionné dans Microsoft Collections pour .NET , Microsoft a écrit (et partagé en ligne) 2 classes PriorityQueue internes dans le .NET Framework. Leur code est disponible pour essayer.

EDIT: Comme l'a commenté @ mathusum-mut, il y a un bogue dans l'une des classes PriorityQueue internes de Microsoft (la communauté SO a, bien sûr, fourni des correctifs pour cela): Bogue dans PriorityQueue interne de Microsoft <T>?


10
Un bogue a été trouvé dans l'une des implémentations ici: stackoverflow.com/questions/44221454/…
MathuSum Mut

ohh! Je peux voir que toutes ces classes PriorityQueue<T>dans la source partagée de Microsoft sont marquées avec internalun spécificateur d'accès. Ils ne sont donc utilisés que par les fonctionnalités internes du framework. Ils ne sont pas disponibles pour une consommation générale simplement en se référant windowsbase.dllà un projet C #. La seule façon est de copier la source partagée dans le projet lui-même à l'intérieur d'un fichier de classe.
RBT


7
class PriorityQueue<T>
{
    IComparer<T> comparer;
    T[] heap;
    public int Count { get; private set; }
    public PriorityQueue() : this(null) { }
    public PriorityQueue(int capacity) : this(capacity, null) { }
    public PriorityQueue(IComparer<T> comparer) : this(16, comparer) { }
    public PriorityQueue(int capacity, IComparer<T> comparer)
    {
        this.comparer = (comparer == null) ? Comparer<T>.Default : comparer;
        this.heap = new T[capacity];
    }
    public void push(T v)
    {
        if (Count >= heap.Length) Array.Resize(ref heap, Count * 2);
        heap[Count] = v;
        SiftUp(Count++);
    }
    public T pop()
    {
        var v = top();
        heap[0] = heap[--Count];
        if (Count > 0) SiftDown(0);
        return v;
    }
    public T top()
    {
        if (Count > 0) return heap[0];
        throw new InvalidOperationException("优先队列为空");
    }
    void SiftUp(int n)
    {
        var v = heap[n];
        for (var n2 = n / 2; n > 0 && comparer.Compare(v, heap[n2]) > 0; n = n2, n2 /= 2) heap[n] = heap[n2];
        heap[n] = v;
    }
    void SiftDown(int n)
    {
        var v = heap[n];
        for (var n2 = n * 2; n2 < Count; n = n2, n2 *= 2)
        {
            if (n2 + 1 < Count && comparer.Compare(heap[n2 + 1], heap[n2]) > 0) n2++;
            if (comparer.Compare(v, heap[n2]) >= 0) break;
            heap[n] = heap[n2];
        }
        heap[n] = v;
    }
}

facile.


13
Parfois, je vois des trucs comme for (var n2 = n / 2; n > 0 && comparer.Compare(v, heap[n2]) > 0; n = n2, n2 /= 2) heap[n] = heap[n2]; et je me demande si ça valait la peine d'être

1
@DustinBreakey personal style :)
Shimou Dong

3
mais certainement pas lisible pour les autres. Envisagez d'écrire du code qui ne laisse pas de point d'interrogation flottant sur la tête du développeur.
alzaimar

3

Utilisez un traducteur Java vers C # sur l'implémentation Java (java.util.PriorityQueue) dans le cadre des collections Java, ou utilisez plus intelligemment l'algorithme et le code principal et branchez-le dans une classe C # de votre propre fabrication qui adhère au cadre des collections C # API pour les files d'attente, ou au moins les collections.


Cela fonctionne, mais malheureusement IKVM ne prend pas en charge les génériques Java, vous perdez donc la sécurité des types.
Escargot mécanique

8
Il n'y a rien de tel que les «génériques Java» lorsque vous avez affaire à du bytecode Java compilé. IKVM ne peut pas le supporter.
Mark

3

AlgoKit

J'ai écrit une bibliothèque open source appelée AlgoKit , disponible via NuGet . Il contient:

  • Tas de d-aire implicites (ArrayHeap),
  • Tas binomiaux ,
  • Jumelage de tas .

Le code a été largement testé. Je vous recommande vraiment de l'essayer.

Exemple

var comparer = Comparer<int>.Default;
var heap = new PairingHeap<int, string>(comparer);

heap.Add(3, "your");
heap.Add(5, "of");
heap.Add(7, "disturbing.");
heap.Add(2, "find");
heap.Add(1, "I");
heap.Add(6, "faith");
heap.Add(4, "lack");

while (!heap.IsEmpty)
    Console.WriteLine(heap.Pop().Value);

Pourquoi ces trois tas?

Le choix optimal d'implémentation dépend fortement des entrées - comme le montrent Larkin, Sen et Tarjan dans A back-to-basics empirical study of priority queues , arXiv: 1403.0252v1 [cs.DS] . Ils ont testé des tas de d-aire implicites, des tas de pairage, des tas de Fibonacci, des tas binomiaux, des tas de d-aire explicites, des tas d'appariement de rang, des tas de tremblement de terre, des tas de violation, des tas de faible relâchement de rang et des tas de Fibonacci stricts.

AlgoKit propose trois types de tas qui semblaient être les plus efficaces parmi ceux testés.

Indice sur le choix

Pour un nombre relativement restreint d'éléments, vous seriez probablement intéressé à utiliser des tas implicites, en particulier des tas quaternaires (implicites à 4 aires). En cas d'opération sur de plus grandes tailles de tas, les structures amorties comme les tas binomiaux et les tas d'appariement devraient mieux fonctionner.



1

J'ai eu le même problème récemment et j'ai fini par créer un package NuGet pour cela.

Cela implémente une file d'attente prioritaire basée sur le tas standard. Il a également toutes les subtilités habituelles des collections BCL: ICollection<T>et l' IReadOnlyCollection<T>implémentation, le IComparer<T>support personnalisé , la possibilité de spécifier une capacité initiale, et unDebuggerTypeProxy de rendre la collection plus facile à utiliser dans le débogueur.

Il existe également une version en ligne du package qui installe simplement un fichier .cs unique dans votre projet (utile si vous voulez éviter de prendre des dépendances visibles de l'extérieur).

Plus d'informations sont disponibles sur la page github .


1

Une implémentation simple du tas max.

https://github.com/bharathkumarms/AlgorithmsMadeEasy/blob/master/AlgorithmsMadeEasy/MaxHeap.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace AlgorithmsMadeEasy
{
    class MaxHeap
    {
        private static int capacity = 10;
        private int size = 0;
        int[] items = new int[capacity];

        private int getLeftChildIndex(int parentIndex) { return 2 * parentIndex + 1; }
        private int getRightChildIndex(int parentIndex) { return 2 * parentIndex + 2; }
        private int getParentIndex(int childIndex) { return (childIndex - 1) / 2; }

        private int getLeftChild(int parentIndex) { return this.items[getLeftChildIndex(parentIndex)]; }
        private int getRightChild(int parentIndex) { return this.items[getRightChildIndex(parentIndex)]; }
        private int getParent(int childIndex) { return this.items[getParentIndex(childIndex)]; }

        private bool hasLeftChild(int parentIndex) { return getLeftChildIndex(parentIndex) < size; }
        private bool hasRightChild(int parentIndex) { return getRightChildIndex(parentIndex) < size; }
        private bool hasParent(int childIndex) { return getLeftChildIndex(childIndex) > 0; }

        private void swap(int indexOne, int indexTwo)
        {
            int temp = this.items[indexOne];
            this.items[indexOne] = this.items[indexTwo];
            this.items[indexTwo] = temp;
        }

        private void hasEnoughCapacity()
        {
            if (this.size == capacity)
            {
                Array.Resize(ref this.items,capacity*2);
                capacity *= 2;
            }
        }

        public void Add(int item)
        {
            this.hasEnoughCapacity();
            this.items[size] = item;
            this.size++;
            heapifyUp();
        }

        public int Remove()
        {
            int item = this.items[0];
            this.items[0] = this.items[size-1];
            this.items[this.size - 1] = 0;
            size--;
            heapifyDown();
            return item;
        }

        private void heapifyUp()
        {
            int index = this.size - 1;
            while (hasParent(index) && this.items[index] > getParent(index))
            {
                swap(index, getParentIndex(index));
                index = getParentIndex(index);
            }
        }

        private void heapifyDown()
        {
            int index = 0;
            while (hasLeftChild(index))
            {
                int bigChildIndex = getLeftChildIndex(index);
                if (hasRightChild(index) && getLeftChild(index) < getRightChild(index))
                {
                    bigChildIndex = getRightChildIndex(index);
                }

                if (this.items[bigChildIndex] < this.items[index])
                {
                    break;
                }
                else
                {
                    swap(bigChildIndex,index);
                    index = bigChildIndex;
                }
            }
        }
    }
}

/*
Calling Code:
    MaxHeap mh = new MaxHeap();
    mh.Add(10);
    mh.Add(5);
    mh.Add(2);
    mh.Add(1);
    mh.Add(50);
    int maxVal  = mh.Remove();
    int newMaxVal = mh.Remove();
*/

-3

L'implémentation suivante d'une PriorityQueueutilisation SortedSetde la bibliothèque système.

using System;
using System.Collections.Generic;

namespace CDiggins
{
    interface IPriorityQueue<T, K> where K : IComparable<K>
    {
        bool Empty { get; }
        void Enqueue(T x, K key);
        void Dequeue();
        T Top { get; }
    }

    class PriorityQueue<T, K> : IPriorityQueue<T, K> where K : IComparable<K>
    {
        SortedSet<Tuple<T, K>> set;

        class Comparer : IComparer<Tuple<T, K>> {
            public int Compare(Tuple<T, K> x, Tuple<T, K> y) {
                return x.Item2.CompareTo(y.Item2);
            }
        }

        PriorityQueue() { set = new SortedSet<Tuple<T, K>>(new Comparer()); }
        public bool Empty { get { return set.Count == 0;  } }
        public void Enqueue(T x, K key) { set.Add(Tuple.Create(x, key)); }
        public void Dequeue() { set.Remove(set.Max); }
        public T Top { get { return set.Max.Item1; } }
    }
}

6
SortedSet.Add échouera (et retournera false) si vous avez déjà un élément dans l'ensemble avec la même "priorité" que l'élément que vous essayez d'ajouter. Donc ... si A.Compare (B) == 0 et A est déjà dans la liste, votre fonction PriorityQueue.Enqueue échouera silencieusement.
Joseph

Pensez à expliquer ce que sont T xet K key? Je suppose que c'est une astuce pour autoriser la duplication T x, et j'ai besoin de générer une clé unique (par exemple UUID)?
Thariq Nugrohotomo
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.