7

I would like to implement a priority queue which would inject my objects - Nodes to the queue with respect to one field - f. I have already written List with custom comparer but this would require from me to:

  • enqueue - sort the List after each insertion
  • dequeue - remove the last(instead of first for performance) like so

    myList.RemoveAt(myList.Count - 1);
    

My List should be always sorted according to some field (here I need it to be sorted by f). I also need the ability to add and dequeue the object with lowest value from the list.

Can someone tell me what would be the best approach to this ?

EDIT

dasblinkenlight has a very good answer but just I have realised that I should be able to store duplicates in this container.

Community
  • 1
  • 1
Patryk
  • 18,244
  • 37
  • 110
  • 212

3 Answers3

11

If you are in .NET 4 or later, you can use a SortedSet<T> class with a custom IComparer<T>.

The class lets you add new objects with the Add method common to all mutable collections. You can retrieve the max element using the Max property, and then call Remove to erase the max from the collection.

Edit: (in response to an edit of the question) If you need to store duplicates, you can use a SortedDictionary<Key,int>, and make a counted set out of it. Again, you have an option of using a custom IComparer<T>. When you enqueue an element, check if it exists already, and increment its count. When dequeueing, again check the count, decrement it, and remove the key only when the count reaches zero.

Sergey Kalinichenko
  • 675,664
  • 71
  • 998
  • 1,399
  • Very good answer but actually I have just realised that I will need to be able to store duplicates in this container :/ – Patryk Dec 20 '12 at 22:32
  • @Patryk as long as the key types are objects and not primitives, you can compare first by the value of interest and then by an arbitrary value, something guaranteed to be unique. – Andrew Mao Mar 19 '14 at 19:32
  • 1
    Your solution is good, but there is a problem. In a `SortedDictionary` approach, you are only storing one value of `K`. There could be two different instances behaving differently but values being equal. So in cases where there are duplicates, you will only ever be dequeing just the first value. Yours is more performant, but Servy's answer is "more correct" – nawfal Jun 06 '14 at 08:26
  • Wouldn't using `SortedDictionary` make the Dequeue operation `O(logn)`? This is because the `remove` method of that dictionary is `O(logn)` according to the docs – Dragolis Jun 19 '20 at 19:20
  • You could use SortedSet with duplicates as well. Store the index into the set and use it to differentiate the duplicates. https://stackoverflow.com/a/66092317/2439234 – re3el Feb 07 '21 at 23:06
6

As mentioned earlier, a SortedDictionary gets you part way there, you just need a way of dealing with duplicate priorities. The way of dealing with duplicates in any Map based structure (Dictionary, SortedDictionary, etc.) is to make the value a collection of what you really want the values to be. In your case, it makes the most sense for the value to be a Queue<T>.

Here is a starting place. You can add additional methods such as Count, implementations of IEnumerable, etc. if needed.

/// <summary>
/// 
/// </summary>
/// <typeparam name="TElement">The type of the actual elements that are stored</typeparam>
/// <typeparam name="TKey">The type of the priority.  It probably makes sense to be an int or long, \
/// but any type that can be the key of a SortedDictionary will do.</typeparam>
public class PriorityQueue<TElement, TKey>
{
    private SortedDictionary<TKey, Queue<TElement>> dictionary = new SortedDictionary<TKey, Queue<TElement>>();
    private Func<TElement, TKey> selector;


    public PriorityQueue(Func<TElement, TKey> selector)
    {
        this.selector = selector;
    }

    public void Enqueue(TElement item)
    {
        TKey key = selector(item);
        Queue<TElement> queue;
        if (!dictionary.TryGetValue(key, out queue))
        {
            queue = new Queue<TElement>();
            dictionary.Add(key, queue);
        }

        queue.Enqueue(item);
    }

