1392. Longest Happy Prefix
Problem Description
A happy prefix is defined as a non-empty string that appears both at the beginning (prefix) and at the end (suffix) of a string, but excludes the entire string itself.
Given a string s
, you need to find and return the longest happy prefix of s
. If no such prefix exists, return an empty string ""
.
For example:
- In the string
"level"
, the substring"l"
appears at both the beginning and end, making it a happy prefix - In the string
"ababab"
, the substring"abab"
appears at both the beginning (positions 0-3) and end (positions 2-5), making it a happy prefix - The entire string cannot be considered as a happy prefix of itself
The task is to identify the longest possible substring that satisfies this condition of being both a prefix and a suffix of the given string.
Intuition
To find the longest happy prefix, we need to identify the longest substring that appears both at the beginning and end of the string. The key insight is that we're looking for a pattern where the start of the string matches the end of the string.
Think about it this way: if we have a string like "ababab"
, we can observe that:
- The first 4 characters
"abab"
match the last 4 characters"abab"
- The first 2 characters
"ab"
match the last 2 characters"ab"
The brute force approach would be to check all possible prefix lengths, starting from the longest possible (which would be len(s) - 1
to avoid including the entire string) and working our way down. For each potential prefix length, we compare the prefix with the corresponding suffix.
However, the given solution takes a different approach - it iterates from the shortest to longest possible happy prefix. It does this by removing characters from both ends and checking if the remaining middle portions are equal. When s[:-i] == s[i:]
is true, it means:
s[:-i]
gives us the string without the lasti
characters (the prefix part)s[i:]
gives us the string without the firsti
characters (the suffix part)- If they're equal, then the first
i
characters must match the lasti
characters
This works because we're essentially checking if removing the same number of characters from the beginning and end leaves us with identical strings - which would only happen if those removed portions were the same.
The string hashing approach mentioned in the reference is an optimization technique. Instead of comparing entire substrings character by character (which can be expensive), we can compute hash values for prefixes and suffixes and compare those hash values instead. This reduces the comparison from O(n) to O(1) for each potential length, making the overall algorithm more efficient.
Solution Approach
The reference solution uses String Hashing to efficiently find the longest happy prefix. Let's walk through how this approach works:
String Hashing Implementation
String hashing maps a string to a numerical value, allowing us to compare strings in O(1) time instead of O(n). The core idea is to treat the string as a number in a certain base system.
Hash Function Setup:
- Choose a
BASE
value (typically 131 or 13331) - Choose a
MOD
value (typically2^64
for automatic overflow handling) - Assign each character a numerical value (e.g., for lowercase letters: a=1, b=2, ..., z=26)
Hash Calculation:
For a string s = c₁c₂...cₙ
, the hash value is calculated as:
hash = (val(c₁) × BASE^(n-1) + val(c₂) × BASE^(n-2) + ... + val(cₙ) × BASE^0) mod MOD
Algorithm Steps:
-
Compute Prefix Hashes: Calculate hash values for all prefixes of the string
prefix_hash[i]
represents the hash value ofs[0:i+1]
-
Compute Suffix Hashes: Calculate hash values for all suffixes of the string
suffix_hash[i]
represents the hash value ofs[i:]
-
Compare Hash Values: For each possible length
i
fromn-1
down to 1:- Check if
prefix_hash[i-1] == suffix_hash[n-i]
- If they match, the prefix
s[0:i]
equals the suffixs[n-i:]
- Return the longest such matching substring
- Check if
Why String Hashing Works:
- Instead of comparing entire substrings character by character (O(n) per comparison)
- We compare single hash values (O(1) per comparison)
- The probability of hash collision is extremely low with proper
BASE
andMOD
values
Handling Collisions: To make the algorithm even more robust, we can:
- Use multiple hash functions with different
BASE
andMOD
values - Only consider strings equal when all hash values match
- This makes it virtually impossible to construct data that causes hash collisions
Time Complexity: O(n) where n is the length of the string
- Computing all prefix/suffix hashes: O(n)
- Comparing hash values: O(n) comparisons, each taking O(1)
Space Complexity: O(n) for storing the hash values
Note: The provided solution code actually uses a simpler brute force approach with string slicing, but the reference describes the more efficient string hashing method that would be preferred for larger inputs.
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 walk through the solution approach using the string s = "ababab"
.
Goal: Find the longest substring that appears both as a prefix and suffix (but not the entire string).
Step 1: Understand what we're looking for
- We need to find matching prefix and suffix pairs
- Possible candidates: "a", "ab", "aba", "abab", "ababa" (not "ababab" as it's the entire string)
Step 2: Apply the solution approach (checking from smallest to largest)
Let's trace through the algorithm that checks s[:-i] == s[i:]
:
-
i = 1: Check if
s[:-1] == s[1:]
s[:-1]
= "ababa" (remove last 1 char)s[1:]
= "babab" (remove first 1 char)- "ababa" ≠ "babab" → Not equal, continue
-
i = 2: Check if
s[:-2] == s[2:]
s[:-2]
= "abab" (remove last 2 chars)s[2:]
= "abab" (remove first 2 chars)- "abab" == "abab" → Match found!
Since we found a match at i = 2, this means the first 2 characters match the last 2 characters. The happy prefix is s[:2]
= "ab".
But wait, we should continue checking for longer matches:
-
i = 3: Check if
s[:-3] == s[3:]
s[:-3]
= "aba" (remove last 3 chars)s[3:]
= "bab" (remove first 3 chars)- "aba" ≠ "bab" → Not equal
-
i = 4: Check if
s[:-4] == s[4:]
s[:-4]
= "ab" (remove last 4 chars)s[4:]
= "ab" (remove first 4 chars)- "ab" == "ab" → Match found!
This match at i = 4 means the first 4 characters match the last 4 characters. The happy prefix is s[:4]
= "abab".
- i = 5: Check if
s[:-5] == s[5:]
s[:-5]
= "a" (remove last 5 chars)s[5:]
= "b" (remove first 5 chars)- "a" ≠ "b" → Not equal
Result: The longest happy prefix is "abab" (found at i = 4).
Verification:
- Prefix "abab": positions [0,1,2,3] = "abab"
- Suffix "abab": positions [2,3,4,5] = "abab"
- They match! ✓
This example shows how the algorithm systematically checks each possible length to find the longest matching prefix-suffix pair.
Solution Implementation
1class Solution:
2 def longestPrefix(self, s: str) -> str:
3 """
4 Find the longest proper prefix which is also a suffix.
5 A proper prefix/suffix cannot be the entire string itself.
6
7 Args:
8 s: Input string
9
10 Returns:
11 The longest string that is both a proper prefix and suffix
12 """
13 # Iterate through possible prefix/suffix lengths from longest to shortest
14 # Start from 1 to exclude the entire string (len(s) - 1 characters max)
15 for i in range(1, len(s)):
16 # Check if prefix of length (len(s) - i) equals suffix of same length
17 # s[:-i] gets all characters except last i characters (prefix)
18 # s[i:] gets all characters from index i to end (suffix)
19 if s[:-i] == s[i:]:
20 # If they match, return this common prefix/suffix
21 return s[i:]
22
23 # No common proper prefix/suffix found
24 return ''
25
1class Solution {
2 private long[] powerOfBase; // Stores powers of the base for hash calculation
3 private long[] prefixHash; // Stores cumulative hash values for prefixes
4
5 /**
6 * Finds the longest prefix of string s that is also a suffix.
7 * Uses rolling hash technique for efficient string comparison.
8 *
9 * @param s The input string
10 * @return The longest prefix that is also a suffix (excluding the entire string)
11 */
12 public String longestPrefix(String s) {
13 final int BASE = 131; // Prime number base for rolling hash
14 int n = s.length();
15
16 // Initialize arrays with extra space to avoid boundary issues
17 powerOfBase = new long[n + 10];
18 prefixHash = new long[n + 10];
19
20 // Initialize first power as 1 (BASE^0 = 1)
21 powerOfBase[0] = 1;
22
23 // Precompute powers of base and prefix hashes
24 for (int i = 0; i < n; i++) {
25 // Calculate BASE^(i+1) = BASE^i * BASE
26 powerOfBase[i + 1] = powerOfBase[i] * BASE;
27
28 // Calculate hash for prefix ending at position i+1
29 // hash[i+1] = hash[i] * BASE + currentChar
30 prefixHash[i + 1] = prefixHash[i] * BASE + s.charAt(i);
31 }
32
33 // Try each possible length from longest to shortest
34 for (int length = n - 1; length > 0; length--) {
35 // Compare hash of prefix [1, length] with suffix [n-length+1, n]
36 // Using 1-based indexing for the get method
37 if (getSubstringHash(1, length) == getSubstringHash(n - length + 1, n)) {
38 return s.substring(0, length);
39 }
40 }
41
42 // No matching prefix-suffix found
43 return "";
44 }
45
46 /**
47 * Calculates hash value for substring from position left to right (1-indexed).
48 * Uses the formula: hash[right] - hash[left-1] * BASE^(right-left+1)
49 *
50 * @param left Starting position (1-indexed)
51 * @param right Ending position (1-indexed)
52 * @return Hash value of the substring
53 */
54 private long getSubstringHash(int left, int right) {
55 return prefixHash[right] - prefixHash[left - 1] * powerOfBase[right - left + 1];
56 }
57}
58
1typedef unsigned long long ULL;
2
3class Solution {
4public:
5 string longestPrefix(string s) {
6 // Use polynomial rolling hash with base 131 (a prime number)
7 const int BASE = 131;
8 int n = s.size();
9
10 // Arrays for storing powers of base and hash values
11 // power[i] = BASE^i, hash[i] = hash value of s[0...i-1]
12 ULL power[n + 10];
13 ULL hash[n + 10];
14
15 // Initialize: power[0] = 1, hash[0] = 0 (empty string)
16 power[0] = 1;
17 hash[0] = 0;
18
19 // Build power array and compute prefix hash values
20 for (int i = 0; i < n; ++i) {
21 // power[i+1] = BASE^(i+1)
22 power[i + 1] = power[i] * BASE;
23 // hash[i+1] = hash of s[0...i]
24 hash[i + 1] = hash[i] * BASE + s[i];
25 }
26
27 // Check from longest possible length to shortest
28 for (int len = n - 1; len > 0; --len) {
29 // Calculate hash value of prefix s[0...len-1]
30 ULL prefixHash = hash[len];
31
32 // Calculate hash value of suffix s[n-len...n-1]
33 // Using formula: hash(s[l...r]) = hash[r+1] - hash[l] * BASE^(r-l+1)
34 ULL suffixHash = hash[n] - hash[n - len] * power[len];
35
36 // If prefix and suffix hashes match, we found the longest happy prefix
37 if (prefixHash == suffixHash) {
38 return s.substr(0, len);
39 }
40 }
41
42 // No happy prefix found (other than empty string)
43 return "";
44 }
45};
46
1/**
2 * Finds the longest proper prefix which is also a suffix in the given string.
3 * A proper prefix is a prefix that is not equal to the entire string.
4 *
5 * @param s - The input string to analyze
6 * @returns The longest prefix that is also a suffix (excluding the entire string)
7 */
8function longestPrefix(s: string): string {
9 // Get the length of the input string
10 const stringLength: number = s.length;
11
12 // Iterate from the second-to-last character down to check all possible prefix lengths
13 // We start from stringLength - 1 to exclude the entire string as a valid answer
14 for (let prefixLength: number = stringLength - 1; prefixLength >= 0; prefixLength--) {
15 // Extract the prefix of current length from the start of the string
16 const prefix: string = s.slice(0, prefixLength);
17
18 // Extract the suffix of the same length from the end of the string
19 const suffix: string = s.slice(stringLength - prefixLength, stringLength);
20
21 // Check if the prefix matches the suffix
22 if (prefix === suffix) {
23 // Return the matching prefix-suffix string
24 return prefix;
25 }
26 }
27
28 // If no matching prefix-suffix is found, return an empty string
29 return '';
30}
31
Time and Space Complexity
Time Complexity: O(n²)
The code uses a loop that iterates from i = 1
to i = len(s) - 1
, which gives us n - 1
iterations where n
is the length of string s
. In each iteration, the code performs string slicing operations s[:-i]
and s[i:]
, which create new strings of lengths (n - i)
and (n - i)
respectively. The comparison s[:-i] == s[i:]
takes O(n - i)
time to compare the two strings character by character.
The total time complexity is:
- When
i = 1
: comparison takesO(n - 1)
time - When
i = 2
: comparison takesO(n - 2)
time - ...
- When
i = n - 1
: comparison takesO(1)
time
Sum = (n - 1) + (n - 2) + ... + 1 = n(n - 1)/2 = O(n²)
Space Complexity: O(n)
In each iteration, the string slicing operations s[:-i]
and s[i:]
create two new string objects. Each of these strings has a length of (n - i)
. Since Python strings are immutable and slicing creates new string objects, the space required for each iteration is O(n - i)
. However, these temporary strings are created and discarded in each iteration, so only the space for one iteration is used at a time. The maximum space used is O(n - 1)
when i = 1
, which simplifies to O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Inefficient String Comparison for Large Inputs
The provided solution uses string slicing and direct comparison (s[:-i] == s[i:]
), which creates new string objects and compares them character by character. This results in O(n²) time complexity in the worst case, making it inefficient for large strings.
Why this is problematic:
- Each string slice operation creates a new string object: O(n)
- Each comparison between two strings of length k takes O(k) time
- For a string of length n, we might check up to n-1 prefixes/suffixes
- Total worst-case complexity: O(n²)
Solution - Use KMP Algorithm's Failure Function:
class Solution:
def longestPrefix(self, s: str) -> str:
n = len(s)
# Build the LPS (Longest Proper Prefix which is also Suffix) array
lps = [0] * n
# Two pointers: i for suffix, j for prefix
j = 0 # Length of previous longest prefix suffix
for i in range(1, n):
# If characters don't match, try shorter prefix
while j > 0 and s[i] != s[j]:
j = lps[j - 1]
# If characters match, extend the prefix/suffix length
if s[i] == s[j]:
j += 1
lps[i] = j
# The last value in LPS array gives us the length of longest happy prefix
# We need lps[n-1] because we want proper prefix (not the entire string)
return s[:lps[n - 1]]
2. Memory Issues with String Slicing
Creating multiple string slices can cause memory problems for very long strings, as each slice creates a new string object in memory.
Solution - Use String Hashing with Rolling Hash:
class Solution:
def longestPrefix(self, s: str) -> str:
n = len(s)
if n <= 1:
return ""
BASE = 31
MOD = 10**9 + 7
# Calculate hash values incrementally
prefix_hash = 0
suffix_hash = 0
power = 1
result_len = 0
# Check all possible lengths from 1 to n-1
for i in range(n - 1):
# Update prefix hash: add next character
prefix_hash = (prefix_hash * BASE + ord(s[i])) % MOD
# Update suffix hash: add character from the end
suffix_hash = (suffix_hash + ord(s[n - 1 - i]) * power) % MOD
power = (power * BASE) % MOD
# If hashes match, we found a valid happy prefix
if prefix_hash == suffix_hash:
result_len = i + 1
return s[:result_len]
3. Edge Case Handling
The original solution doesn't explicitly handle edge cases like empty strings or single-character strings, relying on the range function to handle them implicitly.
Better Practice - Explicit Edge Case Handling:
class Solution:
def longestPrefix(self, s: str) -> str:
# Handle edge cases explicitly
if not s or len(s) == 1:
return ""
# Continue with the main algorithm...
What does the following code do?
1def f(arr1, arr2):
2 i, j = 0, 0
3 new_arr = []
4 while i < len(arr1) and j < len(arr2):
5 if arr1[i] < arr2[j]:
6 new_arr.append(arr1[i])
7 i += 1
8 else:
9 new_arr.append(arr2[j])
10 j += 1
11 new_arr.extend(arr1[i:])
12 new_arr.extend(arr2[j:])
13 return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2 int i = 0, j = 0;
3 List<Integer> newArr = new ArrayList<>();
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.add(arr1[i]);
8 i++;
9 } else {
10 newArr.add(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.add(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.add(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
1function f(arr1, arr2) {
2 let i = 0, j = 0;
3 let newArr = [];
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.push(arr1[i]);
8 i++;
9 } else {
10 newArr.push(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.push(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.push(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
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!