Problem overview
The method Count receives a sorted int[] nums and an int value. It returns how many times value appears in nums.
For nums = [1, 2, 2, 2, 4, 5] and value = 2, the 2 appears three times, so the answer is 3. The statement asks for O(log n) time and O(1) extra space, so counting by looping over the array, which is O(n), is not allowed.
Approach: where the block of equal values starts and ends
In a sorted array, equal values are neighbours. All copies of value form one block of consecutive indices, and the count is the length of that block. If you know the index where the block starts and the index just after where it ends, the count is the difference.
Both positions come from the same tool, the lower bound: the first index whose element is not less than a given number.
LowerBound(nums, value)is the first index holdingvalueor something larger. Ifvalueis present, this is the start of its block.LowerBound(nums, value + 1)is the first index holdingvalue + 1or something larger. Since the array holds integers, that means the first index whose element is greater thanvalue, which is just past the block.
The count is the second result minus the first. When value is absent, both calls land on the same index, and the difference is 0.
On the example, LowerBound(nums, 2) is 1, because nums[1] is the first 2. LowerBound(nums, 3) is 4, because nums[4] = 4 is the first element that is at least 3. The count is 4 - 1 = 3. Each lower bound is a binary search, so the whole method is two O(log n) searches. The array is never scanned element by element, however long the block of equal values is.
Step by step
- Treat
LowerBoundas available. It takes the array and a value and returns the lower-bound index, as in the Lower Bound lesson. DeclarelowasLowerBound(nums, value). - Declare
highasLowerBound(nums, value + 1). Every occurrence ofvaluesits at an index fromlowtohigh - 1. - Return
high - low, the number of indices holdingvalue. - Write the helper above
Count:public int LowerBound(int[] nums, int value). - Set the bounds in one statement:
left = 0andright = nums.Length - 1. - Loop while
left <= right, so the loop keeps going while the window holds at least one element. - Compute
middleas(left + right) / 2. Integer division drops the fraction, so it is a valid index. - If
nums[middle] < value, setleft = middle + 1. Everything up tomiddleis too small. - Otherwise set
right = middle - 1. The element atmiddleis at leastvalue, so the lower bound is there or further left. - Return
leftafter the loop. It is the first index whose element is at leastvalue, ornums.Lengthwhen there is none.
The solution in C#
class Solution {
public int LowerBound(int[] nums, int value) {
int left = 0, right = nums.Length- 1;
while (left <= right) {
int middle = (left + right) / 2;
if (nums[middle] < value) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return left;
}
public int Count(int[] nums, int value) {
int low = LowerBound(nums, value);
int high = LowerBound(nums, value + 1);
return high - low;
}
}
C# details:
- Methods call each other by name. Both methods belong to
Solution, soCountcallsLowerBounddirectly, with no object in front. Their order inside the class does not matter in C#. - Why
value + 1works. It relies onintvalues being whole numbers: there is no integer strictly betweenvalueandvalue + 1. Withdoublevalues it would not work. - Overflow is silent. C# does not check
intarithmetic by default. Ifvalueisint.MaxValue, thenvalue + 1wraps around toint.MinValuewithout any error. See the edge cases below. - Reusing a tested helper. The array is never modified, and the helper is a separate method, so the two searches cannot interfere with each other. Writing one correct
LowerBoundand calling it twice is simpler and safer than writing a second, slightly different search for the last occurrence.
Complexity
Time: O(log n). Count runs two binary searches, each halving its window at every step. Two times O(log n) is still O(log n).
Space: O(1). Only a handful of integers are used.
Edge cases and common mistakes
- Value not present.
[1, 2, 2, 2, 4, 5]with value 3 gives 0: both lower bounds equal 4. A value larger than every element also gives 0, since both calls returnnums.Length. - Empty array. Both lower bounds return 0, so the count is 0.
- Whole array equal.
[7, 7, 7]with value 7 gives3 - 0 = 3. - The largest possible value. With
value = int.MaxValue,value + 1wraps toint.MinValue, sohighbecomes 0. For[1, 2147483647]the method returns0 - 1 = -1instead of 1. A solution that must handle everyintcould compute the upper bound with a separate search that uses<=instead of adding 1. Ordinary test values do not reach this limit. - Loop condition. Using
left < rightskips the last candidate and can return a wrong boundary. - Returning
right. After the looprightis one less than the answer. - Counting with a loop. A scan from the first match works, but with many duplicates it degrades to
O(n).
Testing
[1, 2, 2, 2, 4, 5], value 2 gives3.[1, 2, 2, 2, 4, 5], value 3 gives0.[1, 2, 2, 2, 4, 5], value 9 gives0.[], value 1 gives0.