Problem overview
The method count receives a sorted Array[Int] named nums and an Int named 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.
Counting with a loop takes O(n) time. The statement asks for O(log n) time and O(1) extra space, so the array has to be searched by halving, not scanned.
Approach: two lower bounds
Because the array is sorted, all copies of value sit next to each other in one block. If you know where the block starts and where it ends, the count is the difference of the two positions.
The lower bound of a number is the first index whose element is not less than that number. It is also the index where the number would be inserted. Two calls give both ends of the block:
lowerBound(nums, value)is the index of the first copy ofvalue. Ifvalueis missing, it is the position where it would go.lowerBound(nums, value + 1)is the index of the first element greater thanvalue. The array holds integers, so "greater thanvalue" and "at leastvalue + 1" mean the same thing. This index is just past the last copy.
With [1, 2, 2, 2, 4, 5] and value = 2, the first call returns 1 and the second returns 4 (the lower bound of 3 is the position of 4). The block is indices 1, 2 and 3, and 4 - 1 = 3.
When value is absent, both calls return the same index, because there is nowhere for the two boundaries to differ, and the difference is 0. Each call is an O(log n) binary search, so the total stays O(log n).
Another way to find the end of the block is to write a second binary search that looks for the last occurrence. Reusing lowerBound avoids that: one tested routine does both jobs, and the only new idea is to ask it about value + 1.
Step by step
- In
count, storelowerBound(nums, value)in avalnamedlow. - Store
lowerBound(nums, value + 1)in avalnamedhigh. - Return
high - low. - Write
lowerBoundabovecount. It keeps a window[left, right]in twovars, starting at0andnums.length - 1. - Loop while
left <= right, so a window holding one element is still examined. - Take
middle = (left + right) / 2. Integer division rounds down, so it is a valid index. - If
nums(middle) < value, everything up tomiddleis too small, so setleft = middle + 1. - Otherwise
nums(middle)is at leastvalue, so the boundary ismiddleor further left. Setright = middle - 1. - After the loop, return
left. It is the first index whose element is at leastvalue, ornums.lengthif there is none.
The solution in Scala
class Solution {
def lowerBound(nums: Array[Int], value: Int): Int = {
var left = 0
var right = nums.length - 1
while (left <= right) {
val middle = (left + right) / 2
if (nums(middle) < value) {
left = middle + 1
} else {
right = middle - 1
}
}
return left
}
def count(nums: Array[Int], value: Int): Int = {
val low = lowerBound(nums, value)
val high = lowerBound(nums, value + 1)
return high - low
}
}
Scala details
Arrays are indexed with parentheses: nums(middle). Parameters are immutable, so the search copies the bounds into var variables, while middle, low and high never change after creation and are vals.
Scala methods return the value of their last expression, so return is rarely needed, but it is allowed. Here return left and return high - low are the last statements, so they behave exactly like the bare expressions left and high - low would.
Both methods live in the same class, and the order of definitions inside a class does not matter, so count could also come first.
An Int is 32 bits and overflow wraps silently. That is relevant to value + 1, as the edge cases below show.
Complexity
Each lowerBound call halves its window at every step, which takes at most about log n iterations. count makes two calls, so the time is O(log n). Only a handful of Int variables are used, so the extra space is O(1).
Edge cases and common mistakes
- Value not present. For
[1, 2, 2, 2, 4, 5]andvalue = 3, both lower bounds are 4, so the result is 0. - Value at the end. For the same array and
value = 5, the lower bounds are 5 and 6 (the array length), so the result is 1. The second index can legitimately equalnums.length. - Value larger than every element. Both calls return
nums.length, and the result is 0. - All elements equal. For
[7, 7, 7]andvalue = 7, the bounds are 0 and 3, giving 3. - Empty array. Both searches return 0, so the count is 0.
- Overflow at the top of the range. If
valueisInt.MaxValue, thenvalue + 1wraps around toInt.MinValue. ForArray(1, Int.MaxValue)andvalue = Int.MaxValue, the second lower bound is 0 and the method returns-1instead of 1. If values can reach that limit, handle it separately, for example by computing the upper end with a search for the first element greater thanvalueinstead of usingvalue + 1. - Using a strict comparison for both ends. The trick only works because both ends use the same lower-bound search, which treats equal elements as "not too small". Searching for
valuewith an exact-match binary search would find one copy, not the boundaries.
Testing
[1, 2, 2, 2, 4, 5]withvalue = 2gives 3.[1, 2, 2, 2, 4, 5]withvalue = 3gives 0.[1, 2, 2, 2, 4, 5]withvalue = 5gives 1.[7, 7, 7]withvalue = 7gives 3.
