using System; using System.Collections; using System.Collections.Generic; namespace Priority_Queue { /// /// A simplified priority queue implementation. Is stable, auto-resizes, and thread-safe, at the cost of being slightly slower than /// FastPriorityQueue /// Methods tagged as O(1) or O(log n) are assuming there are no duplicates. Duplicates may increase the algorithmic complexity. /// /// The type to enqueue /// The priority-type to use for nodes. Must extend IComparable<TPriority> public class SimplePriorityQueue : IPriorityQueue where TPriority : IComparable { private class SimpleNode : GenericPriorityQueueNode { public TItem Data { get; private set; } public SimpleNode(TItem data) { Data = data; } } private const int INITIAL_QUEUE_SIZE = 10; private readonly GenericPriorityQueue _queue; private readonly Dictionary> _itemToNodesCache; private readonly IList _nullNodesCache; #region Constructors /// /// Instantiate a new Priority Queue /// public SimplePriorityQueue() : this(Comparer.Default, EqualityComparer.Default) { } /// /// Instantiate a new Priority Queue /// /// The comparer used to compare TPriority values. Defaults to Comparer<TPriority>.default public SimplePriorityQueue(IComparer priorityComparer) : this(priorityComparer.Compare, EqualityComparer.Default) { } /// /// Instantiate a new Priority Queue /// /// The comparison function to use to compare TPriority values public SimplePriorityQueue(Comparison priorityComparer) : this(priorityComparer, EqualityComparer.Default) { } /// /// Instantiate a new Priority Queue /// /// The equality comparison function to use to compare TItem values public SimplePriorityQueue(IEqualityComparer itemEquality) : this(Comparer.Default, itemEquality) { } /// /// Instantiate a new Priority Queue /// /// The comparer used to compare TPriority values. Defaults to Comparer<TPriority>.default /// The equality comparison function to use to compare TItem values public SimplePriorityQueue(IComparer priorityComparer, IEqualityComparer itemEquality) : this(priorityComparer.Compare, itemEquality) { } /// /// Instantiate a new Priority Queue /// /// The comparison function to use to compare TPriority values /// The equality comparison function to use to compare TItem values public SimplePriorityQueue(Comparison priorityComparer, IEqualityComparer itemEquality) { _queue = new GenericPriorityQueue(INITIAL_QUEUE_SIZE, priorityComparer); _itemToNodesCache = new Dictionary>(itemEquality); _nullNodesCache = new List(); } #endregion /// /// Given an item of type T, returns the existing SimpleNode in the queue /// private SimpleNode GetExistingNode(TItem item) { if (item == null) { return _nullNodesCache.Count > 0 ? _nullNodesCache[0] : null; } IList nodes; if (!_itemToNodesCache.TryGetValue(item, out nodes)) { return null; } return nodes[0]; } /// /// Adds an item to the Node-cache to allow for many methods to be O(1) or O(log n) /// private void AddToNodeCache(SimpleNode node) { if (node.Data == null) { _nullNodesCache.Add(node); return; } IList nodes; if (!_itemToNodesCache.TryGetValue(node.Data, out nodes)) { nodes = new List(); _itemToNodesCache[node.Data] = nodes; } nodes.Add(node); } /// /// Removes an item to the Node-cache to allow for many methods to be O(1) or O(log n) (assuming no duplicates) /// private void RemoveFromNodeCache(SimpleNode node) { if (node.Data == null) { _nullNodesCache.Remove(node); return; } IList nodes; if (!_itemToNodesCache.TryGetValue(node.Data, out nodes)) { return; } nodes.Remove(node); if (nodes.Count == 0) { _itemToNodesCache.Remove(node.Data); } } /// /// Returns the number of nodes in the queue. /// O(1) /// public int Count { get { lock(_queue) { return _queue.Count; } } } /// /// Returns the head of the queue, without removing it (use Dequeue() for that). /// Throws an exception when the queue is empty. /// O(1) /// public TItem First { get { lock(_queue) { if(_queue.Count <= 0) { throw new InvalidOperationException("Cannot call .First on an empty queue"); } return _queue.First.Data; } } } /// /// Removes every node from the queue. /// O(n) /// public void Clear() { lock(_queue) { _queue.Clear(); _itemToNodesCache.Clear(); _nullNodesCache.Clear(); } } /// /// Returns whether the given item is in the queue. /// O(1) /// public bool Contains(TItem item) { lock(_queue) { return item == null ? _nullNodesCache.Count > 0 : _itemToNodesCache.ContainsKey(item); } } /// /// Removes the head of the queue (node with minimum priority; ties are broken by order of insertion), and returns it. /// If queue is empty, throws an exception /// O(log n) /// public TItem Dequeue() { lock(_queue) { if(_queue.Count <= 0) { throw new InvalidOperationException("Cannot call Dequeue() on an empty queue"); } SimpleNode node =_queue.Dequeue(); RemoveFromNodeCache(node); return node.Data; } } /// /// Enqueue the item with the given priority, without calling lock(_queue) or AddToNodeCache(node) /// /// /// /// private SimpleNode EnqueueNoLockOrCache(TItem item, TPriority priority) { SimpleNode node = new SimpleNode(item); if (_queue.Count == _queue.MaxSize) { _queue.Resize(_queue.MaxSize * 2 + 1); } _queue.Enqueue(node, priority); return node; } /// /// Enqueue a node to the priority queue. Lower values are placed in front. Ties are broken by first-in-first-out. /// This queue automatically resizes itself, so there's no concern of the queue becoming 'full'. /// Duplicates and null-values are allowed. /// O(log n) /// public void Enqueue(TItem item, TPriority priority) { lock(_queue) { IList nodes; if (item == null) { nodes = _nullNodesCache; } else if (!_itemToNodesCache.TryGetValue(item, out nodes)) { nodes = new List(); _itemToNodesCache[item] = nodes; } SimpleNode node = EnqueueNoLockOrCache(item, priority); nodes.Add(node); } } /// /// Enqueue a node to the priority queue if it doesn't already exist. Lower values are placed in front. Ties are broken by first-in-first-out. /// This queue automatically resizes itself, so there's no concern of the queue becoming 'full'. Null values are allowed. /// Returns true if the node was successfully enqueued; false if it already exists. /// O(log n) /// public bool EnqueueWithoutDuplicates(TItem item, TPriority priority) { lock(_queue) { IList nodes; if (item == null) { if (_nullNodesCache.Count > 0) { return false; } nodes = _nullNodesCache; } else if (_itemToNodesCache.ContainsKey(item)) { return false; } else { nodes = new List(); _itemToNodesCache[item] = nodes; } SimpleNode node = EnqueueNoLockOrCache(item, priority); nodes.Add(node); return true; } } /// /// Removes an item from the queue. The item does not need to be the head of the queue. /// If the item is not in the queue, an exception is thrown. If unsure, check Contains() first. /// If multiple copies of the item are enqueued, only the first one is removed. /// O(log n) /// public void Remove(TItem item) { lock(_queue) { SimpleNode removeMe; IList nodes; if (item == null) { if (_nullNodesCache.Count == 0) { throw new InvalidOperationException("Cannot call Remove() on a node which is not enqueued: " + item); } removeMe = _nullNodesCache[0]; nodes = _nullNodesCache; } else { if (!_itemToNodesCache.TryGetValue(item, out nodes)) { throw new InvalidOperationException("Cannot call Remove() on a node which is not enqueued: " + item); } removeMe = nodes[0]; if (nodes.Count == 1) { _itemToNodesCache.Remove(item); } } _queue.Remove(removeMe); nodes.Remove(removeMe); } } /// /// Call this method to change the priority of an item. /// Calling this method on a item not in the queue will throw an exception. /// If the item is enqueued multiple times, only the first one will be updated. /// (If your requirements are complex enough that you need to enqueue the same item multiple times and be able /// to update all of them, please wrap your items in a wrapper class so they can be distinguished). /// O(log n) /// public void UpdatePriority(TItem item, TPriority priority) { lock (_queue) { SimpleNode updateMe = GetExistingNode(item); if (updateMe == null) { throw new InvalidOperationException("Cannot call UpdatePriority() on a node which is not enqueued: " + item); } _queue.UpdatePriority(updateMe, priority); } } /// /// Returns the priority of the given item. /// Calling this method on a item not in the queue will throw an exception. /// If the item is enqueued multiple times, only the priority of the first will be returned. /// (If your requirements are complex enough that you need to enqueue the same item multiple times and be able /// to query all their priorities, please wrap your items in a wrapper class so they can be distinguished). /// O(1) /// public TPriority GetPriority(TItem item) { lock (_queue) { SimpleNode findMe = GetExistingNode(item); if(findMe == null) { throw new InvalidOperationException("Cannot call GetPriority() on a node which is not enqueued: " + item); } return findMe.Priority; } } #region Try* methods for multithreading /// Get the head of the queue, without removing it (use TryDequeue() for that). /// Useful for multi-threading, where the queue may become empty between calls to Contains() and First /// Returns true if successful, false otherwise /// O(1) public bool TryFirst(out TItem first) { if (_queue.Count > 0) { lock (_queue) { if (_queue.Count > 0) { first = _queue.First.Data; return true; } } } first = default(TItem); return false; } /// /// Removes the head of the queue (node with minimum priority; ties are broken by order of insertion), and sets it to first. /// Useful for multi-threading, where the queue may become empty between calls to Contains() and Dequeue() /// Returns true if successful; false if queue was empty /// O(log n) /// public bool TryDequeue(out TItem first) { if (_queue.Count > 0) { lock (_queue) { if (_queue.Count > 0) { SimpleNode node = _queue.Dequeue(); first = node.Data; RemoveFromNodeCache(node); return true; } } } first = default(TItem); return false; } /// /// Attempts to remove an item from the queue. The item does not need to be the head of the queue. /// Useful for multi-threading, where the queue may become empty between calls to Contains() and Remove() /// Returns true if the item was successfully removed, false if it wasn't in the queue. /// If multiple copies of the item are enqueued, only the first one is removed. /// O(log n) /// public bool TryRemove(TItem item) { lock(_queue) { SimpleNode removeMe; IList nodes; if (item == null) { if (_nullNodesCache.Count == 0) { return false; } removeMe = _nullNodesCache[0]; nodes = _nullNodesCache; } else { if (!_itemToNodesCache.TryGetValue(item, out nodes)) { return false; } removeMe = nodes[0]; if (nodes.Count == 1) { _itemToNodesCache.Remove(item); } } _queue.Remove(removeMe); nodes.Remove(removeMe); return true; } } /// /// Call this method to change the priority of an item. /// Useful for multi-threading, where the queue may become empty between calls to Contains() and UpdatePriority() /// If the item is enqueued multiple times, only the first one will be updated. /// (If your requirements are complex enough that you need to enqueue the same item multiple times and be able /// to update all of them, please wrap your items in a wrapper class so they can be distinguished). /// Returns true if the item priority was updated, false otherwise. /// O(log n) /// public bool TryUpdatePriority(TItem item, TPriority priority) { lock(_queue) { SimpleNode updateMe = GetExistingNode(item); if(updateMe == null) { return false; } _queue.UpdatePriority(updateMe, priority); return true; } } /// /// Attempt to get the priority of the given item. /// Useful for multi-threading, where the queue may become empty between calls to Contains() and GetPriority() /// If the item is enqueued multiple times, only the priority of the first will be returned. /// (If your requirements are complex enough that you need to enqueue the same item multiple times and be able /// to query all their priorities, please wrap your items in a wrapper class so they can be distinguished). /// Returns true if the item was found in the queue, false otherwise /// O(1) /// public bool TryGetPriority(TItem item, out TPriority priority) { lock(_queue) { SimpleNode findMe = GetExistingNode(item); if(findMe == null) { priority = default(TPriority); return false; } priority = findMe.Priority; return true; } } #endregion public IEnumerator GetEnumerator() { List queueData = new List(); lock (_queue) { //Copy to a separate list because we don't want to 'yield return' inside a lock foreach(var node in _queue) { queueData.Add(node.Data); } } return queueData.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public bool IsValidQueue() { lock(_queue) { // Check all items in cache are in the queue foreach (IList nodes in _itemToNodesCache.Values) { foreach (SimpleNode node in nodes) { if (!_queue.Contains(node)) { return false; } } } // Check all items in queue are in cache foreach (SimpleNode node in _queue) { if (GetExistingNode(node.Data) == null) { return false; } } // Check queue structure itself return _queue.IsValidQueue(); } } } /// /// A simplified priority queue implementation. Is stable, auto-resizes, and thread-safe, at the cost of being slightly slower than /// FastPriorityQueue /// This class is kept here for backwards compatibility. It's recommended you use SimplePriorityQueue<TItem, TPriority> /// /// The type to enqueue public class SimplePriorityQueue : SimplePriorityQueue { /// /// Instantiate a new Priority Queue /// public SimplePriorityQueue() { } /// /// Instantiate a new Priority Queue /// /// The comparer used to compare priority values. Defaults to Comparer<float>.default public SimplePriorityQueue(IComparer comparer) : base(comparer) { } /// /// Instantiate a new Priority Queue /// /// The comparison function to use to compare priority values public SimplePriorityQueue(Comparison comparer) : base(comparer) { } } }