Skip to content
This repository has been archived by the owner on Aug 2, 2023. It is now read-only.

Commit

Permalink
Solved priority queue design!
Browse files Browse the repository at this point in the history
  • Loading branch information
TimLovellSmith committed Oct 16, 2017
1 parent ca88785 commit d61eda5
Showing 1 changed file with 73 additions and 128 deletions.
201 changes: 73 additions & 128 deletions docs/specs/priority-queue.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,73 @@
# Priority queue proposal
# Priority queue proposal

## User data

Before anything is proposed, let's start with what can be prioritized...

### Input types

We want to be able to support all of the following configurations:
There are two possible scenarios in which to use a priority queue:

1. User data *is separate* from the priority (two physical instances).
2. User data *contains* the priority (as one or more properties).
3. User data *is* the priority (implements `IComparable<T>`).
4. Rare case: priority is obtainable via some other logic (resides in an object
different from the user data).
1. Each item of user data is *assigned* a *separate* priority (priority is a separate object from the elements in the collection.) We will sometimes call this the *extrinsic priority* scenario.
2. User data either *is itself a priority*, or has some other *natural* priority which can be derived from the element. (It might have a priority property, a it might be a *computed* priority using some function mapping elements to priorities.) We will sometimes call this the *intrinsic priority* scenario.

In this entire document, I will refer to the cases above with **(1)**, **(2)**,
**(3)**, and **(4)**.
### Uniqueness and changes in priority.

### Ideal solution
This implementation of priority queue is actually designed to provide a DICTIONARY-like or SET-like interface, where each element is unique (like a key), and priorities are mutable. This allows you to easily use the data structure for many popular scenarios requiring priority queues.
All elements are equality comparable, and each distinct element (according to equality comparison) can have at most a unique occurence in the queue, and be assigned a single priority value.
That priority value may be updated, over time.

Obviously, our solution should be flexible enough to (respectively):
This means there are really two types of comparators (comparison operators) involved in the definition of a priority queue class - one for comparing *elements* pairwise for equality, and one for comparing *element priorities* pairwise to see which is higher priority. It is unnecessary, and usually undesirable for the same comparator to be used for both priority comparisons and equality comparisons.

1. Simply accept two separate instances. The user should not be forced to create
a wrapper class for the two types only because of our API limitations.
2. Accept an element that already has the priority in it, without duplication
(no copying).
3. Be able to use `IComparable<T>` and don't expect an additional priority.
4. Be able to execute some additional logic that retrieves the priority for a
given element.
### Correct notification of priority changes (single and multiple)

### Our approach
In the case of elements which have an *assigned* priority, the priority is updated by explicitly calling a method updating the priority of the element with a new value. In this case it does not matter whether element priorities are mutated simultaneously, or in sequence.
In the case of elements which have a *natural* priority (not an *assigned* one), but where some event can cause priority of multiple elements to change at one time, it is necessary for the priority queue to be notified *simultaneously* or *atomically* about ALL elements whose priority has changed,
so that it can maintain internal data structure invariant when it performs the update-priority operation.

In order to be able to consume all of that, we need two types of priority
queues:
### Workarounds for uniqueness

* `PriorityQueue<T>`,
* `PriorityQueue<TElement, TPriority>`.
If you need to implement a priority queue scenario where duplicates are allowed, it is possible to do this by e.g. using a 'priority-queue of priority-queues' approach.

## Classes

There are actually two separate PriorityQueue classes. PriorityQueue<TElement, TPriority> implements the *assigned* or *extrinsic priority* scenario, and PriorityQueue<T> implements the *natural* or *intrinsic priority* scenario.

## `PriorityQueue<T>`

