214. Shortest Palindrome
Problem Description
You are given a string s
. Your task is to find the shortest possible palindrome that can be created by adding characters only to the beginning (front) of the string.
A palindrome is a string that reads the same forwards and backwards. For example, "racecar" and "noon" are palindromes.
The goal is to determine what characters need to be prepended to s
to make the entire resulting string a palindrome, and this resulting palindrome should be as short as possible.
For example:
- If
s = "aacecaaa"
, the shortest palindrome would be"aaacecaaa"
(adding just one 'a' to the front) - If
s = "abcd"
, the shortest palindrome would be"dcbabcd"
(adding "dcb" to the front)
The solution uses a rolling hash technique to efficiently find the longest prefix of the string that is also a palindrome. This is done by:
- Computing a forward hash (
prefix
) and a reverse hash (suffix
) simultaneously as we traverse the string - When these hashes match at position
i
, it means the substring from index 0 toi
is a palindrome - Tracking the largest such index (
idx
) where this occurs - The characters after this longest palindromic prefix need to be reversed and prepended to create the shortest palindrome
The final answer is constructed by taking the non-palindromic suffix s[idx:]
, reversing it, and prepending it to the original string s
.
Intuition
To make a string into a palindrome by only adding characters to the front, we need to think about what part of the original string can already serve as the "middle" of our palindrome.
The key insight is that we want to find the longest prefix of the string that is already a palindrome. Why? Because this prefix won't need any corresponding characters added to the front - it's already symmetric.
For example, if s = "aacecaaa"
, the prefix "aacecaa" is already a palindrome. So we only need to handle the remaining part "a" by adding its reverse to the front.
The challenge becomes: how do we efficiently check if a prefix is a palindrome? Checking every possible prefix naively would be expensive. This is where rolling hash comes in.
The rolling hash technique allows us to:
- Build a hash of the string reading forward (
prefix
) - Build a hash of the string reading backward (
suffix
) - When these two hashes are equal at position
i
, the substring from start to positioni
must be a palindrome
Think of it like comparing fingerprints - if the forward fingerprint matches the backward fingerprint, we have a palindrome. We use modular arithmetic with a base (131) to create unique "fingerprints" for each substring.
As we traverse the string, we keep updating both hashes and check if they match. The last position where they match gives us the longest palindromic prefix. Everything after this prefix needs to be "mirrored" to the front to complete the palindrome.
The final step is simple: take whatever remains after the longest palindromic prefix, reverse it, and prepend it to the original string. This gives us the shortest possible palindrome.
Solution Approach
The solution implements a rolling hash algorithm to find the longest palindromic prefix efficiently in O(n) time.
Step-by-step Implementation:
-
Initialize Rolling Hash Variables:
base = 131
: The base for our polynomial hash functionmod = 10^9 + 7
: A large prime number to prevent overflowprefix = 0
: Hash value built from left to rightsuffix = 0
: Hash value built from right to leftmul = 1
: Multiplier for the suffix hashidx = 0
: Tracks the end index of the longest palindromic prefix
-
Build Hashes While Traversing: For each character at position
i
:- Update prefix hash:
prefix = (prefix * base + (ord(c) - ord('a') + 1)) % mod
- This shifts existing hash left by multiplying with base
- Adds the new character's value (1-26 for 'a'-'z')
- Update suffix hash:
suffix = (suffix + (ord(c) - ord('a') + 1) * mul) % mod
- Adds new character with appropriate positional weight
- The weight
mul
increases by factor ofbase
each iteration
- Update multiplier:
mul = (mul * base) % mod
- Update prefix hash:
-
Check for Palindrome:
- If
prefix == suffix
, the substring from index 0 toi
is a palindrome - Update
idx = i + 1
to mark this position
- If
-
Construct the Result:
- If
idx == n
: The entire string is already a palindrome, returns
- Otherwise: Take
s[idx:]
(the non-palindromic suffix), reverse it with[::-1]
, and prepend it tos
- If
Example Walkthrough:
For s = "aacecaaa"
:
- At index 0 ('a'): prefix and suffix both equal hash('a'), idx = 1
- At index 1 ('a'): prefix = hash('aa'), suffix = hash('aa'), idx = 2
- Continue until index 6: prefix = hash('aacecaa'), suffix = hash('aacecaa'), idx = 7
- Final:
s[7:]
= "a", reversed = "a", result = "a" + "aacecaaa" = "aaacecaaa"
The algorithm cleverly uses the property that if two strings have the same hash value, they are likely the same string (with very high probability due to the large prime modulus).
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with s = "abcd"
:
Initial Setup:
base = 131
,mod = 10^9 + 7
prefix = 0
,suffix = 0
,mul = 1
,idx = 0
Iteration 1 (i=0, c='a'):
- Calculate character value:
ord('a') - ord('a') + 1 = 1
- Update prefix:
prefix = (0 * 131 + 1) % mod = 1
- Update suffix:
suffix = (0 + 1 * 1) % mod = 1
- Check:
prefix == suffix
✓ (both equal 1) - Update:
idx = 1
(substring "a" is palindrome) - Update:
mul = (1 * 131) % mod = 131
Iteration 2 (i=1, c='b'):
- Character value:
ord('b') - ord('a') + 1 = 2
- Update prefix:
prefix = (1 * 131 + 2) % mod = 133
- Update suffix:
suffix = (1 + 2 * 131) % mod = 263
- Check:
prefix != suffix
✗ (133 ≠ 263) - No update to
idx
(substring "ab" is not palindrome) - Update:
mul = (131 * 131) % mod = 17161
Iteration 3 (i=2, c='c'):
- Character value:
ord('c') - ord('a') + 1 = 3
- Update prefix:
prefix = (133 * 131 + 3) % mod = 17426
- Update suffix:
suffix = (263 + 3 * 17161) % mod = 51746
- Check:
prefix != suffix
✗ - No update to
idx
- Update:
mul = (17161 * 131) % mod
Iteration 4 (i=3, c='d'):
- Character value:
ord('d') - ord('a') + 1 = 4
- Update prefix and suffix with same process
- Check:
prefix != suffix
✗ - No update to
idx
Final Construction:
- Longest palindromic prefix ends at
idx = 1
(just "a") - Non-palindromic suffix:
s[1:]
= "bcd" - Reverse the suffix: "bcd" → "dcb"
- Final result: "dcb" + "abcd" = "dcbabcd"
We can verify "dcbabcd" is indeed a palindrome: reading forward gives d-c-b-a-b-c-d, reading backward gives d-c-b-a-b-c-d ✓
Solution Implementation
1class Solution:
2 def shortestPalindrome(self, s: str) -> str:
3 """
4 Find the shortest palindrome by adding characters in front of string s.
5 Uses rolling hash to find the longest palindrome prefix efficiently.
6
7 Args:
8 s: Input string to convert to palindrome
9
10 Returns:
11 Shortest palindrome formed by adding characters to the beginning
12 """
13 # Rolling hash parameters
14 BASE = 131 # Prime base for polynomial rolling hash
15 MOD = 10**9 + 7 # Large prime modulus to prevent overflow
16
17 n = len(s)
18
19 # Hash values for prefix and suffix
20 prefix_hash = 0 # Hash of s[0:i+1]
21 suffix_hash = 0 # Hash of reverse of s[0:i+1]
22
23 # Multiplier for suffix hash calculation
24 base_power = 1
25
26 # Track the longest palindrome prefix ending position
27 longest_palindrome_end = 0
28
29 # Iterate through each character to find longest palindrome prefix
30 for i, char in enumerate(s):
31 # Calculate character value (1-indexed)
32 char_value = ord(char) - ord('a') + 1
33
34 # Update prefix hash: hash = hash * base + char_value
35 prefix_hash = (prefix_hash * BASE + char_value) % MOD
36
37 # Update suffix hash: builds hash of reversed string
38 # For reversed string, we add new character at the beginning
39 suffix_hash = (suffix_hash + char_value * base_power) % MOD
40
41 # Update base power for next iteration
42 base_power = (base_power * BASE) % MOD
43
44 # If hashes match, substring s[0:i+1] is a palindrome
45 if prefix_hash == suffix_hash:
46 longest_palindrome_end = i + 1
47
48 # If entire string is palindrome, return as is
49 if longest_palindrome_end == n:
50 return s
51
52 # Otherwise, prepend the reverse of non-palindrome suffix
53 # s[longest_palindrome_end:] is the part that's not palindrome
54 non_palindrome_suffix = s[longest_palindrome_end:]
55 return non_palindrome_suffix[::-1] + s
56
1class Solution {
2 /**
3 * Find the shortest palindrome by adding characters to the beginning of string s.
4 * Uses rolling hash to find the longest palindrome prefix efficiently.
5 *
6 * @param s the input string
7 * @return the shortest palindrome formed by adding characters to the beginning
8 */
9 public String shortestPalindrome(String s) {
10 // Rolling hash parameters
11 final int BASE = 131; // Base for polynomial rolling hash
12 final int MOD = 1000000007; // Large prime modulus to avoid overflow
13
14 // Hash values for prefix and suffix
15 int prefixHash = 0; // Hash of prefix (forward direction)
16 int suffixHash = 0; // Hash of suffix (reverse direction)
17
18 // Multiplier for suffix hash calculation
19 int multiplier = 1;
20
21 // Track the longest palindrome prefix
22 int longestPalindromePrefixLength = 0;
23 int stringLength = s.length();
24
25 // Iterate through each character to find longest palindrome prefix
26 for (int i = 0; i < stringLength; ++i) {
27 // Convert character to numeric value (1-26 for 'a'-'z')
28 int charValue = s.charAt(i) - 'a' + 1;
29
30 // Update prefix hash: hash = hash * base + charValue
31 prefixHash = (int) (((long) prefixHash * BASE + charValue) % MOD);
32
33 // Update suffix hash: hash = hash + charValue * base^i
34 suffixHash = (int) ((suffixHash + (long) charValue * multiplier) % MOD);
35
36 // Update multiplier for next iteration
37 multiplier = (int) (((long) multiplier * BASE) % MOD);
38
39 // If hashes match, we found a palindrome prefix
40 if (prefixHash == suffixHash) {
41 longestPalindromePrefixLength = i + 1;
42 }
43 }
44
45 // If entire string is already a palindrome, return as is
46 if (longestPalindromePrefixLength == stringLength) {
47 return s;
48 }
49
50 // Build result by reversing the non-palindrome suffix and prepending it
51 String nonPalindromeSuffix = s.substring(longestPalindromePrefixLength);
52 String reversedSuffix = new StringBuilder(nonPalindromeSuffix).reverse().toString();
53
54 return reversedSuffix + s;
55 }
56}
57
1typedef unsigned long long ull;
2
3class Solution {
4public:
5 string shortestPalindrome(string s) {
6 // Rolling hash parameters
7 const int BASE = 131; // Prime base for polynomial rolling hash
8
9 // Initialize hash values and variables
10 ull powerMultiplier = 1; // Stores BASE^i for suffix hash calculation
11 ull forwardHash = 0; // Hash of prefix s[0...i] computed left to right
12 ull reverseHash = 0; // Hash of prefix s[0...i] computed right to left
13 int longestPalindromicPrefixLength = 0; // Length of longest prefix that is also a palindrome
14 int stringLength = s.size();
15
16 // Find the longest palindromic prefix using rolling hash
17 for (int i = 0; i < stringLength; ++i) {
18 // Convert character to 1-indexed value (a=1, b=2, ...)
19 int charValue = s[i] - 'a' + 1;
20
21 // Update forward hash: hash = hash * BASE + charValue
22 // This builds hash from left to right
23 forwardHash = forwardHash * BASE + charValue;
24
25 // Update reverse hash: hash = hash + charValue * BASE^i
26 // This builds hash from right to left (as if string is reversed)
27 reverseHash = reverseHash + powerMultiplier * charValue;
28 powerMultiplier *= BASE;
29
30 // If both hashes match, s[0...i] is a palindrome
31 if (forwardHash == reverseHash) {
32 longestPalindromicPrefixLength = i + 1;
33 }
34 }
35
36 // If entire string is already a palindrome, return as is
37 if (longestPalindromicPrefixLength == stringLength) {
38 return s;
39 }
40
41 // Get the non-palindromic suffix and reverse it
42 string nonPalindromicSuffix = s.substr(longestPalindromicPrefixLength,
43 stringLength - longestPalindromicPrefixLength);
44 reverse(nonPalindromicSuffix.begin(), nonPalindromicSuffix.end());
45
46 // Prepend the reversed suffix to make the shortest palindrome
47 return nonPalindromicSuffix + s;
48 }
49};
50
1function shortestPalindrome(s: string): string {
2 // Rolling hash parameters
3 const BASE = 131; // Prime base for polynomial rolling hash
4
5 // Initialize hash values and variables
6 let powerMultiplier = 1; // Stores BASE^i for suffix hash calculation
7 let forwardHash = 0; // Hash of prefix s[0...i] computed left to right
8 let reverseHash = 0; // Hash of prefix s[0...i] computed right to left
9 let longestPalindromicPrefixLength = 0; // Length of longest prefix that is also a palindrome
10 const stringLength = s.length;
11
12 // Find the longest palindromic prefix using rolling hash
13 for (let i = 0; i < stringLength; i++) {
14 // Convert character to 1-indexed value (a=1, b=2, ...)
15 const charValue = s.charCodeAt(i) - 'a'.charCodeAt(0) + 1;
16
17 // Update forward hash: hash = hash * BASE + charValue
18 // This builds hash from left to right
19 forwardHash = forwardHash * BASE + charValue;
20
21 // Update reverse hash: hash = hash + charValue * BASE^i
22 // This builds hash from right to left (as if string is reversed)
23 reverseHash = reverseHash + powerMultiplier * charValue;
24 powerMultiplier *= BASE;
25
26 // If both hashes match, s[0...i] is a palindrome
27 if (forwardHash === reverseHash) {
28 longestPalindromicPrefixLength = i + 1;
29 }
30 }
31
32 // If entire string is already a palindrome, return as is
33 if (longestPalindromicPrefixLength === stringLength) {
34 return s;
35 }
36
37 // Get the non-palindromic suffix and reverse it
38 const nonPalindromicSuffix = s.substring(
39 longestPalindromicPrefixLength,
40 stringLength
41 );
42 const reversedSuffix = nonPalindromicSuffix.split('').reverse().join('');
43
44 // Prepend the reversed suffix to make the shortest palindrome
45 return reversedSuffix + s;
46}
47
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the input string s
.
The algorithm iterates through the string exactly once with a single for loop. Within each iteration, all operations are constant time:
- Computing
prefix
involves multiplication and modulo operations:O(1)
- Computing
suffix
involves addition, multiplication and modulo operations:O(1)
- Updating
mul
involves multiplication and modulo:O(1)
- Comparison
prefix == suffix
:O(1)
The string slicing and reversal operation s[idx:][::-1]
at the end takes O(n-idx)
in the worst case, which is still O(n)
. The string concatenation also takes O(n)
.
Therefore, the overall time complexity is O(n)
.
Space Complexity: O(n)
The algorithm uses:
- Fixed number of integer variables (
base
,mod
,n
,prefix
,suffix
,mul
,idx
):O(1)
- The output string construction
s[idx:][::-1] + s
creates a new string of length at most2n - 1
:O(n)
The dominant space usage comes from the string construction for the return value, giving us a total space complexity of O(n)
.
Common Pitfalls
1. Hash Collision Leading to Incorrect Results
The rolling hash algorithm relies on the assumption that if two hash values are equal, the strings are equal. However, hash collisions can occur where different strings produce the same hash value, leading to incorrectly identifying a non-palindrome as a palindrome.
Example of the problem:
# Vulnerable implementation - single hash
def shortestPalindrome_vulnerable(s: str):
BASE = 131
MOD = 10**9 + 7
prefix_hash = suffix_hash = 0
base_power = 1
longest_palindrome_end = 0
for i, char in enumerate(s):
char_value = ord(char) - ord('a') + 1
prefix_hash = (prefix_hash * BASE + char_value) % MOD
suffix_hash = (suffix_hash + char_value * base_power) % MOD
base_power = (base_power * BASE) % MOD
# Collision might cause false positive here
if prefix_hash == suffix_hash:
longest_palindrome_end = i + 1
return s[longest_palindrome_end:][::-1] + s
Solution - Use Double Hashing or Explicit Verification:
def shortestPalindrome_safe(s: str):
# Use two different hash functions
BASE1, MOD1 = 131, 10**9 + 7
BASE2, MOD2 = 137, 10**9 + 9
n = len(s)
prefix_hash1 = suffix_hash1 = 0
prefix_hash2 = suffix_hash2 = 0
base_power1 = base_power2 = 1
longest_palindrome_end = 0
for i, char in enumerate(s):
char_value = ord(char) - ord('a') + 1
# First hash function
prefix_hash1 = (prefix_hash1 * BASE1 + char_value) % MOD1
suffix_hash1 = (suffix_hash1 + char_value * base_power1) % MOD1
base_power1 = (base_power1 * BASE1) % MOD1
# Second hash function
prefix_hash2 = (prefix_hash2 * BASE2 + char_value) % MOD2
suffix_hash2 = (suffix_hash2 + char_value * base_power2) % MOD2
base_power2 = (base_power2 * BASE2) % MOD2
# Both hashes must match for palindrome confirmation
if prefix_hash1 == suffix_hash1 and prefix_hash2 == suffix_hash2:
longest_palindrome_end = i + 1
if longest_palindrome_end == n:
return s
return s[longest_palindrome_end:][::-1] + s
2. Integer Overflow in Languages Without Automatic Big Integer Handling
In languages like C++ or Java, the multiplication operations can cause integer overflow even with modulo operations if not handled carefully.
Problem example (in conceptual C++ style):
# Simulating overflow issue
def shortestPalindrome_overflow(s: str):
BASE = 131
MOD = 10**9 + 7
prefix_hash = 0
for char in s:
char_value = ord(char) - ord('a') + 1
# This multiplication might overflow before modulo in some languages
prefix_hash = prefix_hash * BASE + char_value # Overflow risk!
prefix_hash = prefix_hash % MOD # Too late if overflow occurred
Solution - Apply Modulo at Each Step:
def shortestPalindrome_no_overflow(s: str):
BASE = 131
MOD = 10**9 + 7
prefix_hash = 0
for char in s:
char_value = ord(char) - ord('a') + 1
# Apply modulo after each operation to prevent overflow
prefix_hash = (prefix_hash * BASE) % MOD
prefix_hash = (prefix_hash + char_value) % MOD
3. Incorrect Character Value Calculation for Non-Lowercase Strings
The current implementation assumes all characters are lowercase letters ('a' to 'z'). If the input contains uppercase letters, digits, or special characters, the hash calculation will fail.
Problem example:
# This will fail for s = "Aa" or s = "a1b"
char_value = ord(char) - ord('a') + 1 # Negative or incorrect values for non-lowercase
Solution - Handle All ASCII Characters:
def shortestPalindrome_all_chars(s: str):
BASE = 131
MOD = 10**9 + 7
n = len(s)
prefix_hash = suffix_hash = 0
base_power = 1
longest_palindrome_end = 0
for i, char in enumerate(s):
# Use the actual ASCII value directly
char_value = ord(char) # Works for all characters
prefix_hash = (prefix_hash * BASE + char_value) % MOD
suffix_hash = (suffix_hash + char_value * base_power) % MOD
base_power = (base_power * BASE) % MOD
if prefix_hash == suffix_hash:
longest_palindrome_end = i + 1
if longest_palindrome_end == n:
return s
return s[longest_palindrome_end:][::-1] + s
How many times is a tree node visited in a depth first search?
Recommended Readings
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!