d_heap 0.3.0 → 0.4.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 +4 -4
- data/.rubocop.yml +30 -1
- data/CHANGELOG.md +42 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +11 -10
- data/README.md +353 -121
- data/benchmarks/push_n.yml +28 -0
- data/benchmarks/push_n_pop_n.yml +31 -0
- data/benchmarks/push_pop.yml +24 -0
- data/bin/bench_n +7 -0
- data/bin/benchmark-driver +29 -0
- data/bin/benchmarks +10 -0
- data/bin/profile +10 -0
- data/d_heap.gemspec +2 -1
- data/docs/benchmarks-2.txt +52 -0
- data/docs/benchmarks.txt +443 -0
- data/docs/profile.txt +392 -0
- data/ext/d_heap/d_heap.c +428 -150
- data/ext/d_heap/d_heap.h +6 -3
- data/ext/d_heap/extconf.rb +8 -3
- data/lib/benchmark_driver/runner/ips_zero_fail.rb +120 -0
- data/lib/d_heap.rb +5 -3
- data/lib/d_heap/benchmarks.rb +111 -0
- data/lib/d_heap/benchmarks/benchmarker.rb +113 -0
- data/lib/d_heap/benchmarks/implementations.rb +168 -0
- data/lib/d_heap/benchmarks/profiler.rb +71 -0
- data/lib/d_heap/benchmarks/rspec_matchers.rb +374 -0
- data/lib/d_heap/version.rb +1 -1
- metadata +34 -3
data/ext/d_heap/d_heap.h
CHANGED
@@ -11,6 +11,12 @@
|
|
11
11
|
// comparisons as d gets further from 4.
|
12
12
|
#define DHEAP_MAX_D 32
|
13
13
|
|
14
|
+
#define DHEAP_DEFAULT_SIZE 16
|
15
|
+
#define DHEAP_MAX_SIZE (LONG_MAX / (int)sizeof(long double))
|
16
|
+
|
17
|
+
// 10MB
|
18
|
+
#define DHEAP_CAPA_INCR_MAX (10 * 1024 * 1024 / (int)sizeof(long double))
|
19
|
+
|
14
20
|
VALUE rb_cDHeap;
|
15
21
|
|
16
22
|
// copied from pg gem
|
@@ -26,9 +32,6 @@ VALUE rb_cDHeap;
|
|
26
32
|
#define dheap_gc_location(x) UNUSED(x)
|
27
33
|
#endif
|
28
34
|
|
29
|
-
// from internal/compar.h
|
30
|
-
#define STRING_P(s) (RB_TYPE_P((s), T_STRING) && CLASS_OF(s) == rb_cString)
|
31
|
-
|
32
35
|
#ifdef __D_HEAP_DEBUG
|
33
36
|
#define debug(v) { \
|
34
37
|
ID sym_puts = rb_intern("puts"); \
|
data/ext/d_heap/extconf.rb
CHANGED
@@ -2,10 +2,15 @@
|
|
2
2
|
|
3
3
|
require "mkmf"
|
4
4
|
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# end
|
5
|
+
# For testing in CI (because I don't otherwise have easy access to Mac OS):
|
6
|
+
# $CFLAGS << " -D__D_HEAP_DEBUG" if /darwin/ =~ RUBY_PLATFORM
|
8
7
|
|
9
8
|
have_func "rb_gc_mark_movable" # since ruby-2.7
|
10
9
|
|
10
|
+
check_sizeof("long")
|
11
|
+
check_sizeof("unsigned long long")
|
12
|
+
check_sizeof("long double")
|
13
|
+
have_macro("LDBL_MANT_DIG", "float.h")
|
14
|
+
|
15
|
+
CONFIG["warnflags"] << " -Werror"
|
11
16
|
create_makefile("d_heap/d_heap")
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English" # $CHILD_STATUS
|
4
|
+
require "timeout" # Timeout::Error
|
5
|
+
|
6
|
+
require "benchmark_driver"
|
7
|
+
|
8
|
+
# monkey-patch to convert miniscule values to 0.0
|
9
|
+
class BenchmarkDriver::Output::Compare
|
10
|
+
|
11
|
+
# monkey-patch to convert miniscule values to 0.0
|
12
|
+
module MinisculeToZero
|
13
|
+
|
14
|
+
def humanize(value, width = 10)
|
15
|
+
value <= 0.0.next_float.next_float ? 0.0 : super(value, width)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
prepend MinisculeToZero
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
# A simple patch to let slow specs error out without
|
25
|
+
class BenchmarkDriver::Runner::IpsZeroFail < BenchmarkDriver::Runner::Ips
|
26
|
+
METRIC = BenchmarkDriver::Runner::Ips::METRIC
|
27
|
+
|
28
|
+
# always run at least once
|
29
|
+
class Job < BenchmarkDriver::DefaultJob
|
30
|
+
attr_accessor :warmup_value, :warmup_duration, :warmup_loop_count
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
# BenchmarkDriver::Runner looks for this class
|
35
|
+
JobParser = BenchmarkDriver::DefaultJobParser.for(klass: Job, metrics: [METRIC])
|
36
|
+
|
37
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/BlockLength, Layout/LineLength, Layout/SpaceInsideBlockBraces, Style/BlockDelimiters
|
38
|
+
|
39
|
+
# This method is dynamically called by `BenchmarkDriver::JobRunner.run`
|
40
|
+
# @param [Array<BenchmarkDriver::Default::Job>] jobs
|
41
|
+
def run(jobs)
|
42
|
+
if jobs.any? { |job| job.loop_count.nil? }
|
43
|
+
@output.with_warmup do
|
44
|
+
jobs = jobs.map do |job|
|
45
|
+
next job if job.loop_count # skip warmup if loop_count is set
|
46
|
+
|
47
|
+
@output.with_job(name: job.name) do
|
48
|
+
context = job.runnable_contexts(@contexts).first
|
49
|
+
duration, loop_count = run_warmup(job, context: context)
|
50
|
+
value, duration = value_duration(duration: duration, loop_count: loop_count)
|
51
|
+
|
52
|
+
@output.with_context(name: context.name, executable: context.executable, gems: context.gems, prelude: context.prelude) do
|
53
|
+
@output.report(values: { metric => value }, duration: duration, loop_count: loop_count)
|
54
|
+
end
|
55
|
+
|
56
|
+
warmup_loop_count = loop_count
|
57
|
+
|
58
|
+
loop_count = (loop_count.to_f * @config.run_duration / duration).floor
|
59
|
+
Job.new(**job.to_h.merge(loop_count: loop_count))
|
60
|
+
.tap {|j| j.warmup_value = value }
|
61
|
+
.tap {|j| j.warmup_duration = duration }
|
62
|
+
.tap {|j| j.warmup_loop_count = warmup_loop_count }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
.compact
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
@output.with_benchmark do
|
70
|
+
jobs.each do |job|
|
71
|
+
@output.with_job(name: job.name) do
|
72
|
+
job.runnable_contexts(@contexts).each do |context|
|
73
|
+
repeat_params = { config: @config, larger_better: true, rest_on_average: :average }
|
74
|
+
result =
|
75
|
+
if job.loop_count&.positive?
|
76
|
+
loop_count = job.loop_count
|
77
|
+
BenchmarkDriver::Repeater.with_repeat(**repeat_params) do
|
78
|
+
run_benchmark(job, context: context)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
loop_count = job.warmup_loop_count
|
82
|
+
repeater_value = [job.warmup_value, job.warmup_duration]
|
83
|
+
BenchmarkDriver::Repeater::RepeatResult.new(
|
84
|
+
value: repeater_value, all_values: [repeater_value]
|
85
|
+
)
|
86
|
+
end
|
87
|
+
value, duration = result.value
|
88
|
+
@output.with_context(name: context.name, executable: context.executable, gems: context.gems, prelude: context.prelude) do
|
89
|
+
@output.report(
|
90
|
+
values: { metric => value },
|
91
|
+
all_values: { metric => result.all_values },
|
92
|
+
duration: duration,
|
93
|
+
loop_count: loop_count,
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/BlockLength, Layout/LineLength, Layout/SpaceInsideBlockBraces, Style/BlockDelimiters
|
103
|
+
|
104
|
+
def run_warmup(job, context:)
|
105
|
+
start = Time.now
|
106
|
+
super(job, context: context)
|
107
|
+
rescue Timeout::Error
|
108
|
+
[Time.now - start, 0.0.next_float]
|
109
|
+
end
|
110
|
+
|
111
|
+
def execute(*args, exception: true)
|
112
|
+
super
|
113
|
+
rescue RuntimeError => ex
|
114
|
+
if args.include?("timeout") && $CHILD_STATUS&.exitstatus == 124
|
115
|
+
raise Timeout::Error, ex.message
|
116
|
+
end
|
117
|
+
raise ex
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
data/lib/d_heap.rb
CHANGED
@@ -14,9 +14,11 @@ require "d_heap/version"
|
|
14
14
|
# worst-case time complexity.
|
15
15
|
#
|
16
16
|
class DHeap
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
# ruby 3.0+ (2.x can just use inherited initialize_clone)
|
18
|
+
if Object.instance_method(:initialize_clone).arity == -1
|
19
|
+
def initialize_clone(other, freeze: nil)
|
20
|
+
__init_clone__(other, freeze ? true : freeze)
|
21
|
+
end
|
20
22
|
end
|
21
23
|
|
22
24
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "d_heap"
|
4
|
+
require "ostruct"
|
5
|
+
|
6
|
+
# Different benchmark scenarios and implementations to benchmark
|
7
|
+
module DHeap::Benchmarks
|
8
|
+
|
9
|
+
def self.puts_version_info(type = "Benchmark", io = $stdout)
|
10
|
+
io.puts "#{type} run at %s" % [Time.now]
|
11
|
+
io.puts "ruby v%s, DHeap v%s" % [RUBY_VERSION, DHeap::VERSION]
|
12
|
+
io.puts
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubocop:disable Style/NumericPredicate
|
16
|
+
|
17
|
+
# moves "rand" outside the benchmarked code, to avoid measuring that too.
|
18
|
+
module Randomness
|
19
|
+
|
20
|
+
def default_randomness_size; 1_000_000 end
|
21
|
+
|
22
|
+
def fill_random_vals(target_size = default_randomness_size, io: $stdout)
|
23
|
+
@dheap_bm_random_vals ||= []
|
24
|
+
count = target_size - @dheap_bm_random_vals.length
|
25
|
+
return 0 if count <= 0
|
26
|
+
millions = (count / 1_000_000.0).round(3)
|
27
|
+
io&.puts "~~~~~~ filling @dheap_bm_random_vals with #{millions}M ~~~~~~"
|
28
|
+
io&.flush
|
29
|
+
count.times do @dheap_bm_random_vals << rand(0..10_000) end
|
30
|
+
@dheap_bm_random_len = @dheap_bm_random_vals.length
|
31
|
+
@dheap_bm_random_idx = (((@dheap_bm_random_idx || -1) + 1) % @dheap_bm_random_len)
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def random_val
|
36
|
+
@dheap_bm_random_vals.fetch(
|
37
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# different scenarios to be benchmarked or profiled
|
44
|
+
module Scenarios
|
45
|
+
|
46
|
+
def push_n_multiple_queues(count, *queues)
|
47
|
+
while 0 < count
|
48
|
+
value = @dheap_bm_random_vals.fetch(
|
49
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
50
|
+
)
|
51
|
+
queues.each do |queue|
|
52
|
+
queue << value
|
53
|
+
end
|
54
|
+
count -= 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def push_n(queue, count)
|
59
|
+
while 0 < count
|
60
|
+
queue << @dheap_bm_random_vals.fetch(
|
61
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
62
|
+
)
|
63
|
+
count -= 1
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def push_n_then_pop_n(queue, count) # rubocop:disable Metrics/MethodLength
|
68
|
+
i = 0
|
69
|
+
while i < count
|
70
|
+
queue << @dheap_bm_random_vals.fetch(
|
71
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
72
|
+
)
|
73
|
+
i += 1
|
74
|
+
end
|
75
|
+
while 0 < i
|
76
|
+
queue.pop
|
77
|
+
i -= 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def repeated_push_pop(queue, count)
|
82
|
+
while 0 < count
|
83
|
+
queue << @dheap_bm_random_vals.fetch(
|
84
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
85
|
+
)
|
86
|
+
queue.pop
|
87
|
+
count -= 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
include Randomness
|
94
|
+
include Scenarios
|
95
|
+
|
96
|
+
def initq(klass, count = 0)
|
97
|
+
queue = klass.new
|
98
|
+
while 0 < count
|
99
|
+
queue << @dheap_bm_random_vals.fetch(
|
100
|
+
@dheap_bm_random_idx = ((@dheap_bm_random_idx + 1) % @dheap_bm_random_len)
|
101
|
+
)
|
102
|
+
count -= 1
|
103
|
+
end
|
104
|
+
queue
|
105
|
+
end
|
106
|
+
|
107
|
+
# rubocop:enable Style/NumericPredicate
|
108
|
+
|
109
|
+
require "d_heap/benchmarks/implementations"
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "d_heap/benchmarks"
|
4
|
+
|
5
|
+
require "benchmark_driver"
|
6
|
+
require "shellwords"
|
7
|
+
require "English"
|
8
|
+
|
9
|
+
module DHeap::Benchmarks
|
10
|
+
# Benchmarks different implementations with different sizes
|
11
|
+
class Benchmarker
|
12
|
+
include Randomness
|
13
|
+
include Scenarios
|
14
|
+
|
15
|
+
N_COUNTS = [
|
16
|
+
5, # 1 + 4
|
17
|
+
21, # 1 + 4 + 16
|
18
|
+
85, # 1 + 4 + 16 + 64
|
19
|
+
341, # 1 + 4 + 16 + 64 + 256
|
20
|
+
1365, # 1 + 4 + 16 + 64 + 256 + 1024
|
21
|
+
5461, # 1 + 4 + 16 + 64 + 256 + 1024 + 4096
|
22
|
+
21_845, # 1 + 4 + 16 + 64 + 256 + 1024 + 4096 + 16384
|
23
|
+
87_381, # 1 + 4 + 16 + 64 + 256 + 1024 + 4096 + 16384 + 65536
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
attr_reader :time
|
27
|
+
attr_reader :iterations_for_push_pop
|
28
|
+
attr_reader :io
|
29
|
+
|
30
|
+
def initialize(
|
31
|
+
time: Integer(ENV.fetch("BENCHMARK_TIME", 10)),
|
32
|
+
iterations_for_push_pop: 10_000,
|
33
|
+
io: $stdout
|
34
|
+
)
|
35
|
+
@time = time
|
36
|
+
@iterations_for_push_pop = Integer(iterations_for_push_pop)
|
37
|
+
@io = io
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(queue_size: ENV.fetch("BENCHMARK_QUEUE_SIZE", :unset))
|
41
|
+
DHeap::Benchmarks.puts_version_info("Benchmarking")
|
42
|
+
sizes = (queue_size == :unset) ? N_COUNTS : [Integer(queue_size)]
|
43
|
+
sizes.each do |size|
|
44
|
+
benchmark_size(size)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def benchmark_size(size)
|
49
|
+
sep "#", "Benchmarks with N=#{size} (t=#{time}sec/benchmark)", big: true
|
50
|
+
io.puts
|
51
|
+
benchmark_push_n size
|
52
|
+
benchmark_push_n_then_pop_n size
|
53
|
+
benchmark_repeated_push_pop size
|
54
|
+
end
|
55
|
+
|
56
|
+
def benchmark_push_n(queue_size)
|
57
|
+
benchmarking("push N", "push_n", queue_size)
|
58
|
+
end
|
59
|
+
|
60
|
+
def benchmark_push_n_then_pop_n(queue_size)
|
61
|
+
benchmarking("push N then pop N", "push_n_pop_n", queue_size)
|
62
|
+
end
|
63
|
+
|
64
|
+
def benchmark_repeated_push_pop(queue_size)
|
65
|
+
benchmarking(
|
66
|
+
"Push/pop with pre-filled queue (size=N)", "push_pop", queue_size
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# TODO: move somewhere else...
|
73
|
+
def skip_profiling?(queue_size, impl)
|
74
|
+
impl.klass == DHeap::Benchmarks::PushAndResort && 10_000 < queue_size
|
75
|
+
end
|
76
|
+
|
77
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
78
|
+
|
79
|
+
def benchmarking(name, file, size)
|
80
|
+
Bundler.with_unbundled_env do
|
81
|
+
sep "==", "#{name} (N=#{size})"
|
82
|
+
cmd = %W[
|
83
|
+
bin/benchmark-driver
|
84
|
+
--bundler
|
85
|
+
--run-duration 6
|
86
|
+
--timeout 15
|
87
|
+
--runner ips_zero_fail
|
88
|
+
benchmarks/#{file}.yml
|
89
|
+
]
|
90
|
+
env = ENV.to_h.merge(
|
91
|
+
"BENCH_N" => size.to_s,
|
92
|
+
"RUBYLIB" => File.expand_path("../..", __dir__),
|
93
|
+
)
|
94
|
+
system(env, *cmd)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def sep(sep, msg = "", width: 80, big: false)
|
99
|
+
txt = String.new
|
100
|
+
txt += "#{sep * (width / sep.length)}\n" if big
|
101
|
+
txt += sep
|
102
|
+
txt += " #{msg}" if msg && !msg.empty?
|
103
|
+
txt += " " unless big
|
104
|
+
txt += sep * ((width - txt.length) / sep.length) unless big
|
105
|
+
txt += "\n"
|
106
|
+
txt += "#{sep * (width / sep.length)}\n" if big
|
107
|
+
io.print txt
|
108
|
+
end
|
109
|
+
|
110
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DHeap::Benchmarks
|
4
|
+
|
5
|
+
# base class for example priority queues
|
6
|
+
class ExamplePriorityQueue
|
7
|
+
attr_reader :a
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@a = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear
|
14
|
+
@a.clear
|
15
|
+
end
|
16
|
+
|
17
|
+
def empty?
|
18
|
+
@a.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
if ENV["LOG_LEVEL"] == "debug"
|
22
|
+
def dbg(msg)
|
23
|
+
puts "%20s: %p, %p" % [msg, @a.first, (@a[1..-1] || []).each_slice(2).to_a]
|
24
|
+
end
|
25
|
+
else
|
26
|
+
def dbg(msg) nil end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
# The most naive approach--completely unsorted!--is ironically not the worst.
|
32
|
+
class FindMin < ExamplePriorityQueue
|
33
|
+
|
34
|
+
# O(1)
|
35
|
+
def <<(score)
|
36
|
+
raise ArgumentError unless score
|
37
|
+
@a.push score
|
38
|
+
end
|
39
|
+
|
40
|
+
# O(n)
|
41
|
+
def pop
|
42
|
+
return unless (score = @a.min)
|
43
|
+
index = @a.rindex(score)
|
44
|
+
@a.delete_at(index)
|
45
|
+
score
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
# Re-sorting after each insert: this both naive and performs the worst.
|
51
|
+
class Sorting < ExamplePriorityQueue
|
52
|
+
|
53
|
+
# O(n log n)
|
54
|
+
def <<(score)
|
55
|
+
raise ArgumentError unless score
|
56
|
+
@a.push score
|
57
|
+
@a.sort!
|
58
|
+
end
|
59
|
+
|
60
|
+
# O(1)
|
61
|
+
def pop
|
62
|
+
@a.shift
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
# A very simple example priority queue that is implemented with a sorted array.
|
68
|
+
#
|
69
|
+
# It uses Array#bsearch + Array#insert to push new values, and Array#pop to pop
|
70
|
+
# the min value.
|
71
|
+
class BSearch < ExamplePriorityQueue
|
72
|
+
|
73
|
+
# Array#bsearch_index is O(log n)
|
74
|
+
# Array#insert is O(n)
|
75
|
+
#
|
76
|
+
# So this should be O(n).
|
77
|
+
#
|
78
|
+
# In practice though, memcpy has a *very* small constant factor.
|
79
|
+
# And bsearch_index uses *exactly* (log n / log 2) comparisons.
|
80
|
+
def <<(score)
|
81
|
+
raise ArgumentError unless score
|
82
|
+
index = @a.bsearch_index {|other| score > other } || @a.length
|
83
|
+
@a.insert(index, score)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Array#pop is O(1). It updates length without changing capacity or contents.
|
87
|
+
#
|
88
|
+
# No comparisons are necessary.
|
89
|
+
#
|
90
|
+
# shift is usually also O(1) and could be used if it were sorted normally.
|
91
|
+
def pop
|
92
|
+
@a.pop
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
# a very simple pure ruby binary heap
|
98
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
99
|
+
class RbHeap < ExamplePriorityQueue
|
100
|
+
|
101
|
+
def <<(score)
|
102
|
+
raise ArgumentError unless score
|
103
|
+
@a.push(score)
|
104
|
+
# shift up
|
105
|
+
index = @a.size - 1
|
106
|
+
while 0 < index # rubocop:disable Style/NumericPredicate
|
107
|
+
parent_index = (index - 1) / 2
|
108
|
+
break if @a[parent_index] <= @a[index]
|
109
|
+
@a[index] = @a[parent_index]
|
110
|
+
index = parent_index
|
111
|
+
@a[index] = score
|
112
|
+
# check_heap!(index)
|
113
|
+
end
|
114
|
+
self
|
115
|
+
end
|
116
|
+
|
117
|
+
def pop
|
118
|
+
return if @a.empty?
|
119
|
+
popped = @a.first
|
120
|
+
@a[0] = shifting = @a.last
|
121
|
+
@a.pop
|
122
|
+
# shift down
|
123
|
+
index = 0
|
124
|
+
last_index = @a.size - 1
|
125
|
+
while (child_index = index * 2 + 1) <= last_index
|
126
|
+
# select min child
|
127
|
+
if child_index < last_index && @a[child_index + 1] < @a[child_index]
|
128
|
+
child_index += 1
|
129
|
+
end
|
130
|
+
break if @a[index] <= @a[child_index]
|
131
|
+
@a[index] = @a[child_index]
|
132
|
+
index = child_index
|
133
|
+
@a[index] = shifting
|
134
|
+
end
|
135
|
+
popped
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def check_heap!(idx, last = @a.size - 1)
|
141
|
+
pscore = @a[idx]
|
142
|
+
child = idx * 2 + 1
|
143
|
+
if child <= last
|
144
|
+
cscore = check_heap!(child)
|
145
|
+
raise "#{pscore} > #{cscore}" if pscore > cscore
|
146
|
+
end
|
147
|
+
child += 1
|
148
|
+
if child <= last
|
149
|
+
check_heap!(child)
|
150
|
+
cscore = check_heap!(child)
|
151
|
+
raise "#{pscore} > #{cscore}" if pscore > cscore
|
152
|
+
end
|
153
|
+
pscore
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
158
|
+
|
159
|
+
# Different duck-typed priority queue implemenations
|
160
|
+
IMPLEMENTATIONS = [
|
161
|
+
OpenStruct.new(name: " push and resort", klass: Sorting).freeze,
|
162
|
+
OpenStruct.new(name: " find min + del", klass: FindMin).freeze,
|
163
|
+
OpenStruct.new(name: "bsearch + insert", klass: BSearch).freeze,
|
164
|
+
OpenStruct.new(name: "ruby binary heap", klass: RbHeap).freeze,
|
165
|
+
OpenStruct.new(name: "quaternary DHeap", klass: DHeap).freeze,
|
166
|
+
].freeze
|
167
|
+
|
168
|
+
end
|