queap 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: e7e14026a37ca2d8ef67eb8e7310b30333604b5b49f583406117c1a6b2beea94
4
+ data.tar.gz: 37e1be46c16584da9d292270c6ecba8117a58e8ace1ef33e52d1fb89f1974894
5
+ SHA512:
6
+ metadata.gz: d627a6b824ca2b00639024cba91b0d717a5c3a8b0babef20f748e8e80d46ab8cbb4b7b0e85000885244f0160b9e8cb9b1ebfa9a5685ddef55765ff7b92d2799c
7
+ data.tar.gz: e518d807fad71698cc230d134952379f7e844cccc1cb16d96e6028fbe908a8255f08232a94d4fa7dcfa34f2b3735c692c39ec5966d798f8edae72867dd05a716
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Jeffrey Crowell
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,156 @@
1
+ # Queap
2
+
3
+ [![RubyGems](https://img.shields.io/gem/v/queap.svg)](https://rubygems.org/gems/queap)
4
+ [![CI](https://github.com/crowell/queap/actions/workflows/ci.yml/badge.svg)](https://github.com/crowell/queap/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.txt)
6
+
7
+ **Queap** is a pure‑Ruby implementation of the *queap* data structure
8
+ a meld of queue and heap that gives
9
+
10
+ | Operation | Amortised | Worst‑case |
11
+ |------------------|-----------|-----------|
12
+ | `insert(x)` | **O(1)** | O(k)\* |
13
+ | `minimum` | **O(1)** | O(1) |
14
+ | `delete_min` | **O(log n)** | O(log n) |
15
+ | `delete(elem)` | **O(log n)** | O(log n) |
16
+
17
+ \*`k` is the number of items flushed from the buffer list into the tree—
18
+ in steady state this cost is spread out, so the amortised cost per insert is constant.
19
+
20
+ Under the hood Queap combines:
21
+
22
+ * A simple buffer list for **O(1) inserts**.
23
+ * A left‑leaning Red‑Black tree for **log‑time deletions** (isomorphic to a 2‑4 tree).
24
+ * A cached pointer to the list‑minimum so `minimum` never touches the tree.
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ Add to your Gemfile:
31
+
32
+ ```ruby
33
+ gem "queap", "~> 0.2"
34
+ ```
35
+
36
+ or install globally:
37
+
38
+ ```console
39
+ $ gem install queap
40
+ ```
41
+
42
+ Supported Rubies: MRI 3.1 – 3.3, TruffleRuby, JRuby.
43
+
44
+ ---
45
+
46
+ ## Quick start
47
+
48
+ ```ruby
49
+ require "queap"
50
+
51
+ q = Queap.new # same as Queap::Queue.new
52
+ [5, 3, 9, 1].each { |k| q.insert(Queap::Queue::Element.new(key: k)) }
53
+
54
+ q.minimum.key # => 1
55
+ q.delete_min.key # => 1
56
+ q.minimum.key # => 3
57
+ ```
58
+
59
+ ---
60
+
61
+ ## API
62
+
63
+ ```ruby
64
+ q = Queap.new
65
+ elem = q.insert(Queap::Queue::Element.new(key: 42))
66
+ q.size # => 1
67
+
68
+ q.delete(elem) # arbitrary deletion
69
+ q.empty? # => true
70
+ ```
71
+
72
+ **Element** is a tiny struct with a single mandatory field:
73
+
74
+ ```ruby
75
+ Element.new(key: <comparable>)
76
+ ```
77
+
78
+ Anything that implements `<=>` works—numbers, strings, structs, etc.
79
+
80
+ ---
81
+
82
+ ## Performance
83
+
84
+ A micro‑benchmark on Ruby 3.3 (M1 Pro macOS 15):
85
+ ```console
86
+ Benchmarking with n = 1000
87
+ ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [arm64-darwin24]
88
+ Warming up --------------------------------------
89
+ insert 69.000 i/100ms
90
+ minimum 324.493k i/100ms
91
+ delete_min 1.113M i/100ms
92
+ Calculating -------------------------------------
93
+ insert 687.073 (± 2.3%) i/s (1.46 ms/i) - 3.450k in 5.024233s
94
+ minimum 3.242M (± 4.4%) i/s (308.42 ns/i) - 16.225M in 5.015994s
95
+ delete_min 10.982M (± 0.8%) i/s (91.06 ns/i) - 55.651M in 5.067814s
96
+
97
+ Comparison:
98
+ delete_min: 10981987.0 i/s
99
+ minimum: 3242293.1 i/s - 3.39x slower
100
+ insert: 687.1 i/s - 15983.72x slower
101
+ ```
102
+
103
+ Such numbers are competitive with `SortedSet`/`RBTree` gems while giving strictly better
104
+ insertion complexity.
105
+
106
+ Run your own benchmarks:
107
+
108
+ ```console
109
+ $ rake benchmark
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Algorithm background
115
+
116
+ In essence, it keeps a *buffered queue* of recent inserts and a *balanced tree* of older
117
+ items. Deletions that hit the buffer trigger a one‑off flush; otherwise they hit the tree.
118
+
119
+ ---
120
+
121
+ ## Development
122
+
123
+ ```console
124
+ $ git clone https://github.com/crowell/queap.git
125
+ $ cd queap
126
+ $ bundle install
127
+ $ bundle exec rspec # run test suite
128
+ ```
129
+
130
+ RuboCop and RSpec run automatically on every push via GitHub Actions.
131
+
132
+ ---
133
+
134
+ ## Contributing
135
+
136
+ Bug reports and pull requests are welcome on GitHub at
137
+ <https://github.com/crowell/queap>.
138
+
139
+ 1. Fork the project
140
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
141
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
142
+ 4. Push to the branch (`git push origin my-new-feature`)
143
+ 5. Open a pull request
144
+
145
+ Please run `bundle exec rspec && rubocop` before submitting.
146
+
147
+ ---
148
+
149
+ ## License
150
+
151
+ The gem is available as open source under the terms of the
152
+ [MIT License](LICENSE.txt).
153
+
154
+ ---
155
+
156
+ © 2025 Jeffrey Crowell
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+
10
+ desc "Run micro-benchmarks (default n = 1_000)"
11
+ task :benchmark, [:n] do |_, args|
12
+ n = args[:n] || 1_000
13
+ exec "bundle exec ruby benchmark/queap_bench.rb #{n}"
14
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Run with:
4
+ # bundle exec ruby benchmark/queap_bench.rb [n]
5
+ #
6
+ # n defaults to 1_000. Use 1_000_000 for a heavier run.
7
+
8
+ require "benchmark/ips"
9
+ require "queap"
10
+
11
+ N = (ARGV.shift || 1_000).to_i
12
+ STDERR.puts "Benchmarking with n = #{N}"
13
+
14
+ Benchmark.ips do |x|
15
+ # ------------------------------------------------------------------
16
+ x.report("insert") do
17
+ q = Queap.new
18
+ N.times { |i| q.insert(Queap::Queue::Element.new(key: i)) }
19
+ end
20
+
21
+ q_for_min = Queap.new
22
+ N.times { |i| q_for_min.insert(Queap::Queue::Element.new(key: i)) }
23
+ x.report("minimum") { q_for_min.minimum }
24
+
25
+ q_for_del = Queap.new
26
+ N.times { |i| q_for_del.insert(Queap::Queue::Element.new(key: i)) }
27
+ x.report("delete_min") { q_for_del.delete_min }
28
+
29
+ x.compare!
30
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tree"
4
+
5
+ module Queap
6
+ # q = Queap::Queue.new
7
+ # elem = q.insert(42)
8
+ # q.minimum # => elem
9
+ # q.delete_min # => elem
10
+ #
11
+ # Elements are small structs that remember whether they currently sit in the
12
+ # buffer list (O(1) insertion area) or inside the balanced search tree.
13
+ class Queue
14
+ Element = Struct.new(:key, :in_list, :node, keyword_init: true) do
15
+ include Comparable
16
+ def <=>(other) = key <=> other.key
17
+ end
18
+
19
+ attr_reader :size
20
+
21
+ BUFFER_FLUSH_FACTOR = 2 # flush when list grows to n / factor
22
+
23
+ def initialize
24
+ @list = [] # simple dynamic array works fine; we always scan O(k)
25
+ @min_l = nil
26
+ @tree = RBTree.new
27
+ @size = 0
28
+ end
29
+
30
+ # O(1) amortised
31
+ def insert(x)
32
+ raise ArgumentError, "Element must respond to #<=>" unless x.respond_to?(:<=>)
33
+
34
+ x.in_list = true
35
+ @list << x
36
+ @min_l = x if @min_l.nil? || x < @min_l
37
+ @size += 1
38
+ maybe_flush
39
+ x
40
+ end
41
+
42
+ # O(1)
43
+ def minimum
44
+ tmin = @tree.min&.key
45
+ return @min_l if tmin.nil? || (@min_l && @min_l < tmin)
46
+
47
+ tmin
48
+ end
49
+
50
+ # O(log n)
51
+ def delete_min
52
+ m = minimum
53
+ return nil unless m
54
+
55
+ delete(m)
56
+ m
57
+ end
58
+
59
+ # O(log n) if element in tree, otherwise amortised O(k) for the one-off flush
60
+ def delete(element)
61
+ if element.in_list
62
+ flush_buffer!
63
+ # element’s node was set during insert_all
64
+ end
65
+ @tree.delete(element.node)
66
+ @size -= 1
67
+ recompute_min_l if element.equal?(@min_l)
68
+ element
69
+ end
70
+
71
+ def empty? = @size.zero?
72
+
73
+ # ----------------------------------------------------------------------
74
+ private
75
+
76
+ def flush_threshold
77
+ # k ≤ n; we flush when buffer length > n / F.
78
+ (@size / BUFFER_FLUSH_FACTOR).clamp(1, @size)
79
+ end
80
+
81
+ def maybe_flush
82
+ flush_buffer! if @list.length >= flush_threshold
83
+ end
84
+
85
+ def flush_buffer!
86
+ @list.each do |elem|
87
+ next unless elem.in_list
88
+
89
+ elem.in_list = false
90
+ elem.node = @tree.insert(elem)
91
+ end
92
+ @list.clear
93
+ @min_l = nil
94
+ end
95
+
96
+ def recompute_min_l
97
+ @min_l = @list.min
98
+ end
99
+ end
100
+ end
data/lib/queap/tree.rb ADDED
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Queap
4
+ # Internal balanced BST. A classic left-leaning Red–Black tree gives the
5
+ # same asymptotics as a 2-4 tree but is ~40 % less code and copies well to
6
+ # Ruby. Public surface is deliberately tiny: insert, delete(node) and min.
7
+ class RBTree
8
+ Node = Struct.new(:key, :left, :right, :parent, :red, keyword_init: true)
9
+ attr_reader :root
10
+
11
+ # ---- helpers ----------------------------------------------------------
12
+
13
+ def min(node = root)
14
+ return nil unless node
15
+
16
+ node = node.left while node.left
17
+ node
18
+ end
19
+
20
+ def insert(key)
21
+ n = Node.new(key:, red: true)
22
+ y = nil
23
+ x = @root
24
+ while x
25
+ y = x
26
+ x = key < x.key ? x.left : x.right
27
+ end
28
+ n.parent = y
29
+ if y.nil?
30
+ @root = n
31
+ elsif key < y.key
32
+ y.left = n
33
+ else
34
+ y.right = n
35
+ end
36
+ insert_fixup(n)
37
+ n
38
+ end
39
+
40
+ def delete(node)
41
+ return unless node
42
+
43
+ y = node
44
+ y_orig = y.red
45
+ if node.left.nil?
46
+ x = node.right
47
+ transplant(node, node.right)
48
+ elsif node.right.nil?
49
+ x = node.left
50
+ transplant(node, node.left)
51
+ else
52
+ y = min(node.right)
53
+ y_orig = y.red
54
+ x = y.right
55
+ if y.parent.equal?(node)
56
+ x&.parent = y
57
+ else
58
+ transplant(y, y.right)
59
+ y.right = node.right
60
+ y.right.parent = y
61
+ end
62
+ transplant(node, y)
63
+ y.left = node.left
64
+ y.left.parent = y
65
+ y.red = node.red
66
+ end
67
+ delete_fixup(x) unless y_orig
68
+ x
69
+ end
70
+
71
+ # ---- private ----------------------------------------------------------
72
+ private
73
+
74
+ def red?(n) = n&.red
75
+ def black?(n) = !red?(n)
76
+
77
+ def left_rotate(x)
78
+ y = x.right
79
+ x.right = y.left
80
+ y.left&.parent = x
81
+ y.parent = x.parent
82
+ if x.parent.nil?
83
+ @root = y
84
+ elsif x.equal?(x.parent.left)
85
+ x.parent.left = y
86
+ else
87
+ x.parent.right = y
88
+ end
89
+ y.left = x
90
+ x.parent = y
91
+ end
92
+
93
+ def right_rotate(x)
94
+ y = x.left
95
+ x.left = y.right
96
+ y.right&.parent = x
97
+ y.parent = x.parent
98
+ if x.parent.nil?
99
+ @root = y
100
+ elsif x.equal?(x.parent.right)
101
+ x.parent.right = y
102
+ else
103
+ x.parent.left = y
104
+ end
105
+ y.right = x
106
+ x.parent = y
107
+ end
108
+
109
+ def insert_fixup(z)
110
+ while red?(z.parent)
111
+ if z.parent.equal?(z.parent.parent.left)
112
+ y = z.parent.parent.right
113
+ if red?(y)
114
+ z.parent.red = y.red = false
115
+ z.parent.parent.red = true
116
+ z = z.parent.parent
117
+ else
118
+ if z.equal?(z.parent.right)
119
+ z = z.parent
120
+ left_rotate(z)
121
+ end
122
+ z.parent.red = false
123
+ z.parent.parent.red = true
124
+ right_rotate(z.parent.parent)
125
+ end
126
+ else
127
+ y = z.parent.parent.left
128
+ if red?(y)
129
+ z.parent.red = y.red = false
130
+ z.parent.parent.red = true
131
+ z = z.parent.parent
132
+ else
133
+ if z.equal?(z.parent.left)
134
+ z = z.parent
135
+ right_rotate(z)
136
+ end
137
+ z.parent.red = false
138
+ z.parent.parent.red = true
139
+ left_rotate(z.parent.parent)
140
+ end
141
+ end
142
+ end
143
+ @root.red = false
144
+ end
145
+
146
+ def delete_fixup(x)
147
+ # keep going while x is a *real* node, not the root, and black
148
+ while x && x != @root && black?(x)
149
+ if x.equal?(x&.parent&.left)
150
+ w = x&.parent&.right
151
+ unless w # ← new: sibling is nil
152
+ x = x.parent
153
+ next
154
+ end
155
+
156
+ if red?(w)
157
+ w.red = false
158
+ x.parent.red = true
159
+ left_rotate(x.parent)
160
+ w = x.parent.right
161
+ end
162
+
163
+ if black?(w&.left) && black?(w&.right) # ← safe-nav
164
+ w.red = true
165
+ x = x.parent
166
+ else
167
+ if black?(w&.right)
168
+ w.left.red = false if w.left
169
+ w.red = true
170
+ right_rotate(w)
171
+ w = x.parent.right
172
+ end
173
+ w.red = x.parent.red
174
+ x.parent.red = false
175
+ w.right.red = false if w.right
176
+ left_rotate(x.parent)
177
+ x = @root
178
+ end
179
+ else
180
+ w = x&.parent&.left
181
+ unless w # ← new: sibling is nil
182
+ x = x.parent
183
+ next
184
+ end
185
+
186
+ if red?(w)
187
+ w.red = false
188
+ x.parent.red = true
189
+ right_rotate(x.parent)
190
+ w = x.parent.left
191
+ end
192
+
193
+ if black?(w&.right) && black?(w&.left) # ← safe-nav
194
+ w.red = true
195
+ x = x.parent
196
+ else
197
+ if black?(w&.left)
198
+ w.right.red = false if w.right
199
+ w.red = true
200
+ left_rotate(w)
201
+ w = x.parent.left
202
+ end
203
+ w.red = x.parent.red
204
+ x.parent.red = false
205
+ w.left.red = false if w.left
206
+ right_rotate(x.parent)
207
+ x = @root
208
+ end
209
+ end
210
+ end
211
+ x&.red = false
212
+ end
213
+
214
+ def transplant(u, v)
215
+ if u.parent.nil?
216
+ @root = v
217
+ elsif u.equal?(u.parent.left)
218
+ u.parent.left = v
219
+ else
220
+ u.parent.right = v
221
+ end
222
+ v&.parent = u.parent
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Queap
4
+ VERSION = "0.1.0"
5
+ end
data/lib/queap.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "queap/version"
4
+ require_relative "queap/tree"
5
+ require_relative "queap/queue"
6
+
7
+ module Queap
8
+ # you can expose convenience aliases here if you like:
9
+ #
10
+ # Queap.new is nicer than Queap::Queue.new
11
+ #
12
+ def self.new(*args, **kw) = Queue.new(*args, **kw)
13
+ end
data/sig/queap.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Queap
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: queap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeffrey Crowell
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-05-02 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Ruby implementation of the 'queap' data structure
14
+ email:
15
+ - jeff@crowell.biz
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rspec"
21
+ - LICENSE.txt
22
+ - README.md
23
+ - Rakefile
24
+ - benchmark/queap_bench.rb
25
+ - lib/queap.rb
26
+ - lib/queap/queue.rb
27
+ - lib/queap/tree.rb
28
+ - lib/queap/version.rb
29
+ - sig/queap.rbs
30
+ homepage: https://github.com/crowell/queap
31
+ licenses:
32
+ - MIT
33
+ metadata:
34
+ homepage_uri: https://github.com/crowell/queap
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 3.1.0
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.5.16
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: A doubly-ended priority queue (Queap) with O(1) amortised insert.
54
+ test_files: []