Problem overview
The method count receives a sorted array of integers, nums, and a number value. It returns how many times value appears in the array. For nums = [1, 2, 2, 2, 4, 5] and value = 2 the answer is 3. The statement asks for O(log n) time and O(1) extra space, so walking through the array and counting is too slow.
Approach: two lower bounds
In a sorted array, equal values sit next to each other. The occurrences of value form one block of consecutive indices, and the answer is the length of that block. If the block starts at index low and ends just before index high, the count is high - low.
Both borders come from the same tool, the lower bound: the index of the first element that is not less than a given number. It is found with a binary search in O(log n).
lowerBound(nums, value)is the index of the first occurrence ofvalue, or the place where it would go if it were missing.lowerBound(nums, value + 1)is the index of the first element greater thanvalue. The array holds integers, so "not less thanvalue + 1" means "greater thanvalue".
With the example, the first call returns 1 and the second returns 4, so the count is 4 - 1 = 3. If value is absent, both calls return the same index and the difference is 0, which is the right answer with no special case.
Another way to see it: the first border says how many elements are strictly smaller than value, and the second says how many are smaller than or equal to it. Their difference is exactly the number of elements equal to value. The lower bound is a general tool, and the same helper answers questions such as how many elements are below a number.
Step by step
- Get
low. Incount, declare a constant by callinglowerBound(nums, value). - Get
high. CalllowerBound(nums, value + 1). Every occurrence ofvaluenow sits at an index fromlowtohigh - 1. - Return
high - low. - Write the helper above
countasfunc lowerBound(_ nums: [Int], _ value: Int) -> Int. - Declare
leftandrightwithvar, set to 0 andnums.count - 1. - Loop while
left <= right, so a window of one element is still examined. - Take the middle as
(left + right) / 2, aletconstant. Integer division rounds down to a valid index. - If
nums[middle] < value, setleft = middle + 1: the lower bound must be further right. - Otherwise set
right = middle - 1: the element atmiddleis at leastvalue, so the lower bound is there or to its left. - Return
leftafter the loop. It is the first index whose element is at leastvalue, ornums.countif none is.
The solution in Swift
class Solution {
func lowerBound(_ nums: [Int], _ value: Int) -> Int {
var left = 0
var 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
}
func count(_ nums: [Int], _ value: Int) -> Int {
let low = lowerBound(nums, value)
let high = lowerBound(nums, value + 1)
return high - low
}
}
The Swift details worth a closer look:
- Methods call each other by bare name. Inside the class,
countcallslowerBounddirectly, and the order of the two methods in the class does not matter. - The
_labels. Because the parameters are declared with_, calls have no labels:lowerBound(nums, value + 1). - A method named
count. Arrays also have acountproperty, as innums.count. They do not conflict:nums.countis the property of the array, whilecount(nums, value)is a method ofSolution. - Overflow traps.
value + 1is ordinaryIntarithmetic. Forvalueequal toInt.maxit would overflow and stop the program, because Swift does not wrap silently. For the values the lesson uses this cannot happen.
Complexity
A plain count with a loop would be correct but O(n); on a sorted array with millions of elements the two searches need only a few dozen comparisons.
Time: O(log n). Each lowerBound call halves its window at every step, and count makes two calls, which is still O(log n).
Space: O(1). Only a few integers are stored; the array is not copied.
Edge cases and common mistakes
- Value not present. For
[1, 2, 2, 2, 4, 5]and 3, both lower bounds are 4, so the result is 0. - Empty array. The loop does not run, both calls return 0, and the result is 0.
- Value at an end of the array. For
[7, 7, 7]and 7 the bounds are 0 and 3, giving 3. The upper border may equalnums.count, and that is fine because it is only used in a subtraction. - Value outside the range of the array. For
[1, 2]and 9, or for 0, the two bounds are equal and the answer is 0. - Finding one occurrence and walking outward. If the array is all one value, that walk is
O(n). Two binary searches avoid it. - Using
<=in the comparison. If the check werenums[middle] <= value, the search would find the border after the block, not before it, and both calls would return the same thing for many inputs.
Testing
nums = [1, 2, 2, 2, 4, 5],value = 2gives3.nums = [1, 2, 2, 2, 4, 5],value = 3gives0.nums = [7, 7, 7],value = 7gives3.nums = [1, 2],value = 9gives0.nums = [],value = 1gives0.
