1636. Sort Array by Increasing Frequency
Problem Description
You are given an array of integers called nums
. Your task is to sort this array according to specific rules:
-
Primary sorting rule: Sort the values based on their frequency (how many times they appear) in increasing order. This means values that appear less frequently should come before values that appear more frequently.
-
Secondary sorting rule: When multiple values have the same frequency, sort those values in decreasing order (larger values come before smaller values).
The function should return the sorted array following these rules.
For example, if you have an array like [1,1,2,2,2,3]
:
- The frequency of
1
is 2 - The frequency of
2
is 3 - The frequency of
3
is 1
After sorting:
3
comes first (frequency 1)1
comes next (frequency 2)2
comes last (frequency 3)
When values have the same frequency, they should be arranged from largest to smallest. So the final result would be [3,1,1,2,2,2]
.
The solution uses Python's Counter
to count frequencies and sorted()
with a custom key function. The key (cnt[x], -x)
sorts first by frequency cnt[x]
in ascending order, then by the negative value -x
to achieve descending order for the values themselves when frequencies are equal.
Intuition
The problem asks us to sort based on two criteria: frequency first, then value. This immediately suggests we need a way to count how often each number appears and then use custom sorting logic.
The key insight is recognizing this as a multi-level sorting problem. When we need to sort by multiple criteria in Python, we can use a tuple as the sorting key. Python sorts tuples by comparing elements from left to right - first by the first element, then by the second element if the first elements are equal, and so on.
For counting frequencies, a hash map (or Python's Counter
) is the natural choice since we need to look up how many times each value appears. This gives us O(1) access to any number's frequency.
The trickier part is handling the secondary sorting rule. When frequencies are equal, we want larger numbers first (descending order). However, Python's default sort is ascending. To achieve descending order while keeping the primary sort ascending, we can use a clever trick: negate the values. Since -5 < -3
but 5 > 3
, negating the values reverses their sort order.
This leads us to the solution: create a sorting key of (frequency, -value)
. The first element handles sorting by frequency in ascending order, and the second element (negated value) handles the tie-breaking in descending order. The sorted()
function with this custom key does all the work in one pass, maintaining the relative positions as needed.
The beauty of this approach is its simplicity - we transform a complex two-condition sorting problem into a standard sort operation with a well-crafted key function.
Learn more about Sorting patterns.
Solution Approach
The implementation uses a straightforward approach with Python's built-in data structures and sorting capabilities.
Step 1: Count Frequencies
We use Python's Counter
from the collections module to count the frequency of each number in the array:
cnt = Counter(nums)
This creates a dictionary-like object where keys are the unique numbers from nums
and values are their frequencies. For example, if nums = [1,1,2,2,2,3]
, then cnt
would be {1: 2, 2: 3, 3: 1}
.
Step 2: Custom Sorting The core of the solution is the sorting with a custom key function:
return sorted(nums, key=lambda x: (cnt[x], -x))
Let's break down how this works:
sorted(nums, ...)
iterates through each element in the original array- For each element
x
, the lambda function creates a tuple(cnt[x], -x)
cnt[x]
gives the frequency of that element-x
is the negated value of the element
- Python sorts based on this tuple key
How the Tuple Sorting Works: When Python compares two tuples, it follows lexicographic ordering:
- First, compare the first elements (frequencies)
- If frequencies are equal, compare the second elements (negated values)
For example, consider sorting [1, 1, 2, 2, 2, 3]
:
- Element
1
gets key(2, -1)
- Element
2
gets key(3, -2)
- Element
3
gets key(1, -3)
Sorting these keys in ascending order:
(1, -3)
comes first (lowest frequency)(2, -1)
comes second(3, -2)
comes last (highest frequency)
This gives us the final sorted array: [3, 1, 1, 2, 2, 2]
.
The time complexity is O(n log n)
due to sorting, and space complexity is O(n)
for storing the frequency counter.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a detailed example with the array [4, 4, 6, 6, 6, 1, 1, 2]
.
Step 1: Count Frequencies First, we count how many times each number appears:
1
appears 2 times2
appears 1 time4
appears 2 times6
appears 3 times
So our frequency counter becomes: {4: 2, 6: 3, 1: 2, 2: 1}
Step 2: Assign Sorting Keys
For each element in the original array, we create a sorting key (frequency, -value)
:
4
β(2, -4)
(frequency is 2, negated value is -4)4
β(2, -4)
6
β(3, -6)
(frequency is 3, negated value is -6)6
β(3, -6)
6
β(3, -6)
1
β(2, -1)
(frequency is 2, negated value is -1)1
β(2, -1)
2
β(1, -2)
(frequency is 1, negated value is -2)
Step 3: Sort by Keys Now we sort these keys in ascending order:
(1, -2)
- lowest frequency (1), so2
comes first(2, -4)
- frequency 2, value 4(2, -4)
- same as above(2, -1)
- frequency 2, but -1 > -4, so1
comes after4
(2, -1)
- same as above(3, -6)
- highest frequency (3), so6
comes last(3, -6)
- same as above(3, -6)
- same as above
Notice how when frequencies are equal (both 4
and 1
have frequency 2), we sort by the second element of the tuple. Since -4 < -1
, the value 4
(larger original value) comes before 1
(smaller original value), achieving descending order for values with the same frequency.
Final Result: [2, 4, 4, 1, 1, 6, 6, 6]
The array is now sorted with:
2
first (frequency 1)4
and1
next (both frequency 2, with4 > 1
so4
comes first)6
last (frequency 3)
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def frequencySort(self, nums: List[int]) -> List[int]:
6 # Count the frequency of each number in the array
7 frequency_map = Counter(nums)
8
9 # Sort the array based on two criteria:
10 # 1. Primary: frequency in ascending order (less frequent first)
11 # 2. Secondary: value in descending order (larger values first when frequencies are equal)
12 sorted_nums = sorted(nums, key=lambda num: (frequency_map[num], -num))
13
14 return sorted_nums
15
1class Solution {
2 public int[] frequencySort(int[] nums) {
3 // Create frequency array for values ranging from -100 to 100
4 // Index 0 represents -100, index 200 represents 100
5 int[] frequency = new int[201];
6
7 // Create list to store shifted values for sorting
8 List<Integer> shiftedValues = new ArrayList<>();
9
10 // Count frequency of each number and add shifted values to list
11 for (int num : nums) {
12 // Shift value by 100 to handle negative numbers (range becomes 0 to 200)
13 int shiftedValue = num + 100;
14 frequency[shiftedValue]++;
15 shiftedValues.add(shiftedValue);
16 }
17
18 // Sort the list based on custom comparator:
19 // - Primary: ascending order by frequency
20 // - Secondary: descending order by value (if frequencies are equal)
21 shiftedValues.sort((a, b) -> {
22 if (frequency[a] == frequency[b]) {
23 // If frequencies are equal, sort by value in descending order
24 return b - a;
25 } else {
26 // Otherwise, sort by frequency in ascending order
27 return frequency[a] - frequency[b];
28 }
29 });
30
31 // Build result array by shifting values back to original range
32 int[] result = new int[nums.length];
33 int index = 0;
34 for (int shiftedValue : shiftedValues) {
35 // Shift back by subtracting 100 to get original value
36 result[index++] = shiftedValue - 100;
37 }
38
39 return result;
40 }
41}
42
1class Solution {
2public:
3 vector<int> frequencySort(vector<int>& nums) {
4 // Create frequency counter array for range [-100, 100]
5 // Index mapping: value v maps to index (v + 100)
6 vector<int> frequencyCount(201);
7
8 // Count frequency of each number
9 for (int value : nums) {
10 ++frequencyCount[value + 100];
11 }
12
13 // Sort nums array based on custom comparator
14 sort(nums.begin(), nums.end(), [&](const int a, const int b) {
15 // If frequencies are equal, sort in descending order by value
16 if (frequencyCount[a + 100] == frequencyCount[b + 100]) {
17 return a > b;
18 }
19 // Otherwise, sort in ascending order by frequency
20 return frequencyCount[a + 100] < frequencyCount[b + 100];
21 });
22
23 return nums;
24 }
25};
26
1/**
2 * Sorts an array of numbers by their frequency in ascending order.
3 * If two numbers have the same frequency, they are sorted in descending order by value.
4 *
5 * @param nums - The input array of numbers to sort
6 * @returns The sorted array based on frequency and value
7 */
8function frequencySort(nums: number[]): number[] {
9 // Create a frequency map to store the count of each number
10 const frequencyMap = new Map<number, number>();
11
12 // Count the frequency of each number in the array
13 for (const num of nums) {
14 frequencyMap.set(num, (frequencyMap.get(num) ?? 0) + 1);
15 }
16
17 // Sort the array based on:
18 // 1. Primary: frequency in ascending order (lower frequency first)
19 // 2. Secondary: value in descending order (higher value first) when frequencies are equal
20 return nums.sort((a: number, b: number) => {
21 const frequencyDifference = frequencyMap.get(a)! - frequencyMap.get(b)!;
22
23 // If frequencies are different, sort by frequency (ascending)
24 if (frequencyDifference !== 0) {
25 return frequencyDifference;
26 }
27
28 // If frequencies are the same, sort by value (descending)
29 return b - a;
30 });
31}
32
Time and Space Complexity
Time Complexity: O(n log n)
The algorithm consists of two main operations:
- Creating a frequency counter using
Counter(nums)
takesO(n)
time, wheren
is the length of the input array - Sorting the array using
sorted()
with a custom key takesO(n log n)
time in the worst case
Since O(n log n)
dominates O(n)
, the overall time complexity is O(n log n)
.
Space Complexity: O(n)
The space usage comes from:
- The
Counter
objectcnt
stores at mostn
key-value pairs (in the worst case where all elements are unique), requiringO(n)
space - The
sorted()
function creates a new list containing alln
elements, requiringO(n)
space - The sorting algorithm itself (Timsort in Python) uses
O(n)
auxiliary space in the worst case
Therefore, the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle the Secondary Sort Correctly
A common mistake is to forget about the secondary sorting rule or implement it incorrectly. Developers might write:
Incorrect Implementation:
# Wrong: This sorts values in ascending order when frequencies are equal
sorted_nums = sorted(nums, key=lambda x: frequency_map[x])
This would sort by frequency alone, leaving the order of elements with the same frequency undefined or in their original order. For example, with [4, 5, 6, 5, 4, 3]
:
- Frequencies:
3
appears 1 time,6
appears 1 time,4
appears 2 times,5
appears 2 times - Wrong output might be:
[3, 6, 4, 4, 5, 5]
- Correct output should be:
[6, 3, 5, 5, 4, 4]
(larger values first when frequencies match)
Solution:
Always include the secondary sort criterion using -x
to ensure larger values come first when frequencies are equal:
sorted_nums = sorted(nums, key=lambda x: (frequency_map[x], -x))
2. Integer Overflow with Negation
When dealing with very large integers or the minimum integer value, negating them might cause issues in some programming languages. In Python, this isn't typically a problem due to arbitrary precision integers, but when translating to other languages like Java or C++, -x
could overflow.
Solution for Other Languages: Instead of using negation, you can use a custom comparator or reverse the comparison logic:
# Alternative approach without negation
from functools import cmp_to_key
def compare(a, b):
if frequency_map[a] != frequency_map[b]:
return frequency_map[a] - frequency_map[b] # Sort by frequency ascending
return b - a # Sort by value descending
sorted_nums = sorted(nums, key=cmp_to_key(compare))
3. Modifying the Original Array In-Place
If the problem requires preserving the original array, using nums.sort()
instead of sorted(nums)
would modify the input array:
Incorrect if original array must be preserved:
nums.sort(key=lambda x: (frequency_map[x], -x)) return nums # Original array is modified
Solution:
Use sorted()
which returns a new sorted list without modifying the original:
return sorted(nums, key=lambda x: (frequency_map[x], -x))
4. Not Handling Empty Arrays
While the current solution handles empty arrays correctly (Counter and sorted both work with empty lists), it's good practice to explicitly check for edge cases:
Enhanced Solution:
def frequencySort(self, nums: List[int]) -> List[int]:
if not nums: # Explicit check for empty array
return []
frequency_map = Counter(nums)
return sorted(nums, key=lambda x: (frequency_map[x], -x))
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!