Facebook Pixel

1392. Longest Happy Prefix

HardStringString MatchingHash FunctionRolling Hash
Leetcode Link

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 last i characters (the prefix part)
  • s[i:] gives us the string without the first i characters (the suffix part)
  • If they're equal, then the first i characters must match the last i 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 (typically 2^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:

  1. Compute Prefix Hashes: Calculate hash values for all prefixes of the string

    • prefix_hash[i] represents the hash value of s[0:i+1]
  2. Compute Suffix Hashes: Calculate hash values for all suffixes of the string

    • suffix_hash[i] represents the hash value of s[i:]
  3. Compare Hash Values: For each possible length i from n-1 down to 1:

    • Check if prefix_hash[i-1] == suffix_hash[n-i]
    • If they match, the prefix s[0:i] equals the suffix s[n-i:]
    • Return the longest such matching substring

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 and MOD values

Handling Collisions: To make the algorithm even more robust, we can:

  • Use multiple hash functions with different BASE and MOD 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 Evaluator

Example 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 takes O(n - 1) time
  • When i = 2: comparison takes O(n - 2) time
  • ...
  • When i = n - 1: comparison takes O(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...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More