2743. Count Substrings Without Repeating Character 🔒
Problem Description
You are given a string s
consisting only of lowercase English letters. A substring is called special if it contains no character that appears more than once (no repeating characters).
Your task is to count the total number of special substrings in the given string.
For example, in the string "pop"
:
"p"
is special (appears at positions 0 and 2, counted separately)"o"
is special"po"
is special"op"
is special"pop"
is NOT special because the character'p'
appears twice in it
A substring is a contiguous sequence of characters within a string. For instance, "abc"
is a substring of "abcd"
, but "acd"
is not.
The solution uses a sliding window approach with two pointers. The algorithm maintains a window [j, i]
where all characters appear at most once. For each position i
, it counts how many special substrings end at that position. Since any substring of a special substring is also special, if the window [j, i]
contains all unique characters, then all substrings ending at i
and starting from any position between j
and i
are special. This gives us exactly i - j + 1
special substrings ending at position i
.
Intuition
The key observation is that if we have a substring with all unique characters, then every substring within it also has unique characters. For example, if "abcd"
has all unique characters, then "abc"
, "bcd"
, "ab"
, "bc"
, "cd"
, "a"
, "b"
, "c"
, and "d"
all have unique characters too.
This leads us to think about maintaining a window of characters where no character repeats. As we expand this window by adding characters from the right, we can count how many valid substrings end at each position.
Consider when we're at position i
and we have a valid window from position j
to i
(all characters in s[j...i]
are unique). How many special substrings end at position i
?
- The substring from
j
toi
is special - The substring from
j+1
toi
is special - The substring from
j+2
toi
is special - ...
- The substring from
i
toi
(just the character itself) is special
That's exactly i - j + 1
special substrings ending at position i
.
When we encounter a character that already exists in our current window, we need to shrink the window from the left until that character appears only once. This ensures our window always maintains the property of having all unique characters.
By processing each position and counting the special substrings ending at that position, we avoid the need to check every possible substring individually, reducing the time complexity from O(n³)
(checking all substrings) to O(n)
(single pass with sliding window).
Learn more about Sliding Window patterns.
Solution Approach
The solution uses a sliding window technique with two pointers to efficiently count all special substrings.
Data Structures Used:
- A
Counter
(hash map) to track the frequency of each character in the current window - Two pointers:
j
(left boundary) andi
(right boundary) of the window
Algorithm Steps:
-
Initialize variables:
cnt
: A Counter to store character frequencies in the current windowans
: Accumulator for the total count of special substringsj
: Left pointer starting at position 0
-
Expand the window by iterating through each character:
for i, c in enumerate(s): cnt[c] += 1
For each position
i
, we add the characters[i]
to our window by incrementing its count incnt
. -
Shrink the window if necessary:
while cnt[c] > 1: cnt[s[j]] -= 1 j += 1
If the newly added character
c
appears more than once (meaning we have a duplicate), we need to shrink the window from the left. We keep removing characters from the left (decrementing their count and movingj
right) until the characterc
appears exactly once. -
Count special substrings ending at position
i
:ans += i - j + 1
After ensuring the window
[j, i]
contains all unique characters, we know that all substrings ending ati
and starting from any position betweenj
andi
are special. The count of such substrings isi - j + 1
. -
Return the total count: After processing all positions,
ans
contains the total number of special substrings.
Time Complexity: O(n)
where n
is the length of the string. Each character is visited at most twice (once by the right pointer and once by the left pointer).
Space Complexity: O(min(n, 26))
for the Counter, which in the worst case stores all unique characters in the string (at most 26 for lowercase English letters).
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 trace through the algorithm with the string s = "abcab"
.
Initial Setup:
cnt = {}
(empty counter)ans = 0
(total count)j = 0
(left pointer)
Step 1: i = 0, c = 'a'
- Add 'a' to window:
cnt = {'a': 1}
- No duplicates (cnt['a'] = 1), so no shrinking needed
- Count substrings ending at position 0:
ans += 0 - 0 + 1 = 1
- Special substrings found: ["a"]
- Running total:
ans = 1
Step 2: i = 1, c = 'b'
- Add 'b' to window:
cnt = {'a': 1, 'b': 1}
- No duplicates (cnt['b'] = 1), so no shrinking needed
- Count substrings ending at position 1:
ans += 1 - 0 + 1 = 2
- Special substrings found: ["ab", "b"]
- Running total:
ans = 3
Step 3: i = 2, c = 'c'
- Add 'c' to window:
cnt = {'a': 1, 'b': 1, 'c': 1}
- No duplicates (cnt['c'] = 1), so no shrinking needed
- Count substrings ending at position 2:
ans += 2 - 0 + 1 = 3
- Special substrings found: ["abc", "bc", "c"]
- Running total:
ans = 6
Step 4: i = 3, c = 'a'
- Add 'a' to window:
cnt = {'a': 2, 'b': 1, 'c': 1}
- Duplicate detected! cnt['a'] = 2, need to shrink:
- Remove s[0] = 'a':
cnt = {'a': 1, 'b': 1, 'c': 1}
,j = 1
- Now cnt['a'] = 1, stop shrinking
- Remove s[0] = 'a':
- Count substrings ending at position 3:
ans += 3 - 1 + 1 = 3
- Special substrings found: ["bca", "ca", "a"]
- Running total:
ans = 9
Step 5: i = 4, c = 'b'
- Add 'b' to window:
cnt = {'a': 1, 'b': 2, 'c': 1}
- Duplicate detected! cnt['b'] = 2, need to shrink:
- Remove s[1] = 'b':
cnt = {'a': 1, 'b': 1, 'c': 1}
,j = 2
- Now cnt['b'] = 1, stop shrinking
- Remove s[1] = 'b':
- Count substrings ending at position 4:
ans += 4 - 2 + 1 = 3
- Special substrings found: ["cab", "ab", "b"]
- Running total:
ans = 12
Final Result: The total number of special substrings is 12.
The algorithm efficiently identifies all special substrings by maintaining a sliding window of unique characters and counting valid substrings ending at each position, avoiding the need to check every possible substring individually.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def numberOfSpecialSubstrings(self, s: str) -> int:
5 # Counter to track character frequencies in current window
6 char_count = Counter()
7
8 # Initialize result and left pointer of sliding window
9 result = 0
10 left = 0
11
12 # Iterate through string with right pointer
13 for right, char in enumerate(s):
14 # Add current character to window
15 char_count[char] += 1
16
17 # Shrink window from left while current character appears more than once
18 while char_count[char] > 1:
19 char_count[s[left]] -= 1
20 left += 1
21
22 # Add count of all substrings ending at current position
23 # All substrings from left to right are valid (no repeating characters)
24 result += right - left + 1
25
26 return result
27
1class Solution {
2 public int numberOfSpecialSubstrings(String s) {
3 int stringLength = s.length();
4 int totalCount = 0;
5
6 // Array to track character frequency in current window
7 // Index represents character (a=0, b=1, ..., z=25)
8 int[] charFrequency = new int[26];
9
10 // Two pointers for sliding window approach
11 int left = 0; // Left boundary of window
12
13 for (int right = 0; right < stringLength; right++) {
14 // Get the index of current character (0-25)
15 int currentCharIndex = s.charAt(right) - 'a';
16
17 // Add current character to the window
18 charFrequency[currentCharIndex]++;
19
20 // Shrink window from left while current character appears more than once
21 while (charFrequency[currentCharIndex] > 1) {
22 // Remove leftmost character from window
23 int leftCharIndex = s.charAt(left) - 'a';
24 charFrequency[leftCharIndex]--;
25 left++;
26 }
27
28 // All substrings ending at position 'right' with no repeating characters
29 // contribute (right - left + 1) to the total count
30 totalCount += right - left + 1;
31 }
32
33 return totalCount;
34 }
35}
36
1class Solution {
2public:
3 int numberOfSpecialSubstrings(string s) {
4 int stringLength = s.size();
5 int charFrequency[26] = {0}; // Array to track frequency of each character (a-z)
6 int totalSpecialSubstrings = 0;
7
8 // Use sliding window technique with two pointers
9 int left = 0; // Left boundary of the sliding window
10
11 for (int right = 0; right < stringLength; ++right) {
12 // Convert character to index (0-25 for a-z)
13 int currentCharIndex = s[right] - 'a';
14
15 // Add current character to the window
16 ++charFrequency[currentCharIndex];
17
18 // Shrink window from left while we have duplicate characters
19 while (charFrequency[currentCharIndex] > 1) {
20 int leftCharIndex = s[left] - 'a';
21 --charFrequency[leftCharIndex];
22 ++left;
23 }
24
25 // All substrings ending at position 'right' with no repeating characters
26 // are valid special substrings. The count is (right - left + 1)
27 totalSpecialSubstrings += right - left + 1;
28 }
29
30 return totalSpecialSubstrings;
31 }
32};
33
1/**
2 * Counts the number of special substrings where each character appears at most once
3 * @param s - The input string to analyze
4 * @returns The total count of special substrings
5 */
6function numberOfSpecialSubstrings(s: string): number {
7 // Helper function to convert character to index (0-25 for 'a'-'z')
8 const charToIndex = (char: string): number => {
9 return char.charCodeAt(0) - 'a'.charCodeAt(0);
10 };
11
12 // Store string length for efficiency
13 const stringLength: number = s.length;
14
15 // Frequency array to track character counts in current window (26 lowercase letters)
16 const charFrequency: number[] = Array(26).fill(0);
17
18 // Result counter for special substrings
19 let specialSubstringCount: number = 0;
20
21 // Two-pointer sliding window approach
22 let leftPointer: number = 0;
23
24 for (let rightPointer: number = 0; rightPointer < stringLength; rightPointer++) {
25 // Get index of current character at right pointer
26 const currentCharIndex: number = charToIndex(s[rightPointer]);
27
28 // Increment frequency of current character
29 charFrequency[currentCharIndex]++;
30
31 // Shrink window from left while we have duplicate characters
32 while (charFrequency[currentCharIndex] > 1) {
33 const leftCharIndex: number = charToIndex(s[leftPointer]);
34 charFrequency[leftCharIndex]--;
35 leftPointer++;
36 }
37
38 // Add count of all valid substrings ending at rightPointer
39 // These are all substrings from [leftPointer...rightPointer] to [rightPointer...rightPointer]
40 specialSubstringCount += rightPointer - leftPointer + 1;
41 }
42
43 return specialSubstringCount;
44}
45
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. Although there is a nested while loop inside the for loop, each character in the string is visited at most twice - once when the right pointer i
includes it, and once when the left pointer j
excludes it. The right pointer i
moves from index 0 to n-1 exactly once, and the left pointer j
also moves from 0 to at most n-1 throughout the entire execution. Therefore, the total number of operations is at most 2n
, which simplifies to O(n)
.
The space complexity is O(C)
, where C
is the size of the character set. The Counter
object cnt
stores at most C
distinct characters. Since the problem deals with lowercase English letters, C = 26
, making the space complexity O(26) = O(1)
in practical terms, though it's more precisely expressed as O(C)
to account for the dependency on the character set size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Count All Valid Substrings at Each Position
The Pitfall: A common mistake is to increment the result by 1 for each valid window position, rather than counting all valid substrings ending at the current position. This would look like:
# INCORRECT approach
for right, char in enumerate(s):
char_count[char] += 1
while char_count[char] > 1:
char_count[s[left]] -= 1
left += 1
result += 1 # Wrong! Only counts one substring per position
This only counts the longest special substring ending at each position, missing all the shorter valid substrings.
Why This Happens:
The sliding window maintains the longest valid substring ending at position right
, but we need to count ALL valid substrings ending there, not just the longest one.
The Solution:
Add right - left + 1
to the result, which accounts for all substrings ending at right
:
- Substring from
left
toright
(length:right - left + 1
) - Substring from
left + 1
toright
(length:right - left
) - ... and so on ...
- Substring from
right
toright
(length: 1)
2. Not Properly Cleaning Up the Counter
The Pitfall: When shrinking the window, forgetting to remove characters with zero count from the Counter:
# Potentially problematic if you check counter size while char_count[char] > 1: char_count[s[left]] -= 1 # Should consider removing if count becomes 0 left += 1
Why This Matters:
While the provided solution works correctly (since we only check char_count[char] > 1
), if you modify the code to check the Counter's size or iterate through its keys, having zero-count entries could cause issues.
The Solution: Clean up zero entries if needed for other operations:
while char_count[char] > 1: char_count[s[left]] -= 1 if char_count[s[left]] == 0: del char_count[s[left]] left += 1
3. Off-by-One Error in Counting Substrings
The Pitfall:
Using right - left
instead of right - left + 1
:
# INCORRECT result += right - left # Misses counting the single character substring
Why This Happens:
When calculating the number of elements in a range [left, right]
inclusive, the formula is right - left + 1
, not right - left
.
Example to Illustrate:
If left = 2
and right = 4
, the window contains positions [2, 3, 4]:
- Substrings: [2,4], [3,4], [4,4]
- Count: 3 substrings = 4 - 2 + 1 ✓
- Not: 2 substrings = 4 - 2 ✗
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
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!