Problem overview
The method lowerBound receives a sorted array of integers, nums, and a number value. It returns the smallest index where value could be inserted so that the array stays sorted. Equivalently, it returns the index of the first element that is not less than value.
With 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]. With nums = [1, 2, 3] and value = 2, the answer is 1, not 2, because the 2 already in the array sits at index 1 and the new 2 may go in front of it. The statement asks for O(log n) time and O(1) extra space.
Approach: binary search for the first element that is not smaller
A linear scan would walk from the left until it meets an element that is at least value. That is O(n). Because the array is sorted, the elements smaller than value form a prefix, and the rest form a suffix. The answer is the border between them, and a border in sorted data can be found by repeatedly halving the search window.
Look at the element in the middle of the window:
- If it is smaller than
value, then it and everything to its left are smaller too. The border lies to the right, so the window shrinks to the part after the middle. - Otherwise it is at least
value. It might be the first such element, or the first one might lie further left. Either way the border is at the middle or to its left, so the window shrinks to the part before the middle.
When the window is empty, the left edge has crossed the border and points at the first element that is not smaller, or just past the end if there is none.
Step by step
- Declare
leftandrightwithvar, since both move. They start at the first index, 0, and the last index,nums.count - 1, so the window covers the whole array. - Loop while
left <= right. Using<=keeps the loop running when the window holds a single element, which could still be the answer. - Compute
middleas(left + right) / 2withlet. Integer division rounds down, so it is always a valid index inside the window. - If
nums[middle] < value, movelefttomiddle + 1. No index up tomiddlecan be the answer. - Otherwise move
righttomiddle - 1. The element atmiddleis at leastvalue, so the answer is further left, ormiddleitself, which the finalleftwill still point at. - Return
leftafter the loop. It ends one step past the last element known to be smaller thanvalue.
The solution in Swift
class Solution {
func lowerBound(_ nums: [Int], _ value: Int) -> Int {
var left = 0, right = nums.count - 1
while left <= right {
let middle = (left + right) / 2
if nums[middle] < value {
left = middle + 1
} else {
right = middle - 1
}
}
return left
}
}
The Swift details worth a closer look:
- Two variables, one
var.var left = 0, right = nums.count - 1declares both at once. Both are mutable, which is why they usevar, whilemiddleis a new constant on every pass and useslet. - Integer division. Dividing two
Intvalues gives anIntand drops the fraction, so no conversion is needed to get a usable index. - The
_labels. Each parameter has_before its name, so the call islowerBound(nums, value)with no argument labels. - Overflow is a trap in Swift.
Intis 64 bits, and an arithmetic overflow stops the program instead of wrapping. The sumleft + rightof two array indices never gets near that limit, so(left + right) / 2is safe here. The formleft + (right - left) / 2gives the same midpoint without ever adding the two indices, and is a good habit. - An empty array works.
nums.count - 1is -1, so the loop condition0 <= -1is false and the method returns 0 straight away.
Complexity
Time: O(log n). Each iteration discards at least half of the window, so after about log2(n) iterations it is empty. An array of a million elements needs at most around 20 iterations.
Space: O(1). The method keeps three integers and never copies the array.
Edge cases and common mistakes
- Value larger than everything. For
[1, 2, 3]and 10, the method returns 3, which isnums.count. That index is one past the end, and it is the correct insertion point. - Value smaller than everything. For
[1, 2, 3]and -4 the result is 0. - Duplicates. For
[1, 2, 2, 2, 3]and 2 the result is 1, the first of the equal elements. That is why equal elements go to theelsebranch: movingrightleft of an equal element keeps searching for an earlier one. - Returning on equality. Stopping the moment
nums[middle] == valuewould return any of the equal elements, not the first. - Using
<in the loop condition. Withleft < right, a window of one element is never examined. For[5]and 6 the loop would not run at all and the method would return 0 instead of 1. - Returning
right. After the looprightisleft - 1, the last element that is smaller thanvalue, not the insertion point.
Testing
nums = [1, 2, 3, 5, 7],value = 4gives3.nums = [1, 2, 3],value = 2gives1.nums = [1, 2, 2, 2, 3],value = 2gives1.nums = [1, 2, 3],value = 10gives3.nums = [],value = 5gives0.
