pairing_heap 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []