Skip to content

Commit

Permalink
Add Array#bsearch
Browse files Browse the repository at this point in the history
  • Loading branch information
suzukaze committed Apr 24, 2014
1 parent 0d9c872 commit c81b983
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 0 deletions.
82 changes: 82 additions & 0 deletions mrbgems/mruby-array-ext/mrblib/array.rb
Expand Up @@ -509,4 +509,86 @@ def insert(idx, *args)
self[idx, 0] = args
self
end

##
# call-seq:
# ary.bsearch {|x| block } -> elem
#
# By using binary search, finds a value from this array which meets
# the given condition in O(log n) where n is the size of the array.
#
# You can use this method in two use cases: a find-minimum mode and
# a find-any mode. In either case, the elements of the array must be
# monotone (or sorted) with respect to the block.
#
# In find-minimum mode (this is a good choice for typical use case),
# the block must return true or false, and there must be an index i
# (0 <= i <= ary.size) so that:
#
# - the block returns false for any element whose index is less than
# i, and
# - the block returns true for any element whose index is greater
# than or equal to i.
#
# This method returns the i-th element. If i is equal to ary.size,
# it returns nil.
#
# ary = [0, 4, 7, 10, 12]
# ary.bsearch {|x| x >= 4 } #=> 4
# ary.bsearch {|x| x >= 6 } #=> 7
# ary.bsearch {|x| x >= -1 } #=> 0
# ary.bsearch {|x| x >= 100 } #=> nil
#
# In find-any mode (this behaves like libc's bsearch(3)), the block
# must return a number, and there must be two indices i and j
# (0 <= i <= j <= ary.size) so that:
#
# - the block returns a positive number for ary[k] if 0 <= k < i,
# - the block returns zero for ary[k] if i <= k < j, and
# - the block returns a negative number for ary[k] if
# j <= k < ary.size.
#
# Under this condition, this method returns any element whose index
# is within i...j. If i is equal to j (i.e., there is no element
# that satisfies the block), this method returns nil.
#
# ary = [0, 4, 7, 10, 12]
# # try to find v such that 4 <= v < 8
# ary.bsearch {|x| 1 - (x / 4).truncate } #=> 4 or 7
# # try to find v such that 8 <= v < 10
# ary.bsearch {|x| 4 - (x / 2).truncate } #=> nil
#
# You must not mix the two modes at a time; the block must always
# return either true/false, or always return a number. It is
# undefined which value is actually picked up at each iteration.

def bsearch(&block)
return to_enum :bsearch unless block_given?

low = 0
high = self.size
satisfied = false
while low < high
mid = low + ((high - low) / 2).truncate
val = self[mid]
v = block.call(val)
if v.is_a?(Integer)
return val if v == 0
smaller = v < 0
elsif v == true
satisfied = true
smaller = true
elsif v == false || v == nil
smaller = false
end
if smaller
high = mid
else
low = mid + 1
end
end
return nil if low == self.size
return nil unless satisfied
self[low]
end
end
16 changes: 16 additions & 0 deletions mrbgems/mruby-array-ext/test/array.rb
Expand Up @@ -206,3 +206,19 @@
b = ["a", "b", "c", "d"]
assert_equal ["a", "b", "c", "d", nil, nil, 99], b.insert(6, 99)
end

assert("Array#bsearch") do
# Find minimum mode
a = [0, 4, 7, 10, 12]
assert_include [4, 7], a.bsearch {|x| x >= 4 }
assert_equal 7, a.bsearch {|x| x >= 6 }
assert_equal 0, a.bsearch {|x| x >= -1 }
assert_nil a.bsearch {|x| x >= 100 }

# Find any mode
a = [0, 4, 7, 10, 12]
assert_include [4, 7], a.bsearch {|x| 1 - (x / 4).truncate }
assert_nil a.bsearch {|x| 4 - (x / 2).truncate }
assert_equal(nil, a.bsearch {|x| 1 })
assert_equal(nil, a.bsearch {|x| -1 })
end

0 comments on commit c81b983

Please sign in to comment.