Reverse Pairs | Hard DSA Interview Question
The Reverse Pairs problem is a classic hard-level Array and Divide-and-Conquer problem. Given an array of integers nums, the objective is to count the total number of reverse pairs such that and . While closely related to the traditional Count Inversions problem (), the scalar factor of breaks the ability to count pairs directly during the merge step, requiring a decoupled two-pointer counting step integrated into Merge Sort.
1. Problem Statement & Mathematical Definition
Given an integer array nums of size , find the total number of reverse pairs.
A Reverse Pair is defined as a pair of indices that satisfies two conditions simultaneously:
- (the left index strictly precedes the right index).
- (the left element is strictly greater than twice the right element).
Note: Pay careful attention to strict inequality: . If and , evaluates to (False), so is not a reverse pair.
2. Naive Brute Force Approach ()
Approach & Intuition
The most straightforward approach is to test every possible index pair using two nested loops.
- The outer loop picks the left index
ifrom0toN - 1. - The inner loop picks the right index
jfromi + 1toN - 1. - If , increment a counter.
Naive Code Implementation
public class Solution {
public int reversePairsNaive(int[] nums) {
int count = 0;
int n = nums.length;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
// Cast to long to avoid integer overflow when multiplying by 2
if ((long) nums[i] > 2L * nums[j]) {
count++;
}
}
}
return count;
}
}
Complexity Analysis
- Time Complexity: — For an array of size , the outer and inner loops iterate through pairs.
- Space Complexity: — Uses only a few primitive counter variables.
3. Why Standard "Count Inversions" Logic Fails
In the standard Count Inversions problem (), we combine counting directly into the Merge step of Merge Sort:
- If
left[i] > right[j], then becauseleftis sorted, all remaining elements fromleft[i]toleft[mid]are also strictly greater thanright[j]. - Hence, we can instantaneously add to our total count and advance
j.
Why this breaks for :
Consider two sorted subarrays:
Left=[6, 13, 21, 25]Right=[1, 2, 3, 4, 5, 9, 11, 13]
- Take
left[0] = 6andright[2] = 3. - Test condition: Is ? No ( is false).
- If we used standard inversion logic, failing at might suggest cannot form pairs. However, for subsequent elements in
Left(), does form valid reverse pairs (, , ). - Conversely, if formed a pair with (), we cannot immediately assume forms pairs with all remaining elements on the right.
The Key Structural Insight
Because multiplying by distorts the simple relative order comparison during a standard merge, we must separate the Counting step from the Merging step.
Before merging two sorted halves, we run a dedicated two-pointer pass to count all valid reverse pairs between the left and right halves.
4. The Two-Pointer Counting Mechanism on Sorted Subarrays
Given two sorted halves:
Leftsubarray spanned by indices[low ... mid]Rightsubarray spanned by indices[mid + 1 ... high]
We want to find how many pairs satisfy , where and .
Left (Sorted): [6, 13, 21, 25] (Indices low..mid)
Right (Sorted): [1, 2, 3, 4, 5, 9] (Indices mid+1..high)
The Monotonicity Property
Since both Left and Right are sorted in ascending order:
- As
imoves to the right, increases. - If a right element satisfies , then for any subsequent larger left element , the condition will automatically hold.
- Crucial Optimization: Pointer
rightdoes NOT need to reset tomid + 1for every newi. It only moves forward!
Step-by-Step Pointer Walkthrough
Left: [ 6, 13, 21, 25 ]
^ (i)
Right: [ 1, 2, 3, 4, 5, 9 ]
^ (right)
-
i = 0(nums[i] = 6):- Check
right = 0(nums[right] = 1): (True) Advanceright. - Check
right = 1(nums[right] = 2): (True) Advanceright. - Check
right = 2(nums[right] = 3): (False) Stopright. - Valid elements in Right for :
[1, 2](Count = ).
- Check
-
i = 1(nums[i] = 13):rightstays at index2(nums[right] = 3).- Check
3: (True) Advanceright. - Check
4: (True) Advanceright. - Check
5: (True) Advanceright. - Check
9: (False) Stopright. - Valid elements in Right added for :
[3, 4, 5](Totalrightadvanced to index5, count contributed = ).
-
i = 2(nums[i] = 21):rightstarts at index5(nums[right] = 9).- Check
9: (True) Advanceright(to index 6, out of bounds). - Valid elements in Right added:
[9](Total count contributed = ).
-
i = 3(nums[i] = 25):rightis already out of bounds (right = 6). Loop does not run.- Valid elements added: .
5. Complete Algorithm & Merge Sort Walkthrough
Vertical Divide-and-Conquer Flow
Execution Steps at Each Sub-Level:
- Base Case: Subarrays of length contain reverse pairs.
- Recursive Step:
- Divide array into
Left(lowtomid) andRight(mid + 1tohigh). - Recursively call
mergeSortonLeft(returns count of pairs internal toLeft). - Recursively call
mergeSortonRight(returns count of pairs internal toRight).
- Divide array into
- Cross-Pair Counting (
countPairs):- Count pairs where and .
- Merge Step (
merge):- Standard Merge Sort combine step to sort elements across
[low ... high].
- Standard Merge Sort combine step to sort elements across
6. Clean Production Code Implementation
Java Implementation
public class Solution {
public int reversePairs(int[] nums) {
if (nums == null || nums.length == 0) return 0;
return mergeSort(nums, 0, nums.length - 1);
}
private int mergeSort(int[] nums, int low, int high) {
if (low >= high) return 0;
int mid = low + (high - low) / 2;
int count = 0;
// 1. Divide & solve left and right halves
count += mergeSort(nums, low, mid);
count += mergeSort(nums, mid + 1, high);
// 2. Count reverse pairs across the two sorted halves
count += countPairs(nums, low, mid, high);
// 3. Merge the two sorted halves together
merge(nums, low, mid, high);
return count;
}
private int countPairs(int[] nums, int low, int mid, int high) {
int count = 0;
int right = mid + 1;
for (int i = low; i <= mid; i++) {
// Move 'right' pointer while condition holds
// Cast to long to handle potential integer overflow during multiplication
while (right <= high && (long) nums[i] > 2L * nums[right]) {
right++;
}
// Number of valid elements in right half for current nums[i]
count += (right - (mid + 1));
}
return count;
}
private void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int left = low;
int right = mid + 1;
int k = 0;
while (left <= mid && right <= high) {
if (nums[left] <= nums[right]) {
temp[k++] = nums[left++];
} else {
temp[k++] = nums[right++];
}
}
while (left <= mid) {
temp[k++] = nums[left++];
}
while (right <= high) {
temp[k++] = nums[right++];
}
for (int i = 0; i < temp.length; i++) {
nums[low + i] = temp[i];
}
}
}
C++ Implementation
#include <vector>
class Solution {
public:
int reversePairs(std::vector<int>& nums) {
if (nums.empty()) return 0;
return mergeSort(nums, 0, nums.size() - 1);
}
private:
int mergeSort(std::vector<int>& nums, int low, int high) {
if (low >= high) return 0;
int mid = low + (high - low) / 2;
int count = 0;
count += mergeSort(nums, low, mid);
count += mergeSort(nums, mid + 1, high);
count += countPairs(nums, low, mid, high);
merge(nums, low, mid, high);
return count;
}
int countPairs(std::vector<int>& nums, int low, int mid, int high) {
int count = 0;
int right = mid + 1;
for (int i = low; i <= mid; i++) {
while (right <= high && (long long)nums[i] > 2LL * nums[right]) {
right++;
}
count += (right - (mid + 1));
}
return count;
}
void merge(std::vector<int>& nums, int low, int mid, int high) {
std::vector<int> temp;
int left = low, right = mid + 1;
while (left <= mid && right <= high) {
if (nums[left] <= nums[right]) {
temp.push_back(nums[left++]);
} else {
temp.push_back(nums[right++]);
}
}
while (left <= mid) temp.push_back(nums[left++]);
while (right <= high) temp.push_back(nums[right++]);
for (int i = 0; i < temp.size(); i++) {
nums[low + i] = temp[i];
}
}
};
7. Complexity & Optimization Analysis
Time Complexity Analysis
- Tree Depth: Merge sort divides the array in half at each step, producing a tree of height .
- Work Per Level:
- Counting Step (
countPairs): Pointerimoves at most times. Pointerrightmoves at most times across the right subarray. Total linear work per merge level = . - Merging Step (
merge): Standard two-pointer merge takes operations.
- Counting Step (
- Total Time: .
Common Misconception: "Nested loops in
countPairsmake it ."
Correction: The innerwhileloop condition advances therightpointer continuously without resetting for subsequent iterations ofi. Across the entire function execution,rightincrements at most times. Thus,countPairsis strictly .
Space Complexity Analysis
- Auxiliary Array Space: auxiliary space for the
temparray during the merge operations. - Recursion Call Stack: stack space for merge sort recursion depth.
- Total Space Complexity: .
Interview Communication Strategy: Mention to the interviewer that this optimal approach mutates (distorts) the original array order by sorting it. If preserving the original array order is required, inform them that you would work on a deep copy of the input array.
8. Comparative Matrix
Summary and Key Takeaways
- Core Problem Distinction: Reverse Pairs requires finding elements where . The extra factor of invalidates the single-pass counting trick embedded inside standard Merge Sort.
- Decoupled Strategy: To preserve efficiency, decouple counting from merging. Perform counting on two sorted halves right before executing the standard merge procedure.
- Monotonic Pointer Advancement: Because both sub-arrays are sorted, as the left pointer
iincreases,nums[i]grows strictly larger. Consequently, the right pointerrightnever needs to reset, guaranteeing an run-time per recursion level. - Integer Overflow Protection: Always cast
nums[j]to a 64-bit integer (longin Java,long longin C++) before multiplying by to avoid sign overflow errors with extreme integer values (e.g.,INT_MAX). - Array Mutation Flag: Be ready to clarify to an interviewer that Merge Sort sorts the array in-place, and offer an explicit array duplication if preserving the input sequence is required.