7

I'm looking for a priority queue with an interface like this:

class PriorityQueue<T>
{
    public void Enqueue(T item, int priority)
    {
    }

    public T Dequeue()
    {
    }
}

All the implementations I've seen assume that item is an IComparable but I don't like this approach; I want to specify the priority when I'm pushing it onto the queue.

If a ready-made implementation doesn't exist, what's the best way to go about doing this myself? What underlying data structure should I use? Some sort of self-balancing tree, or what? A standard C#.net structure would be nice.

mpen
  • 237,624
  • 230
  • 766
  • 1,119
  • Are you going to be calling it from multiple threads? – sohum Dec 21 '09 at 01:05
  • @sohum: No... my program *is* threaded, but only one thread will need access to it. – mpen Dec 21 '09 at 01:17
  • The reason that immediately pops to mind for T supporting IComparable is that if you push two items into the queue with priority 2, you still need to compare the items and decide what order the to process the two priority two items. . . So ultimately you need T to be comparable. So how to do this with your interface... Well you've got some good suggestions below. – Jason D Dec 21 '09 at 01:52
  • 2
    @Jason: Doesn't that defeat the purpose of a priority *queue*? It's a queue because the first one in, is the first one out. That's how it's decided. Otherwise it's just a sorted list. – mpen Dec 21 '09 at 02:06
  • @Jason D: If all the keys are the same then a priority queue should degenerate to a FIFO queue. If the ordering of the items is being used then this will be violated. Thus, any implementation of a priority queue that depends on the ordering of the items is suspect. – jason Dec 21 '09 at 02:12
  • related: http://stackoverflow.com/questions/2046674 – mpen Jan 02 '11 at 01:59
  • Also related: http://cstheory.stackexchange.com/q/593/13809 (stable binary heap) – mpen Feb 18 '13 at 16:31

9 Answers9

12

If you have an existing priority queue implementation based on IComparable, you can easily use that to build the structure you need:

public class CustomPriorityQueue<T>  // where T need NOT be IComparable
{
  private class PriorityQueueItem : IComparable<PriorityQueueItem>
  {
    private readonly T _item;
    private readonly int _priority:

    // obvious constructor, CompareTo implementation and Item accessor
  }

  // the existing PQ implementation where the item *does* need to be IComparable
  private readonly PriorityQueue<PriorityQueueItem> _inner = new PriorityQueue<PriorityQueueItem>();

  public void Enqueue(T item, int priority)
  {
    _inner.Enqueue(new PriorityQueueItem(item, priority));
  }

  public T Dequeue()
  {
    return _inner.Dequeue().Item;
  }
}
itowlson
  • 70,780
  • 16
  • 153
  • 151
6

You can add safety checks and what not, but here is a very simple implementation using SortedList:

class PriorityQueue<T> {
    SortedList<Pair<int>, T> _list;
    int count;

    public PriorityQueue() {
        _list = new SortedList<Pair<int>, T>(new PairComparer<int>());
    }

    public void Enqueue(T item, int priority) {
        _list.Add(new Pair<int>(priority, count), item);
        count++;
    }

    public T Dequeue() {
        T item = _list[_list.Keys[0]];
        _list.RemoveAt(0);
        return item;
    }
}

I'm assuming that smaller values of priority correspond to higher priority items (this is easy to modify).

If multiple threads will be accessing the queue you will need to add a locking mechanism too. This is easy, but let me know if you need guidance here.

SortedList is implemented internally as a binary tree.

The above implementation needs the following helper classes. This address Lasse V. Karlsen's comment that items with the same priority can not be added using the naive implementation using a SortedList.

class Pair<T> {
    public T First { get; private set; }
    public T Second { get; private set; }

    public Pair(T first, T second) {
        First = first;
        Second = second;
    }

    public override int GetHashCode() {
        return First.GetHashCode() ^ Second.GetHashCode();
    }

    public override bool Equals(object other) {
        Pair<T> pair = other as Pair<T>;
        if (pair == null) {
            return false;
        }
        return (this.First.Equals(pair.First) && this.Second.Equals(pair.Second));
    }
}

