HARD Lab · Strings · 11

424. Longest Repeating Character Replacement

Given a string and an allowance of k character replacements, find the longest substring you can make entirely of one repeated character — a classic variable-size sliding window where the window stays valid while windowLength − maxFreq ≤ k.

MediumSliding WindowTypeScript

PROBLEM What we're solving

You have a string of uppercase letters and can change at most k characters. Return the length of the longest substring that consists of a single repeated letter after those replacements.

Example: s="AABABBA", k=1. If you replace the B at index 5 with A, you get AAAA spanning indices 0–3 (length 4). Answer: 4.

KEY IDEA Replacements needed = windowLen − maxFreq

Insight → Inside any window, the minimum replacements needed to make it uniform equals windowLength − maxFreq, where maxFreq is the count of the most frequent character. The window is valid as long as this value is ≤ k. You never need to know which character wins — only how many others need changing.
Classic confusion → When the window becomes invalid, most people shrink maxFreq as L advances. This is both unnecessary and costly. Because you only care about beating the current best length, the window only needs to grow— if it can't grow, it slides as a unit (L and R both advance by 1). A stale maxFreq is fine: it only allows the window to move forward, never backward.

RECIPE Slide the window, never shrink maxFreq

  • 0 · Initialize. L = 0, maxFreq = 0, best = 0, counts map empty.
  • 1 · Expand R. Add s[R] to counts. Update maxFreqif this char's new count exceeds it.
  • 2 · Check validity. If (R - L + 1) - maxFreq > k, the window needs too many replacements. Evict s[L] from counts and advance L. Do not update maxFreq.
  • 3 · Record best. best = Math.max(best, R - L + 1). Because the window can only grow or slide (never shrink), the window size only increases when we find a strictly better answer.
  • 4 · Return best.
Pattern transfer → The formula windowLen − dominant = cost appears in Max Consecutive Ones III (flip 0s), Longest Subarray of 1s After Deleting One Element, and any "at most k changes" string/array problem. Whenever the prompt says "replace at most k" or "flip at most k bits," reach for this window.

COST Complexity & alternatives

Brute force (all substrings)
O(n²)
Try every [i, j] pair, compute replacements needed.
Sliding window
O(n)
Single pass; O(1) space (26-letter fixed alphabet).

The "never shrink maxFreq" trick keeps the inner loop from ever running more than once per iteration — L advances at most once per R step, so total work is O(2n) = O(n). Space is O(26) = O(1) for an uppercase-only alphabet.

RUN IT Expand R, slide L when replacements exceed k

step 0 / 15
STARTBegin with empty window. Expand R rightward, tracking character counts and maxFreq (highest count in window). Window is valid while windowLen − maxFreq ≤ k.
s =A0A1B2A3B4B5A6
State
0
L
R
0
maxFreq
0
wLen − maxFreq
1
k
0
best
L (left pointer)R (right pointer)inside windowL = R (single-char window)valid / best length
slowfast

TYPESCRIPT The solution, annotated

characterReplacement.ts
function characterReplacement(s: string, k: number): number {
  const counts: Record<string, number> = {};
  let maxFreq = 0;
  let best = 0;
  let L = 0;

  for (let R = 0; R < s.length; R++) {
    const ch = s[R];
    counts[ch] = (counts[ch] ?? 0) + 1;
    if (counts[ch] > maxFreq) maxFreq = counts[ch];

    // windowLen - maxFreq = number of replacements needed
    if (R - L + 1 - maxFreq > k) {
      // slide the window: remove leftmost char, keep maxFreq unchanged
      counts[s[L]]--;
      L++;
    }

    best = Math.max(best, R - L + 1);
  }

  return best;
}

Reading it block by block

Lines 2–5 — state. counts tracks character frequencies inside the current window. maxFreq is the highest any single character appears in the window (it only ever grows). Lis the window's left edge.
Lines 7–9 — expand R. Add the incoming character to counts, then immediately check whether it raised maxFreq. This is the only place maxFreq increases.
Lines 11–15 — slide if invalid. (R - L + 1) - maxFreq is the minimum replacements needed. If that exceeds k, evict s[L] and advance L by one. Crucially, we do not recompute maxFreq here — a potentially stale value is safe because it only allows the window to advance, never retrograde.
Line 17 — record best. The window size is R - L + 1. Because the window never shrinks, best only updates when the window actually grew past the previous best — so you could write this as an if instead of Math.max and it would still be correct.
Complexity → O(n) time — L advances at most once per iteration of the outer loop, so total pointer moves ≤ 2n. O(26) = O(1) space for the counts map over uppercase letters; O(n) in the worst case for arbitrary characters.

INTERVIEWFollow-ups they'll ask

  • "What if the string has lowercase or arbitrary characters?" Use a Map<string, number> instead of a fixed array; time stays O(n), space becomes O(k) distinct chars.
  • "Return the actual substring, not just its length?" Record bestL whenever best updates and return s.slice(bestL, bestL + best).
  • "Why is it safe to not shrink maxFreq?" Because we only care about beating the current best. A window the same size as before with a stale maxFreq will always fail the validity check and slide — it will never be accepted as a new best.
  • "What if k = 0?" The window collapses to the longest run of one character — equivalent to longest uniform substring. The algorithm handles it correctly without any special case.
  • "Can you solve it with a different character set, like digits?" Yes — the formula windowLen − maxFreq ≤ k is character-set agnostic. Just swap in a suitable count structure.

MNEMONIC The one-liner

"Replacements needed = windowLen minus the king. If that exceeds k, slide the window — never shrink the king."

TRIGGERS When you see ___ → reach for ___

"replace at most k characters" in a stringsliding window, validity = windowLen − maxFreq ≤ k
"flip at most k zeros / bits"same window formula: windowLen − count(dominant) ≤ k
window slides but never shrinksstale maxFreq trick — only care about growing best
"longest substring / subarray with at most k changes"variable-size sliding window, evict one left element when invalid

SKELETON The reusable shape

skeleton.ts
const counts: Record<string, number> = {};
let maxFreq = 0, best = 0, L = 0;

for (let R = 0; R < s.length; R++) {
  counts[s[R]] = (counts[s[R]] ?? 0) + 1;
  maxFreq = Math.max(maxFreq, counts[s[R]]);

  if (R - L + 1 - maxFreq > k) {
    counts[s[L]]--;
    L++;
  }
  best = Math.max(best, R - L + 1);
}
return best;

FLASHCARDS Tap to flip

What is the key validity invariant for this window?
windowLength − maxFreq ≤ k. The left side counts characters that must be replaced.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What is the time complexity of the sliding window solution?
QUESTION 02
What does windowLen − maxFreq represent?
QUESTION 03
For s="AABABBA", k=1, what length does the algorithm return?
QUESTION 04
Why is it correct to NOT decrease maxFreq when sliding the left pointer?
QUESTION 05
What happens when k = 0?
QUESTION 06
Which of these problems uses the identical sliding window pattern?
QUESTION 07
After the outer loop, maxFreqmight be "stale" — larger than any real frequency in the final window. Is the returned best still correct?