Reverse Pairs
HardPrompt
Given an integer array nums, return the number of reverse pairs in the array.
A reverse pair is a pair (i, j) where:
0 <= i < j < nums.lengthandnums[i] > 2 * nums[j].
Example 1:
Input: nums = [1,3,2,3,1]
Output: 2
Explanation: The reverse pairs are:
(1, 4) --> nums[1] = 3, nums[4] = 1, 3 > 2 * 1
(3, 4) --> nums[3] = 3, nums[4] = 1, 3 > 2 * 1Example 2:
Input: nums = [2,4,3,5,1]
Output: 3
Explanation: The reverse pairs are:
(1, 4) --> nums[1] = 4, nums[4] = 1, 4 > 2 * 1
(2, 4) --> nums[2] = 3, nums[4] = 1, 3 > 2 * 1
(3, 4) --> nums[3] = 5, nums[4] = 1, 5 > 2 * 1
Constraints:
1 <= nums.length <= 5 * 104-231 <= nums[i] <= 231 - 1
Approaches
2 approaches with complexity analysis and trade-offs.
This is the most straightforward approach, which involves checking every possible pair of indices (i, j) where i < j and testing if they satisfy the reverse pair condition.
Algorithm
- Initialize a counter
countto 0. - Use two nested loops. The outer loop iterates with index
ifrom0ton-1. - The inner loop iterates with index
jfromi + 1ton-1. - Inside the inner loop, check if the condition
(long)nums[i] > 2 * (long)nums[j]is met. Casting tolongis important to prevent integer overflow. - If the condition is true, increment the
count. - After the loops complete, return the final
count.
Walkthrough
The algorithm iterates through all possible pairs of elements in the array. For each element nums[i], it scans the rest of the array to its right (elements nums[j] where j > i). For each such pair, it checks if nums[i] is greater than 2 * nums[j]. A counter is used to keep track of how many such pairs are found. To avoid potential integer overflow when calculating 2 * nums[j] (since nums[j] can be large), it's crucial to cast the numbers to a 64-bit integer type (long in Java) before performing the multiplication and comparison.
class Solution { public int reversePairs(int[] nums) { int count = 0; int n = nums.length; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if ((long) nums[i] > 2 * (long) nums[j]) { count++; } } } return count; }}Complexity
Time
O(N^2), where N is the number of elements in the array. The two nested loops result in a quadratic number of pair comparisons.
Space
O(1), as we only use a constant amount of extra space for the counter and loop indices.
Trade-offs
Pros
Very simple to understand and implement.
Requires no extra space apart from a few variables for loops and the counter.
Cons
Highly inefficient for large arrays. It will result in a "Time Limit Exceeded" (TLE) error for the given constraints (
N <= 5 * 10^4).
A much more efficient approach is to use a divide-and-conquer strategy, specifically by modifying the Merge Sort algorithm. The core idea is that while merging two sorted subarrays, we can efficiently count the reverse pairs that span across these two halves.
Algorithm
- Define a recursive function
mergeSortAndCount(nums, low, high). - Base Case: If
low >= high, it means the subarray has 0 or 1 element, so return 0. - Divide: Find the middle index
mid = low + (high - low) / 2. - Conquer: Recursively call the function for the left half (
lowtomid) and the right half (mid + 1tohigh). Sum their results:count = mergeSortAndCount(nums, low, mid) + mergeSortAndCount(nums, mid + 1, high). - Combine & Count:
- This is the key step. Both halves
nums[low...mid]andnums[mid+1...high]are now sorted. - Count pairs
(i, j)whereiis in the left half andjis in the right. Use two pointers:ifor the left half andjfor the right. - For each
ifromlowtomid, advancejin the right half as long as(long)nums[i] > 2L * nums[j]. The number of suchj's found isj - (mid + 1). Add this tocount. - Since both subarrays are sorted, the
jpointer does not need to be reset for eachi, making this counting step O(N).
- This is the key step. Both halves
- Merge: Perform a standard merge operation to combine the two sorted halves into a single sorted array. This is crucial for the correctness of the parent recursive calls.
- Return the total
count.
Walkthrough
This algorithm recursively splits the array into two halves until we have subarrays of size 1. Then, as it merges the subarrays back, it counts the reverse pairs. The total count is the sum of reverse pairs within the left half, within the right half, and across the two halves.
The crucial part is counting the pairs (i, j) where i is in the left sorted half and j is in the right sorted half. Since both halves are sorted, we can do this in linear time. We use two pointers. For each element nums[i] in the left half, we find the number of elements nums[j] in the right half that satisfy nums[i] > 2 * nums[j]. Because the arrays are sorted, we can find these counts efficiently without re-scanning the right half for every element of the left half. After counting, the two halves are merged to maintain the sorted order for the upper levels of recursion.
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 = mergeSort(nums, low, mid) + mergeSort(nums, mid + 1, high); // Count reverse pairs where one element is in the left half and the other in the right int j = mid + 1; for (int i = low; i <= mid; i++) { while (j <= high && (long)nums[i] > 2L * nums[j]) { j++; } count += j - (mid + 1); } // Merge the two sorted halves merge(nums, low, mid, high); return count; } private void merge(int[] nums, int low, int mid, int high) { int[] temp = new int[high - low + 1]; int i = low, j = mid + 1, k = 0; while (i <= mid && j <= high) { if (nums[i] <= nums[j]) { temp[k++] = nums[i++]; } else { temp[k++] = nums[j++]; } } while (i <= mid) { temp[k++] = nums[i++]; } while (j <= high) { temp[k++] = nums[j++]; } for (int l = 0; l < temp.length; l++) { nums[low + l] = temp[l]; } }}Complexity
Time
O(N log N). The algorithm's recurrence relation is T(N) = 2T(N/2) + O(N), which solves to O(N log N). The O(N) part comes from the linear-time scan for counting pairs and the linear-time merge operation at each level of recursion.
Space
O(N), due to the temporary array required for the merge step of the algorithm. The recursion depth adds O(log N) to the call stack, but the O(N) space for the merge buffer dominates.
Trade-offs
Pros
Highly efficient and optimal, passing the time limits for large inputs.
It is a classic and elegant application of the divide-and-conquer paradigm.
Cons
More complex to implement and reason about compared to the brute-force approach.
It either modifies the input array or requires an auxiliary array of the same size.
Solutions
Solution
class Solution {public int reversePairs(int[] nums) { TreeSet<Long> ts = new TreeSet<>(); for (int num : nums) { ts.add((long)num); ts.add((long)num * 2); } Map<Long, Integer> m = new HashMap<>(); int idx = 0; for (long num : ts) { m.put(num, ++idx); } BinaryIndexedTree tree = new BinaryIndexedTree(m.size()); int ans = 0; for (int i = nums.length - 1; i >= 0; --i) { int x = m.get((long)nums[i]); ans += tree.query(x - 1); tree.update(m.get((long)nums[i] * 2), 1); } return ans; }} class BinaryIndexedTree {private int n;private int[] c;public BinaryIndexedTree(int n) { this.n = n; c = new int[n + 1]; }public void update(int x, int delta) { while (x <= n) { c[x] += delta; x += lowbit(x); } }public int query(int x) { int s = 0; while (x > 0) { s += c[x]; x -= lowbit(x); } return s; }public static int lowbit(int x) { return x & -x; }}class BinaryIndexedTree { public: int n ; vector < int > c ; BinaryIndexedTree ( int _n ) : n ( _n ) , c ( _n + 1 ) {} void update ( int x , int delta ) { while ( x <= n ) { c [ x ] += delta ; x += lowbit ( x ); } } int query ( int x ) { int s = 0 ; while ( x > 0 ) { s += c [ x ]; x -= lowbit ( x ); } return s ; } int lowbit ( int x ) { return x & - x ; } }; class Solution { public: int reversePairs ( vector < int >& nums ) { set < long long > s ; for ( int num : nums ) { s . insert ( num ); s . insert ( num * 2ll ); } unordered_map < long long , int > m ; int idx = 0 ; for ( long long num : s ) m [ num ] = ++ idx ; BinaryIndexedTree * tree = new BinaryIndexedTree ( m . size ()); int ans = 0 ; for ( int i = nums . size () - 1 ; i >= 0 ; -- i ) { ans += tree -> query ( m [ nums [ i ]] - 1 ); tree -> update ( m [ nums [ i ] * 2ll ], 1 ); } return ans ; } };class BinaryIndexedTree : def __init__ ( self , n ): self . n = n self . c = [ 0 ] * ( n + 1 ) @ staticmethod def lowbit ( x ): return x & - x def update ( self , x , delta ): while x <= self . n : self . c [ x ] += delta x += BinaryIndexedTree . lowbit ( x ) def query ( self , x ): s = 0 while x > 0 : s += self . c [ x ] x -= BinaryIndexedTree . lowbit ( x ) return s class Solution : def reversePairs ( self , nums : List [ int ]) -> int : s = set () for num in nums : s . add ( num ) s . add ( num * 2 ) alls = sorted ( s ) m = { v : i for i , v in enumerate ( alls , 1 )} ans = 0 tree = BinaryIndexedTree ( len ( m )) for num in nums [:: - 1 ]: ans += tree . query ( m [ num ] - 1 ) tree . update ( m [ num * 2 ], 1 ) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.