Substrings of Size Three with Distinct Characters
EasyPrompt
A string is good if there are no repeated characters.
Given a string s, return the number of good substrings of length three in s.
Note that if there are multiple occurrences of the same substring, every occurrence should be counted.
A substring is a contiguous sequence of characters in a string.
Example 1:
Input: s = "xyzzaz"
Output: 1
Explanation: There are 4 substrings of size 3: "xyz", "yzz", "zza", and "zaz".
The only good substring of length 3 is "xyz".Example 2:
Input: s = "aababcabc"
Output: 4
Explanation: There are 7 substrings of size 3: "aab", "aba", "bab", "abc", "bca", "cab", and "abc".
The good substrings are "abc", "bca", "cab", and "abc".
Constraints:
1 <= s.length <= 100s consists of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach iterates through the string to generate all substrings of length three. For each substring, a helper function is used to determine if it's "good". This helper function leverages a HashSet to efficiently check for duplicate characters. While this method is versatile and can be adapted for substrings of any length, it introduces some overhead for the specific case of length three.
Algorithm
- Initialize a counter
countto 0. - If the string length
nis less than 3, return 0. - Iterate with an index
ifrom 0 ton - 3. - In each iteration, extract the substring of length 3 starting at
i. - Check if this substring has unique characters using a
HashSet:- Create an empty
HashSet. - For each character in the substring, try to add it to the set.
- If
addever returnsfalse, the substring is not good. - If all characters are added successfully, the substring is good.
- Create an empty
- If the substring is good, increment
count. - Return
countafter the loop.
Walkthrough
The main logic iterates from the beginning of the string up to the third-to-last character to define the starting point of each potential substring.
In each step, a substring of length 3 is extracted.
This substring is passed to a helper function, isGood.
The isGood function creates a HashSet of characters. It then iterates through the three characters of the substring. For each character, it tries to add it to the set. The add method of a HashSet returns false if the element is already present. If a duplicate is found, isGood immediately returns false. If all characters are added successfully, it means they are all unique, and the function returns true.
A counter is incremented for every "good" substring found.
Finally, the total count is returned.
import java.util.HashSet;import java.util.Set; class Solution { private boolean isGood(String sub) { Set<Character> charSet = new HashSet<>(); for (char c : sub.toCharArray()) { if (!charSet.add(c)) { // Found a duplicate return false; } } return true; } public int countGoodSubstrings(String s) { int n = s.length(); if (n < 3) { return 0; } int count = 0; for (int i = 0; i <= n - 3; i++) { // Extracting substring can have overhead String sub = s.substring(i, i + 3); if (isGood(sub)) { count++; } } return count; }}Complexity
Time
O(N), where `N` is the length of the string `s`. The loop runs `N-2` times. Inside the loop, creating a substring of length 3 and checking its uniqueness with a `HashSet` takes constant time (`O(1)`), as the size is fixed at 3.
Space
O(1). Although a `HashSet` and a new `String` object are created in each iteration, their size is constant (at most 3 elements), so the space complexity does not scale with the input size `N`.
Trade-offs
Pros
The logic is clear and easy to reason about.
The helper function
isGoodis modular and can be reused to check substrings of any lengthk.
Cons
Incurs overhead from creating new
StringandHashSetobjects in every iteration.For a fixed small size like 3, this is less performant than direct character comparisons.
This is the most efficient approach for this problem. It uses a single pass through the string, conceptually similar to a sliding window of size three. Instead of creating substrings and using data structures, it directly accesses and compares the characters at the relevant indices.
Algorithm
- Initialize a counter
countto 0. - If the string length
nis less than 3, return 0. - Iterate with an index
ifrom 0 ton - 3. - In each iteration, get the three characters in the current window:
c1 = s.charAt(i),c2 = s.charAt(i+1),c3 = s.charAt(i+2). - Compare the characters directly: if
c1 != c2ANDc2 != c3ANDc1 != c3, then the substring is good. - If the substring is good, increment
count. - Return
countafter the loop.
Walkthrough
The algorithm iterates through the string just once, from the first character up to the character at index n-3. The loop variable i represents the start of a 3-character window.
In each iteration, it looks at the characters s.charAt(i), s.charAt(i+1), and s.charAt(i+2).
It performs a simple and direct check: s.charAt(i) != s.charAt(i+1), s.charAt(i+1) != s.charAt(i+2), and s.charAt(i) != s.charAt(i+2).
If all three conditions are true, it means the three characters in the current window are distinct, and a counter is incremented.
This avoids the overhead of creating new string objects or auxiliary data structures like sets for each window.
class Solution { public int countGoodSubstrings(String s) { int count = 0; int n = s.length(); if (n < 3) { return 0; } // Iterate up to the start of the last possible 3-char substring for (int i = 0; i <= n - 3; i++) { char c1 = s.charAt(i); char c2 = s.charAt(i + 1); char c3 = s.charAt(i + 2); // Direct comparison of the three characters if (c1 != c2 && c2 != c3 && c1 != c3) { count++; } } return count; }}Complexity
Time
O(N), where `N` is the length of the string `s`. We perform a single pass through the string. Each step involves a constant number of character accesses and comparisons.
Space
O(1). This approach uses only a few variables for the loop index and counter, requiring constant extra space.
Trade-offs
Pros
Extremely fast and efficient due to direct memory access and simple comparisons.
Minimal memory overhead.
Very simple to implement.
Cons
The comparison logic
(c1 != c2 && c2 != c3 && c1 != c3)is hardcoded for size 3. Generalizing it to an arbitrary sizekwould require a more complex approach.
Solutions
Solution
class Solution { public int countGoodSubstrings ( String s ) { int count = 0 , n = s . length (); for ( int i = 0 ; i < n - 2 ; ++ i ) { char a = s . charAt ( i ), b = s . charAt ( i + 1 ), c = s . charAt ( i + 2 ); if ( a != b && a != c && b != c ) { ++ count ; } } return count ; } }class Solution { public: int countGoodSubstrings ( string s ) { int ans = 0 ; int n = s . length (); for ( int l = 0 , r = 0 , mask = 0 ; r < n ; ++ r ) { int x = s [ r ] - 'a' ; while (( mask >> x & 1 ) == 1 ) { int y = s [ l ++ ] - 'a' ; mask ^= 1 << y ; } mask |= 1 << x ; ans += r - l + 1 >= 3 ? 1 : 0 ; } return ans ; } };class Solution : def countGoodSubstrings ( self , s : str ) -> int : count , n = 0 , len ( s ) for i in range ( n - 2 ): count += s [ i ] != s [ i + 1 ] and s [ i ] != s [ i + 2 ] and s [ i + 1 ] != s [ i + 2 ] return countVideo 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.