String Matching

advanced35 min

Why not the obvious approach

Comparing the pattern against every starting position costs O(NM) — for a text of 106 and a pattern of 103, that is 109 character comparisons. Java’s String.indexOf uses roughly this approach and is fast enough surprisingly often, but it degrades on adversarial input such as a text of all a and a pattern of aaa...ab.

The techniques below are O(N + M) and predictable.

The prefix function

For each position in a string, record the length of the longest proper prefix that is also a suffix ending there. This table is what lets a matcher skip forward without re-examining characters.

static int[] prefixFunction(String s) {
    int n = s.length();
    int[] pi = new int[n];

    for (int i = 1; i < n; i++) {
        int j = pi[i - 1];
        while (j > 0 && s.charAt(i) != s.charAt(j)) {
            j = pi[j - 1];                 // fall back to the next shorter candidate
        }
        if (s.charAt(i) == s.charAt(j)) j++;
        pi[i] = j;
    }
    return pi;
}

For "ababa" the table is [0, 0, 1, 2, 3]: at the last position, "aba" is both a prefix and a suffix.

The while loop looks like it could be expensive, but j only ever decreases inside it and increases at most once per outer iteration, so the total work is O(N).

Concatenate the pattern, a separator that appears in neither string, and the text. Any position where the prefix function equals the pattern length marks a match.

static List<Integer> findAll(String text, String pattern) {
    String combined = pattern + '\1' + text;
    int[] pi = prefixFunction(combined);
    int m = pattern.length();

    List<Integer> matches = new ArrayList<>();
    for (int i = m + 1; i < combined.length(); i++) {
        if (pi[i] == m) {
            matches.add(i - 2 * m);        // start index in the original text
        }
    }
    return matches;
}

The separator is essential. Without it, a prefix-function value could span the boundary and report a match that does not exist. Pick a character guaranteed absent — '\1' works when the input is printable text.

What else the prefix function answers

  • Shortest period. For a string of length n, if n % (n - pi[n-1]) == 0 then n - pi[n-1] is the shortest repeating unit. This is how you test whether a string is some substring repeated.
  • All borders. The lengths of every prefix that is also a suffix are pi[n-1], then pi[pi[n-1]-1], and so on down to zero.
  • Number of occurrences of each prefix, with one extra accumulation pass over the table.

The Z-function

A close relative: z[i] is the length of the longest substring starting at i that matches a prefix of the whole string. Some problems are cleaner with this formulation.

static int[] zFunction(String s) {
    int n = s.length();
    int[] z = new int[n];
    int l = 0, r = 0;                      // the rightmost matching window

    for (int i = 1; i < n; i++) {
        if (i < r) {
            z[i] = Math.min(r - i, z[i - l]);   // reuse what the window already proved
        }
        while (i + z[i] < n && s.charAt(z[i]) == s.charAt(i + z[i])) {
            z[i]++;
        }
        if (i + z[i] > r) { l = i; r = i + z[i]; }
    }
    return z;
}

The [l, r) window records the match that extends furthest right. Inside it, previously computed values transfer directly, which is what keeps the total linear.

Rolling hashes

KMP finds one pattern. When you need to compare arbitrary substrings of a string against each other, hashing is the tool. Precompute prefix hashes and any substring’s hash becomes O(1).

static final long MOD = 1_000_000_007L;
static final long BASE = 131;

static long[] hash, power;

static void build(String s) {
    int n = s.length();
    hash = new long[n + 1];
    power = new long[n + 1];
    power[0] = 1;

    for (int i = 0; i < n; i++) {
        hash[i + 1] = (hash[i] * BASE + s.charAt(i)) % MOD;
        power[i + 1] = power[i] * BASE % MOD;
    }
}

// hash of s[l..r), 0-based half-open
static long substringHash(int l, int r) {
    long h = (hash[r] - hash[l] * power[r - l]) % MOD;
    return (h + MOD) % MOD;                // normalize a possible negative
}

Two substrings of equal length are almost certainly equal when their hashes match. The normalization on the last line is required — the subtraction can go negative, and skipping it gives wrong comparisons. See Modular Arithmetic.

Hash collisions are a real risk

With a single 32-bit-ish modulus and many comparisons, the birthday paradox makes a collision plausible. Two defences:

  • Use a larger modulus near 261, which requires care with multiplication overflow.
  • Use two independent hashes with different bases and moduli, and treat substrings as equal only when both agree. This is the simpler and more common choice.

Contest problems sometimes include anti-hash tests designed to break the popular base/modulus pairs. Choosing a base at random at runtime defeats those, but note the caveat in Debugging Under Time Pressure about reproducibility — seed it so failures can be replayed.

When an exact answer is required and a collision would be fatal, prefer KMP or Z, which have no failure probability.

Choosing

Which string technique

QuestionApproachCost
Find one pattern in a textKMP, or indexOfO(N + M)
Find all occurrences of one patternKMP over the concatenationO(N + M)
Shortest repeating unit of a stringPrefix function, period testO(N)
Compare arbitrary substringsRolling hashO(N) build, O(1) each
Longest common prefix of two suffixesZ-function or hashing plus binary searchO(N) or O(N log N)
Find many patterns at onceAho–Corasick, or a trieO(total length)
Longest palindromic substringHashing with binary search, or Manacher'sO(N log N) or O(N)

Java-specific notes

charAt on a String is a bounds-checked method call. In a hot loop over 106 characters, converting to a char[] once is noticeably faster:

char[] c = s.toCharArray();

Also avoid building strings with + inside a loop — each concatenation copies the whole thing, making an O(N) loop O(N2). Use StringBuilder.

Common mistakes

  • Omitting the separator in the KMP concatenation.
  • Off-by-one in the reported match index. Derive i - 2 * m on a tiny example.
  • Negative hash values from the subtraction.
  • A single hash where a collision breaks correctness.
  • int hash arithmetic. Products need long.
  • String concatenation in a loop.
  • Assuming indexOf is always fast enough. Usually it is; on adversarial input it is not.

Practice

  1. Given a text and a pattern, print every starting index where the pattern occurs.
  2. Given a string, print the length of its shortest repeating unit.
  3. Given a string and Q queries, each giving two substrings, print whether they are identical.
  4. Given a string, print the length of the longest prefix that is also a suffix but not the whole string.
  5. Given two strings, print the length of their longest common substring.
Hints
  1. KMP over pattern + separator + text.
  2. n - pi[n-1], but only if it divides n; otherwise the answer is n.
  3. Rolling hash with prefix hashes. Use two moduli if the constraints are large.
  4. Exactly pi[n-1].
  5. Binary search the answer length, and for each candidate length collect the hashes of all substrings of that length in the first string into a set, then check the second. That gives O(N log N) with a collision risk. A suffix automaton or suffix array is the exact alternative.

Related