Palindromic Substrings
MedPrompt
Given a string s, return the number of palindromic substrings in it.
A string is a palindrome when it reads the same backward as forward.
A substring is a contiguous sequence of characters within the string.
Example 1:
Input: s = "abc"
Output: 3
Explanation: Three palindromic strings: "a", "b", "c".Example 2:
Input: s = "aaa"
Output: 6
Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".
Constraints:
1 <= s.length <= 1000sconsists of lowercase English letters.
Approaches
4 approaches with complexity analysis and trade-offs.
The most straightforward approach is to generate every possible substring and then, for each one, check if it is a palindrome. If it is, we increment a counter. This method is easy to conceptualize but is the least efficient.
Algorithm
- Initialize a counter
countto 0. - Generate all possible substrings of the input string
susing two nested loops. The outer loop with indexidetermines the start of the substring, and the inner loop with indexjdetermines the end. - For each substring, create a helper function
isPalindrometo check if it's a palindrome. - The
isPalindromefunction uses two pointers,startandend, initialized to the beginning and end of the substring. It checks if characters at these pointers are equal while moving them towards the center. - If the helper function returns
true, increment thecount. - After checking all substrings, return the final
count.
Walkthrough
This approach iterates through all possible start and end indices to define a substring. For each substring generated, a separate check is performed to determine if it's a palindrome. A simple way to check for a palindrome is to compare the substring with its reverse, or more efficiently, use a two-pointer technique. The outer loops run in O(n²) time to generate all substrings, and the palindrome check for each substring of length k takes O(k) time. In the worst case, this leads to an overall time complexity of O(n³).
class Solution { public int countSubstrings(String s) { int count = 0; int n = s.length(); for (int i = 0; i < n; i++) { for (int j = i; j < n; j++) { if (isPalindrome(s, i, j)) { count++; } } } return count; } private boolean isPalindrome(String s, int start, int end) { while (start < end) { if (s.charAt(start) != s.charAt(end)) { return false; } start++; end--; } return true; }}Complexity
Time
O(n³), where n is the length of the string. There are O(n²) substrings, and checking if a substring is a palindrome takes O(n) time in the worst case.
Space
O(1) extra space, as the palindrome check can be done in-place with pointers without allocating significant new memory.
Trade-offs
Pros
Simple to understand and implement.
Requires minimal extra space.
Cons
Very inefficient with a cubic time complexity, making it unsuitable for large inputs.
Performs many redundant palindrome checks for overlapping substrings.
A more optimized approach uses dynamic programming to avoid re-computing whether a substring is a palindrome. We use a 2D table to store the results for all substrings. The result for a larger substring can be efficiently calculated using the stored results of smaller substrings.
Algorithm
- Create a 2D boolean array
dp[n][n], wherenis the string length.dp[i][j]will betrueif the substrings[i...j]is a palindrome. - Initialize a counter
countto 0. - Iterate through substring lengths
lenfrom 1 ton. - For each
len, iterate through all possible start indicesi. - The end index
jis calculated asi + len - 1. - Base Case (len=1):
dp[i][i]is alwaystrue. Incrementcount. - Base Case (len=2):
dp[i][i+1]istrueifs.charAt(i) == s.charAt(i+1). If true, incrementcount. - Recursive Step (len > 2):
dp[i][j]istrueifs.charAt(i) == s.charAt(j)and the inner substrings[i+1...j-1]is a palindrome (i.e.,dp[i+1][j-1]istrue). If true, incrementcount. - Return the total
count.
Walkthrough
We define dp[i][j] as a boolean that is true if the substring from index i to j (inclusive) is a palindrome. The key insight is that a string s[i...j] is a palindrome if and only if its first and last characters (s[i] and s[j]) are the same, and the substring between them (s[i+1...j-1]) is also a palindrome. We can build up the dp table by considering substrings of increasing length. We start with substrings of length 1 (all single characters are palindromes), then length 2, and so on, up to length n. Each time we find a palindrome (i.e., set dp[i][j] to true), we increment our total count.
class Solution { public int countSubstrings(String s) { int n = s.length(); if (n == 0) { return 0; } boolean[][] dp = new boolean[n][n]; int count = 0; for (int len = 1; len <= n; len++) { for (int i = 0; i <= n - len; i++) { int j = i + len - 1; if (s.charAt(i) == s.charAt(j)) { if (len <= 2) { // Length 1 or 2 dp[i][j] = true; } else { // Length > 2 dp[i][j] = dp[i + 1][j - 1]; } } if (dp[i][j]) { count++; } } } return count; }}Complexity
Time
O(n²), as we iterate through the O(n²) states of the DP table, and each state is computed in O(1) time.
Space
O(n²), for the 2D DP table used to store palindrome information for all substrings.
Trade-offs
Pros
Significantly more efficient than the brute-force approach.
Systematic and avoids redundant computations by storing intermediate results.
Cons
Requires O(n²) space for the DP table, which can be substantial for large strings.
This clever approach improves upon the DP solution's space complexity. It's based on the observation that every palindrome has a center. A center can be a single character (for odd-length palindromes like 'racecar') or the space between two characters (for even-length palindromes like 'aabbaa'). We can iterate through all 2n-1 possible centers and expand outwards to count all palindromes.
Algorithm
- Initialize a global or member variable
countto 0. - Iterate through the string from
i = 0ton-1. - For each index
i, treat it as the center of a potential palindrome and find all palindromes centered ati. - Call a helper function
expandAroundCenter(s, i, i)to find all odd-length palindromes centered ati. - Call the same helper function
expandAroundCenter(s, i, i+1)to find all even-length palindromes centered betweeniandi+1. - The
expandAroundCenterfunction takes the string and two pointers,leftandright. It expands outwards from this center as long asleftandrightare within bounds ands.charAt(left) == s.charAt(right). For each valid expansion, it increments thecount. - Return the total
count.
Walkthrough
Instead of building a table, we consider each possible center of a palindrome. For a string of length n, there are n single-character centers and n-1 between-character centers. We loop through each of these 2n-1 centers. From each center, we expand outwards with two pointers, left and right. As long as the pointers are valid and the characters at these pointers match, we have found a palindrome and increment our count. Then, we move the pointers further out (left--, right++) and check again. This process continues until the characters don't match or we go out of bounds.
class Solution { int count = 0; public int countSubstrings(String s) { if (s == null || s.length() == 0) { return 0; } for (int i = 0; i < s.length(); i++) { // Odd length palindromes, centered at i expandAroundCenter(s, i, i); // Even length palindromes, centered between i and i+1 expandAroundCenter(s, i, i + 1); } return count; } private void expandAroundCenter(String s, int left, int right) { while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) { count++; left--; right++; } }}Complexity
Time
O(n²). There are `2n-1` centers, and expansion from each center can take up to O(n) time in the worst case (e.g., a string of all the same characters).
Space
O(1), as we only use a few variables to keep track of pointers and the count, regardless of the input string size.
Trade-offs
Pros
Optimal space complexity of O(1).
Time complexity is the same as the DP approach, O(n²).
Often simpler to implement than the DP solution.
Cons
While space-efficient, the time complexity is still quadratic, which might be too slow for very large inputs under strict time limits.
Manacher's algorithm is the most optimal solution, achieving a linear time complexity. It is an extension of the 'Expand Around Center' idea but uses properties of palindromes to avoid redundant character comparisons. It does this by maintaining the boundary of the rightmost-extending palindrome found so far and using its internal symmetry to initialize the palindrome radius for new centers.
Algorithm
- Preprocessing: Transform the input string
sinto a new stringtby inserting a special character (e.g., '#') between each character and at the ends. This handles odd and even length palindromes uniformly. - Initialization: Create a radius array
pof the same size ast. Initializecenter = 0,right = 0(for the rightmost palindrome found), andcount = 0. - Iteration: Loop
ifrom1tot.length() - 1.- Calculate the mirror index
i_mirror = 2 * center - i. - If
iis within the current rightmost palindrome (i < right), initializep[i]using the mirror's radius:p[i] = min(right - i, p[i_mirror]). This is the key optimization. - Expand
p[i]by checking characters:while (t[i + 1 + p[i]] == t[i - 1 - p[i]]) { p[i]++; }. - If the palindrome at
iextends beyondright, updatecenterandright.
- Calculate the mirror index
- Counting: For each
i, the radiusp[i]corresponds to a palindrome of lengthp[i]in the original string. This longest palindrome contains(p[i] + 1) / 2smaller palindromes centered at the same spot. Add this value to the totalcount. - Return
count.
Walkthrough
The algorithm first transforms the string (e.g., "aba" -> "#a#b#a#") so that every palindrome has a distinct center and an odd length. It then iterates through the transformed string, calculating the radius of the palindrome centered at each position. The core optimization comes from using a p array, where p[i] stores the radius of the palindrome at center i. When calculating p[i], if i is inside a previously found larger palindrome, we can use the information from its 'mirror' position to get a guaranteed minimum radius for p[i], reducing the number of character comparisons needed for expansion. The total number of character comparisons across the entire algorithm is amortized to O(n). The number of palindromic substrings is then calculated by summing up the contributions from each center, where a palindrome of length L in the original string contributes (L+1)/2 to the total count.
class Solution { public int countSubstrings(String s) { StringBuilder tBuilder = new StringBuilder("#"); for (char c : s.toCharArray()) { tBuilder.append(c).append('#'); } String t = tBuilder.toString(); int n = t.length(); int[] p = new int[n]; int center = 0, right = 0; int count = 0; for (int i = 1; i < n - 1; i++) { int iMirror = 2 * center - i; if (i < right) { p[i] = Math.min(right - i, p[iMirror]); } while (i - 1 - p[i] >= 0 && i + 1 + p[i] < n && t.charAt(i - 1 - p[i]) == t.charAt(i + 1 + p[i])) { p[i]++; } if (i + p[i] > right) { center = i; right = i + p[i]; } count += (p[i] + 1) / 2; } return count; }}Complexity
Time
O(n). The algorithm processes the string in a single pass. The expansion loop, while nested, has an amortized complexity of O(n) because the `right` boundary pointer only ever moves forward.
Space
O(n), required for the transformed string and the radius array `p`.
Trade-offs
Pros
Optimal O(n) time complexity.
The fastest known algorithm for this problem.
Cons
Significantly more complex to understand and implement correctly than the other approaches.
Requires O(n) extra space, whereas the 'Expand Around Center' approach uses O(1).
Solutions
Solution
class Solution { public int countSubstrings ( String s ) { StringBuilder sb = new StringBuilder ( "^#" ); for ( char ch : s . toCharArray ()) { sb . append ( ch ). append ( '#' ); } String t = sb . append ( '$' ). toString (); int n = t . length (); int [] p = new int [ n ]; int pos = 0 , maxRight = 0 ; int ans = 0 ; for ( int i = 1 ; i < n - 1 ; i ++) { p [ i ] = maxRight > i ? Math . min ( maxRight - i , p [ 2 * pos - i ]) : 1 ; while ( t . charAt ( i - p [ i ]) == t . charAt ( i + p [ i ])) { p [ i ]++; } if ( i + p [ i ] > maxRight ) { maxRight = i + p [ i ]; pos = i ; } ans += p [ i ] / 2 ; } return ans ; } }/** * @param {string} s * @return {number} */ var countSubstrings = function ( s,) { let ans = 0; const n = s.length; for (let k = 0; k < n * 2 - 1; ++k) { let i = k >> 1; let j = (k + 1) >> 1; while (~i && j < n && s[i] == s[j]) { ++ans; --i; ++j; } } return ans;};class Solution { public: int countSubstrings ( string s ) { int ans = 0 ; int n = s . size (); for ( int k = 0 ; k < n * 2 - 1 ; ++ k ) { int i = k / 2 , j = ( k + 1 ) / 2 ; while ( ~ i && j < n && s [ i ] == s [ j ]) { ++ ans ; -- i ; ++ j ; } } return ans ; } };class Solution: def countSubstrings(self, s: str) -> int: t = '^#' + '#' . join(s) + '#$' n = len(t) p = [0 for _ in range(n)] pos, maxRight = 0, 0 ans = 0 for i in range(1, n - 1): p[i] = min(maxRight - i, p[2 * pos - i]) if maxRight > i else 1 while t[i - p[i]] == t[i + p[i]]: p[i] += 1 if i + p[i] > maxRight: maxRight = i + p[i] pos = i ans += p[i] // 2 return ansVideo 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.