class PairComparer<T> : IComparer<Pair<T>> where T : IComparable {
    public int Compare(Pair<T> x, Pair<T> y) {
        if (x.First.CompareTo(y.First) < 0) {
            return -1;
        }
        else if (x.First.CompareTo(y.First) > 0) {
            return 1;
        }
        else {
            return x.Second.CompareTo(y.Second);
        }
    }
}
jason
  • 220,745
  • 31
  • 400
  • 507
  • 3
    Problem with SortedList is that it doesn't allow duplicate priorities, so it forces you to ensure unique priorities. – Lasse V. Karlsen Dec 21 '09 at 01:10
  • 1
    Makes more sense to me that *higher* numbers correspond to a *higher* priority. – mpen Dec 21 '09 at 01:11
  • 1
    Easily fixed, just negate the priority values, if SortedList is good enough. – Lasse V. Karlsen Dec 21 '09 at 01:13
  • @Mark: Like I said, that is _easily_ addressed. Using lower number corresponding to higher priorities just makes the implementation simpler so that the ideas are more lucid. – jason Dec 21 '09 at 01:35
6

You could write a wrapper around one of the existing implementations that modifies the interface to your preference:

using System;

class PriorityQueueThatYouDontLike<T> where T: IComparable<T>
{
    public void Enqueue(T item) { throw new NotImplementedException(); }
    public T Dequeue() { throw new NotImplementedException(); }
}

class PriorityQueue<T>
{
    class ItemWithPriority : IComparable<ItemWithPriority>
    {
        public ItemWithPriority(T t, int priority)
        {
            Item = t;
            Priority = priority;
        }

        public T Item {get; private set;}
        public int Priority {get; private set;}

        public int CompareTo(ItemWithPriority other)
        {
            return Priority.CompareTo(other.Priority);
        }
    }

    PriorityQueueThatYouDontLike<ItemWithPriority> q = new PriorityQueueThatYouDontLike<ItemWithPriority>();

    public void Enqueue(T item, int priority)
    {
        q.Enqueue(new ItemWithPriority(item, priority));
    }

    public T Dequeue()
    {
        return q.Dequeue().Item;
    }
}

This is the same as itowlson's suggestion. I just took longer to write mine because I filled out more of the methods. :-s

Mark Byers
  • 719,658
  • 164
  • 1,497
  • 1,412
  • Still trying to find a nice implementation of `PriorityQueueThatYouDontLike`. Even the ones that uses IComparables don't look very nice. – mpen Dec 21 '09 at 01:52
  • I have a generic heap here: http://vkarlsen.serveftp.com:81/websvn/listing.php?repname=LVK&path=/LVK_3_5/trunk/LVK.Core/Collections/, username and password both 'guest' (without the quotes). The heap still falls prey to the "problem" you mention of requiring IComparable, but you can use the ItemWithPriority class that Mark has posted. – Lasse V. Karlsen Dec 21 '09 at 02:45
6

Here's a very simple lightweight implementation that has O(log(n)) performance for both push and pop. It uses a heap data structure built on top of a List<T>.

/// <summary>Implements a priority queue of T, where T has an ordering.</summary>
/// Elements may be added to the queue in any order, but when we pull
/// elements out of the queue, they will be returned in 'ascending' order.
/// Adding new elements into the queue may be done at any time, so this is
/// useful to implement a dynamically growing and shrinking queue. Both adding
/// an element and removing the first element are log(N) operations. 
/// 
/// The queue is implemented using a priority-heap data structure. For more 
/// details on this elegant and simple data structure see "Programming Pearls"
/// in our library. The tree is implemented atop a list, where 2N and 2N+1 are
/// the child nodes of node N. The tree is balanced and left-aligned so there
/// are no 'holes' in this list. 
/// <typeparam name="T">Type T, should implement IComparable[T];</typeparam>
public class PriorityQueue<T> where T : IComparable<T> {
   /// <summary>Clear all the elements from the priority queue</summary>
   public void Clear () {
      mA.Clear ();
   }

