870. Advantage Shuffle
Problem Description
You have two integer arrays nums1
and nums2
of equal length. The goal is to rearrange nums1
to maximize how many positions where nums1[i] > nums2[i]
.
The advantage is defined as the count of indices i
where nums1[i]
is greater than nums2[i]
. You need to find a permutation (reordering) of nums1
that gives the maximum possible advantage over nums2
.
For example, if nums1 = [2,7,11,15]
and nums2 = [1,10,4,11]
, one optimal arrangement could be [2,11,7,15]
which gives an advantage of 3 (at indices 0, 2, and 3 where 2>1, 7>4, and 15>11).
The solution uses a greedy strategy similar to the "Tian Ji's Horse Racing" problem. It sorts nums1
and creates a sorted list of (value, original_index)
pairs from nums2
. Then it uses two pointers to match elements:
- If the smallest unused element from
nums1
can beat the smallest unmatched element fromnums2
, use it there - Otherwise, "waste" it against the largest unmatched element from
nums2
(since it can't win anyway, might as well save better elements for winnable positions)
This greedy approach ensures maximum advantage by efficiently using elements from nums1
- winning where possible and strategically losing where necessary.
Intuition
The key insight comes from thinking about this as an optimal matching problem. We want to use our resources (elements in nums1
) as efficiently as possible to beat as many elements in nums2
as we can.
Think of it like a card game where you need to play your cards to beat your opponent's cards. If you have a card that's just barely strong enough to beat one of their cards, you should use it! But if your weakest card can't beat any of their remaining cards, you might as well "sacrifice" it against their strongest card to save your better cards for winnable matchups.
This leads us to a greedy strategy:
- Sort both arrays to understand the relative strengths
- Start with the smallest elements from both arrays
- If our smallest can beat their smallest, that's a win - use it!
- If our smallest can't beat their smallest, it probably can't beat anything else either, so "waste" it on their largest element
Why does this work? Consider what happens if we deviate from this strategy:
- If we use a stronger element from
nums1
where a weaker one would suffice, we're wasting potential - that stronger element might be needed elsewhere - If we don't sacrifice weak elements strategically, we might end up having to waste strong elements later
The two-pointer technique naturally implements this: pointer i
tracks the smallest unmatched element in nums2
(sorted), pointer j
tracks the largest. As we iterate through sorted nums1
, we either score a point by beating element at i
(and move i
forward), or sacrifice against element at j
(and move j
backward).
This greedy approach guarantees we get the maximum possible advantage because we're always making the locally optimal choice that preserves our ability to win future matchups.
Learn more about Greedy, Two Pointers and Sorting patterns.
Solution Approach
The implementation follows a greedy algorithm with sorting and two-pointer technique:
Step 1: Sort nums1
nums1.sort()
We sort nums1
in ascending order to process elements from weakest to strongest.
Step 2: Create sorted pairs from nums2
t = sorted((v, i) for i, v in enumerate(nums2))
We create tuples of (value, original_index)
from nums2
and sort them by value. This preserves the original positions while allowing us to work with sorted values. The variable t
stores these sorted pairs.
Step 3: Initialize result array and pointers
n = len(nums2)
ans = [0] * n
i, j = 0, n - 1
ans
: Result array to store the rearrangednums1
in original index positions ofnums2
i
: Points to the smallest unmatched element in sortednums2
j
: Points to the largest unmatched element in sortednums2
Step 4: Greedy assignment
for v in nums1: if v <= t[i][0]: ans[t[j][1]] = v j -= 1 else: ans[t[i][1]] = v i += 1
For each element v
in sorted nums1
:
- If
v <= t[i][0]
(current element can't beat the smallest unmatched innums2
):- Assign
v
to the position of the largest unmatched element (t[j][1]
gives original index) - Decrement
j
to move to next largest
- Assign
- Otherwise (current element can beat the smallest unmatched):
- Assign
v
to the position of the smallest unmatched element (t[i][1]
gives original index) - Increment
i
to move to next smallest
- Assign
The algorithm ensures optimal advantage by:
- Using weak elements efficiently when they can win
- Sacrificing weak elements that can't win against strong opponents
- Preserving stronger elements for winnable matchups
Time Complexity: O(n log n)
for sorting both arrays
Space Complexity: O(n)
for storing the sorted pairs and result array
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Input: nums1 = [12, 24, 8, 32]
, nums2 = [13, 25, 32, 11]
Step 1: Sort nums1
nums1 = [8, 12, 24, 32] (sorted in ascending order)
Step 2: Create sorted pairs from nums2
Original nums2: [13, 25, 32, 11] With indices: [(13,0), (25,1), (32,2), (11,3)] Sorted by value: [(11,3), (13,0), (25,1), (32,2)] t = [(11,3), (13,0), (25,1), (32,2)]
Step 3: Initialize variables
ans = [0, 0, 0, 0] (result array) i = 0 (points to (11,3)) j = 3 (points to (32,2))
Step 4: Process each element from sorted nums1
Processing v = 8:
- Compare: 8 <= 11? Yes
- Can't beat smallest (11), so sacrifice against largest (32)
- Place 8 at index 2:
ans = [0, 0, 8, 0]
- Move j from 3 to 2
Processing v = 12:
- Compare: 12 <= 11? No
- Can beat smallest (11)!
- Place 12 at index 3:
ans = [0, 0, 8, 12]
- Move i from 0 to 1
Processing v = 24:
- Compare: 24 <= 13? No
- Can beat next smallest (13)!
- Place 24 at index 0:
ans = [24, 0, 8, 12]
- Move i from 1 to 2
Processing v = 32:
- Compare: 32 <= 25? No
- Can beat next smallest (25)!
- Place 32 at index 1:
ans = [24, 32, 8, 12]
Final Result:
- Rearranged nums1:
[24, 32, 8, 12]
- Original nums2:
[13, 25, 32, 11]
- Comparisons: 24>13 β, 32>25 β, 8<32 β, 12>11 β
- Advantage = 3
The greedy strategy efficiently used the weakest element (8) against an unbeatable opponent (32), while saving stronger elements (12, 24, 32) for winnable matchups against (11, 13, 25).
Solution Implementation
1class Solution:
2 def advantageCount(self, nums1: List[int], nums2: List[int]) -> List[int]:
3 # Sort nums1 in ascending order for greedy matching
4 nums1.sort()
5
6 # Create tuples of (value, original_index) from nums2 and sort by value
7 # This allows us to track original positions while working with sorted values
8 sorted_nums2_with_indices = sorted((value, index) for index, value in enumerate(nums2))
9
10 # Get the length of the arrays
11 n = len(nums2)
12
13 # Initialize result array with zeros
14 result = [0] * n
15
16 # Two pointers: left for smallest unmatched in nums2, right for largest unmatched
17 left_pointer = 0
18 right_pointer = n - 1
19
20 # Greedy strategy: iterate through sorted nums1
21 for num in nums1:
22 # If current number from nums1 cannot beat the smallest unmatched from nums2
23 if num <= sorted_nums2_with_indices[left_pointer][0]:
24 # Use this number against the largest unmatched element (sacrifice strategy)
25 original_index = sorted_nums2_with_indices[right_pointer][1]
26 result[original_index] = num
27 right_pointer -= 1
28 else:
29 # Current number can beat the smallest unmatched, so match them
30 original_index = sorted_nums2_with_indices[left_pointer][1]
31 result[original_index] = num
32 left_pointer += 1
33
34 return result
35
1class Solution {
2 public int[] advantageCount(int[] nums1, int[] nums2) {
3 int n = nums1.length;
4
5 // Create a 2D array to store nums2 values with their original indices
6 // Each element is [value, original_index]
7 int[][] nums2WithIndex = new int[n][2];
8 for (int i = 0; i < n; ++i) {
9 nums2WithIndex[i] = new int[] {nums2[i], i};
10 }
11
12 // Sort nums2WithIndex by value in ascending order
13 Arrays.sort(nums2WithIndex, (a, b) -> a[0] - b[0]);
14
15 // Sort nums1 in ascending order
16 Arrays.sort(nums1);
17
18 // Initialize result array
19 int[] result = new int[n];
20
21 // Two pointers for nums2WithIndex
22 // left: points to smallest unmatched element in nums2
23 // right: points to largest unmatched element in nums2
24 int left = 0;
25 int right = n - 1;
26
27 // Greedy approach: iterate through sorted nums1
28 for (int currentNum : nums1) {
29 if (currentNum <= nums2WithIndex[left][0]) {
30 // Current number from nums1 cannot beat the smallest unmatched in nums2
31 // Use it against the largest unmatched element (waste it strategically)
32 int originalIndex = nums2WithIndex[right][1];
33 result[originalIndex] = currentNum;
34 right--;
35 } else {
36 // Current number from nums1 can beat the smallest unmatched in nums2
37 // Use it to gain advantage
38 int originalIndex = nums2WithIndex[left][1];
39 result[originalIndex] = currentNum;
40 left++;
41 }
42 }
43
44 return result;
45 }
46}
47
1class Solution {
2public:
3 vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2) {
4 int n = nums1.size();
5
6 // Create pairs of (value, original_index) for nums2 to track positions
7 vector<pair<int, int>> nums2WithIndex;
8 for (int i = 0; i < n; ++i) {
9 nums2WithIndex.push_back({nums2[i], i});
10 }
11
12 // Sort both arrays to apply greedy strategy
13 sort(nums2WithIndex.begin(), nums2WithIndex.end());
14 sort(nums1.begin(), nums1.end());
15
16 // Two pointers for nums2WithIndex
17 int left = 0; // Points to smallest unmatched element in sorted nums2
18 int right = n - 1; // Points to largest unmatched element in sorted nums2
19
20 vector<int> result(n);
21
22 // Greedy assignment: for each element in sorted nums1
23 for (int value : nums1) {
24 if (value <= nums2WithIndex[left].first) {
25 // Current value can't beat smallest unmatched in nums2
26 // Assign it to the largest unmatched element (waste it)
27 result[nums2WithIndex[right].second] = value;
28 right--;
29 } else {
30 // Current value can beat smallest unmatched in nums2
31 // Make the advantageous assignment
32 result[nums2WithIndex[left].second] = value;
33 left++;
34 }
35 }
36
37 return result;
38 }
39};
40
1/**
2 * Rearranges nums1 to maximize its advantage over nums2 (Leetcode 870: Advantage Shuffle)
3 * Uses a greedy approach: assign smallest values from nums1 that can beat nums2 elements,
4 * otherwise assign them to positions that are already lost
5 *
6 * @param nums1 - First array to be rearranged
7 * @param nums2 - Second array to compare against
8 * @returns Rearranged nums1 array that maximizes advantage over nums2
9 */
10function advantageCount(nums1: number[], nums2: number[]): number[] {
11 const arrayLength: number = nums1.length;
12
13 // Create an array of indices [0, 1, 2, ..., n-1]
14 const sortedIndices: number[] = Array.from({ length: arrayLength }, (_, index) => index);
15
16 // Sort indices based on corresponding values in nums2 (ascending order)
17 sortedIndices.sort((indexA: number, indexB: number) => nums2[indexA] - nums2[indexB]);
18
19 // Sort nums1 in ascending order
20 nums1.sort((a: number, b: number) => a - b);
21
22 // Initialize result array with zeros
23 const result: number[] = new Array(arrayLength).fill(0);
24
25 // Two pointers for tracking positions in sortedIndices
26 let leftPointer: number = 0; // Points to smallest unassigned element in nums2
27 let rightPointer: number = arrayLength - 1; // Points to largest unassigned element in nums2
28
29 // Iterate through sorted nums1 and assign each element optimally
30 for (let i = 0; i < arrayLength; i++) {
31 // If current nums1 element can beat the smallest unassigned nums2 element
32 if (nums1[i] > nums2[sortedIndices[leftPointer]]) {
33 // Assign it to beat that position
34 result[sortedIndices[leftPointer]] = nums1[i];
35 leftPointer++;
36 } else {
37 // Otherwise, assign it to the largest unassigned position (already lost)
38 result[sortedIndices[rightPointer]] = nums1[i];
39 rightPointer--;
40 }
41 }
42
43 return result;
44}
45
Time and Space Complexity
Time Complexity: O(n log n)
where n
is the length of the input arrays.
- Sorting
nums1
takesO(n log n)
time - Creating the sorted list
t
fromnums2
with indices takesO(n log n)
time (creating the tuples isO(n)
and sorting them isO(n log n)
) - The for loop iterates through all elements in
nums1
once, performing constant time operations in each iteration, which takesO(n)
time - Overall:
O(n log n) + O(n log n) + O(n) = O(n log n)
Space Complexity: O(n)
where n
is the length of the input arrays.
- The sorted list
t
storesn
tuples, each containing a value and index, requiringO(n)
space - The answer array
ans
storesn
elements, requiringO(n)
space - The sorting operation on
nums1
is done in-place (for Python's Timsort, the worst-case space isO(n)
) - Variables
i
,j
, andv
use constantO(1)
space - Overall:
O(n) + O(n) + O(n) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Pointer Movement Logic
A common mistake is confusing when to move which pointer. Developers often incorrectly move the left pointer when sacrificing an element or move the right pointer when winning a matchup.
Incorrect Implementation:
for num in nums1: if num <= sorted_nums2_with_indices[left_pointer][0]: # Wrong: moving left pointer when sacrificing result[sorted_nums2_with_indices[left_pointer][1]] = num left_pointer += 1 # This is wrong! else: # Wrong: using right pointer when winning result[sorted_nums2_with_indices[right_pointer][1]] = num right_pointer -= 1 # This is wrong!
Why it's wrong: When we can't win (num <= smallest unmatched), we should sacrifice against the largest unmatched element to save potential wins for later. The logic should be inverted.
Correct Approach:
for num in nums1: if num <= sorted_nums2_with_indices[left_pointer][0]: # Sacrifice against largest unmatched result[sorted_nums2_with_indices[right_pointer][1]] = num right_pointer -= 1 else: # Win against smallest unmatched result[sorted_nums2_with_indices[left_pointer][1]] = num left_pointer += 1
2. Not Preserving Original Indices
Another pitfall is forgetting to track the original indices of nums2 when sorting, leading to incorrect placement in the result array.
Incorrect Implementation:
nums1.sort()
nums2_sorted = sorted(nums2) # Lost original indices!
result = []
# Now we have no way to know where each element should go
for num in nums1:
# Can't map back to original positions
...
Correct Approach:
# Preserve original indices using enumerate
sorted_nums2_with_indices = sorted((value, index) for index, value in enumerate(nums2))
# Now sorted_nums2_with_indices[i][1] gives us the original index
3. Off-by-One Errors with Comparison
Using <
instead of <=
in the comparison can lead to suboptimal solutions.
Incorrect Implementation:
if num < sorted_nums2_with_indices[left_pointer][0]: # Using < instead of <= # Sacrifice ...
Why it matters: When nums1[i] == nums2[j]
, there's no advantage gained. These equal cases should be treated as losses and handled by the sacrifice strategy. Using <
would incorrectly treat equal values as wins, potentially wasting elements that could win elsewhere.
4. Modifying Input Arrays Without Consideration
If the problem requires preserving the original arrays or if they're used elsewhere, modifying nums1 directly with nums1.sort()
could cause issues.
Safe Approach:
def advantageCount(self, nums1: List[int], nums2: List[int]) -> List[int]:
# Create a copy if original needs to be preserved
sorted_nums1 = sorted(nums1) # Creates new sorted list
# ... rest of the logic
Which data structure is used in a depth first search?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Donβt Miss This!