Skip to content

Commit c1f4341

Browse files
eiriktsarpalisdanmoseleystephentoub
authored
Implement PriorityQueue.Remove (#93994)
* Implement PriorityQueue.Remove * Update src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs * Update src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs Co-authored-by: Dan Moseley <danmose@microsoft.com> * Update src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs * Update src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs * Update src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs Co-authored-by: Stephen Toub <stoub@microsoft.com> * Address feedback. * Address feedback * Add a Dijkstra smoke test. * Alias distance type --------- Co-authored-by: Dan Moseley <danmose@microsoft.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
1 parent 44a5abd commit c1f4341

File tree

6 files changed

+259
-3
lines changed

6 files changed

+259
-3
lines changed

src/libraries/System.Collections/ref/System.Collections.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ public void EnqueueRange(System.Collections.Generic.IEnumerable<(TElement Elemen
126126
public void EnqueueRange(System.Collections.Generic.IEnumerable<TElement> elements, TPriority priority) { }
127127
public int EnsureCapacity(int capacity) { throw null; }
128128
public TElement Peek() { throw null; }
129+
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; }
129130
public void TrimExcess() { }
130131
public bool TryDequeue([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TElement element, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TPriority priority) { throw null; }
131132
public bool TryPeek([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TElement element, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TPriority priority) { throw null; }

src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,59 @@ public void EnqueueRange(IEnumerable<TElement> elements, TPriority priority)
502502
}
503503
}
504504

505+
/// <summary>
506+
/// Removes the first occurrence that equals the specified parameter.
507+
/// </summary>
508+
/// <param name="element">The element to try to remove.</param>
509+
/// <param name="removedElement">The actual element that got removed from the queue.</param>
510+
/// <param name="priority">The priority value associated with the removed element.</param>
511+
/// <param name="equalityComparer">The equality comparer governing element equality.</param>
512+
/// <returns><see langword="true"/> if matching entry was found and removed, <see langword="false"/> otherwise.</returns>
513+
/// <remarks>
514+
/// 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.
515+
/// In case of duplicate entries, what entry does get removed is non-deterministic and does not take priority into account.
516+
///
517+
/// If no <paramref name="equalityComparer"/> is specified, <see cref="EqualityComparer{TElement}.Default"/> will be used instead.
518+
/// </remarks>
519+
public bool Remove(
520+
TElement element,
521+
[MaybeNullWhen(false)] out TElement removedElement,
522+
[MaybeNullWhen(false)] out TPriority priority,
523+
IEqualityComparer<TElement>? equalityComparer = null)
524+
{
525+
int index = FindIndex(element, equalityComparer);
526+
if (index < 0)
527+
{
528+
removedElement = default;
529+
priority = default;
530+
return false;
531+
}
532+
533+
(TElement Element, TPriority Priority)[] nodes = _nodes;
534+
(removedElement, priority) = nodes[index];
535+
int newSize = --_size;
536+
537+
if (index < newSize)
538+
{
539+
// We're removing an element from the middle of the heap.
540+
// Pop the last element in the collection and sift downward from the removed index.
541+
(TElement Element, TPriority Priority) lastNode = nodes[newSize];
542+
543+
if (_comparer == null)
544+
{
545+
MoveDownDefaultComparer(lastNode, index);
546+
}
547+
else
548+
{
549+
MoveDownCustomComparer(lastNode, index);
550+
}
551+
}
552+
553+
nodes[newSize] = default;
554+
_version++;
555+
return true;
556+
}
557+
505558
/// <summary>
506559
/// Removes all items from the <see cref="PriorityQueue{TElement, TPriority}"/>.
507560
/// </summary>
@@ -809,6 +862,41 @@ private void MoveDownCustomComparer((TElement Element, TPriority Priority) node,
809862
nodes[nodeIndex] = node;
810863
}
811864

865+
/// <summary>
866+
/// Scans the heap for the first index containing an element equal to the specified parameter.
867+
/// </summary>
868+
private int FindIndex(TElement element, IEqualityComparer<TElement>? equalityComparer)
869+
{
870+
equalityComparer ??= EqualityComparer<TElement>.Default;
871+
ReadOnlySpan<(TElement Element, TPriority Priority)> nodes = _nodes.AsSpan(0, _size);
872+
873+
// Currently the JIT doesn't optimize direct EqualityComparer<T>.Default.Equals
874+
// calls for reference types, so we want to cache the comparer instance instead.
875+
// TODO https://github.com/dotnet/runtime/issues/10050: Update if this changes in the future.
876+
if (typeof(TElement).IsValueType && equalityComparer == EqualityComparer<TElement>.Default)
877+
{
878+
for (int i = 0; i < nodes.Length; i++)
879+
{
880+
if (EqualityComparer<TElement>.Default.Equals(element, nodes[i].Element))
881+
{
882+
return i;
883+
}
884+
}
885+
}
886+
else
887+
{
888+
for (int i = 0; i < nodes.Length; i++)
889+
{
890+
if (equalityComparer.Equals(element, nodes[i].Element))
891+
{
892+
return i;
893+
}
894+
}
895+
}
896+
897+
return -1;
898+
}
899+
812900
/// <summary>
813901
/// Initializes the custom comparer to be used internally by the heap.
814902
/// </summary>

src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.Tests.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public void PriorityQueue_EnumerableConstructor_ShouldContainAllElements(int cou
9393

9494
#endregion
9595

96-
#region Enqueue, Dequeue, Peek, EnqueueDequeue, DequeueEnqueue
96+
#region Enqueue, Dequeue, Peek, EnqueueDequeue, DequeueEnqueue, Remove
9797

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

249+
[Theory]
250+
[MemberData(nameof(ValidCollectionSizes))]
251+
public void PriorityQueue_Remove_AllElements(int count)
252+
{
253+
bool result;
254+
TElement removedElement;
255+
TPriority removedPriority;
256+
257+
PriorityQueue<TElement, TPriority> queue = CreatePriorityQueue(count, count, out List<(TElement element, TPriority priority)> generatedItems);
258+
259+
for (int i = count - 1; i >= 0; i--)
260+
{
261+
(TElement element, TPriority priority) = generatedItems[i];
262+
263+
result = queue.Remove(element, out removedElement, out removedPriority);
264+
265+
Assert.True(result);
266+
Assert.Equal(element, removedElement);
267+
Assert.Equal(priority, removedPriority);
268+
Assert.Equal(i, queue.Count);
269+
}
270+
271+
result = queue.Remove(default, out removedElement, out removedPriority);
272+
273+
Assert.False(result);
274+
Assert.Equal(default, removedElement);
275+
Assert.Equal(default, removedPriority);
276+
}
277+
249278
#endregion
250279

251280
#region Clear
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using Xunit;
7+
using NodeId = int;
8+
using Distance = int;
9+
10+
namespace System.Collections.Tests
11+
{
12+
public partial class PriorityQueue_NonGeneric_Tests
13+
{
14+
public record struct Graph(Edge[][] nodes);
15+
public record struct Edge(NodeId neighbor, Distance weight);
16+
17+
[Fact]
18+
public static void PriorityQueue_DijkstraSmokeTest()
19+
{
20+
var graph = new Graph([
21+
[new Edge(1, 7), new Edge(2, 9), new Edge(5, 14)],
22+
[new Edge(0, 7), new Edge(2, 10), new Edge(3, 15)],
23+
[new Edge(0, 9), new Edge(1, 10), new Edge(3, 11), new Edge(5, 2)],
24+
[new Edge(1, 15), new Edge(2, 11), new Edge(4, 6)],
25+
[new Edge(3, 6), new Edge(5, 9)],
26+
[new Edge(0, 14), new Edge(2, 2), new Edge(4, 9)],
27+
]);
28+
29+
NodeId startNode = 0;
30+
31+
(NodeId node, Distance distance)[] expectedDistances =
32+
[
33+
(0, 0),
34+
(1, 7),
35+
(2, 9),
36+
(3, 20),
37+
(4, 20),
38+
(5, 11),
39+
];
40+
41+
(NodeId node, Distance distance)[] actualDistances = RunDijkstra(graph, startNode);
42+
43+
Assert.Equal(expectedDistances, actualDistances);
44+
}
45+
46+
public static (NodeId node, Distance distance)[] RunDijkstra(Graph graph, NodeId startNode)
47+
{
48+
Distance[] distances = Enumerable.Repeat(int.MaxValue, graph.nodes.Length).ToArray();
49+
var queue = new PriorityQueue<NodeId, Distance>();
50+
51+
distances[startNode] = 0;
52+
queue.Enqueue(startNode, 0);
53+
54+
do
55+
{
56+
NodeId nodeId = queue.Dequeue();
57+
Distance nodeDistance = distances[nodeId];
58+
59+
foreach (Edge edge in graph.nodes[nodeId])
60+
{
61+
Distance distance = distances[edge.neighbor];
62+
Distance newDistance = nodeDistance + edge.weight;
63+
if (newDistance < distance)
64+
{
65+
distances[edge.neighbor] = newDistance;
66+
// Simulate priority update by attempting to remove the entry
67+
// before re-inserting it with the new distance.
68+
queue.Remove(edge.neighbor, out _, out _);
69+
queue.Enqueue(edge.neighbor, newDistance);
70+
}
71+
}
72+
}
73+
while (queue.Count > 0);
74+
75+
return distances.Select((distance, nodeId) => (nodeId, distance)).ToArray();
76+
}
77+
}
78+
}

src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Tests.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace System.Collections.Tests
1010
{
11-
public class PriorityQueue_NonGeneric_Tests : TestBase
11+
public partial class PriorityQueue_NonGeneric_Tests : TestBase
1212
{
1313
protected PriorityQueue<string, int> CreateSmallPriorityQueue(out HashSet<(string, int)> items)
1414
{
@@ -167,6 +167,55 @@ public void PriorityQueue_Generic_EnqueueRange_Null()
167167
Assert.Equal("not null", queue.Dequeue());
168168
}
169169

170+
[Fact]
171+
public void PriorityQueue_Generic_Remove_MatchingElement()
172+
{
173+
PriorityQueue<string, int> queue = new PriorityQueue<string, int>();
174+
queue.EnqueueRange([("value0", 0), ("value1", 1), ("value2", 2)]);
175+
176+
Assert.True(queue.Remove("value1", out string removedElement, out int removedPriority));
177+
Assert.Equal("value1", removedElement);
178+
Assert.Equal(1, removedPriority);
179+
Assert.Equal(2, queue.Count);
180+
}
181+
182+
[Fact]
183+
public void PriorityQueue_Generic_Remove_MismatchElement()
184+
{
185+
PriorityQueue<string, int> queue = new PriorityQueue<string, int>();
186+
queue.EnqueueRange([("value0", 0), ("value1", 1), ("value2", 2)]);
187+
188+
Assert.False(queue.Remove("value4", out string removedElement, out int removedPriority));
189+
Assert.Null(removedElement);
190+
Assert.Equal(0, removedPriority);
191+
Assert.Equal(3, queue.Count);
192+
}
193+
194+
[Fact]
195+
public void PriorityQueue_Generic_Remove_DuplicateElement()
196+
{
197+
PriorityQueue<string, int> queue = new PriorityQueue<string, int>();
198+
queue.EnqueueRange([("value0", 0), ("value1", 1), ("value0", 2)]);
199+
200+
Assert.True(queue.Remove("value0", out string removedElement, out int removedPriority));
201+
Assert.Equal("value0", removedElement);
202+
Assert.True(removedPriority is 0 or 2);
203+
Assert.Equal(2, queue.Count);
204+
}
205+
206+
[Fact]
207+
public void PriorityQueue_Generic_Remove_CustomEqualityComparer()
208+
{
209+
PriorityQueue<string, int> queue = new PriorityQueue<string, int>();
210+
queue.EnqueueRange([("value0", 0), ("value1", 1), ("value2", 2)]);
211+
EqualityComparer<string> equalityComparer = EqualityComparer<string>.Create((left, right) => left[^1] == right[^1]);
212+
213+
Assert.True(queue.Remove("someOtherValue1", out string removedElement, out int removedPriority, equalityComparer));
214+
Assert.Equal("value1", removedElement);
215+
Assert.Equal(1, removedPriority);
216+
Assert.Equal(2, queue.Count);
217+
}
218+
170219
[Fact]
171220
public void PriorityQueue_Constructor_int_Negative_ThrowsArgumentOutOfRangeException()
172221
{
@@ -207,6 +256,16 @@ public void PriorityQueue_EmptyCollection_Peek_ShouldReturnFalse()
207256
Assert.Throws<InvalidOperationException>(() => queue.Peek());
208257
}
209258

259+
[Fact]
260+
public void PriorityQueue_EmptyCollection_Remove_ShouldReturnFalse()
261+
{
262+
var queue = new PriorityQueue<string, string>();
263+
264+
Assert.False(queue.Remove(element: "element", out string removedElement, out string removedPriority));
265+
Assert.Null(removedElement);
266+
Assert.Null(removedPriority);
267+
}
268+
210269
#region EnsureCapacity, TrimExcess
211270

212271
[Fact]

src/libraries/System.Collections/tests/System.Collections.Tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
44
<TestRuntime>true</TestRuntime>
@@ -106,6 +106,7 @@
106106
<Compile Include="Generic\PriorityQueue\PriorityQueue.Generic.Tests.cs" />
107107
<Compile Include="Generic\PriorityQueue\PriorityQueue.PropertyTests.cs" />
108108
<Compile Include="Generic\PriorityQueue\PriorityQueue.Tests.cs" />
109+
<Compile Include="Generic\PriorityQueue\PriorityQueue.Tests.Dijkstra.cs" />
109110
<Compile Include="Generic\Queue\Queue.Generic.cs" />
110111
<Compile Include="Generic\Queue\Queue.Generic.Tests.cs" />
111112
<Compile Include="Generic\Queue\Queue.Tests.cs" />

0 commit comments

Comments
 (0)