```csharp
public class PriorityQueue<T> : IQueue<T>
public class PriorityQueue<T> : ISet<T>, ICollection<T>, IEnumerable<T>, IEnumerable,
{
public PriorityQueue();
public PriorityQueue(IComparer<T> comparer);
public PriorityQueue(IComparer<T> priorityComparer);
public PriorityQueue(IComparer<T> priorityComparer, IEqualityComparer<T> equalityComparer);
public PriorityQueue(IEnumerable<T> collection);
public PriorityQueue(IEnumerable<T> collection, IComparer<T> comparer);
public PriorityQueue(IEnumerable<T> collection, IComparer<T> priorityComparer);
public PriorityQueue(IEnumerable<T> collection, IComparer<T> priorityComparer, IEqualityComparer<T> equalityComparer);

public IComparer<T> PriorityComparer { get; }
public IComparer<T> EqualityComparer { get; }

public IComparer<T> Comparer { get; }
public int Count { get; }
public bool IsReadOnly { get; }

public bool IsEmpty { get; }
public bool Add(T element);
public void Clear();
public bool Contains(T element);

public void Enqueue(T element);
public void CopyTo(T[] element, int index);
public bool Remove(T element);

public T Peek();
public T Dequeue();
public bool Remove(T element);

public bool TryPeek(out T element);
public bool TryDequeue(out T element);

public IEnumerator<T> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();

// + various other ISet interface methods (ExceptWith, Overlaps, IsProperSubsetOf, IsSubsetOf, IsSuperSetOf, IntersectWith, UnionWith, etc)
public struct Enumerator : IEnumerator<T>
{
public T Current { get; }
Expand All @@ -77,9 +79,9 @@ public class PriorityQueue<T> : IQueue<T>
}
```

### Scenarios
### Examples

#### (2)
#### (1)

Custom class with a priority inside:

Expand All @@ -99,22 +101,25 @@ var comparer = Comparer<MyClass>.Create((a, b) =>
});
```

And simply uses our priority queue:
And uses our priority queue with the default equalityComparer:

```csharp
var queue = new PriorityQueue<MyClass>(comparer);
var equalityComparer = EqualityComparer.<T>.Default;
var queue = new PriorityQueue<MyClass>(comparer, equalityComparer);

queue.Enqueue(new MyClass());
```

#### (3)
#### (2)

Already comparable type:
Already comparable (and equality-comparable, because all objects are!) type, where the objects themselves are the priorities:

```csharp
public class MyClass : IComparable<MyClass>
{
public int CompareTo(MyClass other) => /* some logic */
public int CompareTo(MyClass other) => /* some logic for comparing priorities*/
public int GetHashCode() => /* if overriding object equality comparison! */
public bool Equals(object other) => /* if overriding object equality comparison! */
}
```

Expand All @@ -124,46 +129,53 @@ Then simply call the default constructor (`Comparer<T>.Default` is assumed):
var queue = new PriorityQueue<MyClass>();
```

#### (4)

Priority for `MyClass` is obtainable from some other objects, for example a
dictionary. It is done analogically to **(2)**, simply by some custom logic in
the comparer.

## `PriorityQueue<TElement, TPriority>`

