Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement PriorityQueue.Remove #93994

Merged
merged 10 commits into from
Oct 30, 2023
1 change: 1 addition & 0 deletions src/libraries/System.Collections/ref/System.Collections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public partial class PriorityQueue<TElement, TPriority>
public void EnqueueRange(System.Collections.Generic.IEnumerable<TElement> elements, TPriority priority) { }
public int EnsureCapacity(int capacity) { throw null; }
public TElement Peek() { throw null; }
public bool Remove(TElement element, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TElement removedElement, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TPriority priority, System.Collections.Generic.IEqualityComparer<TElement>? equalityComparer = null) { throw null; }
public void TrimExcess() { }
public bool TryDequeue([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TElement element, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TPriority priority) { throw null; }
public bool TryPeek([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TElement element, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TPriority priority) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,56 @@ public void EnqueueRange(IEnumerable<TElement> elements, TPriority priority)
}
}

/// <summary>
/// Removes the first occurrence the equals the specified parameter.
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
/// <param name="element">The element to look for.</param>
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
/// <param name="removedElement">The actual element that got removed from the queue.</param>
/// <param name="priority">The priority value associated with the removed element.</param>
/// <param name="equalityComparer">The equality comparer governing element equality.</param>
/// <returns><see langword="true"/> if matching entry was found and removed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The method performs a linear-time scan of every element in the heap, removing the first value found to match the <paramref name="element"/> parameter.
/// In case of duplicate entries, what entry does get removed is non-deterministic, does not take priority into account and reflects the structure of the heap at the time of removal.
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
/// </remarks>
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
public bool Remove(
TElement element,
[MaybeNullWhen(false)] out TElement removedElement,
[MaybeNullWhen(false)] out TPriority priority,
IEqualityComparer<TElement>? equalityComparer = null)
{
int index = FindIndex(element, equalityComparer);
if (index == -1)
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
{
removedElement = default;
priority = default;
return false;
}

(TElement Element, TPriority Priority)[] nodes = _nodes;
(removedElement, priority) = nodes[index];
int newSize = --_size;

if (index < newSize)
{
// We're removing an element from the middle of the heap.
// Pop the last element in the collection and sift downward from the removed index.
(TElement Element, TPriority Priority) lastNode = nodes[newSize];

if (_comparer == null)
{
MoveDownDefaultComparer(lastNode, index);
}
else
{
MoveDownCustomComparer(lastNode, index);
}
}

nodes[newSize] = default;
return true;
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Removes all items from the <see cref="PriorityQueue{TElement, TPriority}"/>.
/// </summary>
Expand Down Expand Up @@ -809,6 +859,42 @@ private void MoveDownCustomComparer((TElement Element, TPriority Priority) node,
nodes[nodeIndex] = node;
}

/// <summary>
/// Scans the heap for the first index containing an element equal to the specified parameter.
/// </summary>
private int FindIndex(TElement element, IEqualityComparer<TElement>? equalityComparer)
{
equalityComparer ??= EqualityComparer<TElement>.Default;
(TElement Element, TPriority Priority)[] nodes = _nodes;
int size = _size;
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved

// Currently the JIT doesn't optimize direct EqualityComparer<T>.Default.Equals
// calls for reference types, so we want to cache the comparer instance instead.
// TODO https://github.com/dotnet/runtime/issues/10050: Update if this changes in the future.
if (typeof(TElement).IsValueType && equalityComparer == EqualityComparer<TElement>.Default)
{
for (int i = 0; i < size; i++)
{
if (EqualityComparer<TElement>.Default.Equals(element, nodes[i].Element))
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
{
return i;
}
}
}
else
{
for (int i = 0; i < size; i++)
{
if (equalityComparer.Equals(element, nodes[i].Element))
{
return i;
}
}
}

return -1;
}

/// <summary>
/// Initializes the custom comparer to be used internally by the heap.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public void PriorityQueue_EnumerableConstructor_ShouldContainAllElements(int cou

#endregion

#region Enqueue, Dequeue, Peek, EnqueueDequeue, DequeueEnqueue
#region Enqueue, Dequeue, Peek, EnqueueDequeue, DequeueEnqueue, Remove

[Theory]
[MemberData(nameof(ValidCollectionSizes))]
Expand Down Expand Up @@ -246,6 +246,35 @@ public void PriorityQueue_DequeueEnqueue(int count)
AssertExtensions.CollectionEqual(expectedItems, queue.UnorderedItems, EqualityComparer<(TElement, TPriority)>.Default);
}

[Theory]
[MemberData(nameof(ValidCollectionSizes))]
public void PriorityQueue_Remove_AllElements(int count)
{
bool result;
TElement removedElement;
TPriority removedPriority;

PriorityQueue<TElement, TPriority> queue = CreatePriorityQueue(count, count, out List<(TElement element, TPriority priority)> generatedItems);

for (int i = count - 1; i >= 0; i--)
{
(TElement element, TPriority priority) = generatedItems[i];
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved

result = queue.Remove(element, out removedElement, out removedPriority);

Assert.True(result);
Assert.Equal(element, removedElement);
Assert.Equal(priority, removedPriority);
Assert.Equal(i, queue.Count);
}

result = queue.Remove(default, out removedElement, out removedPriority);

Assert.False(result);
Assert.Equal(default, removedElement);
Assert.Equal(default, removedPriority);
}

#endregion

#region Clear
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,41 @@ public void PriorityQueue_Generic_EnqueueRange_Null()
Assert.Equal("not null", queue.Dequeue());
}

[Fact]
public void PriorityQueue_Generic_Remove_MatchingElement()
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
{
PriorityQueue<string, int> queue = new PriorityQueue<string, int>();
queue.EnqueueRange([("value0", 0), ("value1", 1), ("value2", 2)]);


Assert.True(queue.Remove("value1", out string removedElement, out int removedPriority));
Assert.Equal("value1", removedElement);
Assert.Equal(1, removedPriority);
}

[Fact]
public void PriorityQueue_Generic_Remove_MismatchElement()
{
PriorityQueue<string, int> queue = new PriorityQueue<string, int>();
queue.EnqueueRange([("value0", 0), ("value1", 1), ("value2", 2)]);

Assert.False(queue.Remove("value4", out string removedElement, out int removedPriority));
Assert.Null(removedElement);
Assert.Equal(0, removedPriority);
}

[Fact]
public void PriorityQueue_Generic_Remove_CustomEqualityComparer()
{
PriorityQueue<string, int> queue = new PriorityQueue<string, int>();
queue.EnqueueRange([("value0", 0), ("value1", 1), ("value2", 2)]);
EqualityComparer<string> equalityComparer = EqualityComparer<string>.Create((left, right) => left[^1] == right[^1]);

Assert.True(queue.Remove("someOtherValue1", out string removedElement, out int removedPriority, equalityComparer));
Assert.Equal("value1", removedElement);
Assert.Equal(1, removedPriority);
}

[Fact]
public void PriorityQueue_Constructor_int_Negative_ThrowsArgumentOutOfRangeException()
{
Expand Down Expand Up @@ -207,6 +242,16 @@ public void PriorityQueue_EmptyCollection_Peek_ShouldReturnFalse()
Assert.Throws<InvalidOperationException>(() => queue.Peek());
}

[Fact]
public void PriorityQueue_EmptyCollection_Remove_ShouldReturnFalse()
{
var queue = new PriorityQueue<string, string>();

Assert.False(queue.Remove(element: "element", out string removedElement, out string removedPriority));
Assert.Null(removedElement);
Assert.Null(removedPriority);
}

eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
#region EnsureCapacity, TrimExcess

[Fact]
Expand Down
Loading