Problem overview
The method count receives a sorted array of integers, nums, and a number, value. It returns how many times value appears in nums. The statement asks for O(log n) time and O(1) extra space, so walking through the array and counting is not allowed: that takes O(n).
In the example, nums = [1, 2, 2, 2, 4, 5] and value = 2. The 2 sits at indices 1, 2 and 3, so the answer is 3.
Approach: two lower bounds
Because the array is sorted, equal values are neighbours. All the 2s form one block, so if you know where the block starts and where it ends, the count is the length of the block.
The lower bound of a value is the first index whose element is not less than it. If the value is missing, it is the index where the value would be inserted to keep the array sorted. Binary search finds it in O(log n).
The trick is to call the lower bound twice:
- the lower bound of
valueis the index where the block ofvaluestarts; - the lower bound of
value + 1is the first index holding something greater thanvalue, which is the index just past the block.
The difference between the two is the block's length. When value does not occur, both calls return the same index and the difference is 0, so no special case is needed.
On the example, the lower bound of 2 is index 1, and the lower bound of 3 is index 4 (the element 4 is the first one that is not less than 3). Then 4 - 1 gives 3.
Step by step
- Find where the block starts. In
count, callthis.lowerBound(nums, value)and keep the result inlow. Writingcountfirst, withlowerBoundstill to come, keeps attention on the idea. - Find where the block ends. Call
lowerBoundagain withvalue + 1and keep it inhigh. This works because the array holds integers: nothing lies betweenvalueandvalue + 1. - Return
high - low. - Declare the helper above
count:lowerBound(nums: number[], value: number): number. - Set the search window to the whole array:
leftis 0 andrightisnums.length - 1. - Loop while the window is non-empty, that is, while
left <= right. - Take the middle as
Math.floor((left + right) / 2). The rounding matters because/on numbers can give a fraction, andnums[2.5]isundefined. - If
nums[middle]is less thanvalue, the middle element and everything to its left are too small, so movelefttomiddle + 1. - Otherwise the middle element is at least
value, so the answer is atmiddleor to its left. Moverighttomiddle - 1. - Return
leftafter the loop. At that pointleftis the first index whose element is at leastvalue, ornums.lengthif there is no such element.
The solution in TypeScript
class Solution {
lowerBound(nums: number[], value: number): number {
let left = 0, right = nums.length - 1;
while (left <= right) {
let middle = Math.floor((left + right) / 2);
if (nums[middle] < value) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return left;
}
count(nums: number[], value: number): number {
let low = this.lowerBound(nums, value);
let high = this.lowerBound(nums, value + 1);
return high - low;
}
}
Some TypeScript details:
- Division does not round. In TypeScript
numberis a double, so(0 + 5) / 2is2.5.Math.floorturns it into a valid index. - Calling a sibling method. Inside a class,
countreacheslowerBoundthroughthis. Writing the bare namelowerBound(...)would look for a variable or function with that name and fail. - Type annotations are only for the compiler.
number[]and: numberare erased when TypeScript is compiled to JavaScript. They do not check what a caller really passes at run time. - The
value + 1trick needs integers. With fractions it breaks: for[1.5, 2, 2.5, 2.5]andvalue = 1.5the function returns 2, although 1.5 appears once. The lesson promises integers, so it is fine here.
Complexity
Time: O(log n). Each pass of the loop halves the window, so lowerBound runs about log2(n) times, and count calls it twice. Two times O(log n) is still O(log n).
Space: O(1). Only a few numbers are stored, whatever the size of the array.
Edge cases and common mistakes
- Value missing. For the example array and
value = 3, both lower bounds are 4, so the result is 0. - Value larger than everything. With
value = 5in the example array, the lower bounds are 5 and 6. The second one isnums.length, which is a valid answer for a lower bound, and the count is 1. - Value smaller than everything. Both calls return 0, and the count is 0.
- Empty array.
rightstarts at -1, the loop never runs, both calls return 0, and the count is 0. - Every element equal to the value. For
[7, 7, 7]andvalue = 7, the lower bounds are 0 and 3, so the count is 3. - Using
middleinstead ofmiddle - 1. When the window shrinks to one element, the loop would never end. - Unsorted input. Binary search silently gives wrong answers on an unsorted array.
Testing
nums = [1, 2, 2, 2, 4, 5],value = 2gives3.- The same array with
value = 3gives0. - The same array with
value = 5gives1. nums = [],value = 1gives0.nums = [7, 7, 7],value = 7gives3.