```csharp
public class PriorityQueue<TElement, TPriority> :
IEnumerable,
IEnumerable<(TElement element, TPriority priority)>,
IReadOnlyCollection<(TElement element, TPriority priority)>
IDictionary<TElement, TPriority>,
ICollection<KeyValuePair<TElement element, TPriority priority>>,
IEnumerable<KeyValuePair<TElement element, TPriority priority>>,
IEnumerable
{
public PriorityQueue();
public PriorityQueue(IComparer<TPriority> comparer);
public PriorityQueue(IComparer<TPriority> priorityComparer);
public PriorityQueue(IComparer<TPriority> priorityComparer, IEqualityComparer<TElement> equalityComparer);
public PriorityQueue(IEnumerable<(TElement, TPriority)> collection);
public PriorityQueue(IEnumerable<(TElement, TPriority)> collection, IComparer<TPriority> comparer);
public PriorityQueue(IEnumerable<(TElement, TPriority)> collection, IComparer<TPriority> priorityComparer);
public PriorityQueue(IEnumerable<(TElement, TPriority)> collection, IComparer<TPriority> priorityComparer, IEqualityComparer<TElement> equalityComparer);

public IComparer<TPriority> Comparer { get; }
public IComparer<TPriority> PriorityComparer { get; }
public int Count { get; }
public bool IsReadOnly { get; }

public TPriority Item[TElement] { get; set; }
public ICollection<TElement> Keys { get; }
public ICollection<TPriority> Values { get; }

public bool IsEmpty { get; }
public void Clear();
public bool Contains(TElement element);
public bool Contains(TElement element, out TPriority priority);

public void Enqueue(TElement element, TPriority priority);
public bool Add(TElement element, TPriority priority);

public (TElement element, TPriority priority) Peek();
public TElement Peek(TElement element);
public bool TryPeek(out TElement element, out TPriority priority);
public bool TryPeek(out TElement element);

public (TElement element, TPriority priority) Dequeue();
public TElement Dequeue();
public bool TryDequeue(out TElement element, out TPriority priority);
public bool TryDequeue(out TElement element);

public bool Remove(TElement element);
public bool Remove(TElement element, out TPriority priority);

public bool TryGetValue(TElement element, out TPriority priority);

public bool ICollection<KeyValuePair<TElement, TPriority>>.Add(KeyValuePair<TElement, TPriority> value);
public bool ICollection<KeyValuePair<TElement, TPriority>>.Contains(KeyValuePair<TElement, TPriority> value
public bool ICollection<KeyValuePair<TElement, TPriority>>.Remove(KeyValuePair<TElement, TPriority> value);
public bool IDictionary<TElement, TPriority>.ContainsKey(TElement key);

public IEnumerator<(TElement element, TPriority priority)> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();

Expand Down Expand Up @@ -202,80 +214,13 @@ queue.Enqueue(userData, priority);

To both priority queues:

* If the `IComparer<T>` is not delivered, `Comparer<T>.Default` is summoned.
* If the `IComparer<T>` is not delivered, `Comparer<T>.Default` is used.
* If the `IEqualityComparer<T>` is not delivered, `EqualityComparer<T>.Default` is used.
* `Peek` and `Dequeue` throw an exception if the collection is empty.
* `TryPeek` and `TryDequeue` only return false.
* `TryPeek` and `TryDequeue` return false if the collection is empty.
* `Add` returns false if the element was already in the set.
* `Remove` returns false if the element to remove is not found.
* `Remove` removes only the first occurrence of the specified element.

## `IQueue<T>`

With the design above, we can address some voices regarding the introduction of
`IQueue<T>`:

```csharp
public interface IQueue<T> :
IEnumerable,
IEnumerable<T>,
IReadOnlyCollection<T>
{
int Count { get; }

void Clear();
bool IsEmpty { get; }

void Enqueue(T element);

T Peek();
T Dequeue();

bool TryPeek(out T element);
bool TryDequeue(out T element);
}
```

### Notes

* Only `PriorityQueue<T>` would implement this interface.
* `IsEmpty` needs to be added to `Queue<T>`.


## Open questions

1. Do we really need `IQueue<T>`?
2. In priority queues, we have `Peek`, and `TryPeek`. There is `Dequeue` and
`TryDequeue`. Should there also be `Remove` and `TryRemove` following the same
pattern or only `bool Remove(T)` (as it is now)?
3. Do we want to be able to remove and update priorities of elements in O(log n)
instead of O(n)? Also, to be able to do conduct such operations on unique nodes
(instead of *whichever is found first*)? This would require us to use some sort
of a handle:

```csharp
void Enqueue(TElement element, TPriority priority, out object handle);

void Update(object handle, TPriority priority);

void Remove(object handle);
```

4. Do we want to provide an interface for priority queues in the future? If we
release two `PriorityQueue` classes, it may be hard to create an interface for
them.
5. If we don't want to add the concept of handles in our priority queues, we are
basically locking our solution on less efficient and less correct support for
updating / removing arbitrary elements from the collection (problem in Java).
It is additionally more problematic if we don't add a proper interface. A
solution could be to add a proper support for the heaps family (`IHeap` +
possibility of various implementations) in `System.Collections.Specialized`.
* Developers could write their third-party solutions based on a single,
standardized interface. Their code can depend on an interface rather than an
implementation (`PriorityQueue<T>` or `PriorityQueue<T, U>` — which to choose
as an argument?).
* If such a functionality is added to `CoreFXExtensions`, there would be no
common ground for third-party libraries.
* Decision where this would eventually land (`CoreFX` or not) directly
impacts whether missing features in our support for priority queues are an
issue or not. If we add heaps to `System.Collections.Specialized`, priority
queues can be lacking more power and enhanceability. If we don't, there is an
issue.
## No more open issues
Yay!

0 comments on commit d61eda5

Please sign in to comment.