1. Introduction
String manipulation and parsing are foundational elements of backend system design, data validation, and text processing pipelines. Among the classic string algorithms, finding the "Longest Palindromic Substring" (often encountered as LeetCode 5) stands out as a quintessential test of algorithmic thinking.
For highly competitive Software Development Engineer 1 (SDE 1) online assessments, this problem acts as a crucial gatekeeper. It doesn't just test if you can find the right answer; it evaluates how efficiently you can optimize your logic. Interviewers use this problem to observe your progression from a naive approach to an optimal, production-ready solution that minimizes memory allocation—a critical skill for robust backend engineering.
A palindrome is a string that reads the same forwards and backwards (like "racecar" or "madam"). Finding the longest one within a larger string has real-world analogs in computational biology (DNA sequence analysis), data compression, and cryptography. In this comprehensive guide, we will break down the mechanics of the problem, gradually stripping away inefficiencies until we arrive at the optimal solution.
2. Problem Statement
Description
Given a string s, return the longest palindromic substring in s. A substring is a contiguous non-empty sequence of characters within a string.
Input
- A single string s consisting of digits and English letters.
Output
- A string representing the longest contiguous palindromic sequence.
Constraints
- 1≤s.length≤1000
- s consists of only digits and English letters.
Example 1
Input: s = "babad"
Output: "bab"
(Note: "aba" is also a valid answer and is equally acceptable).
Example 2
Input: s = "cbbd"
Output: "bb"
Approach 1: Brute Force
Intuition
The most straightforward way to solve this problem is to generate every possible substring of the given string and check if each one is a palindrome. If it is, we compare its length to our longest recorded palindrome and update our record if the current one is longer.
This approach mimics human trial-and-error but lacks the systemic efficiency required for large-scale data.
Algorithm
- Initialize a variable
maxLength to 0 and a string longestStr to hold the result. - Use two nested loops to generate all possible starting indices i and ending indices j of substrings.
- For every substring s[i…j], use a helper function to verify if it is a palindrome.
- The helper function uses two pointers (one at the start, one at the end), moving inwards and comparing characters.
- If the substring is a palindrome and its length (j−i+1) is greater than
maxLength, update maxLength and longestStr.
Dry Run
Let's dry run the string s = "babad".
| i (Start) |
j (End) |
Substring |
Palindrome? |
Current Longest |
| 0 |
0 |
"b" |
Yes |
"b" |
| 0 |
1 |
"ba" |
No |
"b" |
| 0 |
2 |
"bab" |
Yes |
"bab" |
| 0 |
3 |
"baba" |
No |
"bab" |
| 0 |
4 |
"babad" |
No |
"bab" |
| 1 |
1 |
"a" |
Yes |
"bab" |
| 1 |
2 |
"ab" |
No |
"bab" |
| 1 |
3 |
"aba" |
Yes |
"bab" |
C++ Code
#include <iostream>
#include <string>
using namespace std;
class Solution {
private:
bool isPalindrome(const string& s, int left, int right) {
while (left < right) {
if (s[left] != s[right]) {
return false;
}
left++;
right--;
}
return true;
}
public:
string longestPalindrome(string s) {
int n = s.length();
if (n <= 1)
return s;
string longestStr = "";
int maxLength = 0;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
if (isPalindrome(s, i, j)) {
int currentLength = j - i + 1;
if (currentLength > maxLength) {
maxLength = currentLength;
longestStr = s.substr(i, currentLength);
}
}
}
}
return longestStr;
}
};
Complexity Analysis
- Time Complexity: O(n3). Generating all substrings takes O(n2) time. For each substring, checking if it is a palindrome takes O(n) time. O(n2)×O(n)=O(n3).
- Space Complexity: O(1). We only use a few variables for tracking indices and lengths, requiring constant extra space (excluding the space needed for the output string).
Why This Approach Is Inefficient
While logically sound, O(n3) operations will result in a Time Limit Exceeded (TLE) error for strings approaching lengths of 1000 characters. We are repeatedly checking the same inner substrings. For example, to check if "ababa" is a palindrome, we check if the outer 'a's match, and then we check if "bab" is a palindrome. But we likely already checked "bab" in a previous iteration! This overlapping computation is a glaring inefficiency.
Approach 2: The "Reverse and Compare" Trap (Intermediate)
Intuition
Before reaching the optimal solutions, many candidates attempt to reverse the original string s to create s′ and then find the Longest Common Substring between s and s′.
Algorithm Outline
- Reverse s to get s′.
- Use standard dynamic programming to find the longest common substring between the two.
Limitations & Why We Skip It
This sounds elegant, but there is a massive trap here.
Consider s = "abacdfgdcaba" and its reverse s' = "abacdgfdcaba".
The longest common substring between them is "abacd". However, "abacd" is not a palindrome!
To make this approach work, whenever a common substring is found, you must verify that the indices of the matched characters correspond to the original string's exact mirrored positions. This requires an extra O(1) index check. While it brings the time down to O(n2), the setup is overly complex and requires O(n2) space for the Longest Common Substring DP table.
Because of these pitfalls, we explicitly bypass this intermediate step and move to a much safer, native Dynamic Programming approach.
Approach 3: Dynamic Programming
Intuition
To eliminate the redundant checks from our Brute Force approach, we can cache our previous findings. This introduces Dynamic Programming (DP).
The core realization is that a string is a palindrome if:
- Its first and last characters are identical.
- The remaining inner substring is also a palindrome.
For instance, the string "cabac" is a palindrome because the outer characters 'c' and 'c' match, and the inner string "aba" is already known to be a palindrome.
DP State
Let dp[i][j] be a boolean table where dp[i][j]=true if the substring s[i…j] is a palindrome, and false otherwise.
Transition
dp[i][j]=(s[i]==s[j]) and dp[i+1][j−1]
Base Cases
- Length 1: Every single character is a palindrome.dp[i][i]=true
- Length 2: Two characters form a palindrome if they are identical.dp[i][i+1]=(s[i]==s[i+1])
Algorithm
- Create an n×n boolean matrix initialized to false.
- Fill all dp[i][i] with true.
- Check all substrings of length 2.
- Iterate through substring lengths from 3 up to n.
- For each length, iterate through all valid starting positions i. Calculate the ending position j.
- Apply the state transition equation.
- Keep track of the maximum length and the starting index to construct the final string.
Dry Run
Input: s = "babad"
Base Case (Length 1):
- dp[0][0],dp[1][1],dp[2][2],dp[3][3],dp[4][4] are all
true.
Base Case (Length 2):
s[0..1] = "ba" → dp[0][1]=falses[1..2] = "ab" → dp[1][2]=falses[2..3] = "ba" → dp[2][3]=false
Length 3:
s[0..2] = "bab" → s[0]==s[2] ('b' == 'b') and dp[1][1] is true. dp[0][2]=true. (Longest = 3)s[1..3] = "aba" → s[1]==s[3] ('a' == 'a') and dp[2][2] is true. dp[1][3]=true.
C++ Code
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
string longestPalindrome(string s) {
int n = s.length();
if (n <= 1)
return s;
vector<vector<bool>> dp(n, vector<bool>(n, false));
int start = 0;
int maxLength = 1;
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
for (int i = 0; i < n - 1; i++) {
if (s[i] == s[i + 1]) {
dp[i][i + 1] = true;
start = i;
maxLength = 2;
}
}
for (int len = 3; len <= n; len++) {
for (int i = 0; i < n - len + 1; i++) {
int j = i + len - 1;
if (s[i] == s[j] && dp[i + 1][j - 1]) {
dp[i][j] = true;
if (len > maxLength) {
start = i;
maxLength = len;
}
}
}
}
return s.substr(start, maxLength);
}
};
Complexity Analysis
- Time Complexity: O(n2). We fill an n×n matrix where each lookup takes O(1) time.
- Space Complexity: O(n2). We allocate a 2D boolean array of size n×n.
Advantages and Disadvantages
- Advantages: Solves the overlapping subproblems issue, reducing time complexity from O(n3) to O(n2). The logic is highly declarative and easy to trace.
- Disadvantages: Space complexity is heavy. In a modern backend environment, allocating an O(n2) matrix for a string parsing utility is poor practice. If the string has 10,000 characters, we are allocating a massive 100,000,000-cell array just to check string bounds. There is an algorithm that maintains the O(n2) time but drastically reduces space to O(1).
Approach 4: Expand Around Center (Optimal Interview Solution)
Intuition
Every palindrome mirrors around its center. Instead of checking the boundaries and looking inwards, what if we choose a center point and expand outwards?
Since a palindrome mirrors around its center, we can try treating every character (and every space between characters) as a potential center and expand pointers outwards as long as the characters match.
Center Types:
- Odd-length palindromes have a distinct single character as the center. (e.g., in "aba", the center is 'b').
- Even-length palindromes have the space between two identical characters as the center. (e.g., in "abba", the center is between the two 'b's).
For a string of length n, there are n single-character centers and n−1 space-between-character centers, resulting in 2n−1 total possible centers. Expanding from all 2n−1 centers ensures we find every single palindrome without needing a massive 2D matrix.
Visualizing Expansion
Algorithm
- Initialize
start and maxLength variables. - Loop through the string, treating every index i as a potential center.
- For each index, call a helper function
expandAroundCenter twice:- Once for an odd-length palindrome (center is i, left = i, right = i).
- Once for an even-length palindrome (center is between i and i+1, left = i, right = i+1).
- The helper function expands left and right pointers as long as characters match and boundaries are valid. It returns the length of the palindrome found.
- Take the maximum length found from both the odd and even expansion.
- If this length is greater than the current
maxLength, update start and maxLength based on index offsets.
Dry Run
Input: s = "babad"
| Index (i) |
Odd Expansion (left = i, right = i) |
Even Expansion (left = i, right = i + 1) |
Max Length Found |
| 0 ('b') |
Expands to "b", next comparison fails |
Expands to "ba", fails immediately |
1 ("b") |
| 1 ('a') |
Expands to "bab", next comparison fails |
Expands to "ab", fails immediately |
3 ("bab") |
| 2 ('b') |
Expands to "aba", next comparison fails |
Expands to "ba", fails immediately |
3 ("bab") |
| 3 ('a') |
Expands to "a", next comparison fails |
Expands to "ad", fails immediately |
3 ("bab") |
| 4 ('d') |
Expands to "d", bounds reached |
N/A (no character to the right) |
3 ("bab") |
Result: "bab" (starting from index 0, length 3).
C++ Code
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
class Solution {
private:
int expandAroundCenter(const string& s, int left, int right) {
while (left >= 0 &&
right < s.length() &&
s[left] == s[right]) {
left--;
right++;
}
return right - left - 1;
}
public:
string longestPalindrome(string s) {
if (s.empty())
return "";
int start = 0;
int maxLength = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = max(len1, len2);
if (len > maxLength) {
start = i - (len - 1) / 2;
maxLength = len;
}
}
return s.substr(start, maxLength);
}
}
Complexity Analysis
- Time Complexity: O(n2). Expanding around 2n−1 centers takes O(n) time in the worst-case scenario (e.g., if the string is entirely identical characters like
"aaaaa"). - Space Complexity: O(1). We only use a few integer variables, avoiding the massive O(n2) spatial footprint of Dynamic Programming.
Why This Is Better Than DP
When facing rigorous online assessments that measure computational efficiency against stringent test cases, O(n2) time with O(1) space is the expected standard. It demonstrates you understand core data structure efficiencies—namely, avoiding state arrays when inline variables can accomplish the task mathematically.
Approach 5: Manacher's Algorithm (Advanced)
Introduction
While the Expand Around Center approach satisfies almost all interview requirements, there exists a legendary algorithm that pushes the boundaries of theoretical optimization. Invented by Glenn K. Manacher in 1975, this algorithm finds the longest palindromic substring in strictly O(n) time.
Core Idea
Manacher's Algorithm achieves linear time by intelligently reusing previously computed palindrome lengths, completely avoiding redundant expansions.
To handle both odd and even length palindromes elegantly, Manacher's first preprocesses the string by inserting special delimiter characters (like #) between every letter, as well as distinct boundary markers at the ends.
"babad" becomes "$#b#a#b#a#d#@"
The Three Pillars of Manacher's:
- The Array P[i]: Stores the "palindrome radius" at each center i.
- Center (C) and Right Boundary (R): We keep track of the palindrome that extends furthest to the right. C is its center, and R is its rightmost character index.
- Mirror Index: When we move to a new center i that is within the right boundary R (i.e., i<R), we can find its mirror index on the left side of C, called imirror. We initialize P[i] to P[imirror], dramatically skipping redundant expansions!
Algorithm
- Transform s into T with
# boundaries to unify even/odd logic. - Initialize array P of the same length as T to store radii.
- Iterate i through T.
- If i<R, the current index is inside a known palindrome. Set P[i] to the minimum of R−i and P[imirror].
- Expand outward from i using normal checks, incrementing P[i] for matches.
- If the new palindrome centered at i expands past R, update C=i and R=i+P[i].
- Track the maximum value in P to find the longest palindrome.
C++ Code
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
private:
string preProcess(const string& s) {
if (s.empty())
return "^$";
string ret = "^";
for (char c : s) {
ret += "#" + string(1, c);
}
ret += "#$";
return ret;
}
public:
string longestPalindrome(string s) {
string T = preProcess(s);
int n = T.length();
vector<int> P(n, 0);
int C = 0, R = 0;
int maxLen = 0;
int centerIndex = 0;
for (int i = 1; i < n - 1; i++) {
int i_mirror = 2 * C - i;
if (R > i) {
P[i] = min(R - i, P[i_mirror]);
}
while (T[i + 1 + P[i]] == T[i - 1 - P[i]]) {
P[i]++;
}
if (i + P[i] > R) {
C = i;
R = i + P[i];
}
if (P[i] > maxLen) {
maxLen = P[i];
centerIndex = i;
}
}
int start = (centerIndex - 1 - maxLen) / 2;
return s.substr(start, maxLen);
}
};
Complexity Analysis
- Time Complexity: O(n). Even with the inner
while loop, the right boundary R only ever moves rightwards. It can move right at most 2n times. Therefore, the amortized time complexity is strictly linear. - Space Complexity: O(n) to store the preprocessed string T and the radius array P.
When To Use
Manacher's Algorithm is a marvel of computer science but is rarely expected in standard SDE 1 interviews due to its intricate implementation. However, for competitive programming, coding club hackathons, or environments where massive strings need instantaneous validation, Manacher's is an ace up the sleeve that differentiates exceptional logic from standard solutions.
Correctness Proof (Expand Around Center)
To prove that Expand Around Center finds the absolute longest palindrome, we rely on the foundational definition: every palindrome must have exactly one geometric center.
- If a palindrome has an odd length, its geometric center is a single distinct character.
- If a palindrome has an even length, its geometric center falls perfectly between two characters.
Because our algorithm systematically treats every character (and every space between characters) as a potential center and expands to the absolute maximum mathematical boundary for that specific center, it is impossible for a valid palindromic sequence to be missed. The global maximum is simply extracted from these local maximums.
Edge Cases to Consider
When writing backend logic for parsing, robust code handles edge cases gracefully:
- Empty String: Returns an empty string immediately.
- Single Character (
s = "a"): Returns the character itself. The loops handle this, but an early return if (s.length() <= 1) prevents unnecessary operations. - Entire String is Palindrome (
s = "racecar"): The algorithm will expand from the exact middle out to the boundaries correctly. - Repeated Characters (
s = "aaaaa"): This is the worst-case scenario for the Expand Around Center approach (triggering maximum O(n2) expansions), but it still runs efficiently within time limits.
Common Mistakes
- Off-by-One Errors in Substring Extraction:In C++,
s.substr(start, length) expects the starting index and the length. A common mistake is passing the ending index instead of the length. - Forgetting Even-Length Centers:Candidates often write the logic to expand around single characters
expand(s, i, i) but completely forget to check between characters expand(s, i, i+1), missing all even-length palindromes like "abba". - Boundaries Checking:Failing to check
left >= 0 and right < s.length() before comparing s[left] == s[right] will trigger out-of-bounds memory access errors.
Interview Tips
- Vocalize the Inefficiencies: If an interviewer presents this problem, do not immediately code the O(1) space solution. State the brute force, explain why it overlaps (the O(n3) problem), and then transition to Expand Around Center.
- Master the Mathematical Offsets: The line
start = i - (len - 1) / 2; in the Expand approach often confuses candidates under pressure. Practice deriving this by drawing it out on a whiteboard or paper. If len = 4 and center index i is 1 (the left character of the middle pair), start becomes 1 - (4-1)/2 = 0. - Understand the Follow-Ups: Interviewers may ask, "How would you modify this to find the number of palindromic substrings instead of the longest?" (LeetCode 647). The Expand Around Center logic handles this perfectly—just count the expansions instead of recording max length!
Key Takeaways
- Evolution of Logic: The journey from Brute Force to Expand Around Center showcases the importance of removing redundant operations. We moved from generating unrelated substrings to expanding structurally valid boundaries.
- Best for Interviews: The Expand Around Center approach (O(n2) time, O(1) space) is the golden standard. It perfectly balances clean readability with optimal memory constraints, exactly what logic-driven backend architectures demand.
- Best for Competitive Programming: Manacher's Algorithm (O(n) time) is the ultimate mathematical weapon for extreme constraints, though its intricate array manipulations make it prone to typos in a 45-minute technical screen.
FAQ
1. Why don't we use Dynamic Programming as the primary solution?
While DP is an excellent paradigm, allocating an n×n matrix for string comparison requires O(n2) space. For a string of length 1000, this requires a million boolean checks. The "Expand Around Center" method operates in the same time complexity but requires essentially zero extra memory.
2. Does checking odd and even length centers double the time complexity?
It increases the constant factor (checking 2n−1 centers instead of n), but in Big-O notation, 2n is still O(n). The worst-case runtime remains bounded strictly by O(n2).
3. Can I use recursion instead of iteration for the Expand approach?
Yes, you can write the expandAroundCenter helper recursively. However, iteration (while loop) is heavily preferred as it prevents stack overflow errors on massively long palindromes.
4. Why are # symbols inserted in Manacher's Algorithm?
The # symbols normalize the string. By forcing an artificial character between every actual character, all palindromes mathematically become odd-length (having a defined, single # or letter as a center), allowing a single unified loop logic.
5. What if there are multiple longest palindromic substrings of the same length?
The problem only requires you to return any one of them. In the Expand Around Center code, we only update maxLength if the new length is strictly greater (>), meaning it returns the first one encountered. Changing it to >= would return the last one encountered. Both are perfectly valid.
code link:
https://leetcode.com/problems/longest-palindromic-substring/description/
reference video link: