The OpenD Programming Language

1 /**
2 This module provides a $(D BinaryHeap) (aka priority queue)
3 adaptor that makes a binary heap out of any user-provided random-access range.
4 
5 Current implementation is suitable for Mir/BetterC concepts.
6 
7 This module is a submodule of $(MREF mir, container).
8 
9 Copyright: 2020 Ilia Ki, Kaleidic Associates Advisory Limited, Symmetry Investments
10 
11 License: $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache-2.0)
12 
13 Authors: $(HTTP erdani.com, Andrei Alexandrescu) (original Phobos code), Ilia Ki (Mir & BetterC rework).
14 */
15 module mir.container.binaryheap;
16 
17 import mir.primitives;
18 import std.range.primitives: isRandomAccessRange, hasSwappableElements;
19 import std.traits;
20 
21 ///
22 @system nothrow @nogc version(mir_test) unittest
23 {
24     import mir.algorithm.iteration : equal;
25     import std.range : takeExactly;
26     static a = [4, 7, 3, 1, 5], b = [7, 5, 4];
27     auto maxHeap = a.heapify;
28     assert((&maxHeap).takeExactly(3).equal(b));
29 
30     static c = [4, 7, 3, 1, 5], d = [1, 3, 4];
31     auto minHeap = c.heapify!"a > b";
32     assert((&minHeap).takeExactly(3).equal(d));
33 }
34 
35 /++
36 Implements a $(HTTP en.wikipedia.org/wiki/Binary_heap, binary heap)
37 container on top of a given random-access range type (usually $(D
38 T[])) or a random-access container type (usually $(D Array!T)). The
39 documentation of $(D BinaryHeap) will refer to the underlying range or
40 container as the $(I store) of the heap.
41 
42 The binary heap induces structure over the underlying store such that
43 accessing the largest element (by using the $(D front) property) is a
44 $(BIGOH 1) operation and extracting it (by using the $(D
45 removeFront()) method) is done fast in $(BIGOH log n) time.
46 
47 If $(D less) is the less-than operator, which is the default option,
48 then $(D BinaryHeap) defines a so-called max-heap that optimizes
49 extraction of the $(I largest) elements. To define a min-heap,
50 instantiate BinaryHeap with $(D "a > b") as its predicate.
51 
52 Simply extracting elements from a $(D BinaryHeap) container is
53 tantamount to lazily fetching elements of $(D Store) in descending
54 order. Extracting elements from the $(D BinaryHeap) to completion
55 leaves the underlying store sorted in ascending order but, again,
56 yields elements in descending order.
57 
58 If $(D Store) is not a container, the $(D BinaryHeap) cannot grow beyond the
59 size of that range. If $(D Store) is a container that supports $(D
60 insertBack), the $(D BinaryHeap) may grow by adding elements to the
61 container.
62 +/
63 struct BinaryHeap(alias less = "a < b", Store)
64 if (isRandomAccessRange!Store || isRandomAccessRange!(typeof(Store.init[])))
65 {
66     import mir.utility : min;
67     import mir.functional : naryFun;
68     import core.lifetime: move; 
69     import std.algorithm.mutation : swapAt;
70 
71     static if (isRandomAccessRange!Store)
72         alias Range = Store;
73     else
74         alias Range = typeof(Store.init[]);
75 
76     alias percolate = HeapOps!(less, Range).percolate;
77     alias siftDown = HeapOps!(less, Range).siftDown;
78     alias buildHeap = HeapOps!(less, Range).buildHeap;
79 
80     @disable this();
81     @disable this(this);
82 
83     // Comparison predicate
84     private alias comp = naryFun!(less);
85     // Convenience accessors
86 
87     // Asserts that the heap property is respected.
88     private void assertValid() scope
89     {
90         debug
91         {
92             if (_length < 2) return;
93             for (size_t n = _length - 1; n >= 1; --n)
94             {
95                 auto parentIdx = (n - 1) / 2;
96                 assert(!comp(_store[parentIdx], _store[n]));
97             }
98         }
99     }
100 
101 public:
102 
103     /// The payload includes the support store and the effective length
104     size_t _length;
105     /// ditto
106     Store _store;
107 
108 
109     /++
110        Converts the store $(D s) into a heap. If $(D initialSize) is
111        specified, only the first $(D initialSize) elements in $(D s)
112        are transformed into a heap, after which the heap can grow up
113        to $(D r.length) (if $(D Store) is a range) or indefinitely (if
114        $(D Store) is a container with $(D insertBack)). Performs
115        $(BIGOH min(r.length, initialSize)) evaluations of $(D less).
116     +/
117     this(Store s, size_t initialSize = size_t.max)
118     {
119         acquire(s, initialSize);
120     }
121 
122     /++
123     Takes ownership of a store. After this, manipulating $(D s) may make
124     the heap work incorrectly.
125     +/
126     void acquire(Store s, size_t initialSize = size_t.max)
127     {
128         _store = move(s);
129         _length = min(_store.length, initialSize);
130         if (_length < 2) return;
131         buildHeap(_store[]);
132         assertValid();
133     }
134 
135     /++
136     Takes ownership of a store assuming it already was organized as a
137     heap.
138     +/
139     void assume(Store s, size_t initialSize = size_t.max)
140     {
141         _store = move(s);
142         _length = min(_store.length, initialSize);
143         assertValid();
144     }
145 
146     /++
147     Returns the _length of the heap.
148     +/
149     @property size_t length() scope const
150     {
151         return _length;
152     }
153 
154     /++
155     Returns $(D true) if the heap is _empty, $(D false) otherwise.
156     +/
157     @property bool empty() scope const
158     {
159         return !length;
160     }
161 
162     /++
163     Returns the _capacity of the heap, which is the length of the
164     underlying store (if the store is a range) or the _capacity of the
165     underlying store (if the store is a container).
166     +/
167     @property size_t capacity() scope const
168     {
169         static if (is(typeof(_store.capacity) : size_t))
170         {
171             return _store.capacity;
172         }
173         else
174         {
175             return _store.length;
176         }
177     }
178 
179     /++
180     Returns a _front of the heap, which is the largest element
181     according to `less`.
182     +/
183     @property auto ref ElementType!Store front() return scope
184     {
185         assert(!empty, "Cannot call front on an empty heap.");
186         return _store.front;
187     }
188 
189 
190 
191     /++
192     Inserts `value` into the store. If the underlying store is a range
193     and `length == capacity`, throws an AssertException.
194     +/
195     size_t insert(ElementType!Store value) scope
196     {
197         static if (is(typeof(_store.insertBack(value))))
198         {
199             if (length == _store.length)
200             {
201                 // reallocate
202                 _store.insertBack(value);
203             }
204             else
205             {
206                 // no reallocation
207                 _store[_length] = value;
208             }
209         }
210         else
211         {
212             // can't grow
213             assert(length < _store.length, "Cannot grow a heap created over a range");
214             _store[_length] = value;
215         }
216 
217         // sink down the element
218         for (size_t n = _length; n; )
219         {
220             auto parentIdx = (n - 1) / 2;
221             if (!comp(_store[parentIdx], _store[n])) break; // done!
222             // must swap and continue
223             _store.swapAt(parentIdx, n);
224             n = parentIdx;
225         }
226         ++_length;
227         debug(BinaryHeap) assertValid();
228         return 1;
229     }
230 
231     /++
232     Removes the largest element from the heap.
233     +/
234     void removeFront() scope
235     {
236         assert(!empty, "Cannot call removeFront on an empty heap.");
237         if (--_length)
238             _store.swapAt(0, _length);
239         percolate(_store[], 0, _length);
240     }
241 
242     /// ditto
243     alias popFront = removeFront;
244 
245     /++
246     Removes the largest element from the heap and returns
247     it. The element still resides in the heap's store. For performance
248     reasons you may want to use $(D removeFront) with heaps of objects
249     that are expensive to copy.
250     +/
251     auto ref removeAny() scope
252     {
253         removeFront();
254         return _store[_length];
255     }
256 
257     /++
258     Replaces the largest element in the store with `value`.
259     +/
260     void replaceFront(ElementType!Store value) scope
261     {
262         // must replace the top
263         assert(!empty, "Cannot call replaceFront on an empty heap.");
264         _store.front = value;
265         percolate(_store[], 0, _length);
266         debug(BinaryHeap) assertValid();
267     }
268 
269     /++
270     If the heap has room to grow, inserts `value` into the store and
271     returns $(D true). Otherwise, if $(D less(value, front)), calls $(D
272     replaceFront(value)) and returns again $(D true). Otherwise, leaves
273     the heap unaffected and returns $(D false). This method is useful in
274     scenarios where the smallest $(D k) elements of a set of candidates
275     must be collected.
276     +/
277     bool conditionalInsert(ElementType!Store value) scope
278     {
279         if (_length < _store.length)
280         {
281             insert(value);
282             return true;
283         }
284 
285         assert(!_store.empty, "Cannot replace front of an empty heap.");
286         if (!comp(value, _store.front)) return false; // value >= largest
287         _store.front = value;
288 
289         percolate(_store[], 0, _length);
290         debug(BinaryHeap) assertValid();
291         return true;
292     }
293 
294     /++
295     Swapping is allowed if the heap is full. If $(D less(value, front)), the
296     method exchanges store.front and value and returns $(D true). Otherwise, it
297     leaves the heap unaffected and returns $(D false).
298     +/
299     bool conditionalSwap(ref ElementType!Store value) scope
300     {
301         assert(_length == _store.length);
302         assert(!_store.empty, "Cannot swap front of an empty heap.");
303 
304         if (!comp(value, _store.front)) return false; // value >= largest
305 
306         import std.algorithm.mutation : swap;
307         swap(_store.front, value);
308 
309         percolate(_store[], 0, _length);
310         debug(BinaryHeap) assertValid();
311 
312         return true;
313     }
314 }
315 
316 /// Example from "Introduction to Algorithms" Cormen et al, p 146
317 @system nothrow version(mir_test) unittest
318 {
319     import mir.algorithm.iteration : equal;
320     int[] a = [ 4, 1, 3, 2, 16, 9, 10, 14, 8, 7 ];
321     auto h = heapify(a);
322     // largest element
323     assert(h.front == 16);
324     // a has the heap property
325     assert(equal(a, [ 16, 14, 10, 8, 7, 9, 3, 2, 4, 1 ]));
326 }
327 
328 /// $(D BinaryHeap) implements the standard input range interface, allowing
329 /// lazy iteration of the underlying range in descending order.
330 @system nothrow version(mir_test) unittest
331 {
332     import mir.algorithm.iteration : equal;
333     import std.range : takeExactly;
334     int[] a = [4, 1, 3, 2, 16, 9, 10, 14, 8, 7];
335     auto heap = heapify(a);
336     auto top5 = (&heap).takeExactly(5);
337     assert(top5.equal([16, 14, 10, 9, 8]));
338 }
339 
340 /++
341 Convenience function that returns a $(D BinaryHeap!Store) object
342 initialized with $(D s) and $(D initialSize).
343 +/
344 BinaryHeap!(less, Store) heapify(alias less = "a < b", Store)(Store s,
345         size_t initialSize = size_t.max)
346 {
347 
348     return BinaryHeap!(less, Store)(s, initialSize);
349 }
350 
351 ///
352 @system nothrow version(mir_test) unittest
353 {
354     import std.range.primitives;
355     {
356         // example from "Introduction to Algorithms" Cormen et al., p 146
357         int[] a = [ 4, 1, 3, 2, 16, 9, 10, 14, 8, 7 ];
358         auto h = heapify(a);
359         h = heapify!"a < b"(a);
360         assert(h.front == 16);
361         assert(a == [ 16, 14, 10, 8, 7, 9, 3, 2, 4, 1 ]);
362         auto witness = [ 16, 14, 10, 9, 8, 7, 4, 3, 2, 1 ];
363         for (; !h.empty; h.removeFront(), witness.popFront())
364         {
365             assert(!witness.empty);
366             assert(witness.front == h.front);
367         }
368         assert(witness.empty);
369     }
370     {
371         int[] a = [ 4, 1, 3, 2, 16, 9, 10, 14, 8, 7 ];
372         int[] b = new int[a.length];
373         BinaryHeap!("a < b", int[]) h = BinaryHeap!("a < b", int[])(b, 0);
374         foreach (e; a)
375         {
376             h.insert(e);
377         }
378         assert(b == [ 16, 14, 10, 8, 7, 3, 9, 1, 4, 2 ]);
379     }
380 }
381 
382 @system nothrow version(mir_test) unittest
383 {
384     // Test range interface.
385     import mir.primitives: isInputRange;
386     import mir.algorithm.iteration : equal;
387     int[] a = [4, 1, 3, 2, 16, 9, 10, 14, 8, 7];
388     auto h = heapify(a);
389     static assert(isInputRange!(typeof(h)));
390     assert((&h).equal([16, 14, 10, 9, 8, 7, 4, 3, 2, 1]));
391 }
392 
393 @system nothrow version(mir_test) unittest // 15675
394 {
395     import std.container.array : Array;
396 
397     Array!int elements = [1, 2, 10, 12];
398     auto heap = heapify(elements);
399     assert(heap.front == 12);
400     heap.insert(100);
401     assert(heap.front == 100);
402     heap.insert(1);
403     assert(heap.front == 100);
404 }
405 
406 @system version(mir_test) unittest
407 {
408     import std.container.array : Array;
409 
410     Array!Object elements;
411     auto heap = heapify(elements);
412     Object obj;
413     heap.insert(obj);
414 }
415 
416 @system nothrow version(mir_test) unittest
417 {
418     import mir.algorithm.iteration : equal;
419     import std.internal.test.dummyrange;
420 
421     alias RefRange = DummyRange!(ReturnBy.Reference, Length.Yes, RangeType.Random);
422 
423     RefRange a;
424     RefRange b;
425     a.reinit();
426     b.reinit();
427 
428     auto heap = heapify(a);
429     foreach (ref elem; b)
430     {
431         heap.conditionalSwap(elem);
432     }
433 
434     assert(equal(&heap, [ 5, 5, 4, 4, 3, 3, 2, 2, 1, 1]));
435     assert(equal(b, [10, 9, 8, 7, 6, 6, 7, 8, 9, 10]));
436 }
437 
438 /// Heap operations for random-access ranges
439 template HeapOps(alias less, Range)
440 {
441     import std.range.primitives : hasSwappableElements, hasAssignableElements;
442     import mir.functional;
443     import std.algorithm.mutation : swapAt;
444 
445     static assert(isRandomAccessRange!Range);
446     static assert(hasLength!Range);
447     static assert(hasSwappableElements!Range || hasAssignableElements!Range);
448 
449     alias lessFun = naryFun!less;
450 
451     /// template because of @@@12410@@@
452     void heapSort()(Range r)
453     {
454         // If true, there is nothing to do
455         if (r.length < 2) return;
456         // Build Heap
457         buildHeap(r);
458         // Sort
459         for (size_t i = r.length - 1; i > 0; --i)
460         {
461             r.swapAt(0, i);
462             percolate(r, 0, i);
463         }
464     }
465 
466     /// template because of @@@12410@@@
467     void buildHeap()(Range r)
468     {
469         immutable n = r.length;
470         for (size_t i = n / 2; i-- > 0; )
471         {
472             siftDown(r, i, n);
473         }
474         assert(isHeap(r));
475     }
476 
477     ///
478     bool isHeap()(Range r)
479     {
480         size_t parent = 0;
481         foreach (child; 1 .. r.length)
482         {
483             if (lessFun(r[parent], r[child])) return false;
484             // Increment parent every other pass
485             parent += !(child & 1);
486         }
487         return true;
488     }
489 
490     /// Sifts down r[parent] (which is initially assumed to be messed up) so the
491     /// heap property is restored for r[parent .. end].
492     /// template because of @@@12410@@@
493     void siftDown()(Range r, size_t parent, immutable size_t end)
494     {
495         for (;;)
496         {
497             auto child = (parent + 1) * 2;
498             if (child >= end)
499             {
500                 // Leftover left child?
501                 if (child == end && lessFun(r[parent], r[--child]))
502                     r.swapAt(parent, child);
503                 break;
504             }
505             auto leftChild = child - 1;
506             if (lessFun(r[child], r[leftChild])) child = leftChild;
507             if (!lessFun(r[parent], r[child])) break;
508             r.swapAt(parent, child);
509             parent = child;
510         }
511     }
512 
513     /// Alternate version of siftDown that performs fewer comparisons, see
514     /// https://en.wikipedia.org/wiki/Heapsort#Bottom-up_heapsort. The percolate
515     /// process first sifts the parent all the way down (without comparing it
516     /// against the leaves), and then a bit up until the heap property is
517     /// restored. So there are more swaps but fewer comparisons. Gains are made
518     /// when the final position is likely to end toward the bottom of the heap,
519     /// so not a lot of sifts back are performed.
520     /// template because of @@@12410@@@
521     void percolate()(Range r, size_t parent, immutable size_t end)
522     {
523         immutable root = parent;
524 
525         // Sift down
526         for (;;)
527         {
528             auto child = (parent + 1) * 2;
529 
530             if (child >= end)
531             {
532                 if (child == end)
533                 {
534                     // Leftover left node.
535                     --child;
536                     r.swapAt(parent, child);
537                     parent = child;
538                 }
539                 break;
540             }
541 
542             auto leftChild = child - 1;
543             if (lessFun(r[child], r[leftChild])) child = leftChild;
544             r.swapAt(parent, child);
545             parent = child;
546         }
547 
548         // Sift up
549         for (auto child = parent; child > root; child = parent)
550         {
551             parent = (child - 1) / 2;
552             if (!lessFun(r[parent], r[child])) break;
553             r.swapAt(parent, child);
554         }
555     }
556 }