Problem overview
The function find(nums, value) receives a sorted slice of integers and a target. It returns the index where value sits, or -1 when it isn't there. The statement asks for O(log n) time and O(log n) extra space, and for a recursive solution.
For nums = [1, 2, 4, 5] and value = 4 the answer is 2, because nums[2] is 4.
Approach: discard half the range at each call
A linear scan checks every element, which takes O(n). A sorted slice allows something better. Look at the middle element. If it equals value, you are done. If it is smaller than value, everything to its left is smaller still, so the answer can only be in the right half. If it is larger, the answer can only be in the left half. Each comparison throws away half of what remains, so after about log2(n) comparisons the range is empty.
The recursion works on ranges of one array. Copying half of the slice at each call would cost extra time, so the helper binarySearch takes the bounds left and right and always looks at the same nums. The public function find starts it with the whole slice.
Here is the search for 4 in [1, 2, 4, 5]. The first call covers indices 0 to 3, and the middle is index 1, which holds 2. That is smaller than 4, so the next call covers indices 2 to 3. Its middle is index 2, which holds 4, so the function returns 2. Searching for 3 in the same slice goes the same way until the call for indices 2 to 3 finds 4 at the middle, which is too large. The next call covers indices 2 to 1, where left > right, and returns -1.
Step by step
- Delegate from
find. ReturnbinarySearch(nums, 0, len(nums)-1, value). The helper's result, including-1, is already the final answer. - Declare the helper with the parameters
nums []int,left,rightandvalue, returning anint. Go allows the helper to appear after the function that calls it. - Base case. When
left > rightthe range is empty, so the value isn't present: return-1. This is what stops the recursion. - Find the middle index with
:=. - Found it. If
nums[middle]equalsvalue, returnmiddle. - Middle too small. Recurse on the upper half, from
middle + 1toright, and return the call's result. - Middle too large. Recurse on the lower half, from
lefttomiddle - 1, and return that result.
The solution in Go
func find(nums []int, value int) int {
return binarySearch(nums, 0, len(nums)-1, value)
}
func binarySearch(nums []int, left, right, value int) int {
if left > right {
return -1
}
middle := (left + right) / 2
if nums[middle] == value {
return middle
}
if nums[middle] < value {
return binarySearch(nums, middle+1, right, value)
} else {
return binarySearch(nums, left, middle-1, value)
}
}
Go details:
- Slices are cheap to pass. A slice value holds a pointer, a length and a capacity, so passing
numsinto each recursive call copies only that small header, not the elements. - Integer division truncates.
(left + right) / 2is a whole number. With non-negative bounds it rounds down, somiddlealways lies betweenleftandright. - Shared type names.
left, right, value intdeclares threeintparameters. - Every path returns. The last
if/elsereturns in both branches, so Go accepts the function without a trailingreturn. - Overflow. Adding two very large indices could overflow in principle, and
left + (right - left) / 2avoids that. With 64-bitintand slice lengths, the simple average is safe here.
Complexity
Time: O(log n). Each call does a constant amount of work and the range roughly halves, so there are at most about log2(n) + 1 calls.
Space: O(log n). No new slices are created, but the recursion keeps one stack frame per active call, and the depth is also about log2(n).
Edge cases and common mistakes
- Empty slice.
len(nums)-1is-1, so the first call hasleft > rightand returns-1without touchingnums. - One element. The range has
left == right, the middle is that element, and the function returns 0 or recurses into an empty range. - Using
middleinstead ofmiddle + 1ormiddle - 1. The element atmiddlehas already been ruled out. Keeping it can leave the range unchanged and recurse forever. - Forgetting
returnbefore the recursive call. The index found deep in the recursion would be dropped, and the function wouldn't compile or would lose the result. - Duplicates. The function returns the index of some matching element, not necessarily the first one. In
[1, 2, 2, 2, 3]searching for 2 returns 2. - Unsorted input. The halving logic relies on order. On an unsorted slice the result can be wrong.
Testing
With nums = [1, 2, 4, 5]: searching for 4 returns 2, for 1 returns 0, for 5 returns 3, for 3 returns -1 and for 9 returns -1. With an empty slice, any search returns -1.