Compare Strings by Frequency of the Smallest Character
MedPrompt
Let the function f(s) be the frequency of the lexicographically smallest character in a non-empty string s. For example, if s = "dcce" then f(s) = 2 because the lexicographically smallest character is 'c', which has a frequency of 2.
You are given an array of strings words and another array of query strings queries. For each query queries[i], count the number of words in words such that f(queries[i]) < f(W) for each W in words.
Return an integer array answer, where each answer[i] is the answer to the ith query.
Example 1:
Input: queries = ["cbd"], words = ["zaaaz"]
Output: [1]
Explanation: On the first query we have f("cbd") = 1, f("zaaaz") = 3 so f("cbd") < f("zaaaz").Example 2:
Input: queries = ["bbb","cc"], words = ["a","aa","aaa","aaaa"]
Output: [1,2]
Explanation: On the first query only f("bbb") < f("aaaa"). On the second query both f("aaa") and f("aaaa") are both > f("cc").
Constraints:
1 <= queries.length <= 20001 <= words.length <= 20001 <= queries[i].length, words[i].length <= 10queries[i][j],words[i][j]consist of lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly translates the problem statement into code. For each query string, we iterate through all the words in the words array. In each inner iteration, we calculate the frequency of the smallest character for both the query string and the current word string and compare them.
Algorithm
- Initialize an integer array
answerof the same size asqueries. - Define a helper function
f(s)that calculates the frequency of the smallest character ins. - Loop through each
queryat indexifrom0toqueries.length - 1. - Calculate
queryFreq = f(queries[i]). - Initialize a counter
count = 0. - Loop through each
wordin thewordsarray. - Calculate
wordFreq = f(word). - If
queryFreq < wordFreq, incrementcount. - After the inner loop finishes, set
answer[i] = count. - Return
answer.
Walkthrough
We'll need a helper function, let's call it f(s), that takes a string s and returns the frequency of its lexicographically smallest character. To implement f(s), we can use a frequency map (an array of size 26 for lowercase English letters). We iterate through the string s to populate this map. Then, we iterate through the map from index 0 to 25 (representing 'a' to 'z') and return the first non-zero frequency we find.
The main function will iterate through each query in the queries array. For each query, we first calculate its frequency, f(query). Then, we start a nested loop to iterate through every word in the words array. Inside the nested loop, we calculate f(word). If f(query) < f(word), we increment a counter for the current query. After checking all words, the value of the counter is the result for the current query, which we store in our answer array. This process is repeated for all queries.
class Solution { public int[] numSmallerByFrequency(String[] queries, String[] words) { int[] answer = new int[queries.length]; for (int i = 0; i < queries.length; i++) { int queryFreq = f(queries[i]); int count = 0; for (String word : words) { if (queryFreq < f(word)) { count++; } } answer[i] = count; } return answer; } private int f(String s) { int[] counts = new int[26]; for (char c : s.toCharArray()) { counts[c - 'a']++; } for (int count : counts) { if (count > 0) { return count; } } return 0; }}Complexity
Time
O(M * N * L), where `M` is the number of queries, `N` is the number of words, and `L` is the maximum length of a string. For each of the `M` queries, we iterate through `N` words, and for each pair, we call the `f` function which takes O(L) time.
Space
O(M) for the output array. If we exclude the output array, the auxiliary space is O(1) (the frequency map in `f` has a constant size of 26).
Trade-offs
Pros
Simple to understand and implement.
Cons
Inefficient due to redundant calculations. The
f(word)is recalculated for every query, leading to a high time complexity.
The brute-force approach is slow because it repeatedly calculates the frequencies for the words array. We can optimize this by pre-calculating all these frequencies once and storing them. Then, for each query, we can efficiently find the number of words with a higher frequency by sorting the pre-calculated frequencies and using binary search.
Algorithm
- Define a helper function
f(s)as in the previous approach. - Create an integer array
wordFreqsof sizewords.length. - Iterate through the
wordsarray, and for eachwordat indexj, computewordFreqs[j] = f(words[j]). - Sort the
wordFreqsarray. - Initialize an integer array
answerof sizequeries.length. - Loop through each
queryat indexifrom0toqueries.length - 1. - Calculate
queryFreq = f(queries[i]). - Perform a binary search on
wordFreqsto find the number of elements greater thanqueryFreq. This can be done by finding the index of the first element strictly greater thanqueryFreq. Let this index bek. - The count is
wordFreqs.length - k. - Store this count in
answer[i]. - Return
answer.
Walkthrough
First, we create an integer array, say wordFreqs, with the same size as the words array. We iterate through the words array once, calculate f(word) for each word, and store the result in wordFreqs. This avoids redundant calculations. To quickly count how many words have a frequency greater than a query's frequency, we can sort the wordFreqs array in ascending order.
Now, for each query, we calculate its frequency, q_freq. We then perform a binary search on the sorted wordFreqs array to find the index of the first element that is strictly greater than q_freq. If such an element is found at index k, then all elements from k to the end of the array (wordFreqs.length - 1) will also be greater than q_freq. The number of such elements is wordFreqs.length - k. This is the answer for the current query. If no such element is found, it means all word frequencies are less than or equal to q_freq, so the answer is 0.
import java.util.Arrays; class Solution { public int[] numSmallerByFrequency(String[] queries, String[] words) { int[] wordFreqs = new int[words.length]; for (int i = 0; i < words.length; i++) { wordFreqs[i] = f(words[i]); } Arrays.sort(wordFreqs); int[] answer = new int[queries.length]; for (int i = 0; i < queries.length; i++) { int queryFreq = f(queries[i]); // Binary search to find first element > queryFreq int left = 0, right = wordFreqs.length - 1; int firstGreaterIndex = wordFreqs.length; while (left <= right) { int mid = left + (right - left) / 2; if (wordFreqs[mid] > queryFreq) { firstGreaterIndex = mid; right = mid - 1; } else { left = mid + 1; } } answer[i] = wordFreqs.length - firstGreaterIndex; } return answer; } private int f(String s) { int[] counts = new int[26]; for (char c : s.toCharArray()) { counts[c - 'a']++; } for (int count : counts) { if (count > 0) { return count; } } return 0; }}Complexity
Time
O(N*L + N*log(N) + M*(L + log(N))), where M is `queries.length`, N is `words.length`, and L is max string length. This is composed of O(N*L) to compute all word frequencies, O(N*log(N)) to sort them, and O(M*(L + log(N))) to process all queries.
Space
O(N + M). O(N) for `wordFreqs` and O(M) for the output array.
Trade-offs
Pros
Much more efficient than brute force, especially for a large number of queries and words.
Avoids redundant computations by pre-calculating word frequencies.
Cons
Sorting can be overkill if the range of frequencies is small, as is the case in this problem.
This is the most optimal approach, which leverages the problem's constraints. The maximum length of any string is 10, which means the frequency f(s) can be at most 10. Since the range of possible frequencies is very small (1 to 10), we can use a frequency array (similar to counting sort) instead of a general sort and binary search.
Algorithm
- Define a helper function
f(s)as before. - Create an integer array
freqCountsof size 12, initialized to zeros. - Iterate through the
wordsarray. For eachword, calculate its frequencyw_freq = f(word)and incrementfreqCounts[w_freq]. - Compute the suffix sums for
freqCounts. Iterate fromi = 10down to0:freqCounts[i] += freqCounts[i+1]. - Initialize an integer array
answerof sizequeries.length. - Loop through each
queryat indexi. - Calculate
queryFreq = f(queries[i]). - The number of words with frequency greater than
queryFreqisfreqCounts[queryFreq + 1]. - Set
answer[i] = freqCounts[queryFreq + 1]. - Return
answer.
Walkthrough
First, we observe that the maximum length of any word is 10. This implies the maximum possible frequency of the smallest character is also 10. We create a frequency-of-frequencies array, let's call it freqCounts, of size 12 (to handle frequencies from 0 to 11, providing a safe buffer). freqCounts[k] will store the number of words in the words array that have a frequency f(word) = k. We iterate through the words array, calculate f(word) for each word, and increment the corresponding counter in freqCounts.
To efficiently answer the queries, we need to find the total count of words whose frequency is greater than a given q_freq. This is the sum of freqCounts[k] for all k > q_freq. Instead of summing this up for each query, we can pre-calculate a suffix sum on freqCounts. We can update freqCounts in place to store these suffix sums. We iterate from right to left (from index 10 down to 0) and update freqCounts[i] = freqCounts[i] + freqCounts[i+1]. After this, freqCounts[i] will store the number of words with frequency f(word) >= i.
Finally, we iterate through the queries. For each query, we calculate q_freq = f(query). The number of words W with f(W) > q_freq is the same as the number of words with f(W) >= q_freq + 1. This value is readily available in our pre-computed suffix sum array at index q_freq + 1. This lookup takes O(1) time for each query.
class Solution { public int[] numSmallerByFrequency(String[] queries, String[] words) { // Max frequency is 10 (max string length). Array size 12 for safety (indices 0-11). int[] freqCounts = new int[12]; for (String word : words) { freqCounts[f(word)]++; } // Calculate suffix sums. freqCounts[i] will store count of words with frequency >= i. for (int i = 10; i >= 0; i--) { freqCounts[i] += freqCounts[i + 1]; } int[] answer = new int[queries.length]; for (int i = 0; i < queries.length; i++) { int queryFreq = f(queries[i]); // We need count of words with frequency > queryFreq. // This is equivalent to count of words with frequency >= queryFreq + 1. answer[i] = freqCounts[queryFreq + 1]; } return answer; } private int f(String s) { int[] counts = new int[26]; for (char c : s.toCharArray()) { counts[c - 'a']++; } for (int count : counts) { if (count > 0) { return count; } } return 0; }}Complexity
Time
O(N*L + M*L), where M is `queries.length`, N is `words.length`, and L is max string length. This consists of O(N*L) to compute all word frequencies, O(1) to compute suffix sums, and O(M*L) to process all queries.
Space
O(M). O(1) for `freqCounts` (constant size 12) and O(M) for the output array.
Trade-offs
Pros
The most efficient solution due to constant time lookups for each query after pre-computation.
Effectively utilizes the problem's constraints on string length.
Cons
Slightly more complex to reason about due to the frequency-of-frequencies and suffix sum concepts.
Solutions
Solution
class Solution { public int [] numSmallerByFrequency ( String [] queries , String [] words ) { int n = words . length ; int [] nums = new int [ n ]; for ( int i = 0 ; i < n ; ++ i ) { nums [ i ] = f ( words [ i ]); } Arrays . sort ( nums ); int m = queries . length ; int [] ans = new int [ m ]; for ( int i = 0 ; i < m ; ++ i ) { int x = f ( queries [ i ]); int l = 0 , r = n ; while ( l < r ) { int mid = ( l + r ) >> 1 ; if ( nums [ mid ] > x ) { r = mid ; } else { l = mid + 1 ; } } ans [ i ] = n - l ; } return ans ; } private int f ( String s ) { int [] cnt = new int [ 26 ]; for ( int i = 0 ; i < s . length (); ++ i ) { ++ cnt [ s . charAt ( i ) - 'a' ]; } for ( int x : cnt ) { if ( x > 0 ) { return x ; } } return 0 ; } }class Solution { public: vector < int > numSmallerByFrequency ( vector < string >& queries , vector < string >& words ) { auto f = []( string s ) { int cnt [ 26 ] = { 0 }; for ( char c : s ) { cnt [ c - 'a' ] ++ ; } for ( int x : cnt ) { if ( x ) { return x ; } } return 0 ; }; int n = words . size (); int nums [ n ]; for ( int i = 0 ; i < n ; i ++ ) { nums [ i ] = f ( words [ i ]); } sort ( nums , nums + n ); vector < int > ans ; for ( auto & q : queries ) { int x = f ( q ); ans . push_back ( n - ( upper_bound ( nums , nums + n , x ) - nums )); } return ans ; } };class Solution : def numSmallerByFrequency ( self , queries : List [ str ], words : List [ str ]) -> List [ int ]: def f ( s : str ) -> int : cnt = Counter ( s ) return next ( cnt [ c ] for c in ascii_lowercase if cnt [ c ]) n = len ( words ) nums = sorted ( f ( w ) for w in words ) return [ n - bisect_right ( nums , f ( q )) for q in queries ]Video 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.