LeetCode 315 - Count of Smaller Numbers After Self
The problem asks us to compute, for every element in the array, how many elements to its right are strictly smaller than it. Given an array nums, we must return another array counts of the same length.
Difficulty: 🔴 Hard
Topics: Array, Binary Search, Divide and Conquer, Binary Indexed Tree, Segment Tree, Merge Sort, Ordered Set
Solution
Problem Understanding
The problem asks us to compute, for every element in the array, how many elements to its right are strictly smaller than it.
Given an array nums, we must return another array counts of the same length. For each index i, counts[i] represents the number of indices j such that:
j > inums[j] < nums[i]
In other words, for every number, we look only at the elements that appear after it in the array and count how many of them are smaller.
For example, in the array [5,2,6,1]:
5has two smaller numbers after it,2and12has one smaller number after it,16has one smaller number after it,11has none
So the result is [2,1,1,0].
The constraints are important:
- The array can contain up to
10^5elements - Values range from
-10^4to10^4
A naive solution that compares every pair of elements would require O(n^2) time, which becomes too slow when n = 100000. A quadratic algorithm would potentially perform around 10^10 comparisons, which is not feasible within typical LeetCode time limits.
The problem guarantees that the input array is non-empty, but several edge cases still matter:
- Arrays with one element
- Arrays with duplicate values
- Arrays already sorted ascending
- Arrays sorted descending
- Negative numbers
- Repeated negative numbers
Handling duplicates correctly is especially important because we only count strictly smaller elements, not equal ones.
Approaches
Brute Force Approach
The simplest approach is to process every index independently.
For each element nums[i], we scan all elements to its right and count how many are smaller than nums[i]. This directly follows the problem definition and is easy to implement.
The algorithm works because it explicitly checks every valid pair (i, j) where j > i. If nums[j] < nums[i], we increment the count for index i.
However, this approach performs poorly for large arrays. Since each element may scan nearly the entire remainder of the array, the total number of comparisons becomes:
$$1 + 2 + 3 + \dots + (n-1) = O(n^2)$$
With n = 100000, this is too slow.
Optimal Approach, Modified Merge Sort
The key insight is that this problem resembles inversion counting.
During merge sort, the array is repeatedly divided and merged in sorted order. While merging two sorted halves, we can determine how many elements from the right half are smaller than elements from the left half.
Suppose we are merging:
- Left half: sorted elements originally appearing earlier
- Right half: sorted elements originally appearing later
If an element from the right half is placed before an element from the left half during merging, then that right-side element is smaller and originally appeared after the left-side element in the array.
This means we can accumulate counts during the merge process itself.
The important optimization is that merge sort already organizes elements in sorted order, allowing us to count many relationships at once instead of checking every pair individually.
Because merge sort runs in O(n log n) time, this approach is efficient enough for the constraints.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) extra | Compare every element with all elements to its right |
| Optimal Merge Sort | O(n log n) | O(n) | Counts smaller elements during merge operations |
Algorithm Walkthrough
Optimal Merge Sort Algorithm
- Create an array of pairs containing both the value and its original index.
We cannot sort the array directly because we must return answers in the original order. Each element is stored as:
(value, original_index)
Example:
nums = [5,2,6,1]
pairs = [
(5,0),
(2,1),
(6,2),
(1,3)
]
- Initialize a result array filled with zeros.
This array stores the final counts for each original index.
result = [0,0,0,0]
- Recursively divide the array using merge sort.
Merge sort splits the array into halves until each subarray contains one element.
Single-element arrays are already sorted. 4. Merge the two sorted halves while counting smaller elements.
During merging, maintain two pointers:
- One pointer for the left half
- One pointer for the right half
Also maintain a variable:
right_smaller_count
This represents how many elements from the right half have already been placed before the current left element. 5. Compare elements from both halves.
If the right element is smaller:
right_value < left_value
then:
- The right element should be merged first
- It contributes one smaller element for all remaining left elements
- Increment
right_smaller_count
- When placing a left element into the merged array, update its answer.
Since right_smaller_count elements from the right half have already moved ahead of it, all those elements are:
- Originally to its right
- Smaller than it
So:
result[left_index] += right_smaller_count
- Continue until the merge completes.
Eventually, every merge operation contributes counts to the appropriate indices. 8. Return the result array after merge sort finishes.
Why it works
The correctness comes from the merge sort invariant.
At every merge step:
- The left half contains elements that originally appeared earlier
- The right half contains elements that originally appeared later
- Both halves are already sorted
Whenever an element from the right half moves ahead of a left-half element during merging, it represents a smaller element appearing later in the original array.
Because merge sort processes all such relationships exactly once, every smaller-right relationship is counted correctly.
Python Solution
from typing import List
class Solution:
def countSmaller(self, nums: List[int]) -> List[int]:
n = len(nums)
result = [0] * n
indexed_nums = [(nums[i], i) for i in range(n)]
def merge_sort(left: int, right: int) -> None:
if left >= right:
return
mid = (left + right) // 2
merge_sort(left, mid)
merge_sort(mid + 1, right)
merge(left, mid, right)
def merge(left: int, mid: int, right: int) -> None:
temp = []
i = left
j = mid + 1
right_smaller_count = 0
while i <= mid and j <= right:
if indexed_nums[j][0] < indexed_nums[i][0]:
temp.append(indexed_nums[j])
right_smaller_count += 1
j += 1
else:
value, original_index = indexed_nums[i]
result[original_index] += right_smaller_count
temp.append(indexed_nums[i])
i += 1
while i <= mid:
value, original_index = indexed_nums[i]
result[original_index] += right_smaller_count
temp.append(indexed_nums[i])
i += 1
while j <= right:
temp.append(indexed_nums[j])
j += 1
for k in range(len(temp)):
indexed_nums[left + k] = temp[k]
merge_sort(0, n - 1)
return result
The implementation begins by pairing each value with its original index. This is necessary because merge sort rearranges elements, but the final answers must correspond to the original positions.
The result array stores the number of smaller elements for each index.
The recursive merge_sort function divides the array until subarrays contain only one element. Since single-element arrays are already sorted, the recursion then starts merging upward.
The real logic happens inside the merge function. Two sorted halves are merged while tracking how many right-half elements have already been placed before the current left-half element.
Whenever a right-side value is smaller, it increments right_smaller_count. Later, when a left-side element is inserted into the merged array, that count is added to its answer.
Finally, the merged result is copied back into indexed_nums, preserving sorted order for higher merge levels.
Go Solution
func countSmaller(nums []int) []int {
n := len(nums)
result := make([]int, n)
type Pair struct {
value int
index int
}
indexedNums := make([]Pair, n)
for i := 0; i < n; i++ {
indexedNums[i] = Pair{
value: nums[i],
index: i,
}
}
var mergeSort func(int, int)
mergeSort = func(left int, right int) {
if left >= right {
return
}
mid := (left + right) / 2
mergeSort(left, mid)
mergeSort(mid+1, right)
merge(left, mid, right)
}
merge := func(left int, mid int, right int) {
temp := make([]Pair, 0, right-left+1)
i := left
j := mid + 1
rightSmallerCount := 0
for i <= mid && j <= right {
if indexedNums[j].value < indexedNums[i].value {
temp = append(temp, indexedNums[j])
rightSmallerCount++
j++
} else {
result[indexedNums[i].index] += rightSmallerCount
temp = append(temp, indexedNums[i])
i++
}
}
for i <= mid {
result[indexedNums[i].index] += rightSmallerCount
temp = append(temp, indexedNums[i])
i++
}
for j <= right {
temp = append(temp, indexedNums[j])
j++
}
for k := 0; k < len(temp); k++ {
indexedNums[left+k] = temp[k]
}
}
mergeSort(0, n-1)
return result
}
The Go implementation follows the same algorithmic structure as the Python solution. The main difference is that Go requires an explicit Pair struct to store both the value and original index.
Slices are used for temporary merge storage. Since Go does not support nested function declarations in exactly the same way as Python, function variables are declared before assignment.
Integer overflow is not a concern because the constraints are small enough for Go's standard int type.
Worked Examples
Example 1
Input:
[5,2,6,1]
Initial indexed array:
| Position | Pair |
|---|---|
| 0 | (5,0) |
| 1 | (2,1) |
| 2 | (6,2) |
| 3 | (1,3) |
Initial result:
[0,0,0,0]
Merge Step: [5] and [2]
Compare:
2 < 5
So:
- Move
(2,1)first right_smaller_count = 1
Then place (5,0):
result[0] += 1
Result becomes:
[1,0,0,0]
Merged section:
[(2,1), (5,0)]
Merge Step: [6] and [1]
Compare:
1 < 6
Move (1,3) first.
Then place (6,2):
result[2] += 1
Result becomes:
[1,0,1,0]
Merged section:
[(1,3), (6,2)]
Final Merge
Merge:
[(2,1), (5,0)]
[(1,3), (6,2)]
Compare 1 and 2:
1 < 2
Move (1,3) first.
right_smaller_count = 1
Now compare 6 and 2.
2 is smaller, so:
result[1] += 1
Result:
[1,1,1,0]
Now compare 6 and 5.
5 is smaller, so:
result[0] += 1
Final result:
[2,1,1,0]
Example 2
Input:
[-1]
Single-element arrays already satisfy merge sort base conditions.
Output:
[0]
Example 3
Input:
[-1,-1]
During merging:
-1 < -1
is false because we only count strictly smaller elements.
No counts are added.
Output:
[0,0]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) | Merge sort performs log n levels of recursion and processes all elements at each level |
| Space | O(n) | Extra arrays are needed during merge operations |
The time complexity comes from standard merge sort behavior. The array is repeatedly divided into halves, producing log n recursive levels. At each level, all n elements participate in merging exactly once.
The space complexity is O(n) because temporary arrays are created during merging, and the indexed representation also stores all elements.
Test Cases
sol = Solution()
assert sol.countSmaller([5,2,6,1]) == [2,1,1,0] # standard example
assert sol.countSmaller([-1]) == [0] # single element
assert sol.countSmaller([-1,-1]) == [0,0] # duplicates
assert sol.countSmaller([1,2,3,4]) == [0,0,0,0] # ascending order
assert sol.countSmaller([4,3,2,1]) == [3,2,1,0] # descending order
assert sol.countSmaller([2,0,1]) == [2,0,0] # mixed ordering
assert sol.countSmaller([1,1,1,1]) == [0,0,0,0] # all equal
assert sol.countSmaller([-5,-2,-6,-1]) == [1,1,0,0] # negative numbers
assert sol.countSmaller([10]) == [0] # minimum size input
assert sol.countSmaller([3,2,2,6,1]) == [3,1,1,1,0] # duplicates with smaller elements
assert sol.countSmaller([10000,-10000,0]) == [2,0,0] # extreme constraint values
Test Summary
| Test | Why |
|---|---|
[5,2,6,1] |
Standard reference example |
[-1] |
Smallest possible input |
[-1,-1] |
Ensures duplicates are not counted |
[1,2,3,4] |
No smaller elements to the right |
[4,3,2,1] |
Maximum smaller counts |
[2,0,1] |
Mixed ordering case |
[1,1,1,1] |
All elements identical |
[-5,-2,-6,-1] |
Negative values |
[10] |
Boundary size validation |
[3,2,2,6,1] |
Duplicate handling during merge |
[10000,-10000,0] |
Extreme value constraints |
Edge Cases
Arrays With Duplicate Values
Duplicate handling is one of the easiest places to introduce bugs. The problem asks for strictly smaller elements, not smaller-or-equal elements.
For example:
[-1,-1]
should produce:
[0,0]
During merging, the algorithm only increments counts when:
right_value < left_value
not when values are equal. This guarantees duplicates are handled correctly.
Already Sorted Ascending Arrays
In an ascending array such as:
[1,2,3,4]
every element already has no smaller elements to its right.
A faulty implementation might accidentally count inversions incorrectly during merging. In this implementation, the right-side element is only moved first if it is strictly smaller, so no counts are added.
The result correctly remains:
[0,0,0,0]
Descending Arrays
Descending arrays create the maximum possible counts.
Example:
[4,3,2,1]
Every element is larger than all elements after it.
This case stresses whether the merge process correctly accumulates many smaller elements across recursive merge levels. The algorithm handles this naturally because each right-half insertion increments right_smaller_count, which is then added to all remaining left-half elements.
The final result becomes:
[3,2,1,0]
which is correct.