   /// <summary>Add an element to the priority queue - O(log(n)) time operation.</summary>
   /// <param name="item">The item to be added to the queue</param>
   public void Add (T item) {
      // We add the item to the end of the list (at the bottom of the
      // tree). Then, the heap-property could be violated between this element
      // and it's parent. If this is the case, we swap this element with the 
      // parent (a safe operation to do since the element is known to be less
      // than it's parent). Now the element move one level up the tree. We repeat
      // this test with the element and it's new parent. The element, if lesser
      // than everybody else in the tree will eventually bubble all the way up
      // to the root of the tree (or the head of the list). It is easy to see 
      // this will take log(N) time, since we are working with a balanced binary
      // tree.
      int n = mA.Count; mA.Add (item);
      while (n != 0) {
         int p = n / 2;    // This is the 'parent' of this item
         if (mA[n].CompareTo (mA[p]) >= 0) break;  // Item >= parent
         T tmp = mA[n]; mA[n] = mA[p]; mA[p] = tmp; // Swap item and parent
         n = p;            // And continue
      }
   }

   /// <summary>Returns the number of elements in the queue.</summary>
   public int Count {
      get { return mA.Count; }
   }

   /// <summary>Returns true if the queue is empty.</summary>
   /// Trying to call Peek() or Next() on an empty queue will throw an exception.
   /// Check using Empty first before calling these methods.
   public bool Empty {
      get { return mA.Count == 0; }
   }

   /// <summary>Allows you to look at the first element waiting in the queue, without removing it.</summary>
   /// This element will be the one that will be returned if you subsequently call Next().
   public T Peek () {
      return mA[0];
   }

   /// <summary>Removes and returns the first element from the queue (least element)</summary>
   /// <returns>The first element in the queue, in ascending order.</returns>
   public T Next () {
      // The element to return is of course the first element in the array, 
      // or the root of the tree. However, this will leave a 'hole' there. We
      // fill up this hole with the last element from the array. This will 
      // break the heap property. So we bubble the element downwards by swapping
      // it with it's lower child until it reaches it's correct level. The lower
      // child (one of the orignal elements with index 1 or 2) will now be at the
      // head of the queue (root of the tree).
      T val = mA[0];
      int nMax = mA.Count - 1;
      mA[0] = mA[nMax]; mA.RemoveAt (nMax);  // Move the last element to the top

      int p = 0;
      while (true) {
         // c is the child we want to swap with. If there
         // is no child at all, then the heap is balanced
         int c = p * 2; if (c >= nMax) break;

         // If the second child is smaller than the first, that's the one
         // we want to swap with this parent.
         if (c + 1 < nMax && mA[c + 1].CompareTo (mA[c]) < 0) c++;
         // If the parent is already smaller than this smaller child, then
         // we are done
         if (mA[p].CompareTo (mA[c]) <= 0) break;

         // Othewise, swap parent and child, and follow down the parent
         T tmp = mA[p]; mA[p] = mA[c]; mA[c] = tmp;
         p = c;
      }
      return val;
   }

   /// <summary>The List we use for implementation.</summary>
   List<T> mA = new List<T> ();
}
Tarydon
  • 4,791
  • 19
  • 24
  • He said that he did not want `T` to have to be `IComparable`. – jason Dec 21 '09 at 02:48
  • 1
    He wrote "even the ones that uses IComparable don't look very nice" and I was responding to that. The key point is that a priority queue does not require a fully sorted list; all we need to know is which is the first element in the list at any time. The Heap data structure I am using is not fully sorted, but maintains just enough sorting to efficiently implement log(n) insert and remove. Obviously, this is much lighter than a full-blown binary tree will be, and will have same performance overall. – Tarydon Dec 21 '09 at 02:53
  • 2
    To clarify: This PriorityQueue uses no more space than a plain List and has log(n) insert and remove performance. – Tarydon Dec 21 '09 at 02:56
  • 1
    Tarydon's right, I could use this with the other suggestions about just "generating" an IComparable instead. However, a good PriorityQueue should have O(1) removal, not O(log(n)). – mpen Dec 21 '09 at 03:59
  • 2
    O(1) removal would mean you maintain a sorted list at all times. That would lead to a n*log(n) time for inserting in the naive case (to re-sort). If you were to use a simple linear list and do a binary search and then insert in, that would still take O(n) time to insert (to shift the elements to make room). And, any tree-based algorithm (like the SortedList) will be O(log(n)) for both insertion and removal (since it will take O(log(n)) to re-balance the tree). So, I am not sure we can actually achieve O(1) removal with O(log(n)) insertion. – Tarydon Dec 21 '09 at 10:48
  • The only near-O(1) retrieval I've ever seen was a "closest soundex search" using a funky hash-of-the-priority algorithm... the downside was a few of the returned buckets were huge... requiring a human to answer a "Did you mean" question with too many (IMHO) items in the list... but it was damn fast to fetch the list. – corlettk Sep 08 '12 at 06:36
