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 }