# PairingHeap [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard) PairingHeap is a pure Ruby priority queue implementation using a pairing heap as the underlying data structure. While a pairing heap is asymptotically less efficient than the Fibonacci heap, it is usually faster in practice. This makes it a popular choice for Prim's MST or Dijkstra's algorithm implementations. PairingHeap is currently being used as the priority queue data structure in [RGL](https://github.com/monora/rgl/). Also implementation without priority change support is provided(`SimplePairingHeap`), while the asymptotical complexity of the methods stay the same, bookkeeping of elements is not needed making, the constant smaller. ## Installation Add this line to your application's Gemfile: ```ruby gem 'pairing_heap' ``` And then execute: $ bundle install Or install it yourself as: $ gem install pairing_heap ## Documentation https://rubydoc.info/gems/pairing_heap ## Usage ```ruby require 'pairing_heap' # Simple PairingHeap simple_heap = PairingHeap::SimplePairingHeap.new simple_heap.push(:a, 1) simple_heap.push(:b, 2) simple_heap.push(:c, 3) simple_heap.peek # => :a simple_heap.peek_priority # => 1 simple_heap.pop_with_priority # => [:a, 1] simple_heap.pop # => :b # Min priority queue best_defenses = PairingHeap::MinPriorityQueue.new best_defenses.push('Chelsea', 24) best_defenses.push('City', 30) best_defenses.push('Tottenham', 25) best_defenses.any? # => true best_defenses.size # => 3 best_defenses.decrease_key('City', 15) best_defenses.min # => 'City' best_defenses.pop # => 'City' best_defenses.extract_min # => 'Chelsea' best_defenses.extract_min # => 'Tottenham' best_defenses.any? # => false # Max priority queue best_teams = PairingHeap::MaxPriorityQueue.new best_teams.push('City', 56) best_teams.push('United', 46) best_teams.push('Leicester', 46) best_teams.increase_key('Leicester', 47) best_teams.max # => 'City' best_teams.pop # => 'City' best_teams.extract_max # => 'Leicester' # Custom comparator(it defaults to :<=.to_proc) compare_by_length = PairingHeap::PairingHeap.new { |l, r| l.length <= r.length } compare_by_length.push(:a, '11') compare_by_length.push(:b, '1') compare_by_length.push(:c, '11') compare_by_length.change_priority(:c, '') compare_by_length.peek # => :c compare_by_length.pop # => :c compare_by_length.pop # => :b compare_by_length.pop # => :a # SafeChangePriortyQueue queue = PairingHeap::SafeChangePriorityQueue.new queue.push(:a, 1) queue.push(:b, 2) queue.change_priority(:a, 3) # This works and does not throw an exception queue.peek # => :b ``` See also [test/performance_dijkstra.rb](./test/performance_dijkstra.rb) for a Dijkstra algorithm implementation. ### Changes from lazy_priority_queue This API is a drop-in replacement of [lazy_priority_queue](https://github.com/matiasbattocchia/lazy_priority_queue) with the following differences: * Custom comparator provided to constructur, compares weights, not internal nodes * `change_priority` returns `self` instead of the first argument * `enqueue` returns `self` instead of the first argument * Queue classes are in the `PairingHeap` namespace, so `require 'pairing_heap` does not load `MinPriorityQueue` to the global scope * `top_condition` constructor argument is removed ## Time Complexity | Operation | Time complexity | Amortized time complexity | | --------------- | --------------- | ------------------------- | | enqueue | O(1) | O(1) | | peek | O(1) | O(1) | | dequeue | O(n) | O(log n) | | * change_priority | O(1) | o(log n) | | * delete | O(n) | O(log n) | `*` Not available in `SimplePairingHeap` ## Benchmarks I picked the three fastest pure Ruby priority queue implementations I was aware of for the comparison: * [lazy_priority_queue](https://github.com/matiasbattocchia/lazy_priority_queue) that uses a lazy binomial heap. This is probably the most popular option. It was used in [RGL](https://github.com/monora/rgl/) until PairingHeap replaced it. * Pure Ruby implementation of Fibonacci Heap from [priority-queue](https://github.com/supertinou/priority-queue) ([link to source](https://github.com/supertinou/priority-queue/blob/master/lib/priority_queue/ruby_priority_queue.rb)) * [rb_heap](https://github.com/florian/rb_heap) that uses a binary heap. Note however that this implementation does not support change_priority operation. All tests except for the third one were executed by [benchmark-ips](https://github.com/evanphx/benchmark-ips) with parameters `time = 60` and `warmup = 15`, on an `Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz`. ### Stress test without changing priority test(N = 1000) [source code](./test/performance.rb) Original performance test from [lazy_priority_queue](https://github.com/matiasbattocchia/lazy_priority_queue) > A stress test of 1,000,000 operations: starting with 1,000 pushes/0 pops, following 999 pushes/1 pop, and so on till 0 pushes/1000 pops.
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21]
Library Iterations Seconds Iterations per second
pairing_heap (SimplePairingHeap) 23 62.014773 0.371
pairing_heap (PairingHeap) 16 63.135240 0.253(1.46x slower)
rb_heap 14 61.123304 0.229(1.62x slower)
lazy_priority_queue 10 66.208647 0.151(2.46x slower)
Fibonacci 8 66.353147 0.121(3.08x slower)
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) +YJIT [x86_64-darwin21]
Library Iterations Seconds Iterations per second
pairing_heap (SimplePairingHeap) 25 60.423579 0.414
rb_heap 19 60.869907 0.312(1.33x slower)
pairing_heap (PairingHeap) 17 61.389127 0.277(1.49x slower)
Fibonacci 14 64.417807 0.217(1.90x slower)
lazy_priority_queue 11 63.151856 0.174(2.38x slower)
jruby 9.3.7.0 (2.6.8) 2022-08-16 c79ef237e0 OpenJDK 64-Bit Server VM 17.0.2+8-86 on 17.0.2+8-86 +indy +jit [x86_64-darwin]
Library Iterations Seconds Iterations per second
pairing_heap (SimplePairingHeap) 47 60.391633 0.778
rb_heap 34 60.878639 0.559(1.39x slower)
pairing_heap (PairingHeap) 32 61.211985 0.523(1.49x slower)
Fibonacci 23 60.297670 0.382(2.04x slower)
lazy_priority_queue 23 61.973538 0.371(2.10x slower)
truffleruby 22.2.0, like ruby 3.0.3, GraalVM CE JVM [x86_64-darwin]
Library Iterations Seconds Iterations per second
pairing_heap (SimplePairingHeap) 206 60.191686 3.433
rb_heap 97 60.134011 1.614(1.93x slower)
pairing_heap (PairingHeap) 85 60.193608s 1.434(2.40x slower)
lazy_priority_queue 19 63.212429 0.301(11.45x slower)
Fibonacci 2 83.508571 0.024(143.70x slower)
### Stress test with changing priority(N = 1000) [source code](./test/performance_with_change_priority.rb) A stress test of 1,501,500 operations: starting with 1,000 pushes/1000 change_priorities/0 pops, following 999 pushes/999 change_priorities/1 pop, and so on till 0 pushes/0 change_priorities/1000 pops.
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21]
Library Iterations Seconds Iterations per second
pairing_heap 15 62.946988 0.238
lazy_priority_queue 9 61.876691 0.145(1.64x slower)
Fibonacci 8 67.809982 0.118(2.02x slower)
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) +YJIT [x86_64-darwin21]
Library Iterations Seconds Iterations per second
pairing_heap 16 62.576693 0.256
Fibonacci 13 63.164614 0.206(1.24x slower)
lazy_priority_queue 10 63.172995s 0.158(1.62x slower)
jruby 9.3.7.0 (2.6.8) 2022-08-16 c79ef237e0 OpenJDK 64-Bit Server VM 17.0.2+8-86 on 17.0.2+8-86 +indy +jit [x86_64-darwin]
Library Iterations Seconds Iterations per second
pairing_heap 28 60.280368 0.465
Fibonacci 22 61.405561 0.465(1.30x slower)
lazy_priority_queue 20 60.397535 0.331(1.40x slower)
truffleruby 22.2.0, like ruby 3.0.3, GraalVM CE JVM [x86_64-darwin]
Library Iterations Seconds Iterations per second
pairing_heap 70 60.663184 1.160
lazy_priority_queue 23 60.474587 0.382(3.04x slower)
Fibonacci 2 74.873854 0.027(43.44x slower)
### Stress test with changing priority or push/pop(test ignored in summary) [source code](./test/performance_pop_versus_change_priority.rb) Start with 500 pushes, then: * If queue supports changing priority 500 change_priority calls, then 500 pops * If does not support changing priority 500 push calls, then 1000 pops
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21]
Library Iterations per second
pairing_heap (PairingHeap) 436.4
lazy_priority_queue 380.2(1.94x slower)
pairing_heap (SimplePairingHeap) 339.9.02(2.17x slower)
Fibonacci 313.9(2.35x slower)
rb_heap 194.7(3.78 slower)
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) +YJIT [x86_64-darwin21]
Library Iterations per second
pairing_heap 854.6
Fibonacci 651.3(1.31x slower)
lazy_priority_queue 453.6(1.88x slower)
pairing_heap(SimplePairingHeap) 390.9(2.19x slower)
rb_heap 268.8(3.18x slower)
jruby 9.3.7.0 (2.6.8) 2022-08-16 c79ef237e0 OpenJDK 64-Bit Server VM 17.0.2+8-86 on 17.0.2+8-86 +indy +jit [x86_64-darwin]
Library Iterations per second
pairing_heap(PairingHeap) 1591
Fibonacci 1092(1.46x slower)
lazy_priority_queue 986(1.61x slower)
pairing_heap(SimplePairingHeap) 562(2.37x slower)
rb_heap 623(2.55x slower)
truffleruby 22.2.0, like ruby 3.0.3, GraalVM CE JVM [x86_64-darwin]
Library Iterations per second
pairing_heap(PairingHeap) 7404
pairing_heap(SimplePairingHeap) 5104(1.45x slower)
rb_heap 1575(4.70x slower)
Fibonacci 1258(5.88x slower)
lazy_priority_queue 1004(7.38x slower)
### Dijkstra's algorithm with RGL [source code](./test/performance_rgl.rb)
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21]
Library Iterations Seconds Iterations per second
pairing_heap 9 61.469343 0.116
lazy_priority_queue 8 64.312672 0.125(1.18x slower)
Fibonacci 7 60.555716 0.116(1.27x slower)
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) +YJIT [x86_64-darwin21]
Library Iterations Seconds Iterations per second
pairing_heap 10 65.160945s 0.154
Fibonacci 9 61.950587 0.145(1.06x slower)
lazy_priority_queue 9 66.592123 0.135(1.14x slower)
jruby 9.3.7.0 (2.6.8) 2022-08-16 c79ef237e0 OpenJDK 64-Bit Server VM 17.0.2+8-86 on 17.0.2+8-86 +indy +jit [x86_64-darwin]
Library Iterations Seconds Iterations per second
lazy_priority_queue 20 61.149944 0.328
pairing_heap 20 61.210225s 0.328
Fibonacci 18 62.330882 0.292(1.12x slower)
truffleruby 22.2.0, like ruby 3.0.3, GraalVM CE JVM [x86_64-darwin]
Library Iterations Seconds Iterations per second
pairing_heap 59 60.053843 0.991
lazy_priority_queue 34 60.586461 0.563(1.76x slower)
Fibonacci 31 60.633711 0.520(1.90x slower)
### Simple Dijkstra's algorithm implementation [source code](./test/performance_dijkstra.rb) Heaps that support change_priority operation use it. Heaps that do not support it use dijkstra implementation that do not rely on change_priority instead and do additional pops and pushes instead(see Dijkstra-NoDec from [Priority Queues and Dijkstra’s Algorithm](https://www3.cs.stonybrook.edu/~rezaul/papers/TR-07-54.pdf)).
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21]
Library Iterations Seconds Iterations per second
pairing_heap (SimplePairingHeap) 28 62.100299 0.451
pairing_heap (PairingHeap) 23 60.633153 0.380(1.19x slower)
rb_heap 14 62.019763 0.226(2.00x slower)
lazy_priority_queue 11 63.105064s 0.174(2.58x slower)
Fibonacci 10 64.407187 0.155(2.90x slower)
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) +YJIT [x86_64-darwin21]
Library Iterations Seconds Iterations per second
pairing_heap (SimplePairingHeap) 32 61.289321 0.522
pairing_heap (PairingHeap) 26 60.657625 0.429(1.22x slower)
rb_heap 19 60.710888s 0.313(1.67x slower)
Fibonacci 19 61.471203 0.310(1.69x slower)
lazy_priority_queue 12 60.125779 0.200(2.61x slower)
jruby 9.3.7.0 (2.6.8) 2022-08-16 c79ef237e0 OpenJDK 64-Bit Server VM 17.0.2+8-86 on 17.0.2+8-86 +indy +jit [x86_64-darwin]
Library Iterations Seconds Iterations per second
pairing_heap (SimplePairingHeap) 46 61.226924 0.753
rb_heap 38 60.563995 0.628(1.20x slower)
pairing_heap (PairingHeap) 37 60.928350 0.608(1.24x slower)
Fibonacci 28 61.136970 0.461(1.63x slower)
lazy_priority_queue 22 62.214796 0.354(2.13x slower)
truffleruby 22.2.0, like ruby 3.0.3, GraalVM CE JVM [x86_64-darwin]
Library Iterations Seconds Iterations per second
pairing_heap (SimplePairingHeap) 176 60.029817 3.006
pairing_heap (PairingHeap) 124 60.366607 2.078(1.45x slower)
rb_heap 95 60.021043 1.585(1.90x slower)
Fibonacci 38 60.020976 0.636(4.72x slower)
lazy_priority_queue 27 61.349925 0.445(6.75x slower)
### Summary #### Change priority required
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21]
Library Slower geometric mean
pairing_heap 1
lazy_priority_queue 1.688x slower
Fibonacci 1.987x slower
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) +YJIT [x86_64-darwin21]
Library Slower geometric mean
pairing_heap 1
Fibonacci 1.256x slower
lazy_priority_queue 1.648x slower
jruby 9.3.7.0 (2.6.8) 2022-08-16 c79ef237e0 OpenJDK 64-Bit Server VM 17.0.2+8-86 on 17.0.2+8-86 +indy +jit [x86_64-darwin]
Library Slower geometric mean
pairing_heap 1
Fibonacci 1.327x slower
lazy_priority_queue 1.383x slower
truffleruby 22.2.0, like ruby 3.0.3, GraalVM CE JVM [x86_64-darwin]
Library Slower geometric mean
pairing_heap 1
lazy_priority_queue 3.878x slower
Fibonacci 9.889x slower
#### Change priority not required
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21]
Library Slower geometric mean
pairing_heap (SimplePairingHeap) 1
pairing_heap (PairingHeap) 1.318x slower
rb_heap 1.8x slower
lazy_priority_queue 2.519x slower
Fibonacci 2.989x slower
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) +YJIT [x86_64-darwin21]
Library Slower geometric mean
pairing_heap (SimplePairingHeap) 1
pairing_heap (PairingHeap) 1.348x slower
rb_heap 1.490x slower
Fibonacci 1.792x slower
lazy_priority_queue 2.492x slower
jruby 9.3.7.0 (2.6.8) 2022-08-16 c79ef237e0 OpenJDK 64-Bit Server VM 17.0.2+8-86 on 17.0.2+8-86 +indy +jit [x86_64-darwin]
Library Slower geometric mean
pairing_heap (SimplePairingHeap) 1
rb_heap 1.292x slower
pairing_heap (PairingHeap) 1.359x slower
Fibonacci 1.824x slower
lazy_priority_queue 2.115x slower
truffleruby 22.2.0, like ruby 3.0.3, GraalVM CE JVM [x86_64-darwin]
Library Slower geometric mean
pairing_heap (SimplePairingHeap) 1
pairing_heap (PairingHeap) 1.865x slower
rb_heap 1.915x slower
lazy_priority_queue 8.791x slower
Fibonacci 26.044x slower
## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/mhib/pairing_heap. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).