Skip to content

Commit 23c86d7

Browse files
Andrei ConstantinescuP4P5T123
authored andcommitted
feat: Add PriorityQueue (#392)
* Partial implementation. * Fix bug and add peek. * Started with tests, and more functions. * Towards more tests, adding SetPriorityQueue. * Completing first implementation, with tests. * Add comments to PriorityQueue.mo * Move set implementation and add benchmark. * Better holes. * Extra benchmark. * More tests. * . * Comments. * Optimization. * Comments for SetWrapper. * Move SetWrapper. * Run format. * . * npm run validate * Fix imports in docstrings. * Add Changelog entry. * Fix Changelog entry. * Remove inefficient push and pop operations (and benchmarks for them). * Move PriorityQueueSet.mo * Fix validation. * Update Changelog.
1 parent cd11e0c commit 23c86d7

File tree

7 files changed

+1158
-1
lines changed

7 files changed

+1158
-1
lines changed

Changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## Next
22

3-
* internal: updates `matchers` dev-dependency (#394)
3+
* internal: updates `matchers` dev-dependency (#394).
4+
* Add `PriorityQueue` (#392).
45
* Add support for Weak references (#388).
56
* Clarify difference between `List` and `pure/List` in doc comments (#386).
67

bench/PriorityQueues.bench.mo

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import Bench "mo:bench";
2+
3+
import Array "../src/Array";
4+
import Nat "../src/Nat";
5+
import PriorityQueue "../src/PriorityQueue";
6+
import PriorityQueueSet "utils/PriorityQueueSet";
7+
import Random "../src/Random";
8+
import Runtime "../src/Runtime";
9+
import Map "../src/Map";
10+
import Text "../src/Text";
11+
12+
module {
13+
14+
type PriorityQueueUpdateOperation<T> = {
15+
#Push : T;
16+
#Pop;
17+
#Clear
18+
};
19+
20+
// Generates a randomized sequence of PriorityQueueUpdateOperations on Nat values.
21+
// The distribution of operations is controlled by weights.
22+
//
23+
// randomSeed - seed for reproducible RNG
24+
// operationsCount – total number of operations to generate
25+
// maxValueExclusive – upper bound (exclusive) for values pushed into the queue
26+
// wPush – relative weight of #Push operations (values in [0, maxValueExclusive))
27+
// wPop – relative weight of #Pop operations
28+
// wClear – relative weight of #Clear operations
29+
func genOpsNatRandom(
30+
randomSeed : Nat64,
31+
operationsCount : Nat,
32+
maxValueExclusive : Nat,
33+
wPush : Nat,
34+
wPop : Nat,
35+
wClear : Nat
36+
) : [PriorityQueueUpdateOperation<Nat>] {
37+
let rng = Random.seed(randomSeed);
38+
Array.tabulate<PriorityQueueUpdateOperation<Nat>>(
39+
operationsCount,
40+
func(_) {
41+
let aux = rng.natRange(0, wPush + wPop + wClear);
42+
if (aux < wPush) {
43+
#Push(rng.natRange(0, maxValueExclusive))
44+
} else if (aux < wPush + wPop) {
45+
#Pop
46+
} else {
47+
#Clear
48+
}
49+
}
50+
)
51+
};
52+
53+
// Generates a sequence of PriorityQueueUpdateOperations on Nat values:
54+
// 1. pushOperationsCount pushes, followed by
55+
// 2. pushOperationsCount pops.
56+
//
57+
// randomSeed - seed for reproducible RNG
58+
// pushOperationsCount – total number of push operations to generate
59+
// maxValueExclusive – upper bound (exclusive) for values pushed into the queue
60+
func genOpsPushThenPop(
61+
randomSeed : Nat64,
62+
pushOperationsCount : Nat,
63+
maxValueExclusive : Nat
64+
) : [PriorityQueueUpdateOperation<Nat>] {
65+
let rng = Random.seed(randomSeed);
66+
Array.tabulate<PriorityQueueUpdateOperation<Nat>>(
67+
2 * pushOperationsCount,
68+
func(i) {
69+
switch (i < pushOperationsCount) {
70+
case true #Push(rng.natRange(0, maxValueExclusive));
71+
case false #Pop
72+
}
73+
}
74+
)
75+
};
76+
77+
// Generates a sequence of PriorityQueueUpdateOperations on Nat values:
78+
// 1. size pushes, followed by
79+
// 2. popPushCount instances of a pop followed by a push.
80+
//
81+
// randomSeed - seed for reproducible RNG
82+
// size – initial size
83+
// popPushCount - number of times to pop and then push
84+
// maxValueExclusive – upper bound (exclusive) for values pushed into the queue
85+
func genOpsKeepConstantSize(
86+
randomSeed : Nat64,
87+
size : Nat,
88+
popPushCount : Nat,
89+
maxValueExclusive : Nat
90+
) : [PriorityQueueUpdateOperation<Nat>] {
91+
let rng = Random.seed(randomSeed);
92+
Array.tabulate<PriorityQueueUpdateOperation<Nat>>(
93+
size + 2 * popPushCount,
94+
func(i) {
95+
if (i < size or (i - size) % 2 == 1) {
96+
#Push(rng.natRange(0, maxValueExclusive))
97+
} else {
98+
#Pop
99+
}
100+
}
101+
)
102+
};
103+
104+
public func init() : Bench.Bench {
105+
let bench = Bench.Bench();
106+
107+
bench.name("Different priority queue implementations");
108+
bench.description("Compare the performance of the following priority queue implementations:
109+
- `PriorityQueue`: Binary heap implementation over `List`.
110+
- `PriorityQueueSet`: Wrapper over `Set<(T, Nat)>`.");
111+
112+
let testInstances : Map.Map<Text, [PriorityQueueUpdateOperation<Nat>]> = Map.fromIter(
113+
[
114+
(
115+
"1.) 100000 operations (push:pop = 1:1)",
116+
genOpsNatRandom(
117+
/* randomSeed = */ 23,
118+
/* operationsCount = */ 100000,
119+
/* maxValueExclusive = */ 100000,
120+
/* wPush = */ 1,
121+
/* wPop = */ 1,
122+
/* wClear = */ 0
123+
)
124+
),
125+
(
126+
"2.) 100000 operations (push:pop = 2:1)",
127+
genOpsNatRandom(
128+
/* randomSeed = */ 24,
129+
/* operationsCount = */ 100000,
130+
/* maxValueExclusive = */ 100000,
131+
/* wPush = */ 2,
132+
/* wPop = */ 1,
133+
/* wClear = */ 0
134+
)
135+
),
136+
(
137+
"3.) 100000 operations (push:pop = 10:1)",
138+
genOpsNatRandom(
139+
/* randomSeed = */ 42,
140+
/* operationsCount = */ 100000,
141+
/* maxValueExclusive = */ 100000,
142+
/* wPush = */ 10,
143+
/* wPop = */ 1,
144+
/* wClear = */ 0
145+
)
146+
),
147+
(
148+
"4.) 100000 operations (only push)",
149+
genOpsNatRandom(
150+
/* randomSeed = */ 33,
151+
/* operationsCount = */ 100000,
152+
/* maxValueExclusive = */ 100000,
153+
/* wPush = */ 1,
154+
/* wPop = */ 0,
155+
/* wClear = */ 0
156+
)
157+
),
158+
(
159+
"5.) 50000 pushes, then 50000 pops",
160+
genOpsPushThenPop(
161+
/* randomSeed = */ 13,
162+
/* pushOperationsCount = */ 50000,
163+
/* maxValueExclusive */ 100000
164+
)
165+
),
166+
(
167+
"6.) 50000 pushes, then 25000 \"pop;push\"es",
168+
genOpsKeepConstantSize(
169+
/* randomSeed = */ 55,
170+
/* size = */ 50000,
171+
/* popPushCount = */ 25000,
172+
/* maxValueExclusive = */ 100000
173+
)
174+
)
175+
].values(),
176+
Text.compare
177+
);
178+
bench.rows(Map.keys<Text, [PriorityQueueUpdateOperation<Nat>]>(testInstances) |> Array.fromIter(_));
179+
180+
let testRunners : Map.Map<Text, [PriorityQueueUpdateOperation<Nat>] -> ()> = Map.fromIter(
181+
[
182+
(
183+
"A) PriorityQueue",
184+
func(ops : [PriorityQueueUpdateOperation<Nat>]) {
185+
let priorityQueue = PriorityQueue.empty<Nat>();
186+
for (op in ops.values()) {
187+
switch (op) {
188+
case (#Push element) PriorityQueue.push(priorityQueue, Nat.compare, element);
189+
case (#Pop) ignore PriorityQueue.pop(priorityQueue, Nat.compare);
190+
case (#Clear) PriorityQueue.clear(priorityQueue)
191+
}
192+
}
193+
}
194+
),
195+
(
196+
"B) PriorityQueueSet",
197+
func(ops : [PriorityQueueUpdateOperation<Nat>]) {
198+
let priorityQueueSet = PriorityQueueSet.empty<Nat>();
199+
for (op in ops.values()) {
200+
switch (op) {
201+
case (#Push element) PriorityQueueSet.push(priorityQueueSet, Nat.compare, element);
202+
case (#Pop) ignore PriorityQueueSet.pop(priorityQueueSet, Nat.compare);
203+
case (#Clear) PriorityQueueSet.clear(priorityQueueSet)
204+
}
205+
}
206+
}
207+
)
208+
].values(),
209+
Text.compare
210+
);
211+
bench.cols(Map.keys<Text, [PriorityQueueUpdateOperation<Nat>] -> ()>(testRunners) |> Array.fromIter(_));
212+
213+
bench.runner(
214+
func(row, col) {
215+
switch (
216+
Map.get(testInstances, Text.compare, row),
217+
Map.get(testRunners, Text.compare, col)
218+
) {
219+
case (?ops, ?runner) runner(ops);
220+
case _ Runtime.trap("Missing test instance or runner")
221+
}
222+
}
223+
);
224+
bench
225+
}
226+
}

bench/utils/PriorityQueueSet.mo

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/// A mutable priority queue of elements.
2+
/// !!! In contrast to the implementation in `src/PriorityQueue.mo`, this one
3+
/// is a wrapper over `Set`. !!!
4+
///
5+
/// Always returns the element with the highest priority first,
6+
/// as determined by a user-provided comparison function.
7+
///
8+
/// Internally implemented as a wrapper over a core library `Set<(T, Nat)>`.
9+
/// The `Nat` values serve as unique tags to distinguish elements
10+
/// with equal priority, since a `Set` cannot store duplicates.
11+
///
12+
/// Performance:
13+
/// * Runtime: `O(log n)` for `push`, `pop` and `peek`.
14+
/// * Runtime: `O(1)` for `clear`, `size`, and `isEmpty`.
15+
/// * Space: `O(n)`, where `n` is the number of stored elements.
16+
import Set "../../src/Set";
17+
import Order "../../src/Order";
18+
import Nat "../../src/Nat";
19+
import { Tuple2 } "../../src/Tuples";
20+
21+
module {
22+
public type PriorityQueue<T> = {
23+
set : Set.Set<(T, Nat)>;
24+
var counter : Nat
25+
};
26+
27+
/// Returns an empty priority queue.
28+
///
29+
/// Example:
30+
/// ```motoko
31+
/// import PriorityQueue "mo:core/internal/PriorityQueueSet";
32+
///
33+
/// let pq = PriorityQueue.empty<Nat>();
34+
/// assert PriorityQueue.isEmpty(pq);
35+
/// ```
36+
///
37+
/// Runtime: `O(1)`. Space: `O(1)`.
38+
public func empty<T>() : PriorityQueue<T> = {
39+
set = Set.empty<(T, Nat)>();
40+
var counter = 0
41+
};
42+
43+
/// Returns a priority queue containing a single element.
44+
///
45+
/// Example:
46+
/// ```motoko
47+
/// import PriorityQueue "mo:core/internal/PriorityQueueSet";
48+
///
49+
/// let pq = PriorityQueue.singleton<Nat>(42);
50+
/// assert PriorityQueue.peek(pq) == ?42;
51+
/// ```
52+
///
53+
/// Runtime: `O(1)`. Space: `O(1)`.
54+
public func singleton<T>(element : T) : PriorityQueue<T> = {
55+
set = Set.singleton((element, 0));
56+
var counter = 1
57+
};
58+
59+
/// Returns the number of elements in the priority queue.
60+
///
61+
/// Runtime: `O(1)`.
62+
public func size<T>(priorityQueue : PriorityQueue<T>) : Nat = Set.size(priorityQueue.set);
63+
64+
/// Returns `true` iff the priority queue is empty.
65+
///
66+
/// Example:
67+
/// ```motoko
68+
/// import PriorityQueue "mo:core/internal/PriorityQueueSet";
69+
/// import Nat "mo:core/Nat";
70+
///
71+
/// let pq = PriorityQueue.empty<Nat>();
72+
/// assert PriorityQueue.isEmpty(pq);
73+
/// PriorityQueue.push(pq, Nat.compare, 5);
74+
/// assert not PriorityQueue.isEmpty(pq);
75+
/// ```
76+
///
77+
/// Runtime: `O(1)`. Space: `O(1)`.
78+
public func isEmpty<T>(priorityQueue : PriorityQueue<T>) : Bool = Set.isEmpty(priorityQueue.set);
79+
80+
/// Removes all elements from the priority queue.
81+
///
82+
/// Example:
83+
/// ```motoko
84+
/// import PriorityQueue "mo:core/internal/PriorityQueueSet";
85+
/// import Nat "mo:core/Nat";
86+
///
87+
/// let pq = PriorityQueue.empty<Nat>();
88+
/// PriorityQueue.push(pq, Nat.compare, 5);
89+
/// PriorityQueue.push(pq, Nat.compare, 10);
90+
/// assert not PriorityQueue.isEmpty(pq);
91+
/// PriorityQueue.clear(pq);
92+
/// assert PriorityQueue.isEmpty(pq);
93+
/// ```
94+
///
95+
/// Runtime: `O(1)`. Space: `O(1)`.
96+
public func clear<T>(priorityQueue : PriorityQueue<T>) = Set.clear(priorityQueue.set);
97+
98+
/// Inserts a new element into the priority queue.
99+
///
100+
/// `compare` – comparison function that defines priority ordering.
101+
///
102+
/// Example:
103+
/// ```motoko
104+
/// import PriorityQueue "mo:core/internal/PriorityQueueSet";
105+
/// import Nat "mo:core/Nat";
106+
///
107+
/// let pq = PriorityQueue.empty<Nat>();
108+
/// PriorityQueue.push(pq, Nat.compare, 5);
109+
/// PriorityQueue.push(pq, Nat.compare, 10);
110+
/// assert PriorityQueue.peek(pq) == ?10;
111+
/// ```
112+
///
113+
/// Runtime: `O(log n)`. Space: `O(log n)`.
114+
public func push<T>(priorityQueue : PriorityQueue<T>, compare : (T, T) -> Order.Order, element : T) {
115+
Set.add(priorityQueue.set, Tuple2.makeCompare(compare, Nat.compare), (element, priorityQueue.counter));
116+
priorityQueue.counter += 1
117+
};
118+
119+
/// Returns the element with the highest priority, without removing it.
120+
/// Returns `null` if the queue is empty.
121+
///
122+
/// Example:
123+
/// ```motoko
124+
/// import PriorityQueue "mo:core/internal/PriorityQueueSet";
125+
/// import Nat "mo:core/Nat";
126+
///
127+
/// let pq = PriorityQueue.singleton<Nat>(42);
128+
/// assert PriorityQueue.peek(pq) == ?42;
129+
/// ```
130+
///
131+
/// Runtime: `O(log n)`. Space: `O(1)`.
132+
public func peek<T>(priorityQueue : PriorityQueue<T>) : ?T = do ? {
133+
let (element, _) = Set.max(priorityQueue.set)!;
134+
element
135+
};
136+
137+
/// Removes and returns the element with the highest priority.
138+
/// Returns `null` if the queue is empty.
139+
///
140+
/// `compare` – comparison function that defines priority ordering.
141+
///
142+
/// Example:
143+
/// ```motoko
144+
/// import PriorityQueue "mo:core/internal/PriorityQueueSet";
145+
/// import Nat "mo:core/Nat";
146+
///
147+
/// let pq = PriorityQueue.empty<Nat>();
148+
/// PriorityQueue.push(pq, Nat.compare, 5);
149+
/// PriorityQueue.push(pq, Nat.compare, 10);
150+
/// assert PriorityQueue.pop(pq, Nat.compare) == ?10;
151+
/// ```
152+
///
153+
/// Runtime: `O(log n)`. Space: `O(log n)`.
154+
public func pop<T>(priorityQueue : PriorityQueue<T>, compare : (T, T) -> Order.Order) : ?T = do ? {
155+
let (element, nonce) = Set.max(priorityQueue.set)!;
156+
Set.remove(priorityQueue.set, Tuple2.makeCompare(compare, Nat.compare), (element, nonce));
157+
element
158+
}
159+
}

0 commit comments

Comments
 (0)