LeetCode #451 Medium

Sort Characters By Frequency

Rearrange a string so characters appear in blocks ordered by descending frequency.

Constraints
  • 1 <= s.length <= 5 * 10⁵
  • s consists of uppercase and lowercase English letters and digits.
heaphash-mapsortingstring
Open on LeetCode ↗
02

Intuition

Sort characters by frequency rearranges a string so characters appear in descending order of how often they occur, with all copies of each character grouped together. The shape is direct — count, then order by count: - Tally each character's frequency, sort the distinct characters by that frequency descending, then emit each character repeated its count times. Sorting the distinct characters rather than the string itself is what keeps this efficient. There are at most k distinct characters, so the sort is O(k log k) rather than O(n log n) — a real difference when the string is long and its alphabet small. The output must group identical characters, not interleave them. Emitting each character's full run at once satisfies that automatically. Characters with equal frequency may appear in any relative order, so no tie-breaking rule is needed — a detail worth confirming against the problem statement rather than assuming. A bucket sort avoids comparison sorting entirely. Frequencies range from 1 to n, so bucketing characters by count and reading the buckets from high to low is O(n). That is asymptotically better, though the comparison sort is simpler and fast enough given the small alphabet. Building the result with repeated string concatenation is O(n²) in languages with immutable strings. Appending to a list and joining once keeps it linear. The input may include uppercase, lowercase, and digits, so a hash map is safer than a fixed 26-element array here. Counting is O(n), sorting O(k log k), and building O(n), giving O(n + k log k) overall.

How to spot this pattern

Count, sort the distinct characters by count descending, then emit each one repeated. Sorting the 26-ish distinct keys rather than the string's characters is what keeps this near-linear in the input length.

03

Approach

Try it first

Before reading on: price up what counting everything costs here, then ask why only the extreme element matters and the rest need no ordering. Aim for O(n + k log k) time and O(n) space.

1

Count each character

Tally frequencies in one pass. A hash map is safer than a fixed array here, since the input includes uppercase, lowercase, and digits.

2

Sort the distinct characters

Sort the k distinct characters, not the n positions. That makes the sort O(k log k) rather than O(n log n) on a long string.

3

Emit each run together

Write every copy of a character consecutively. The output must group identical characters, which emitting full runs satisfies automatically.

4

Ignore ties

Characters with equal frequency may appear in any relative order, so no tie-breaking is required — worth confirming rather than assuming.

5

Consider bucket sort

Frequencies range from 1 to n, so bucketing by count and reading high to low is O(n), avoiding comparison sorting altogether.

6

Build with a join

Repeated concatenation is O(n²) on immutable strings. Append to a list and join once to keep the construction linear.

7

Cost of the approach

Counting is O(n), sorting O(k log k), building O(n) — O(n + k log k) overall with O(n) space.

04

Solution & live demo

▶1class Solution:
▶2 def frequencySort(self, s:
▶3 str) -> str:
▶4 from collections import Counter
▶5 counts = Counter(s)
▶6 ordered = sorted(counts, key=lambda c: -counts[c])
▶7 result = []
▶8 for c in ordered:
▶9 result.append(c * counts[c])
▶10 return ''.join(result)
05

Common pitfalls

Sorting the characters of the string

✗ Wrong
return ''.join(sorted(s, key=lambda c: -counts[c]))
✓ Right
ordered = sorted(counts, key=lambda c: -counts[c])

That's O(n log n) on the full string rather than O(k log k) on the distinct keys. It also relies on sort stability to keep equal-frequency characters grouped, which is fragile reasoning.

Building the result character by character

✗ Wrong
for c in ordered:
    for _ in range(counts[c]): result.append(c)
✓ Right
result.append(c * counts[c])

String repetition emits the whole run in one operation. The inner loop does the same work with per-character overhead and more code.

Sorting ascending

✗ Wrong
sorted(counts, key=lambda c: counts[c])
✓ Right
sorted(counts, key=lambda c: -counts[c])

The problem asks for decreasing frequency, so the most common character must come first. The negation (or reverse=True) is the whole ordering requirement.

06

Edge cases

empty string

the frequency map is empty, the loop over distinct characters does nothing, and the result is the empty string

all characters distinct

every count is 1, so any order among them is a valid answer since ties break arbitrarily

one character repeated

a single distinct entry with a large count still emits correctly as one block

mixed case letters

'A' and 'a' are different keys in the frequency map, since character equality is case-sensitive

07

Complexity

Time
O(n + k log k)
Space
O(n)
n is the string length, k is the number of distinct characters; counting is O(n), sorting the distinct keys is O(k log k).