pairing_heap 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 06be98427116306a2d0968edb9757188dde8d9ee97226ab03edf7fe4aba8d9d6
4
+ data.tar.gz: f91175ec148649d8a1987e8af3935b6f96740c0b21b6bf2e34dc154b355c35a8
5
+ SHA512:
6
+ metadata.gz: 5441d35660f0fec2c66f4e1ea5c4b89499d9e4d97f96e01b39f5fada5c462fafe75196562a611a2ae85e95bceb006a4b9fbd1252d9135a5859e4cb165b57ab74
7
+ data.tar.gz: 01b5f44ca3f608ca26091c5fd983965e95591fd097f9a82f87ef87e28bb01c8f66f0f6bf21bdbad2f9dcfc1f0b70fe34243a277d6a743b24c4d03f6377b419af
@@ -0,0 +1,18 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: 3.0.0
14
+ - name: Run the default task
15
+ run: |
16
+ gem install bundler -v 2.2.3
17
+ bundle install
18
+ bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .DS_Store
10
+ .tool_versions
data/.rubocop.yml ADDED
@@ -0,0 +1,17 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'test/fib.rb'
4
+
5
+ Style/InfiniteLoop:
6
+ Enabled: false
7
+
8
+ Style/StringLiterals:
9
+ Enabled: false
10
+ EnforcedStyle: double_quotes
11
+
12
+ Style/StringLiteralsInInterpolation:
13
+ Enabled: true
14
+ EnforcedStyle: double_quotes
15
+
16
+ Layout/LineLength:
17
+ Max: 120
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in pairing_heap.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,21 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pairing_heap (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.14.3)
10
+ rake (13.0.3)
11
+
12
+ PLATFORMS
13
+ x86_64-darwin-20
14
+
15
+ DEPENDENCIES
16
+ minitest (~> 5.0)
17
+ pairing_heap!
18
+ rake (~> 13.0)
19
+
20
+ BUNDLED WITH
21
+ 2.2.3
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Marcin Henryk Bartkowiak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,426 @@
1
+ # PairingHeap
2
+
3
+ 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.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'pairing_heap'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install pairing_heap
20
+
21
+ ## Usage
22
+ ```ruby
23
+ require 'pairing_heap'
24
+
25
+ # Min priority queue
26
+ best_defenses = PairingHeap::MinPriorityQueue.new
27
+ best_defenses.push('Chelsea', 24)
28
+ best_defenses.push('City', 30)
29
+ best_defenses.push('Tottenham', 25)
30
+ best_defenses.any? # => true
31
+ best_defenses.size # => 3
32
+ best_defenses.decrease_key('City', 15)
33
+ best_defenses.min # => 'City'
34
+ best_defenses.pop # => 'City'
35
+ best_defenses.extract_min # => 'Chelsea'
36
+ best_defenses.extract_min # => 'Tottenham'
37
+ best_defenses.any? # => false
38
+
39
+ # Max priority queue
40
+ best_teams = PairingHeap::MaxPriorityQueue.new
41
+ best_teams.push('City', 56)
42
+ best_teams.push('United', 46)
43
+ best_teams.push('Leicester', 46)
44
+ best_teams.increase_key('Leicester', 47)
45
+ best_teams.max # => 'City'
46
+ best_teams.pop # => 'City'
47
+ best_teams.extract_max # => 'Leicester'
48
+
49
+ # Custom comparator(it defaults to :<=.to_proc)
50
+ compare_by_length = PairingHeap::PairingHeap.new { |l, r| l.length <= r.length }
51
+ compare_by_length.push(:a, '11')
52
+ compare_by_length.push(:b, '1')
53
+ compare_by_length.push(:c, '11')
54
+ compare_by_length.change_priority(:c, '')
55
+ compare_by_length.peek # => :c
56
+ compare_by_length.pop # => :c
57
+ compare_by_length.pop # => :b
58
+ compare_by_length.pop # => :a
59
+
60
+ # SafeChangePriortyQueue
61
+ queue = PairingHeap::SafeChangePriorityQueue.new
62
+ queue.push(:a, 1)
63
+ queue.push(:b, 2)
64
+ queue.change_priority(:a, 3) # This works and does not throw an exception
65
+ queue.peek # => :b
66
+ ```
67
+ See also [test/performance_dijkstra.rb](./test/performance_dijkstra.rb) for a Dijkstra algorithm implementation.
68
+ ### Changes from lazy_priority_queue
69
+ This API is a drop-in replacement of [lazy_priority_queue](https://github.com/matiasbattocchia/lazy_priority_queue) with the following differences:
70
+
71
+ * Custom comparator provided to constructur, compares weights, not internal nodes
72
+ * `change_priority` returns `self` instead of the first argument
73
+ * `enqueue` returns `self` instead of the first argument
74
+ * Queue classes are in the `PairingHeap` namespace, so `require 'pairing_heap` does not load `MinPriorityQueue` to the global scope
75
+ * `top_condidition` constructor argument is removed
76
+
77
+ ## Time Complexity
78
+ | Operation | Time complexity | Amortized time complexity |
79
+ | --------------- | --------------- | ------------------------- |
80
+ | enqueue | O(1) | O(1) |
81
+ | peek | O(1) | O(1) |
82
+ | change_priority | O(1) | o(log n) |
83
+ | dequeue | O(n) | O(log n) |
84
+ | delete | O(n) | O(log n) |
85
+
86
+ ## Benchmarks
87
+ I picked the two fastest pure Ruby priority queue implementations I was aware of for the comparison:
88
+
89
+ * [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/)
90
+ * 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))
91
+
92
+ 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`.
93
+ ### Stress test without changing priority test(N = 1000) [source code](./test/performance.rb)
94
+ Original performance test from [lazy_priority_queue](https://github.com/matiasbattocchia/lazy_priority_queue)
95
+ > 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.
96
+ <table>
97
+ <tr>
98
+ <th colspan="4">ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20]</th>
99
+ </tr>
100
+ <tr>
101
+ <th>Library</th>
102
+ <th>Iterations</th>
103
+ <th>Seconds</th>
104
+ <th>Iterations per second</th>
105
+ </tr>
106
+ <tr>
107
+ <td>pairing_heap</td>
108
+ <td>14</td>
109
+ <td>60.564595</td>
110
+ <td>0.231</td>
111
+ </tr>
112
+ <tr>
113
+ <td>lazy_priority_queue</td>
114
+ <td>8</td>
115
+ <td>62.489819</td>
116
+ <td>0.128(1.81x slower)</td>
117
+ </tr>
118
+ <tr>
119
+ <td>Fibonacci</td>
120
+ <td>8</td>
121
+ <td>68.719194</td>
122
+ <td>0.116(1.99x slower)</td>
123
+ </tr>
124
+ <tr>
125
+ <th colspan="4">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]</th>
126
+ </tr>
127
+ <tr>
128
+ <th>Library</th>
129
+ <th>Iterations</th>
130
+ <th>Seconds</th>
131
+ <th>Iterations per second</th>
132
+ </tr>
133
+ <tr>
134
+ <td>pairing_heap</td>
135
+ <td>17</td>
136
+ <td>61.195794</td>
137
+ <td>0.278</td>
138
+ </tr>
139
+ <tr>
140
+ <td>lazy_priority_queue</td>
141
+ <td>14</td>
142
+ <td>64.375927</td>
143
+ <td>0.218(1.28x slower)</td>
144
+ </tr>
145
+ <tr>
146
+ <td>Fibonacci</td>
147
+ <td>9</td>
148
+ <td>67.415358</td>
149
+ <td>0.134(2.08x slower)</td>
150
+ </tr>
151
+ </table>
152
+
153
+ ### Stress test with changing priority(N = 1000) [source code](./test/performance_with_change_priority.rb)
154
+ 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.
155
+ <table>
156
+ <tr>
157
+ <th colspan="4">ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20]</th>
158
+ </tr>
159
+ <tr>
160
+ <th>Library</th>
161
+ <th>Iterations</th>
162
+ <th>Seconds</th>
163
+ <th>Iterations per second</th>
164
+ </tr>
165
+ <tr>
166
+ <td>pairing_heap</td>
167
+ <td>13</td>
168
+ <td>60.280165</td>
169
+ <td>0.216</td>
170
+ </tr>
171
+ <tr>
172
+ <td>lazy_priority_queue</td>
173
+ <td>8</td>
174
+ <td>67.414861s</td>
175
+ <td>0.119(1.82x slower)</td>
176
+ </tr>
177
+ <tr>
178
+ <td>Fibonacci</td>
179
+ <td>7</td>
180
+ <td>61.067436</td>
181
+ <td>0.115(1.88x slower)</td>
182
+ </tr>
183
+ <tr>
184
+ <th colspan="4">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]</th>
185
+ </tr>
186
+ <tr>
187
+ <th>Library</th>
188
+ <th>Iterations</th>
189
+ <th>Seconds</th>
190
+ <th>Iterations per second</th>
191
+ </tr>
192
+ <tr>
193
+ <td>pairing_heap</td>
194
+ <td>16</td>
195
+ <td>62.519677</td>
196
+ <td>0.256</td>
197
+ </tr>
198
+ <tr>
199
+ <td>lazy_priority_queue</td>
200
+ <td>13</td>
201
+ <td>63.832733</td>
202
+ <td>0.204(1.26x slower)</td>
203
+ </tr>
204
+ <tr>
205
+ <td>Fibonacci</td>
206
+ <td>8</td>
207
+ <td>60.250658</td>
208
+ <td>0.133(1.93x slower)</td>
209
+ </tr>
210
+ </table>
211
+
212
+ ### Stress test with changing priority(N = 10) [source code](./test/performance_with_change_priority.rb)
213
+ 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.
214
+ <table>
215
+ <tr>
216
+ <th colspan="4">ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20]</th>
217
+ </tr>
218
+ <tr>
219
+ <th>Library</th>
220
+ <th>Iterations per second</th>
221
+ </tr>
222
+ <tr>
223
+ <td>pairing_heap</td>
224
+ <td>5991.2</td>
225
+ </tr>
226
+ <tr>
227
+ <td>Fibonacci</td>
228
+ <td>3803.5(1.58x slower)</td>
229
+ </tr>
230
+ <tr>
231
+ <td>lazy_priority_queue</td>
232
+ <td>3681.9(1.64x slower)</td>
233
+ </tr>
234
+ <tr>
235
+ <th colspan="4">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]</th>
236
+ </tr>
237
+ <tr>
238
+ <th>Library</th>
239
+ <th>Iterations per second</th>
240
+ </tr>
241
+ <tr>
242
+ <td>pairing_heap</td>
243
+ <td>6784.3</td>
244
+ </tr>
245
+ <tr>
246
+ <td>lazy_priority_queue</td>
247
+ <td>6044.5(1.12x slower)</td>
248
+ </tr>
249
+ <tr>
250
+ <td>Fibonacci</td>
251
+ <td>4070.5(1.67x slower)</td>
252
+ </tr>
253
+ </table>
254
+
255
+ ### Dijkstra's algorithm with RGL [source code](./test/performance_rgl.rb)
256
+ <table>
257
+ <tr>
258
+ <th colspan="4">ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20]</th>
259
+ </tr>
260
+ <tr>
261
+ <th>Library</th>
262
+ <th>Iterations</th>
263
+ <th>Seconds</th>
264
+ <th>Iterations per second</th>
265
+ </tr>
266
+ <tr>
267
+ <td>pairing_heap</td>
268
+ <td>7</td>
269
+ <td>64.768526</td>
270
+ <td>0.108</td>
271
+ </tr>
272
+ <tr>
273
+ <td>lazy_priority_queue</td>
274
+ <td>6</td>
275
+ <td>63.278091</td>
276
+ <td>0.095(1.14x slower)</td>
277
+ </tr>
278
+ <tr>
279
+ <td>Fibonacci</td>
280
+ <td>6</td>
281
+ <td>65.898081</td>
282
+ <td>0.091(1.19x slower)</td>
283
+ </tr>
284
+ <tr>
285
+ <th colspan="4">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]</th>
286
+ </tr>
287
+ <tr>
288
+ <th>Library</th>
289
+ <th>Iterations</th>
290
+ <th>Seconds</th>
291
+ <th>Iterations per second</th>
292
+ </tr>
293
+ <tr>
294
+ <td>pairing_heap</td>
295
+ <td>12</td>
296
+ <td>60.277567</td>
297
+ <td>0.199</td>
298
+ </tr>
299
+ <tr>
300
+ <td>lazy_priority_queue</td>
301
+ <td>12</td>
302
+ <td>61.238395</td>
303
+ <td>0.196(1.02x slower)</td>
304
+ </tr>
305
+ <tr>
306
+ <td>Fibonacci</td>
307
+ <td>10</td>
308
+ <td>62.687378</td>
309
+ <td>0.160(1.25x slower)</td>
310
+ </tr>
311
+ </table>
312
+
313
+ ### Simple Dijkstra's algorithm implementation [source code](./test/performance_dijkstra.rb)
314
+ <table>
315
+ <tr>
316
+ <th colspan="4">ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20]</th>
317
+ </tr>
318
+ <tr>
319
+ <th>Library</th>
320
+ <th>Iterations</th>
321
+ <th>Seconds</th>
322
+ <th>Iterations per second</th>
323
+ </tr>
324
+ <tr>
325
+ <td>pairing_heap</td>
326
+ <td>20</td>
327
+ <td>60.028380</td>
328
+ <td>0.334</td>
329
+ </tr>
330
+ <tr>
331
+ <td>Fibonacci</td>
332
+ <td>10</td>
333
+ <td>64.471303</td>
334
+ <td>0.155(2.14x slower)</td>
335
+ </tr>
336
+ <tr>
337
+ <td>lazy_priority_queue</td>
338
+ <td>9</td>
339
+ <td>65.986618</td>
340
+ <td>0.136(2.45x slower)</td>
341
+ </tr>
342
+ <tr>
343
+ <th colspan="4">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]</th>
344
+ </tr>
345
+ <tr>
346
+ <th>Library</th>
347
+ <th>Iterations</th>
348
+ <th>Seconds</th>
349
+ <th>Iterations per second</th>
350
+ </tr>
351
+ <tr>
352
+ <td>pairing_heap</td>
353
+ <td>21</td>
354
+ <td>61.727259</td>
355
+ <td>0.340</td>
356
+ </tr>
357
+ <tr>
358
+ <td>lazy_priority_queue</td>
359
+ <td>14</td>
360
+ <td>63.436863</td>
361
+ <td>0.221(1.54x slower)</td>
362
+ </tr>
363
+ <tr>
364
+ <td>Fibonacci</td>
365
+ <td>10</td>
366
+ <td>62.447662</td>
367
+ <td>0.160(2.12x slower)</td>
368
+ </tr>
369
+ </table>
370
+
371
+ ### Summary
372
+ <table>
373
+ <tr>
374
+ <th colspan="4">ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20]</th>
375
+ </tr>
376
+ <tr>
377
+ <th>Library</th>
378
+ <th>Slower geometric mean</th>
379
+ </tr>
380
+ <tr>
381
+ <td>pairing_heap</td>
382
+ <td>1</td>
383
+ </tr>
384
+ <tr>
385
+ <td>Fibonacci</td>
386
+ <td>1.720x slower</td>
387
+ </tr>
388
+ <tr>
389
+ <td>lazy_priority_queue</td>
390
+ <td>1.721x slower</td>
391
+ </tr>
392
+ <tr>
393
+ <th colspan="4">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]</th>
394
+ </tr>
395
+ <tr>
396
+ <th>Library</th>
397
+ <th>Slower geometric mean</th>
398
+ </tr>
399
+ <tr>
400
+ <td>pairing_heap</td>
401
+ <td>1</td>
402
+ </tr>
403
+ <tr>
404
+ <td>lazy_priority_queue</td>
405
+ <td>1.23x slower</td>
406
+
407
+ </tr>
408
+ <tr>
409
+ <td>Fibonacci</td>
410
+ <td>1.78x slower</td>
411
+ </tr>
412
+ </table>
413
+
414
+ ## Development
415
+
416
+ 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.
417
+
418
+ 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).
419
+
420
+ ## Contributing
421
+
422
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mhib/pairing_heap.
423
+
424
+ ## License
425
+
426
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: %i[test]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "pairing_heap"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PairingHeap
4
+ # Pairing heap data structure implementation
5
+ # @see https://en.wikipedia.org/wiki/Pairing_heap
6
+ class PairingHeap
7
+ class Node
8
+ attr_accessor :elem, :priority, :subheaps, :parent, :prev_sibling, :next_sibling
9
+ def initialize(elem, priority, subheaps, parent, prev_sibling, next_sibling)
10
+ @elem = elem
11
+ @priority = priority
12
+ @subheaps = subheaps
13
+ @parent = parent
14
+ @prev_sibling = prev_sibling
15
+ @next_sibling = next_sibling
16
+ end
17
+ end
18
+ private_constant :Node
19
+
20
+ # @param &block Optional heap property priority comparator. `<:=.to_proc` by default
21
+ def initialize(&block)
22
+ @root = nil
23
+ @nodes = {}
24
+ @order = block || :<=.to_proc
25
+ end
26
+
27
+ # Pushes element to the heap.
28
+ # Time Complexity: O(1)
29
+ # @param elem Element to be pushed
30
+ # @param priority Priority of the element
31
+ # @raise [ArgumentError] if the element is already in the heap
32
+ # @return [PairingHeap]
33
+ def push(elem, priority)
34
+ raise ArgumentError, "Element already in the heap" if @nodes.key?(elem)
35
+
36
+ node = Node.new(elem, priority, nil, nil, nil, nil)
37
+ @nodes[elem] = node
38
+ @root = meld(@root, node)
39
+ self
40
+ end
41
+ alias enqueue push
42
+
43
+ # Returns the element at the top of the heap
44
+ # Time Complexity: O(1)
45
+ def peek
46
+ @root&.elem
47
+ end
48
+
49
+ # Time Complexity: O(1)
50
+ # @return [Boolean]
51
+ def empty?
52
+ @root.nil?
53
+ end
54
+
55
+ # Time Complexity: O(1)
56
+ # @return [Boolean]
57
+ def any?
58
+ !@root.nil?
59
+ end
60
+
61
+ # Time Complexity: O(1)
62
+ # @return [Integer]
63
+ def size
64
+ @nodes.size
65
+ end
66
+ alias length size
67
+
68
+ # Removes element from the top of the heap
69
+ # Time Complexity: O(N)
70
+ # Amortized time Complexity: O(log(N))
71
+ # @raise [ArgumEntError] if the heap is empty
72
+ # @return [PairingHeap]
73
+ def pop
74
+ raise ArgumentError, "Cannot remove from an empty heap" if @root.nil?
75
+
76
+ elem = @root.elem
77
+ @nodes.delete(elem)
78
+ @root = merge_pairs(@root.subheaps)
79
+ if @root
80
+ @root.parent = nil
81
+ @root.next_sibling = nil
82
+ @root.prev_sibling = nil
83
+ end
84
+ elem
85
+ end
86
+ alias dequeue pop
87
+
88
+ # Changes a priority of element to a more prioritary one
89
+ # Time Complexity: O(1)
90
+ # Amortized Time Complexity: o(log(N))
91
+ # @param elem Element
92
+ # @param priority New priority
93
+ # @raise [ArgumentError] if the element heap is not in heap or the new priority is less prioritary
94
+ # @return [PairingHeap]
95
+ def change_priority(elem, priority)
96
+ node = @nodes[elem]
97
+ raise ArgumentError, "Provided element is not in heap" if node.nil?
98
+ unless @order[priority, node.priority]
99
+ raise ArgumentError, "Priority cannot be changed to a less prioritary value."
100
+ end
101
+
102
+ node.priority = priority
103
+ return if node.parent.nil?
104
+ return if @order[node.parent.priority, node.priority]
105
+
106
+ remove_from_parents_list(node)
107
+ @root = meld(node, @root)
108
+ @root.parent = nil
109
+ self
110
+ end
111
+
112
+ # Removes element from the top of the heap
113
+ # Time Complexity: O(N)
114
+ # Amortized Time Complexity: O(log(N))
115
+ # @raise [ArgumentError] if the element heap is not in heap
116
+ # @return [PairingHeap]
117
+ def delete(elem)
118
+ node = @nodes[elem]
119
+ raise ArgumentError, "Provided element is not in heap" if node.nil?
120
+
121
+ @nodes.delete(elem)
122
+ if node.parent.nil?
123
+ @root = merge_pairs(node.subheaps)
124
+ else
125
+ remove_from_parents_list(node)
126
+ new_heap = merge_pairs(node.subheaps)
127
+ if new_heap
128
+ new_heap.prev_sibling = nil
129
+ new_heap.next_sibling = nil
130
+ end
131
+ @root = meld(new_heap, @root)
132
+ end
133
+ @root&.parent = nil
134
+ self
135
+ end
136
+
137
+ private
138
+
139
+ def remove_from_parents_list(node)
140
+ if node.prev_sibling
141
+ node.prev_sibling.next_sibling = node.next_sibling
142
+ node.next_sibling.prev_sibling = node.prev_sibling if node.next_sibling
143
+ elsif node.parent.subheaps.equal?(node)
144
+ node.parent.subheaps = node.next_sibling
145
+ node.next_sibling.prev_sibling = nil if node.next_sibling
146
+ elsif node.next_sibling
147
+ node.next_sibling.prev_sibling = nil
148
+ end
149
+ node.prev_sibling = nil
150
+ node.next_sibling = nil
151
+ end
152
+
153
+ def meld(left, right)
154
+ return right if left.nil?
155
+ return left if right.nil?
156
+
157
+ if @order[left.priority, right.priority]
158
+ parent = left
159
+ child = right
160
+ else
161
+ parent = right
162
+ child = left
163
+ end
164
+ child.next_sibling = parent.subheaps
165
+ parent.subheaps = child
166
+ child.next_sibling.prev_sibling = child if child.next_sibling
167
+ child.prev_sibling = nil
168
+ child.parent = parent
169
+ parent
170
+ end
171
+
172
+ # Non-recursive implementation of method described in https://en.wikipedia.org/wiki/Pairing_heap#delete-min
173
+ def merge_pairs(heaps)
174
+ return nil if heaps.nil?
175
+ return heaps if heaps.next_sibling.nil?
176
+
177
+ # [H1, H2, H3, H4, H5, H6, H7] => [H1H2, H3H4, H5H6, H7]
178
+ stack = []
179
+ current = heaps
180
+ while current
181
+ prev = current
182
+ current = current.next_sibling
183
+ unless current
184
+ stack << prev
185
+ break
186
+ end
187
+ next_val = current.next_sibling
188
+ stack << meld(prev, current)
189
+ current = next_val
190
+ end
191
+
192
+ # [H1H2, H3H4, H5H6, H7]
193
+ # [H1H2, H3H4, H5H67]
194
+ # [H1H2, H3H45H67]
195
+ # [H1H2H3H45H67]
196
+ # return H1H2H3H45H67
197
+ while true
198
+ right = stack.pop
199
+ return right if stack.empty?
200
+
201
+ left = stack.pop
202
+ stack << meld(left, right)
203
+ end
204
+ end
205
+ end
206
+
207
+ # Priority queue where the smallest priority is the most prioritary
208
+ class MinPriorityQueue < PairingHeap
209
+ def initialize
210
+ super(&:<=)
211
+ end
212
+
213
+ alias decrease_key change_priority
214
+ alias min peek
215
+ alias extract_min dequeue
216
+ end
217
+
218
+ # Priority queue where the highest priority is the most prioritary
219
+ class MaxPriorityQueue < PairingHeap
220
+ def initialize
221
+ super(&:>=)
222
+ end
223
+
224
+ alias increase_key change_priority
225
+ alias max peek
226
+ alias extract_max dequeue
227
+ end
228
+
229
+ # Priority queue with change_priority, that accepts changing to a less prioritary priority
230
+ class SafeChangePriorityQueue < PairingHeap
231
+ # Changes a priority of the element to a more prioritary one
232
+ # Time Complexity: O(N)
233
+ # Amortized Time Complexity: O(log(N))
234
+ # @raise [ArgumentError] if the element heap is not in the heap
235
+ # @return [PairingHeap]
236
+ def change_priority(elem, priority)
237
+ raise ArgumentError, "Provided element is not in heap" unless @nodes.key?(elem)
238
+ if !@order[priority, @nodes[elem].priority]
239
+ delete(elem)
240
+ push(elem, priority)
241
+ else
242
+ super(elem, priority)
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PairingHeap
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/pairing_heap/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "pairing_heap"
7
+ spec.version = PairingHeap::VERSION
8
+ spec.authors = ["Marcin Henryk Bartkowiak"]
9
+ spec.email = ["mhbartkowiak@gmail.com"]
10
+
11
+ spec.summary = "Performant priority queue in pure Ruby with support for changing priority"
12
+ spec.description = "Performant priority queue in pure Ruby with support for changing priority using pairing heap data structure"
13
+ spec.homepage = "https://github.com/mhib/pairing_heap"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ # Uncomment to register a new dependency of your gem
30
+ # spec.add_dependency "example-gem", "~> 1.0"
31
+
32
+ spec.add_development_dependency "minitest", "~> 5.0"
33
+ spec.add_development_dependency "rake", "~> 13.0"
34
+
35
+ # For more information and examples about making a new gem, checkout our
36
+ # guide at: https://bundler.io/guides/creating_gem.html
37
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pairing_heap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Marcin Henryk Bartkowiak
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-02-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ description: Performant priority queue in pure Ruby with support for changing priority
42
+ using pairing heap data structure
43
+ email:
44
+ - mhbartkowiak@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".github/workflows/main.yml"
50
+ - ".gitignore"
51
+ - ".rubocop.yml"
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - bin/console
58
+ - bin/setup
59
+ - lib/pairing_heap.rb
60
+ - lib/pairing_heap/version.rb
61
+ - pairing_heap.gemspec
62
+ homepage: https://github.com/mhib/pairing_heap
63
+ licenses:
64
+ - MIT
65
+ metadata:
66
+ homepage_uri: https://github.com/mhib/pairing_heap
67
+ source_code_uri: https://github.com/mhib/pairing_heap
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 2.3.0
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.2.3
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: Performant priority queue in pure Ruby with support for changing priority
87
+ test_files: []