4

That is the exact interface used by my highly optimized C# priority-queue.

It was developed specifically for pathfinding applications (A*, etc.), but should work perfectly for any other application as well.

The only possible issue is that, in order to squeeze out the absolute maximum performance, the implementation requires the values enqueued to extend PriorityQueueNode.

public class User : PriorityQueueNode
{
    public string Name { get; private set; }
    public User(string name)
    {
        Name = name;
    }
}

...

HeapPriorityQueue<User> priorityQueue = new HeapPriorityQueue<User>(MAX_USERS_IN_QUEUE);
priorityQueue.Enqueue(new User("Jason"), 1);
priorityQueue.Enqueue(new User("Joseph"), 10);

//Because it's a min-priority queue, the following line will return "Jason"
User user = priorityQueue.Dequeue();
BlueRaja - Danny Pflughoeft
  • 75,675
  • 28
  • 177
  • 259
2

What would be so terrible about something like this?

class PriorityQueue<TItem, TPriority> where TPriority : IComparable
{
    private SortedList<TPriority, Queue<TItem>> pq = new SortedList<TPriority, Queue<TItem>>();
    public int Count { get; private set; }

    public void Enqueue(TItem item, TPriority priority)
    {
        ++Count;
        if (!pq.ContainsKey(priority)) pq[priority] = new Queue<TItem>();
        pq[priority].Enqueue(item);
    }

    public TItem Dequeue()
    {
        --Count;
        var queue = pq.ElementAt(0).Value;
        if (queue.Count == 1) pq.RemoveAt(0);
        return queue.Dequeue();
    }
}

class PriorityQueue<TItem> : PriorityQueue<TItem, int> { }
mpen
  • 237,624
  • 230
  • 766
  • 1,119
  • 2
    Looks neat. Unity—for some reason—doesn't provide `ElementAt()`. So I used `Values[0]` there. – noio Mar 10 '15 at 13:56
1

I realise that your question specifically asks for a non-IComparable-based implementation, but I want to point out a recent article from Visual Studio Magazine.

http://visualstudiomagazine.com/articles/2012/11/01/priority-queues-with-c.aspx

This article with @itowlson's can give a complete answer.

kevinarpe
  • 17,685
  • 21
  • 107
  • 133
1

A little late but I'll add it here for reference

https://github.com/ERufian/Algs4-CSharp

Key-value-pair priority queues are implemented in Algs4/IndexMaxPQ.cs, Algs4/IndexMinPQ.cs and Algs4/IndexPQDictionary.cs

Notes:

  1. If the Priorities are not IComparable's, an IComparer can be specified in the constructor
  2. Instead of enqueueing the object and its priority, what is enqueued is an index and its priority (and, for the original question, a separate List or T[] would be needed to convert that index to the expected result)
0

Seems like you could roll your own with a seriews of Queues, one for each priority. Dictionary and just add it to the appropriate one.

No Refunds No Returns
  • 7,077
  • 4
  • 26
  • 38
  • 2
    This has horrible performance for pop, as you then have to find the first non-empty queue. – Yuliy Dec 21 '09 at 01:08
  • @Yuliy: I thought this too at first, but if we pop off the queues as they become empty, it's not really an issue is it? – mpen Dec 21 '09 at 04:03
  • You're going to pay somewhere to manage priority. If you have a linked list, you're going to have to walk it on insert to find the "end" of the current priority. Managing priority is not free. – No Refunds No Returns Dec 21 '09 at 14:02
  • I guess you could have a balanced binary search tree of queues, giving you log n performance for the standard operations, but at that point, you might as well just implement a proper heap. – Yuliy Jan 07 '10 at 06:51