LeetCode 791 - Custom Sort String
The problem gives us two strings, order and s. The string order defines a custom character ordering. Unlike normal alphabetical ordering, the characters in order specify exactly how characters should be prioritized relative to one another.
Difficulty: 🟡 Medium
Topics: Hash Table, String, Sorting
Solution
LeetCode 791, Custom Sort String
Problem Understanding
The problem gives us two strings, order and s.
The string order defines a custom character ordering. Unlike normal alphabetical ordering, the characters in order specify exactly how characters should be prioritized relative to one another. Every character in order is unique, which means there are no conflicts or duplicate priorities.
The string s contains the characters we must rearrange. Our goal is to permute the characters in s so that any characters appearing in order follow the same relative ordering defined by order.
For example, if order = "cba", then:
'c'must appear before'b''b'must appear before'a'
Any characters in s that do not appear in order can be placed anywhere in the final string.
The important detail is that the problem only requires relative ordering for characters present in order. Characters outside of order are unrestricted.
The constraints are small:
order.length <= 26s.length <= 200
Since only lowercase English letters are involved, the total possible distinct characters is very limited. This means we can comfortably use hash maps, counting arrays, or even sorting approaches without performance concerns.
Several edge cases are important:
- Characters in
smay not exist inorder smay contain repeated charactersordermay contain characters that never appear insordercould contain only one character- All characters in
scould be outsideorder
The problem guarantees that all characters in order are unique, which simplifies implementation because we never need to resolve conflicting priorities.
Approaches
Brute Force Approach
A straightforward solution is to repeatedly scan s and build the result according to the order specified in order.
We can process each character in order, and for every such character, scan through s to collect all matching occurrences. After that, we perform another pass through s to append characters not present in order.
This works because:
- Characters from
orderare appended in the correct relative sequence - Characters not in
orderare added afterward
However, this approach repeatedly scans the string. If order has length m and s has length n, then the repeated scans lead to O(m * n) time complexity.
Although the constraints are small enough that this would pass, it is not the most efficient or elegant solution.
Optimal Approach
The key insight is that we only care about character frequencies.
Instead of repeatedly scanning s, we can first count how many times each character appears. Then:
- Iterate through
order - Append each character as many times as it appears in
s - Append any remaining characters afterward
This avoids redundant scanning and produces the answer in linear time.
A hash map is ideal here because:
- We need fast frequency lookup
- Characters are used as keys
- Frequency counting is naturally modeled with a dictionary
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(m × n) | O(n) | Repeatedly scans s for each character in order |
| Optimal | O(m + n) | O(1) or O(k) | Uses frequency counting for direct construction |
Here:
m = len(order)n = len(s)kis the number of distinct characters, at most 26
Algorithm Walkthrough
Optimal Algorithm
- Create a frequency map for all characters in
s.
We count how many times each character appears because the final string must preserve every occurrence. 2. Initialize an empty result list.
Using a list is more efficient than repeated string concatenation because strings are immutable in Python.
3. Iterate through each character in order.
For every character:
- Check how many times it appears in
s - Append that character to the result exactly that many times
- Remove or reset its count afterward
This guarantees that all ordered characters appear in the correct relative sequence. 4. Iterate through the remaining characters in the frequency map.
These are characters that were not present in order.
Append each remaining character according to its frequency. 5. Join the result list into a string and return it.
Why it works
The algorithm works because every character in order is processed sequentially in the exact required order. Since all occurrences of a character are added before moving to the next ordered character, the relative ordering constraint is satisfied.
Characters not present in order have no restrictions, so appending them afterward still produces a valid answer.
Every character from s is included exactly as many times as it originally appeared, ensuring the result is a valid permutation of s.
Python Solution
from collections import Counter
class Solution:
def customSortString(self, order: str, s: str) -> str:
frequency = Counter(s)
result = []
for char in order:
if char in frequency:
result.append(char * frequency[char])
del frequency[char]
for char, count in frequency.items():
result.append(char * count)
return "".join(result)
The implementation begins by using Counter from Python's collections module to count character frequencies in s.
The result list stores partial string fragments efficiently. Instead of appending characters one by one, the code appends repeated strings such as "c" * 3, which is both simple and efficient.
The first loop processes characters according to the custom order. If a character exists in the frequency map, all its occurrences are appended immediately.
After processing ordered characters, the remaining entries in the frequency map correspond to characters not found in order. Since these characters have no ordering restrictions, they are appended in any order.
Finally, "".join(result) combines all fragments into the final string.
Go Solution
func customSortString(order string, s string) string {
frequency := make(map[rune]int)
for _, ch := range s {
frequency[ch]++
}
result := make([]rune, 0)
for _, ch := range order {
count, exists := frequency[ch]
if exists {
for i := 0; i < count; i++ {
result = append(result, ch)
}
delete(frequency, ch)
}
}
for ch, count := range frequency {
for i := 0; i < count; i++ {
result = append(result, ch)
}
}
return string(result)
}
The Go solution follows the same logic as the Python implementation.
A map[rune]int stores character frequencies. Since Go strings are UTF-8 encoded, iterating with range produces rune values safely.
The result is built using a rune slice because repeated string concatenation in Go is inefficient. After construction, the rune slice is converted back into a string.
Unlike Python's Counter, Go requires manual incrementing and explicit deletion from the map.
Worked Examples
Example 1
Input:
order = "cba"
s = "abcd"
Step 1: Build Frequency Map
| Character | Count |
|---|---|
| a | 1 |
| b | 1 |
| c | 1 |
| d | 1 |
Step 2: Process order
| Current Character | Action | Result |
|---|---|---|
| c | Append "c" |
"c" |
| b | Append "b" |
"cb" |
| a | Append "a" |
"cba" |
Remaining frequency map:
| Character | Count |
|---|---|
| d | 1 |
Step 3: Append Remaining Characters
Append "d".
Final result:
"cbad"
Example 2
Input:
order = "bcafg"
s = "abcd"
Step 1: Build Frequency Map
| Character | Count |
|---|---|
| a | 1 |
| b | 1 |
| c | 1 |
| d | 1 |
Step 2: Process order
| Current Character | Action | Result |
|---|---|---|
| b | Append "b" |
"b" |
| c | Append "c" |
"bc" |
| a | Append "a" |
"bca" |
| f | Not present | "bca" |
| g | Not present | "bca" |
Remaining frequency map:
| Character | Count |
|---|---|
| d | 1 |
Step 3: Append Remaining Characters
Append "d".
Final result:
"bcad"
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m + n) | One pass for counting and one pass for construction |
| Space | O(1) or O(k) | At most 26 lowercase English letters |
The algorithm performs a single pass over s to compute frequencies and a single pass over order plus remaining characters to construct the result.
Because the alphabet is limited to lowercase English letters, the auxiliary storage never grows beyond 26 entries, making the effective space complexity constant.
Test Cases
solution = Solution()
# Provided examples
assert solution.customSortString("cba", "abcd") in {
"cbad", "cbda", "cdba", "dcba"
} # basic custom ordering
assert solution.customSortString("bcafg", "abcd") in {
"bcad", "bcda", "dbca"
} # characters outside order
# Single character order
assert solution.customSortString("a", "aaaa") == "aaaa" # repeated same character
# Characters not in order
result = solution.customSortString("abc", "xyz")
assert sorted(result) == sorted("xyz") # no ordered characters
# Mixed ordered and unordered characters
result = solution.customSortString("kqep", "pekeq")
assert result.startswith("kqe") or result.startswith("kqqe")
# Empty ordering effect
result = solution.customSortString("z", "abc")
assert sorted(result) == sorted("abc") # order irrelevant for all chars
# Duplicate characters
assert solution.customSortString("cba", "aaabbbccc") == "cccbbbaaa"
# Order contains unused characters
result = solution.customSortString("xyzabc", "abccba")
assert result == "aabbcc"
# All characters already in correct order
assert solution.customSortString("abc", "aabbcc") == "aabbcc"
# Reverse ordering
assert solution.customSortString("zyx", "xyzxyz") == "zzyyxx"
Test Summary
| Test | Why |
|---|---|
"cba", "abcd" |
Verifies standard custom ordering |
"bcafg", "abcd" |
Ensures unordered characters are handled |
"a", "aaaa" |
Tests repeated identical characters |
"abc", "xyz" |
Tests case where no characters appear in order |
"kqep", "pekeq" |
Tests mixed ordering and duplicates |
"z", "abc" |
Tests minimal order string |
"cba", "aaabbbccc" |
Tests heavy duplication |
"xyzabc", "abccba" |
Tests unused order characters |
"abc", "aabbcc" |
Tests already sorted input |
"zyx", "xyzxyz" |
Tests reverse custom ordering |
Edge Cases
Characters in s Not Present in order
One common source of bugs is forgetting about characters that do not appear in the custom ordering string.
For example:
order = "abc"
s = "abcdxyz"
The problem only constrains the relative order of 'a', 'b', and 'c'. Characters like 'd', 'x', 'y', and 'z' may appear anywhere.
The implementation handles this correctly by leaving those characters in the frequency map after processing order, then appending them afterward.
Duplicate Characters
Another important case is repeated characters.
For example:
order = "cba"
s = "aaabbbccc"
A naive implementation might accidentally append only one occurrence of each character.
The frequency map guarantees correctness because every character is appended exactly according to its stored count.
Order Contains Unused Characters
The order string may include characters that never appear in s.
For example:
order = "xyzabc"
s = "aabbcc"
Characters 'x', 'y', and 'z' are irrelevant because they do not appear in s.
The implementation safely skips them because the frequency map lookup simply fails, resulting in no additions to the result.