How do you find the first and last position of a target value in a sorted array?
Run two binary searches. The first one looks for the leftmost occurrence: when it finds a match it keeps going to the left. The second looks for the rightmost occurrence: when it finds a match it keeps going to the right. If the first search finds nothing, the value is absent and both positions are minus one.
def search_range(values, target):
low, high = 0, len(values) - 1
start = -1
end = -1
while low <= high:
mid = (low + high) // 2
if values[mid] == target:
start = mid
high = mid - 1
elif values[mid] < target:
low = mid + 1
else:
high = mid - 1
low, high = 0, len(values) - 1
while low <= high:
mid = (low + high) // 2
if values[mid] == target:
end = mid
low = mid + 1
elif values[mid] < target:
low = mid + 1
else:
high = mid - 1
return [start, end]
data = [1, 3, 3, 3, 5, 7]
print(search_range(data, 3))
print(search_range(data, 4))The outputs are 3, 3 and minus one, minus one. The three occurrences of 3 sit at indices one, two and three, so the range is from one to three. The target 4 is not in the array, so both positions stay at minus one.
The mechanism is that a binary search which stops at the first match it happens to land on cannot tell you whether it landed on the first or the fifth copy. Continuing the search in the appropriate direction resolves that. The asymmetry between the two loops is the whole trick: one keeps high just below the midpoint, the other keeps low just above it.
The trade-off to name is that this does two searches where one would do, so it costs roughly twice the comparisons. The alternative is the lower bound formulation, where the first position is the lower bound of the target and the last is the lower bound of the target plus one minus one, which is the same amount of work but with a tidier structure and no special case for not found. The brute force alternative scans the array for the first and last match and is linear, and it is the right answer when the array is not actually sorted.