Problem overview
The function lowerBound receives a sorted slice of integers, nums, and a value. It returns the smallest index at which value can be inserted while keeping the slice sorted. Equivalently, it is the index of the first element that is not smaller than value.
For nums = [1, 2, 3, 5, 7] and value = 4 the answer is 3: inserting 4 at index 3 gives [1, 2, 3, 4, 5, 7]. For nums = [1, 2, 3] and value = 2 the answer is 1, not 2, because the smallest valid position is in front of the existing 2.
The statement asks for O(log n) time and O(1) extra space, so a left-to-right scan is too slow and the function must use binary search.
Approach: binary search for the boundary
Think of the sorted slice as two parts: the elements smaller than value come first, and the elements that are at least value come after them. The answer is the index where the second part begins. Because the slice is sorted, "is this element smaller than value?" is true for a prefix and false for the rest, which is exactly the shape binary search can handle.
Look at the middle element. If it is smaller than value, then it and everything before it belong to the first part, so the boundary lies to its right. If it is at least value, then it could be the first element of the second part, or the boundary could be further left. Either way, half of the range is dropped each time.
Unlike an ordinary search, this one doesn't stop when it sees an equal element. An equal element only says that the boundary is at that index or to the left, so the search keeps going left to find the first one. That is how duplicates are handled: with [1, 2, 2, 2, 3] and value = 2, the search ends at index 1.
Step by step
- Set up the window. Declare
leftandrightin one:=: the first index and the last index,len(nums)-1. - Loop while the window is non-empty, with a
forthat has only the conditionleft <= right. (Go has nowhile.) - Compute
middle.(left + right) / 2rounds down, so it is always a valid index inside the window. - Too small: if
nums[middle] < value, the boundary is to the right, so setleft = middle + 1. - Big enough: otherwise
nums[middle]is at leastvalue, so the answer ismiddleor somewhere before it. Setright = middle - 1. - Return
left. When the loop ends,rightis just one less thanleft. Everything beforeleftis smaller thanvalue, and everything fromlefton is at leastvalue, soleftis the boundary.
The solution in Go
func lowerBound(nums []int, value int) int {
left, right := 0, len(nums)-1
for left <= right {
middle := (left + right) / 2
if nums[middle] < value {
left = middle + 1
} else {
right = middle - 1
}
}
return left
}
Trace with nums = [1, 2, 3, 5, 7] and value = 4. First left = 0, right = 4, middle = 2, and nums[2] = 3 is smaller than 4, so left = 3. Next middle = 3 and nums[3] = 5 is not smaller, so right = 2. Now left > right, and the function returns 3.
Go details in this code
- Integer division.
(left + right) / 2is integer division because both operands areint; it truncates. Both indices are never negative inside the loop, so truncation is the same as rounding down. - The sum doesn't overflow. Go's
intis 64 bits on 64-bit platforms, and slice indices are far below that. The variantleft + (right-left)/2is a good habit for languages with 32-bit integers, but it isn't needed here. - The result can be
len(nums). When every element is smaller thanvalue,leftends one past the last index. That is correct as an insert position, but it is not a valid index to read, so never writenums[lowerBound(...)]without checking. - A short condition-only
for.for left <= right { ... }is Go's replacement forwhile.
Complexity
Time: O(log n). Each iteration discards at least half of the window, so the loop runs about log₂(n) times.
Space: O(1). Only left, right and middle are stored.
Edge cases and common mistakes
- Empty slice.
rightis -1, the loop doesn't run, and the function returns 0. - Value smaller than everything. For
[1, 2, 3]andvalue = 0,rightkeeps moving left until it reaches -1, andleftstays 0. The answer is 0. - Value larger than everything. For
[1, 2, 3]andvalue = 9, the answer is 3, the length of the slice. - Duplicates. The answer is the first of the equal elements. Returning as soon as
nums[middle] == valuewould give any of them, which is wrong here. - Using
<=in the comparison. Withnums[middle] <= valuethe search moves past equal elements and finds the position after the last one, a different function (the upper bound). - Returning
right. After the looprightis one before the boundary, so returning it is off by one.
Testing
[1, 2, 3, 5, 7],4gives 3.[1, 2, 3],2gives 1.[1, 2, 2, 2, 3],2gives 1.[1, 2, 3],9gives 3.[],5gives 0.