    public TElement Dequeue()
    {
        if (dictionary.Count == 0)
            throw new Exception("No items to Dequeue:");
        var key = dictionary.Keys.First();

        var queue = dictionary[key];
        var output = queue.Dequeue();
        if (queue.Count == 0)
            dictionary.Remove(key);

        return output;
    }
}
Servy
  • 193,745
  • 23
  • 295
  • 406
  • In the `Dequeue` method you can avoid the lookup, by getting `KeyValuePair` like this: `dictionary.First()`, so you get both `key` and `queue`. Nothing much, perhaps a little better. – nawfal Jun 06 '14 at 08:13
  • but isn't the dequeue `O(logn)` because of the usage of `dictionary.Remove(key)` which is `O(logn)`? Doesn't this ruin the desired `O(1)` Dequeue of priority queue? – Dragolis Jun 19 '20 at 19:18
  • 1
    @Dragolis I don't see anywhere that I said this was O(1). But yes, adding and removing items are, in the worst case, O(log(n)) where n is the number of unique priority values used. Note that it doesn't scale with the number of queued items, but the number of unique priority values, which is potentially *much* smaller than the number of total items. If the number of possible unique priority values is constant, then the whole thing is *actually* O(1) because of that. Also enqueuing and dequeuing items to be O(1) whenever you're not using a new priority value. – Servy Jun 19 '20 at 19:22
0

You could use the SortedSet with duplicates as well. Instead of storing the value in the set, you store the index of the list and then use the index as a differentiating factor in the comparison for duplicates.

List<int> nums = new List<int>{1,2,1,3,3,2};
SortedSet<int> pq = new SortedSet<int>(Comparer<int>.Create(
    (x,y) => nums[x] != nums[y] ? nums[x]-nums[y] : x-y
));
    
for (int i=0; i<nums.Count; i++) pq.Add(i);
while (pq.Any())
{
    Console.Write(nums[pq.Min] + " ");
    pq.Remove(pq.Min);
}

https://dotnetfiddle.net/Egg3oN

Edit:

I reused the same code from above to add the two methods as requested by OP

public static void Main()
{
    List<int> nums = new List<int>{1,2,1,3,3,2};
    SortedSet<int> pq = new SortedSet<int>(Comparer<int>.Create(
        (x,y) => nums[x] != nums[y] ? nums[x]-nums[y] : x-y
    ));
    
    for (int i=0; i<nums.Count; i++) Enqueue(pq, i);
    while (pq.Any())
    {
        int topIndex = Dequeue(pq);
        Console.Write(nums[topIndex] + " ");
    }
}

private static void Enqueue(SortedSet<int> pq, int numIndex)
{
    pq.Add(numIndex);
}

private static int Dequeue(SortedSet<int> pq)
{
    int topIndex = pq.Min;
    pq.Remove(pq.Min);
    return topIndex;
}
re3el
  • 675
  • 9
  • 24
  • This solution doesn't satisfy the requirement of enqueuing and dequeuing objects to/from the queue at any time. If the requirement was to enqueue all objects at once and then dequeue them in order, a simple `new Queue(objects.OrderBy(/*...*/))` would be sufficient. – Theodor Zoulias Feb 07 '21 at 20:46
  • I don't understand what you mean. The question was about having a sorted list after insertion and removing the last item as in PQ. This answer does both – re3el Feb 07 '21 at 22:52
  • At any instance you can run pq.Remove(pq.Min) to remove the min element and pq.Remove(pq.Max) to remove the max element – re3el Feb 07 '21 at 22:54
  • You are enqueuing indices into the `pq`, but what about the `nums`? These two are becoming out of sync. – Theodor Zoulias Feb 07 '21 at 23:24
  • The element is either present or not present. I don't understand when it will go out of sync. At any point there can only be one element with a certain index in both the SortedSet and nums. If you are removing an element from nums, you are removing from the sortedSet as well. Could you think of an example and share where it could fail? – re3el Feb 07 '21 at 23:32
  • I don't see anything happening related to the `nums` in your `Enqueue` and `Dequeue` methods. – Theodor Zoulias Feb 07 '21 at 23:53