I've just completed this binary search kata - which has some weird requirements - and I spent a little while "cleaning" up my code and making it more compact. I would appreciate some feedback on the readability of the BinarySearch
class. I've packed a lot of logic into some of the lines and I'd like to know if that makes it more or less readable.
A Ruby Kata for Binary Search of Arrays:
Binary Search Kata
Create a binary search class that is initialized with an array of integers
- detail: Do not use built in array functions
- detail: The initial array is [1,3]
- detail: return the index of the search value if found
- example: searching for 3 returns 1
completed (Y|n):
Successfully deal with values that are not in the data set
- example: searching for 5 indicates to the caller the value is not found
completed (Y|n):
Binary Search handles an odd number of elements in the data set
- detail: Allows the data set to be redefined with [1,3,5,7,9]
- example: Searching for 5 returns 2
- example: Searching for 7 returns 3
completed (Y|n):
Handles more than trivial sized data set
- detail: consider a data set of the first 10000 integers
- example: Searching for 899 returns 898
completed (Y|n):
Supports duplicate elements in the data set
- detail: Use [1,3,5,5,7,9] as the data set
- example: Searching for 3 returns 1
- example: Searching for 5 returns 2
completed (Y|n):
Handles expired call for duplicate elements
- detail: Use [1,3,5,5,7,9] as the data set
- example: Third call to search for 5 is handled like missing element
completed (Y|n):
Supports sorted array of strings
- detail: Use ["a", "b", "c", "d"] as the data set
- example: Searching for "c" returns 2
completed (Y|n):
Supports duplicate strings
- detail: Use ["a", "b", "c", "c", "d"] as the data set
- example: First search for "c" returns 2
- example: Second search for "c" returns 3
completed (Y|n):
Handles expired call for duplicate string elements
- detail: Use ["a", "b", "c", "c", "d"] as the data set
- example: Third call to search for "c" is handled like missing element
completed (Y|n):
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Requirement ┃ Time ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╊━━━━━━━━━━┫
┃ Create a binary search class that is initialized with an array of integers ┃ 00:04:02 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╊━━━━━━━━━━┫
┃ Successfully deal with values that are not in the data set ┃ 00:04:09 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╊━━━━━━━━━━┫
┃ Binary Search handles an odd number of elements in the data set ┃ 00:10:49 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╊━━━━━━━━━━┫
┃ Handles more than trivial sized data set ┃ 00:02:28 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╊━━━━━━━━━━┫
┃ Supports duplicate elements in the data set ┃ 00:05:17 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╊━━━━━━━━━━┫
┃ Handles expired call for duplicate elements ┃ 00:10:35 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╊━━━━━━━━━━┫
┃ Supports sorted array of strings ┃ 00:02:27 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╊━━━━━━━━━━┫
┃ Supports duplicate strings ┃ 00:02:35 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╊━━━━━━━━━━┫
┃ Handles expired call for duplicate string elements ┃ 00:00:57 ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━┛
Questions:
- What about supporting duplicate data changed your solution?
- Did your approach get better or worse after dealing with duplicates?
- What was the hardest part about adding support for strings?
- Can you think of a different technique for building the kata?
- What can you say about the relative merits of the various techniques you chose?
- Which is the most likely to make it into production code?
- Which was the most fun to write?
- Which requirement was the hardest to get working?
- For all of the above, ask yourself why?
My Specs:
1 require 'spec_helper'
2 require 'binary_search'
3 require 'its'
4
5 describe BinarySearch do
6 subject(:binary_search) { BinarySearch.new(data) }
7 let(:data) { [1,3] }
8
9 describe "#initialize" do
10 it "instantiates" do
11 expect { binary_search }.to_not raise_exception
12 end
13 end
14
15 describe '.find' do
16 its(:find, 3) { should eq(1) }
17 its(:find, 5) { should be_nil }
18
19 context 'odd numbers of elements in data set' do
20 let(:data) { [1,3,5,7,9] }
21 its(:find, 0) { should be_nil }
22 its(:find, 1) { should eq(0) }
23 its(:find, 2) { should be_nil }
24 its(:find, 3) { should eq(1) }
25 its(:find, 4) { should be_nil }
26 its(:find, 5) { should eq(2) }
27 its(:find, 6) { should be_nil }
28 its(:find, 7) { should eq(3) }
29 its(:find, 8) { should be_nil }
30 its(:find, 9) { should eq(4) }
31 its(:find, 10) { should be_nil }
32 end
33
34 context 'non trivial data sets' do
35 let(:data) { [] }
36 before { for i in 1..10000 do data << i end }
37 its(:find, 899) { should eq(898) }
38 end
39
40 context 'duplicate data elements' do
41 let(:data) { [1,3,5,5,7,9] }
42 its(:find, 3) { should eq(1) }
43 its(:find, 5) { should eq(2) }
44 it 'handles expired calls for duplicate elements' do
45 2.times do binary_search.find(5) end
46 binary_search.find(5).should be_nil
47 end
48 end
49
50 context 'sorted array of strings' do
51 let(:data) { ['a','b','c','d'] }
52 its(:find, 'a') { should eq(0) }
53 its(:find, 'b') { should eq(1) }
54 its(:find, 'c') { should eq(2) }
55 its(:find, 'd') { should eq(3) }
56 its(:find, 'x') { should be_nil }
57 end
58
59 context 'duplicate strings' do
60 let(:data) { ['a', 'b', 'c', 'c', 'd'] }
61 its(:find, 'c') { should eq(2) }
62 it 'handles multiple searches' do
63 binary_search.find('c')
64 binary_search.find('c').should eq(3)
65 end
66 it 'handles expired calls for strings' do
67 2.times do binary_search.find('c') end
68 binary_search.find('c').should be_nil
69 end
70 end
71 end
72 end
And, the BinarySearch
Class:
1 class BinarySearch
2 def initialize(data)
3 @data = data
4 @search_hash = Hash.new
5 end
6
7 def find(target)
8 return @search_hash[target] = first_find(target) unless @search_hash[target]
9 return @data[@search_hash[target] + 1] == target ? @search_hash[target] += 1 : nil
10 end
11
12 private
13
14 def first_find(target)
15 return nil unless pivot = bsearch(target, @data)
16 pivot -= 1 while @data[pivot - 1] == target
17 return pivot
18 end
19
20 def bsearch(target, data)
21 pivot = data.length / 2
22
23 return pivot if data[pivot] == target
24 return nil if pivot == 0
25
26 if data[pivot] > target
27 return bsearch(target, data[0..pivot - 1])
28 else
29 offset = bsearch(target, data[pivot..-1])
30 return offset ? pivot + offset : nil
31 end
32 end
33 end
its
? \$\endgroup\$