diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..132a47f
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,18 @@
+name: Ruby
+
+on: [push,pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.0.0
+ - name: Run the default task
+ run: |
+ gem install bundler -v 2.2.3
+ bundle install
+ bundle exec rake
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e192b8f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+/.bundle/
+/.yardoc
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+.DS_Store
+.tool_versions
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..ba37713
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,17 @@
+AllCops:
+ Exclude:
+ - 'test/fib.rb'
+
+Style/InfiniteLoop:
+ Enabled: false
+
+Style/StringLiterals:
+ Enabled: false
+ EnforcedStyle: double_quotes
+
+Style/StringLiteralsInInterpolation:
+ Enabled: true
+ EnforcedStyle: double_quotes
+
+Layout/LineLength:
+ Max: 120
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..b377aa9
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+# Specify your gem's dependencies in pairing_heap.gemspec
+gemspec
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..496afbd
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,21 @@
+PATH
+ remote: .
+ specs:
+ pairing_heap (0.1.0)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ minitest (5.14.3)
+ rake (13.0.3)
+
+PLATFORMS
+ x86_64-darwin-20
+
+DEPENDENCIES
+ minitest (~> 5.0)
+ pairing_heap!
+ rake (~> 13.0)
+
+BUNDLED WITH
+ 2.2.3
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..8d77be7
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2021 Marcin Henryk Bartkowiak
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ed40273
--- /dev/null
+++ b/README.md
@@ -0,0 +1,426 @@
+# PairingHeap
+
+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.
+
+## 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
+
+## Usage
+```ruby
+require 'pairing_heap'
+
+# 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_condidition` constructor argument is removed
+
+## Time Complexity
+| Operation | Time complexity | Amortized time complexity |
+| --------------- | --------------- | ------------------------- |
+| enqueue | O(1) | O(1) |
+| peek | O(1) | O(1) |
+| change_priority | O(1) | o(log n) |
+| dequeue | O(n) | O(log n) |
+| delete | O(n) | O(log n) |
+
+## Benchmarks
+I picked the two 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, used for example in [RGL](https://github.com/monora/rgl/)
+* 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))
+
+All tests except for the third one were executed by [benchmark-ips](https://github.com/evanphx/benchmark-ips) with parameters `time = 180` and `warmup = 30`, 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.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20] |
+
+
+ Library |
+ Iterations |
+ Seconds |
+ Iterations per second |
+
+
+ pairing_heap |
+ 14 |
+ 60.564595 |
+ 0.231 |
+
+
+ lazy_priority_queue |
+ 8 |
+ 62.489819 |
+ 0.128(1.81x slower) |
+
+
+ Fibonacci |
+ 8 |
+ 68.719194 |
+ 0.116(1.99x slower) |
+
+
+ jruby 9.2.14.0 (2.5.7) 2020-12-08 ebe64bafb9 OpenJDK 64-Bit Server VM 15.0.2+7 on 15.0.2+7 +jit [darwin-x86_64] |
+
+
+ Library |
+ Iterations |
+ Seconds |
+ Iterations per second |
+
+
+ pairing_heap |
+ 17 |
+ 61.195794 |
+ 0.278 |
+
+
+ lazy_priority_queue |
+ 14 |
+ 64.375927 |
+ 0.218(1.28x slower) |
+
+
+ Fibonacci |
+ 9 |
+ 67.415358 |
+ 0.134(2.08x slower) |
+
+
+
+### Stress test with changing priority(N = 1000) [source code](./test/performance_with_change_priority.rb)
+A stress test of 2,000,000 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.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20] |
+
+
+ Library |
+ Iterations |
+ Seconds |
+ Iterations per second |
+
+
+ pairing_heap |
+ 13 |
+ 60.280165 |
+ 0.216 |
+
+
+ lazy_priority_queue |
+ 8 |
+ 67.414861s |
+ 0.119(1.82x slower) |
+
+
+ Fibonacci |
+ 7 |
+ 61.067436 |
+ 0.115(1.88x slower) |
+
+
+ jruby 9.2.14.0 (2.5.7) 2020-12-08 ebe64bafb9 OpenJDK 64-Bit Server VM 15.0.2+7 on 15.0.2+7 +jit [darwin-x86_64] |
+
+
+ Library |
+ Iterations |
+ Seconds |
+ Iterations per second |
+
+
+ pairing_heap |
+ 16 |
+ 62.519677 |
+ 0.256 |
+
+
+ lazy_priority_queue |
+ 13 |
+ 63.832733 |
+ 0.204(1.26x slower) |
+
+
+ Fibonacci |
+ 8 |
+ 60.250658 |
+ 0.133(1.93x slower) |
+
+
+
+### Stress test with changing priority(N = 10) [source code](./test/performance_with_change_priority.rb)
+A stress test of 200 operations: starting with 10 pushes/10 change_priorities/0 pops, following 9 pushes/9 change_priorities/1 pop, and so on till 0 pushes/0 change_priorities/10 pops.
+
+
+ ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20] |
+
+
+ Library |
+ Iterations per second |
+
+
+ pairing_heap |
+ 5991.2 |
+
+
+ Fibonacci |
+ 3803.5(1.58x slower) |
+
+
+ lazy_priority_queue |
+ 3681.9(1.64x slower) |
+
+
+ jruby 9.2.14.0 (2.5.7) 2020-12-08 ebe64bafb9 OpenJDK 64-Bit Server VM 15.0.2+7 on 15.0.2+7 +jit [darwin-x86_64] |
+
+
+ Library |
+ Iterations per second |
+
+
+ pairing_heap |
+ 6784.3 |
+
+
+ lazy_priority_queue |
+ 6044.5(1.12x slower) |
+
+
+ Fibonacci |
+ 4070.5(1.67x slower) |
+
+
+
+### Dijkstra's algorithm with RGL [source code](./test/performance_rgl.rb)
+
+
+ ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20] |
+
+
+ Library |
+ Iterations |
+ Seconds |
+ Iterations per second |
+
+
+ pairing_heap |
+ 7 |
+ 64.768526 |
+ 0.108 |
+
+
+ lazy_priority_queue |
+ 6 |
+ 63.278091 |
+ 0.095(1.14x slower) |
+
+
+ Fibonacci |
+ 6 |
+ 65.898081 |
+ 0.091(1.19x slower) |
+
+
+ jruby 9.2.14.0 (2.5.7) 2020-12-08 ebe64bafb9 OpenJDK 64-Bit Server VM 15.0.2+7 on 15.0.2+7 +jit [darwin-x86_64] |
+
+
+ Library |
+ Iterations |
+ Seconds |
+ Iterations per second |
+
+
+ pairing_heap |
+ 12 |
+ 60.277567 |
+ 0.199 |
+
+
+ lazy_priority_queue |
+ 12 |
+ 61.238395 |
+ 0.196(1.02x slower) |
+
+
+ Fibonacci |
+ 10 |
+ 62.687378 |
+ 0.160(1.25x slower) |
+
+
+
+### Simple Dijkstra's algorithm implementation [source code](./test/performance_dijkstra.rb)
+
+
+ ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20] |
+
+
+ Library |
+ Iterations |
+ Seconds |
+ Iterations per second |
+
+
+ pairing_heap |
+ 20 |
+ 60.028380 |
+ 0.334 |
+
+
+ Fibonacci |
+ 10 |
+ 64.471303 |
+ 0.155(2.14x slower) |
+
+
+ lazy_priority_queue |
+ 9 |
+ 65.986618 |
+ 0.136(2.45x slower) |
+
+
+ jruby 9.2.14.0 (2.5.7) 2020-12-08 ebe64bafb9 OpenJDK 64-Bit Server VM 15.0.2+7 on 15.0.2+7 +jit [darwin-x86_64] |
+
+
+ Library |
+ Iterations |
+ Seconds |
+ Iterations per second |
+
+
+ pairing_heap |
+ 21 |
+ 61.727259 |
+ 0.340 |
+
+
+ lazy_priority_queue |
+ 14 |
+ 63.436863 |
+ 0.221(1.54x slower) |
+
+
+ Fibonacci |
+ 10 |
+ 62.447662 |
+ 0.160(2.12x slower) |
+
+
+
+### Summary
+
+
+ ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20] |
+
+
+ Library |
+ Slower geometric mean |
+
+
+ pairing_heap |
+ 1 |
+
+
+ Fibonacci |
+ 1.720x slower |
+
+
+ lazy_priority_queue |
+ 1.721x slower |
+
+
+ jruby 9.2.14.0 (2.5.7) 2020-12-08 ebe64bafb9 OpenJDK 64-Bit Server VM 15.0.2+7 on 15.0.2+7 +jit [darwin-x86_64] |
+
+
+ Library |
+ Slower geometric mean |
+
+
+ pairing_heap |
+ 1 |
+
+
+ lazy_priority_queue |
+ 1.23x slower |
+
+
+
+ Fibonacci |
+ 1.78x 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).
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..4ee764e
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "bundler/gem_tasks"
+require "rake/testtask"
+
+Rake::TestTask.new(:test) do |t|
+ t.libs << "test"
+ t.libs << "lib"
+ t.test_files = FileList["test/**/*_test.rb"]
+end
+
+task default: %i[test]
diff --git a/bin/console b/bin/console
new file mode 100755
index 0000000..090b90e
--- /dev/null
+++ b/bin/console
@@ -0,0 +1,15 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "pairing_heap"
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+# (If you use this, don't forget to add pry to your Gemfile!)
+# require "pry"
+# Pry.start
+
+require "irb"
+IRB.start(__FILE__)
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..dce67d8
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+set -vx
+
+bundle install
+
+# Do any other automated setup that you need to do here
diff --git a/lib/pairing_heap.rb b/lib/pairing_heap.rb
new file mode 100644
index 0000000..3643fed
--- /dev/null
+++ b/lib/pairing_heap.rb
@@ -0,0 +1,246 @@
+# frozen_string_literal: true
+
+module PairingHeap
+ # Pairing heap data structure implementation
+ # @see https://en.wikipedia.org/wiki/Pairing_heap
+ class PairingHeap
+ class Node
+ attr_accessor :elem, :priority, :subheaps, :parent, :prev_sibling, :next_sibling
+ def initialize(elem, priority, subheaps, parent, prev_sibling, next_sibling)
+ @elem = elem
+ @priority = priority
+ @subheaps = subheaps
+ @parent = parent
+ @prev_sibling = prev_sibling
+ @next_sibling = next_sibling
+ end
+ end
+ private_constant :Node
+
+ # @param &block Optional heap property priority comparator. `<:=.to_proc` by default
+ def initialize(&block)
+ @root = nil
+ @nodes = {}
+ @order = block || :<=.to_proc
+ end
+
+ # Pushes element to the heap.
+ # Time Complexity: O(1)
+ # @param elem Element to be pushed
+ # @param priority Priority of the element
+ # @raise [ArgumentError] if the element is already in the heap
+ # @return [PairingHeap]
+ def push(elem, priority)
+ raise ArgumentError, "Element already in the heap" if @nodes.key?(elem)
+
+ node = Node.new(elem, priority, nil, nil, nil, nil)
+ @nodes[elem] = node
+ @root = meld(@root, node)
+ self
+ end
+ alias enqueue push
+
+ # Returns the element at the top of the heap
+ # Time Complexity: O(1)
+ def peek
+ @root&.elem
+ end
+
+ # Time Complexity: O(1)
+ # @return [Boolean]
+ def empty?
+ @root.nil?
+ end
+
+ # Time Complexity: O(1)
+ # @return [Boolean]
+ def any?
+ !@root.nil?
+ end
+
+ # Time Complexity: O(1)
+ # @return [Integer]
+ def size
+ @nodes.size
+ end
+ alias length size
+
+ # Removes element from the top of the heap
+ # Time Complexity: O(N)
+ # Amortized time Complexity: O(log(N))
+ # @raise [ArgumEntError] if the heap is empty
+ # @return [PairingHeap]
+ def pop
+ raise ArgumentError, "Cannot remove from an empty heap" if @root.nil?
+
+ elem = @root.elem
+ @nodes.delete(elem)
+ @root = merge_pairs(@root.subheaps)
+ if @root
+ @root.parent = nil
+ @root.next_sibling = nil
+ @root.prev_sibling = nil
+ end
+ elem
+ end
+ alias dequeue pop
+
+ # Changes a priority of element to a more prioritary one
+ # Time Complexity: O(1)
+ # Amortized Time Complexity: o(log(N))
+ # @param elem Element
+ # @param priority New priority
+ # @raise [ArgumentError] if the element heap is not in heap or the new priority is less prioritary
+ # @return [PairingHeap]
+ def change_priority(elem, priority)
+ node = @nodes[elem]
+ raise ArgumentError, "Provided element is not in heap" if node.nil?
+ unless @order[priority, node.priority]
+ raise ArgumentError, "Priority cannot be changed to a less prioritary value."
+ end
+
+ node.priority = priority
+ return if node.parent.nil?
+ return if @order[node.parent.priority, node.priority]
+
+ remove_from_parents_list(node)
+ @root = meld(node, @root)
+ @root.parent = nil
+ self
+ end
+
+ # Removes element from the top of the heap
+ # Time Complexity: O(N)
+ # Amortized Time Complexity: O(log(N))
+ # @raise [ArgumentError] if the element heap is not in heap
+ # @return [PairingHeap]
+ def delete(elem)
+ node = @nodes[elem]
+ raise ArgumentError, "Provided element is not in heap" if node.nil?
+
+ @nodes.delete(elem)
+ if node.parent.nil?
+ @root = merge_pairs(node.subheaps)
+ else
+ remove_from_parents_list(node)
+ new_heap = merge_pairs(node.subheaps)
+ if new_heap
+ new_heap.prev_sibling = nil
+ new_heap.next_sibling = nil
+ end
+ @root = meld(new_heap, @root)
+ end
+ @root&.parent = nil
+ self
+ end
+
+ private
+
+ def remove_from_parents_list(node)
+ if node.prev_sibling
+ node.prev_sibling.next_sibling = node.next_sibling
+ node.next_sibling.prev_sibling = node.prev_sibling if node.next_sibling
+ elsif node.parent.subheaps.equal?(node)
+ node.parent.subheaps = node.next_sibling
+ node.next_sibling.prev_sibling = nil if node.next_sibling
+ elsif node.next_sibling
+ node.next_sibling.prev_sibling = nil
+ end
+ node.prev_sibling = nil
+ node.next_sibling = nil
+ end
+
+ def meld(left, right)
+ return right if left.nil?
+ return left if right.nil?
+
+ if @order[left.priority, right.priority]
+ parent = left
+ child = right
+ else
+ parent = right
+ child = left
+ end
+ child.next_sibling = parent.subheaps
+ parent.subheaps = child
+ child.next_sibling.prev_sibling = child if child.next_sibling
+ child.prev_sibling = nil
+ child.parent = parent
+ parent
+ end
+
+ # Non-recursive implementation of method described in https://en.wikipedia.org/wiki/Pairing_heap#delete-min
+ def merge_pairs(heaps)
+ return nil if heaps.nil?
+ return heaps if heaps.next_sibling.nil?
+
+ # [H1, H2, H3, H4, H5, H6, H7] => [H1H2, H3H4, H5H6, H7]
+ stack = []
+ current = heaps
+ while current
+ prev = current
+ current = current.next_sibling
+ unless current
+ stack << prev
+ break
+ end
+ next_val = current.next_sibling
+ stack << meld(prev, current)
+ current = next_val
+ end
+
+ # [H1H2, H3H4, H5H6, H7]
+ # [H1H2, H3H4, H5H67]
+ # [H1H2, H3H45H67]
+ # [H1H2H3H45H67]
+ # return H1H2H3H45H67
+ while true
+ right = stack.pop
+ return right if stack.empty?
+
+ left = stack.pop
+ stack << meld(left, right)
+ end
+ end
+ end
+
+ # Priority queue where the smallest priority is the most prioritary
+ class MinPriorityQueue < PairingHeap
+ def initialize
+ super(&:<=)
+ end
+
+ alias decrease_key change_priority
+ alias min peek
+ alias extract_min dequeue
+ end
+
+ # Priority queue where the highest priority is the most prioritary
+ class MaxPriorityQueue < PairingHeap
+ def initialize
+ super(&:>=)
+ end
+
+ alias increase_key change_priority
+ alias max peek
+ alias extract_max dequeue
+ end
+
+ # Priority queue with change_priority, that accepts changing to a less prioritary priority
+ class SafeChangePriorityQueue < PairingHeap
+ # Changes a priority of the element to a more prioritary one
+ # Time Complexity: O(N)
+ # Amortized Time Complexity: O(log(N))
+ # @raise [ArgumentError] if the element heap is not in the heap
+ # @return [PairingHeap]
+ def change_priority(elem, priority)
+ raise ArgumentError, "Provided element is not in heap" unless @nodes.key?(elem)
+ if !@order[priority, @nodes[elem].priority]
+ delete(elem)
+ push(elem, priority)
+ else
+ super(elem, priority)
+ end
+ end
+ end
+end
diff --git a/lib/pairing_heap/version.rb b/lib/pairing_heap/version.rb
new file mode 100644
index 0000000..b56d403
--- /dev/null
+++ b/lib/pairing_heap/version.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+module PairingHeap
+ VERSION = "0.1.0"
+end
diff --git a/pairing_heap.gemspec b/pairing_heap.gemspec
new file mode 100644
index 0000000..ebe67d1
--- /dev/null
+++ b/pairing_heap.gemspec
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require_relative "lib/pairing_heap/version"
+
+Gem::Specification.new do |spec|
+ spec.name = "pairing_heap"
+ spec.version = PairingHeap::VERSION
+ spec.authors = ["Marcin Henryk Bartkowiak"]
+ spec.email = ["mhbartkowiak@gmail.com"]
+
+ spec.summary = "Performant priority queue in pure ruby with support for changing priority"
+ spec.description = "Performant priority queue in pure ruby with support for changing priority using pairing heap data structure"
+ spec.homepage = "https://github.com/mhib/pairing_heap"
+ spec.license = "MIT"
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+
+ # Uncomment to register a new dependency of your gem
+ # spec.add_dependency "example-gem", "~> 1.0"
+
+ spec.add_development_dependency "minitest", "~> 5.0"
+ spec.add_development_dependency "rake", "~> 13.0"
+
+ # For more information and examples about making a new gem, checkout our
+ # guide at: https://bundler.io/guides/creating_gem.html
+end
diff --git a/test/.tool-versions b/test/.tool-versions
new file mode 100644
index 0000000..2c0c270
--- /dev/null
+++ b/test/.tool-versions
@@ -0,0 +1 @@
+ruby 3.0.0
diff --git a/test/fib.rb b/test/fib.rb
new file mode 100644
index 0000000..9e1d069
--- /dev/null
+++ b/test/fib.rb
@@ -0,0 +1,515 @@
+# From: https://github.com/supertinou/priority-queue/blob/master/lib/priority_queue/ruby_priority_queue.rb
+# Pure ruby Priority Queue
+class RubyPriorityQueue
+
+ include Enumerable
+
+ private
+
+ #
+ def link_nodes(b1, b2)
+ return link_nodes(b2, b1) if b2.priority < b1.priority
+
+ b2.parent = b1
+ child = b1.child
+ b1.child = b2
+ if child
+ b2.left = child.left
+ b2.left.right = b2
+ b2.right = child
+ child.left = b2
+ else
+ b2.left = b2
+ b2.right = b2
+ end
+ b1.degree += 1
+ b2.mark = false # TODO: Check if this is correct, or if b1 should be marked as false
+ return b1
+ end
+
+ # Does not change length
+ def delete_first
+ return nil unless @rootlist
+
+ result = @rootlist
+ if result == result.right
+ @min = @rootlist = nil
+ else
+ @rootlist = result.right
+ @rootlist.left = result.left
+ @rootlist.left.right = @rootlist
+
+ result.right = result.left = result
+ end
+ return result;
+ end
+
+ def cut_node(n)
+ return self unless n.parent
+ n.parent.degree -= 1
+ if n.parent.child == n
+ if n.right == n
+ n.parent.child = nil
+ else
+ n.parent.child = n.right;
+ end
+ end
+ n.parent = nil
+ n.right.left = n.left
+ n.left.right = n.right
+
+ n.right = @rootlist
+ n.left = @rootlist.left
+ @rootlist.left.right = n
+ @rootlist.left = n
+
+ n.mark = false
+
+ return self
+ end
+
+ # Does not change length
+ def insert_tree(tree)
+ if @rootlist == nil
+ @rootlist = @min = tree
+ else
+ l = @rootlist.left
+ l.right = tree
+ @rootlist.left = tree
+ tree.left = l
+ tree.right = @rootlist
+ @min = tree if tree.priority < @min.priority
+ end
+ self
+ end
+
+ def consolidate
+ return self if self.empty?
+ array_size = (2.0 * Math.log(self.length) / Math.log(2) + 1.0).ceil
+ tree_by_degree = Array.new(array_size)
+
+ while n = delete_first
+ while n1 = tree_by_degree[n.degree]
+ tree_by_degree[n.degree] = nil;
+ n = link_nodes(n, n1);
+ end
+ tree_by_degree[n.degree] = n;
+ end
+
+ @rootlist = @min = nil;
+ tree_by_degree.each do | tree |
+ next unless tree
+ insert_tree(tree)
+ end
+ self
+ end
+
+ # Node class used internally
+ class Node # :nodoc:
+ attr_accessor :parent, :left, :right, :key, :priority, :degree, :mark
+ attr_reader :child
+
+ def child=(c)
+ raise "Circular Child" if c == self
+ raise "Child is neighbour" if c == self.right
+ raise "Child is neighbour" if c == self.left
+ @child = c
+ end
+
+ def to_dot(only_down = false, known_nodes = [])
+ p known_nodes.map { | n | n.dot_id }
+ p self.dot_id
+ result = []
+ if only_down
+ raise "Circular #{caller.inspect}" if known_nodes.include?(self)
+ known_nodes << self
+
+ result << "#{dot_id} [label=\"#{@key}: #{@priority}\"];"
+ l = " "
+ #l << "#{@left.dot_id} <- #{dot_id}; " if @left
+ l << "#{dot_id} -> #{@left.dot_id} [constraint=false]; " if @left and @left.dot_id < self.dot_id
+ l << "#{dot_id} -> #{@right.dot_id} [constraint=false];\t\t\t\t/*neighbours*/" if @right and @right.dot_id <= self.dot_id
+ result << l
+ result << " #{dot_id} -> #{@child.dot_id}; //child" if @child
+ result << @child.to_dot(false, known_nodes) if @child
+ else
+ n = self
+ begin
+ result.concat(n.to_dot(true, known_nodes))
+ n = n.right
+ end while n != self
+ end
+ result.flatten.map{|r| " " << r}
+ end
+
+ def dot_id
+ "N#{@key}"
+ end
+
+ def initialize(key, priority)
+ @key = key; @priority = priority; @degree = 0
+ end
+ end
+
+ public
+
+ # Returns the number of elements of the queue.
+ #
+ # q = PriorityQueue.new
+ # q.length #=> 0
+ # q[0] = 1
+ # q.length #=> 1
+ attr_reader :length
+
+ # Create a new, empty PriorityQueue
+ def initialize
+ @nodes = Hash.new
+ @rootlist = nil
+ @min = nil
+ @length = 0
+ end
+
+ # Print a priority queue as a dot-graph. The output can be fed to dot from the
+ # vizgraph suite to create a tree depicting the internal datastructure.
+ def to_dot
+ r = ["digraph fibheap {"]
+ #r << @rootlist.to_dot.join("\n") if @rootlist
+ r << "ROOT -> #{@rootlist.dot_id};" if @rootlist
+ @nodes.to_a.sort.each do | (_, n) |
+ r << " #{n.dot_id} [label=\"#{n.key}: #{n.priority}\"];"
+ r << " #{n.dot_id} -> #{n.right.dot_id} [constraint=false];" if n.right# and n.dot_id < n.right.dot_id
+ r << " #{n.dot_id} -> #{n.left.dot_id} [constraint=false];" if n.left #and n.dot_id < n.left.dot_id
+ r << " #{n.dot_id} -> #{n.child.dot_id}" if n.child
+ end
+ r << "}"
+ r.join("\n")
+ r
+ end
+
+ # Call dot and gv displaying the datstructure
+ def display_dot
+ puts to_dot
+ system "echo '#{to_dot}' | twopi -Tps -Groot=ROOT -Goverlap=false> /tmp/dotfile.ps; gv /tmp/dotfile.ps"
+ end
+
+ # call-seq:
+ # [key] = priority
+ # change_priority(key, priority)
+ # push(key, priority)
+ #
+ # Set the priority of a key.
+ #
+ # q = PriorityQueue.new
+ # q["car"] = 50
+ # q["train"] = 50
+ # q["bike"] = 10
+ # q.min #=> ["bike", 10]
+ # q["car"] = 0
+ # q.min #=> ["car", 0]
+ def change_priority(key, priority)
+ return push(key, priority) unless @nodes[key]
+
+ n = @nodes[key]
+ if n.priority < priority # Priority was increased. Remove the node and reinsert.
+ self.delete(key)
+ self.push(key, priority);
+ return self
+ end
+ n.priority = priority;
+ @min = n if n.priority < @min.priority
+
+ return self if !n.parent or n.parent.priority <= n.priority # Already in rootlist or bigger than parent
+ begin # Cascading Cuts
+ p = n.parent
+ cut_node(n)
+ n = p
+ end while n.mark and n.parent
+ n.mark = true if n.parent
+
+ self
+ end
+ alias decrease_key change_priority
+
+ # Add an object to the queue.
+ def push(key, priority)
+ return change_priority(key, priority) if @nodes[key]
+ @nodes[key] = node = Node.new(key, priority)
+ @min = node if !@min or priority < @min.priority
+ if not @rootlist
+ @rootlist = node
+ node.left = node.right = node
+ else
+ node.left = @rootlist.left
+ node.right = @rootlist
+ @rootlist.left.right = node
+ @rootlist.left = node
+ end
+ @length += 1
+ self
+ end
+
+ # Returns true if the array is empty, false otherwise.
+ def empty?
+ @rootlist.nil?
+ end
+
+ # call-seq:
+ # [key] -> priority
+ #
+ # Return the priority of a key or nil if the key is not in the queue.
+ #
+ # q = PriorityQueue.new
+ # (0..10).each do | i | q[i.to_s] = i end
+ # q["5"] #=> 5
+ # q[5] #=> nil
+ def [](key)
+ @nodes[key] and @nodes[key].priority
+ end
+
+ # call-seq:
+ # has_key? key -> boolean
+ #
+ # Return false if the key is not in the queue, true otherwise.
+ #
+ # q = PriorityQueue.new
+ # (0..10).each do | i | q[i.to_s] = i end
+ # q.has_key("5") #=> true
+ # q.has_key(5) #=> false
+ def has_key?(key)
+ @nodes.has_key?(key)
+ end
+
+ alias :[]= :push
+
+ # Call the given block with each [key, priority] pair in the queue
+ #
+ # Beware: Changing the queue in the block may lead to unwanted behaviour and
+ # even infinite loops.
+ def each
+ @nodes.each do | key, node |
+ yield(key, node.priority)
+ end
+ end
+
+ # call-seq:
+ # min -> [object, priority]
+ #
+ # Return the pair [object, priority] with minimal priority or nil when the
+ # queue is empty.
+ #
+ # q = PriorityQueue.new
+ # q["a"] = 10
+ # q["b"] = 20
+ # q.min #=> ["a", 10]
+ # q.delete_min #=> ["a", 10]
+ # q.min #=> ["b", 20]
+ # q.delete_min #=> ["b", 20]
+ # q.min #=> nil
+ def min
+ [@min.key, @min.priority] rescue nil
+ end
+
+ # call-seq:
+ # min_key -> object
+ #
+ # Return the key that has the minimal priority or nil when the queue is empty.
+ #
+ # q = PriorityQueue.new
+ # q["a"] = 10
+ # q["b"] = 20
+ # q.min_key #=> "a"
+ # q.delete_min #=> ["a", 10]
+ # q.min_key #=> "b"
+ # q.delete_min #=> ["b", 20]
+ # q.min_key #=> nil
+ def min_key
+ @min.key rescue nil
+ end
+
+ # call-seq:
+ # min_priority -> priority
+ #
+ # Return the minimal priority or nil when the queue is empty.
+ #
+ # q = PriorityQueue.new
+ # q["a"] = 10
+ # q["b"] = 20
+ # q.min_priority #=> 10
+ # q.delete_min #=> ["a", 10]
+ # q.min_priority #=> 20
+ # q.delete_min #=> ["b", 20]
+ # q.min_priority #=> nil
+ def min_priority
+ @min.priority rescue nil
+ end
+
+ # call-seq:
+ # delete(key) -> [key, priority]
+ # delete(key) -> nil
+ #
+ # Delete a key from the priority queue. Returns nil when the key was not in
+ # the queue and [key, priority] otherwise.
+ #
+ # q = PriorityQueue.new
+ # (0..10).each do | i | q[i.to_s] = i end
+ # q.delete(5) #=> ["5", 5]
+ # q.delete(5) #=> nil
+ def delete(key)
+ return nil unless n = @nodes.delete(key)
+
+ if n.child
+ c = n.child
+ e = n.child
+ begin
+ r = c.right
+ cut_node(c)
+ c = r
+ end while c != e
+ end
+ cut_node(n) if n.parent
+
+ if n == n.right
+ @min = nil;
+ @rootlist = nil;
+ else
+ @rootlist = n.right if @rootlist == n
+ if @min == n
+ n1 = n.right
+ @min = n1
+ begin
+ @min = n1 if n1.priority < @min.priority
+ n1 = n1.right
+ end while(n1 != n);
+ end
+ n.right.left = n.left
+ n.left.right = n.right
+ n.left = n
+ n.right = n
+ end
+ @length -= 1
+ return [n.key, n.priority]
+ end
+
+ # call-seq:
+ # delete_min_return_key -> key
+ #
+ # Delete key with minimal priority and return the key
+ #
+ # q = PriorityQueue.new
+ # q["a"] = 1
+ # q["b"] = 0
+ # q.delete_min_return_key #=> "b"
+ # q.delete_min_return_key #=> "a"
+ # q.delete_min_return_key #=> nil
+ def delete_min_return_key
+ delete_min[0] rescue nil
+ end
+
+ # call-seq:
+ # delete_min_return_priority -> priority
+ #
+ # Delete key with minimal priority and return the priority value
+ #
+ # q = PriorityQueue.new
+ # q["a"] = 1
+ # q["b"] = 0
+ # q.delete_min_return_priority #=> 0
+ # q.delete_min_return_priority #=> 1
+ # q.delete_min_return_priority #=> nil
+ def delete_min_return_priority
+ delete_min[1] rescue nil
+ end
+
+ # call-seq:
+ # delete_min -> [key, priority]
+ #
+ # Delete key with minimal priority and return [key, priority]
+ #
+ # q = PriorityQueue.new
+ # q["a"] = 1
+ # q["b"] = 0
+ # q.delete_min #=> ["b", 0]
+ # q.delete_min #=> ["a", 1]
+ # q.delete_min #=> nil
+ def delete_min
+ return nil if self.empty?
+ result = self.min
+
+ @nodes.delete(@min.key)
+
+ if @length == 1
+ @rootlist = @min = nil
+ @length = 0
+ else
+ min = @min
+ if @min == @rootlist # If the rootlist is anchored at the minimum, shift to the right
+ if @rootlist == @rootlist.right
+ @rootlist = @min = nil
+ else
+ @rootlist = @min = @min.right
+ end
+ end
+ min.left.right = min.right;
+ min.right.left = min.left;
+ min.left = min.right = min;
+ if min.child
+ # Kinder und Eltern trennen, Markierung aufheben
+ n = min.child;
+ begin
+ n.parent = nil;
+ n.mark = false;
+ n = n.right;
+ end while n != min.child
+
+ # Kinder einfügen
+ if @rootlist
+ l1 = @rootlist.left
+ l2 = n.left
+
+ l1.right = n
+ n.left = l1
+ l2.right = @rootlist
+ @rootlist.left = l2
+ else
+ @rootlist = n
+ end
+ end
+
+ # Größe anpassen
+ @length -= 1
+
+ # Wieder aufhübschen
+ consolidate
+ end
+
+ result
+ end
+ alias pop delete_min_return_key
+
+ # Returns a string representation of the priority queue.
+ def inspect
+ ""
+ end
+
+ def initialize_copy(copy)
+ copy_nodes = @nodes
+ @nodes = {}
+
+ copy_nodes.each do | (_, cn) |
+ n = @nodes[cn.key] = Node.new(cn.key, cn.priority)
+ n.mark = cn.mark
+ n.degree = cn.degree
+ end
+
+ copy_nodes.each do | (_, cn) |
+ n = @nodes[cn.key]
+ n.left = @nodes[cn.left.key] if cn.left
+ n.right = @nodes[cn.right.key] if cn.right
+ n.parent = @nodes[cn.parent.key] if cn.parent
+ n.child = @nodes[cn.child.key] if cn.child
+ end
+ @rootlist = @nodes[@rootlist.key] if @rootlist
+ @min = @nodes[@min.key] if @min
+ self
+ end
+end
\ No newline at end of file
diff --git a/test/pairing_heap_test.rb b/test/pairing_heap_test.rb
new file mode 100644
index 0000000..1ef7520
--- /dev/null
+++ b/test/pairing_heap_test.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+describe PairingHeap do
+ describe PairingHeap::PairingHeap do
+ it 'works correctly with random usage' do
+ queue = PairingHeap::PairingHeap.new
+ class Element
+ attr_accessor :priority
+ def initialize(priority)
+ @priority = priority
+ end
+ end
+
+ items = []
+
+ 5_000.times do |id|
+ _(queue.empty?).must_equal(items.empty?)
+ _(-> { queue.peek }).must_be_silent
+ _(queue.size).must_equal(items.size)
+
+ if rand(2).zero?
+ priority = rand(1000)
+ item = Element.new(priority)
+ queue.enqueue(item, item.priority)
+ items << item
+ end
+
+ next if items.empty?
+
+ if rand(2).zero?
+ item = items.sample
+ item.priority -= rand(1000)
+ queue.change_priority(item, item.priority)
+ end
+
+ if rand(4).zero?
+ items.delete(queue.dequeue)
+ end
+
+ if items.any? && rand(6).zero?
+ sample = items.sample
+ queue.delete(sample)
+ items.delete(sample)
+ end
+
+ end
+
+ sorted_items = []
+ sorted_items << queue.pop until queue.empty?
+ sorted_items.map!(&:priority)
+
+ _(sorted_items).must_equal(sorted_items.sort)
+ end
+
+ it 'does not crash with a large number of consecutive pushes' do
+ queue = PairingHeap::PairingHeap.new
+ 1.upto(5_000_000) do |i|
+ queue.push(i, i)
+ end
+ _(queue.pop).must_equal(1)
+ end
+
+ it 'throws when trying to change priority to a less prioritary one' do
+ queue = PairingHeap::PairingHeap.new
+ queue.push(1, 1)
+ _(-> { queue.change_priority(1, 2) }).must_raise(ArgumentError)
+ end
+ end
+
+ describe PairingHeap::MinPriorityQueue do
+ it 'sorts correctly' do
+ queue = PairingHeap::MinPriorityQueue.new
+ array = (1..10).to_a
+ array.each { |i| queue.push(i, i) }
+ result = []
+ result << queue.pop while queue.any?
+ _(result).must_equal(array)
+ end
+ end
+
+ describe PairingHeap::MaxPriorityQueue do
+ it 'sorts correctly' do
+ queue = PairingHeap::MaxPriorityQueue.new
+ array = (1..10).to_a
+ array.each { |i| queue.push(i, i) }
+ result = []
+ result << queue.pop while queue.any?
+ _(result).must_equal(array.reverse)
+ end
+ end
+
+ describe PairingHeap::SafeChangePriorityQueue do
+ it 'does not throw when trying to change priority to a less prioritary one' do
+ queue = PairingHeap::SafeChangePriorityQueue.new(&:<=)
+ queue.push(1, 1)
+ queue.push(2, 2)
+ _(-> { queue.change_priority(1, 3) }).must_be_silent
+ _(queue.pop).must_equal(2)
+ _(queue.pop).must_equal(1)
+ end
+ end
+end
diff --git a/test/performance.rb b/test/performance.rb
new file mode 100644
index 0000000..e3a1c5c
--- /dev/null
+++ b/test/performance.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+# Based on https://github.com/matiasbattocchia/lazy_priority_queue/blob/master/test/performance.rb
+require 'benchmark/ips'
+require_relative '../lib/pairing_heap'
+require 'lazy_priority_queue'
+require_relative 'fib'
+
+N = 1_000
+def iterator(push, pop)
+ N.times do |i|
+ (N - i).times do |j|
+ push.call i.to_s + ':' + j.to_s
+ end
+
+ i.times do
+ pop.call
+ end
+ end
+end
+
+Benchmark.ips do |bm|
+ bm.time = 60
+ bm.warmup = 15
+
+ bm.report('lazy_priority_queue') do
+ q = MinPriorityQueue.new
+ iterator(->(n) { q.enqueue(n, rand) }, -> { q.dequeue })
+ end
+
+ bm.report('pairing_heap') do
+ q = PairingHeap::MinPriorityQueue.new
+ iterator(->(n) { q.enqueue n, rand }, -> { q.dequeue })
+ end
+
+ bm.report('Fibonacci') do
+ q = RubyPriorityQueue.new
+ iterator(->(n) { q.push n, rand }, -> { q.delete_min })
+ end
+
+ bm.compare!
+end
diff --git a/test/performance_dijkstra.rb b/test/performance_dijkstra.rb
new file mode 100644
index 0000000..d189145
--- /dev/null
+++ b/test/performance_dijkstra.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'csv'
+require 'benchmark/ips'
+require_relative '../lib/pairing_heap'
+require 'lazy_priority_queue'
+require_relative 'fib'
+Edge = Struct.new(:to, :weight)
+
+@neighbourhood = []
+# https://gist.githubusercontent.com/mhib/ed03bb9eb67ae871fa6f199f024379c7/raw/4acd08372b3ffec2de1982449b2a4ee9fbaa48cd/Tokyo_Edgelist.csv
+CSV.foreach("Tokyo_Edgelist.csv", headers: true) do |row|
+ from = row["START_NODE"].to_i
+ to = row["END_NODE"].to_i
+ weight = row["LENGTH"].to_f
+ @neighbourhood[from] ||= []
+ @neighbourhood[from] << Edge.new(to, weight)
+ @neighbourhood[to] ||= []
+ @neighbourhood[to] << Edge.new(from, weight)
+end
+
+def get_costs(q)
+ distance = Array.new(@neighbourhood.size, Float::INFINITY)
+ distance[1] = 0
+ @neighbourhood.each_with_index do |value, vertex|
+ next unless value
+
+ q.push(vertex, distance[vertex])
+ end
+ until q.empty?
+ el = q.pop
+ @neighbourhood[el].each do |edge|
+ alt = distance[el] + edge.weight
+ if alt < distance[edge.to]
+ distance[edge.to] = alt
+ q.change_priority(edge.to, alt)
+ end
+ end
+ end
+ distance
+end
+
+Benchmark.ips do |bm|
+ bm.time = 60
+ bm.warmup = 15
+ bm.report('pairing_heap') do
+ get_costs(PairingHeap::MinPriorityQueue.new)
+ end
+
+ bm.report('Fibonacci') do
+ get_costs(RubyPriorityQueue.new)
+ end
+
+ bm.report('lazy_priority_queue') do
+ get_costs(MinPriorityQueue.new)
+ end
+
+ bm.compare!
+end
diff --git a/test/performance_rgl.rb b/test/performance_rgl.rb
new file mode 100644
index 0000000..6b17bc2
--- /dev/null
+++ b/test/performance_rgl.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'rgl/dijkstra'
+require 'rgl/adjacency'
+require_relative '../lib/pairing_heap'
+require 'csv'
+require 'benchmark/ips'
+require_relative 'fib'
+
+include RGL
+
+@graph = AdjacencyGraph[]
+@edge_weights = {}
+
+# https://gist.githubusercontent.com/mhib/ed03bb9eb67ae871fa6f199f024379c7/raw/4acd08372b3ffec2de1982449b2a4ee9fbaa48cd/Tokyo_Edgelist.csv
+CSV.foreach("Tokyo_Edgelist.csv", headers: true) do |row|
+ @graph.add_edge(row["START_NODE"].to_i, row["END_NODE"].to_i)
+ @edge_weights[[row["START_NODE"].to_i, row["END_NODE"].to_i]] = row["LENGTH"].to_f
+end
+
+class PairingDijkstraAlgorithm < RGL::DijkstraAlgorithm
+ def init(source)
+ @visitor.set_source(source)
+
+ @queue = PairingHeap::MinPriorityQueue.new
+ @queue.push(source, 0)
+ end
+end
+
+class FibDijkstraAlgorithm < RGL::DijkstraAlgorithm
+ def init(source)
+ @visitor.set_source(source)
+
+ @queue = RubyPriorityQueue.new
+ @queue.push(source, 0)
+ end
+end
+
+Benchmark.ips do |bm|
+ bm.time = 60
+ bm.warmup = 15
+
+ bm.report('Fibonacci') do
+ FibDijkstraAlgorithm.new(@graph, @edge_weights, DijkstraVisitor.new(@graph)).shortest_paths(1)
+ end
+
+ bm.report('lazy_priority_queue') do
+ DijkstraAlgorithm.new(@graph, @edge_weights, DijkstraVisitor.new(@graph)).shortest_paths(1)
+ end
+
+ bm.report('pairing_heap') do
+ PairingDijkstraAlgorithm.new(@graph, @edge_weights, DijkstraVisitor.new(@graph)).shortest_paths(1)
+ end
+
+ bm.compare!
+end
diff --git a/test/performance_with_change_priority.rb b/test/performance_with_change_priority.rb
new file mode 100644
index 0000000..ff65532
--- /dev/null
+++ b/test/performance_with_change_priority.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+# Based on https://github.com/matiasbattocchia/lazy_priority_queue/blob/master/test/performance.rb
+require 'benchmark/ips'
+require_relative '../lib/pairing_heap'
+require 'lazy_priority_queue'
+require_relative 'fib'
+
+N = 1_000
+def iterator(push, pop, change_priority)
+ N.times do |i|
+ (N - i).times do |j|
+ push.call i.to_s + ':' + j.to_s
+ end
+
+ (N - i).times do |j|
+ change_priority.call(i.to_s + ':' + j.to_s)
+ end
+
+ i.times do
+ pop.call
+ end
+ end
+end
+
+Benchmark.ips do |bm|
+ bm.time = 60
+ bm.warmup = 15
+
+ bm.report('lazy_priority_queue') do
+ q = MinPriorityQueue.new
+ iterator(->(n) { q.enqueue(n, rand) }, -> { q.dequeue }, ->(n) { q.change_priority(n, -rand) })
+ end
+
+ bm.report('pairing_heap') do
+ q = PairingHeap::MinPriorityQueue.new
+ iterator(->(n) { q.enqueue n, rand }, -> { q.dequeue }, ->(n) { q.change_priority(n, -rand) })
+ end
+
+ bm.report('Fibonacci') do
+ q = RubyPriorityQueue.new
+ iterator(->(n) { q.push n, rand }, -> { q.delete_min }, ->(n) { q.change_priority(n, -rand) })
+ end
+
+ bm.compare!
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..204713b
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
+require "pairing_heap"
+
+require "minitest/autorun"