Skip to content
Ameen Abozaid
PublishedBEG-044-L05

Demo: solving a problem with a sliding window

  • Lesson 5 of 5
  • 11 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

This is a hands-on lesson. We will solve one classic interview problem from start to finish, the way you would at a keyboard: find the length of the longest substring of a string that has no repeated characters. For "abcabcbb" the answer is 3 ("abc"). For "aaaa" it is 1. For the empty string it is 0.

We will go through the full workflow: state the problem, think of the obvious slow solution, design the window, write it, make a deliberate off-by-one mistake and find it with a test, then test the edge cases and state the cost. This is the same workflow you will need in the module lab and in timed problem sets.

You will need a place to run Java: any IDE or the command line. Menus and shortcuts differ between IDEs and operating systems, so use whatever way you normally have to compile and run a single file.

This lesson builds on the sliding window lesson (previous lessons of this module), on String and charAt (BEG-015), and on HashSet (BEG-041).

Learning Objectives

By the end of this lesson you will be able to:

  • Turn a problem statement into a precise input, output, and examples before writing code.
  • Describe a brute-force solution and its cost, and say why it is wasteful.
  • Design a variable-size window with a Set<Character> that tracks what is inside it.
  • Find and fix the off-by-one in the window length using a test.
  • Choose edge-case tests: empty string, all-same characters, and a repeat that is not at the window's left edge.
  • State the time and space cost of the final solution.

Written Explanation

Step 1: state the problem exactly

A substring is a block of consecutive characters. So "abc" is a substring of "abcabcbb", but "acb" is not (it is a subsequence, not consecutive). We want the length of the longest one with all characters different.

Write examples before code:

InputLongest valid substringAnswer
"abcabcbb""abc"3
"aaaa""a"1
""none0
"pwwkew""wke"3

"pwwkew" is a good trap: "pwke" has four distinct letters, but it is not consecutive, so it does not count.

Step 2: the brute-force idea

Try every starting position. From each start, extend to the right, remembering characters in a set, until you meet a character that is already in the set. Record the length, then try the next start.

java
static int brute(String s) {
    int best = 0;
    for (int i = 0; i < s.length(); i++) {
        Set<Character> seen = new HashSet<>();
        for (int j = i; j < s.length(); j++) {
            if (!seen.add(s.charAt(j))) break;      // add returns false if already present
            best = Math.max(best, j - i + 1);
        }
    }
    return best;
}

The Set.add method returns false when the element is already present (the HashSet documentation says so), which gives us a neat way to detect a repeat. This brute force is O(n²) in the worst case: a nested loop over start and end. An even simpler version that checks every substring separately and tests each one for repeats is O(n³). We keep this method, because later it works as a trusted checker for the fast version.

The waste: when the window from start 0 is "abc" and a repeated a stops it, the next start (1) rebuilds "bc" from scratch, even though we already knew those characters were fine.

Step 3: design the window

Use a window [left, right] that always contains no repeated character, and a Set<Character> holding exactly the characters inside it.

Each step of the loop:

  1. Look at the new character c = s.charAt(right).
  2. While c is already in the set, remove s.charAt(left) from the set and move left forward. This shrinks the window from the left until c no longer repeats.
  3. Add c to the set. The window [left, right] is valid again.
  4. The window length is right - left + 1; update the best.

Step 2 needs while, not if: the repeated character may be several positions back, so one removal may not be enough.

Step 4: write it, with the planned mistake

First, the version with the length written the wrong way:

java
static int buggy(String s) {
    Set<Character> w = new HashSet<>();
    int left = 0, best = 0;
    for (int right = 0; right < s.length(); right++) {
        while (w.contains(s.charAt(right))) { w.remove(s.charAt(left)); left++; }
        w.add(s.charAt(right));
        best = Math.max(best, right - left);      // BUG: length is missing + 1
    }
    return best;
}

Test it on "abc", where we know the answer is 3:

java
System.out.println(buggy("abc"));

Output:

text
2

Wrong. Trace it: when right = 2, left = 0, the window is a, b, c: three items, but right - left is 2. Both ends are included, so the number of items is right - left + 1. A one-item window (left == right) shows the problem most simply: right - left says 0.

The fix:

java
static int longest(String s) {
    Set<Character> w = new HashSet<>();
    int left = 0, best = 0;
    for (int right = 0; right < s.length(); right++) {
        while (w.contains(s.charAt(right))) { w.remove(s.charAt(left)); left++; }
        w.add(s.charAt(right));
        best = Math.max(best, right - left + 1);
    }
    return best;
}

Step 5: test edge cases and compare with brute force

I ran longest and brute on several inputs, printing both results:

text
"abc" -> 3 brute 3
"" -> 0 brute 0
"aaaa" -> 1 brute 1
"abcabcbb" -> 3 brute 3
"pwwkew" -> 3 brute 3
"abba" -> 2 brute 2

The empty string works because the loop never runs and best stays 0. "aaaa" works because each new a removes the previous one. "abba" is a great test: when the second a arrives, the window is "b" (the first a was already dropped while handling the second b), and the set correctly does not contain a any more.

Step 6: state the cost

Each character enters the window once (add) and leaves at most once (remove), and both pointers only move forward. That is at most about 2n moves, so the time is O(n), assuming the HashSet operations take constant time on average, which the Java documentation states for a well-spread hash function. The set holds at most as many characters as the window, and never more than the number of distinct characters in the string, so the space is O(k) where k is the size of the character set that can appear (O(min(n, k))).

Step 7: why a window, and not another technique?

Recognizing the technique is part of the skill. Ask these questions in order:

Question about the problemIf yes
Is the answer a consecutive block (substring or subarray) with a rule on what is inside?Sliding window (this problem)
Is the data sorted, and do you look for a pair or a cleanup in place?Two pointers
Does the problem contain a smaller copy of itself (digits, nested folders)?Recursion
Do you need all subsets, orderings, or combinations?Backtracking

Here the answer is a consecutive block, and the rule ("no repeated character") can be updated one character at a time, so a window fits. The data is not sorted, which rules out two pointers.

Examples

Example 1: the brute force on a short string (simple)

brute("abc"): start 0 reads a, b, c with no repeat: length 3. Starts 1 and 2 give 2 and 1. Maximum 3. Small, correct, and the checker for the faster version.

Example 2: tracing the window on "pwwkew" (practical)

Walk through longest("pwwkew"); the result printed 3.

rightcharactionwindowlength
0padd[0,0] p1
1wadd[0,1] pw2
2wrepeat: remove p, remove w, then add w[2,2] w1
3kadd[2,3] wk2
4eadd[2,4] wke3
5wrepeat: remove w, then add w[3,5] kew3

The best is 3. Notice that at right = 2 one removal was not enough: removing p left w still in the set, so the while ran again.

Example 3: a bug that tests miss unless you choose "abba" (realistic)

A common attempt replaces while with if. I tried it on "abba" and it printed 3, but the correct answer is 2. With if, the repeated b removes only a, leaving b in the set and then adding b again; the window is then wrongly treated as valid, and the later a extends it to "bba". Simple inputs like "abc" hide this bug, because they never need more than one removal. This is why the edge-case list should include inputs where the repeated character is not at the left edge.

Backend Insight

Real systems run this same logic on streams: "has this user session ID, request ID, or token already appeared within the last N events?" A sliding window with a set of recent IDs is a standard way to detect duplicate or replayed messages. The pattern window plus a structure that answers membership quickly is the same one used here.

The workflow matters as much as the code. Writing a slow, obviously correct version first and using it as a checker for the fast version is how backend developers refactor with confidence: when you replace an O(n²) algorithm with an O(n) one, you compare the two on many inputs before trusting the new one.

Common Mistakes

Mistake 1: Window length without + 1

right - left counts the steps between the ends, not the items. Use right - left + 1.

Mistake 2: if instead of while when shrinking

One removal does not always remove the earlier copy of the repeated character. Shrink until the new character no longer repeats.

Mistake 3: Adding to the set before shrinking

If you add s.charAt(right) first, the set already contains it and the while loop would remove characters forever. Shrink first, then add.

Mistake 4: Testing only the example from the problem statement

"abcabcbb" alone does not expose Mistake 2 or the empty-string case. Choose tests for the empty string, one character, all-same characters, and a repeat inside the window.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. What are the answers for "", "x", and "aaaa"?

Show solution A1

A1. "" gives 0, "x" gives 1, "aaaa" gives 1.

A2. A window has left = 2 and right = 4. How many characters does it hold?

Show solution A2

A2. 4 - 2 + 1 = 3 characters (indexes 2, 3, 4).

A3. Why does the window loop use a Set<Character> and not a nested loop to test for repeats?

Show solution A3

A3. The HashSet answers "is this character in the window?" in about constant time, so the check does not cost another loop over the window. A nested loop for every check would bring the cost back toward O(n²) or worse.

Level B: Application

B1. Trace longest("abba") in a table like the one in Example 2.

Show solution B1

B1. right = 0, a: add, window a, length 1. right = 1, b: add, window ab, length 2. right = 2, b: repeat; remove a (left becomes 1), b still in set, remove b (left becomes 2); add b: window b, length 1. right = 3, a: not in set, add; window ba, length 2. Best 2, matching my run.

B2. Predict what the buggy version (length right - left) returns for "a" and for "ab". What is the correct answer for each?

Show solution B2

B2. For "a", right = 0 and left = 0, so right - left is 0 and the buggy version returns 0 (correct: 1). For "ab" the last window is [0, 1], so it returns 1 (correct: 2). The buggy method is always one too small. I worked this out by tracing, and it matches the "abc" run (2 instead of 3).

B3. Change longest to return the substring itself (the first longest one found). What do you get for "abcabcbb" and for "pwwkew"?

Show solution B3

B3.

java
static String longestText(String s) {
    Set<Character> w = new HashSet<>();
    int left = 0, best = 0, start = 0;
    for (int right = 0; right < s.length(); right++) {
        while (w.contains(s.charAt(right))) { w.remove(s.charAt(left)); left++; }
        w.add(s.charAt(right));
        if (right - left + 1 > best) { best = right - left + 1; start = left; }
    }
    return s.substring(start, start + best);
}

My run gave abc for "abcabcbb", wke for "pwwkew", and an empty string for "". It keeps the first window of maximal length because it updates only on a strictly greater length.

Level C: Challenge

C1. Write a test method that runs longest and brute on several strings and prints a message for any disagreement.

Show solution C1

C1.

java
static void check() {
    String[] tests = {"abc", "", "aaaa", "abcabcbb", "pwwkew", "abba"};
    for (String s : tests) {
        if (longest(s) != brute(s)) System.out.println("Mismatch on \"" + s + "\"");
    }
    System.out.println("done");
}

For the six strings above, longest and brute agreed in my run (so only done would be printed).

C2. Explain why the cost is O(n) although there is a while inside the for.

Show solution C2

C2. right advances n times. left also only moves forward, at most n times in total across the whole run, so the inner while does at most n iterations in total over all outer iterations, not n per iteration. In total there are at most about 2n steps.

Mini Challenge

Find the length of the longest substring that contains at most two different characters. Hint: a Set is not enough, because a character may appear several times in the window and must only count as gone when its last copy leaves. Count the copies instead, for example with an int[128] array indexed by the character (assume plain ASCII text) and a variable for how many different characters are inside.

Check your work: write the brute force first, then compare on "eceba" (answer 3, "ece"), "aaaa" (4), and "" (0).

Summary & Next Lesson

Key takeaways

  • Start with exact examples, then a slow, correct solution that can be a checker.
  • Variable window recipe: grow with right, shrink with while until the rule holds, then update the answer.
  • The window length is right - left + 1; test one-item windows to catch off-by-one errors.
  • Use a Set for fast membership tests inside the window.
  • The cost is O(n) time because each pointer only moves forward.

Next lesson

This is the last lesson of the module. The module lab, "Technique drills", gives you eight problems, two for each technique: two pointers, sliding window, recursion, and backtracking. Practice stating the complexity of each in a comment.

Self-check

  1. What is a substring, and how is it different from a subsequence?
  2. Why does the window shrink with while and not if?
  3. Which expression gives the length of window [left, right]?
  4. What test input would expose a bug where if replaces while?
  5. What is the time cost of the final solution?
Show answers
  1. A substring is a block of consecutive characters; a subsequence may skip characters.
  2. The earlier copy of the repeated character may be several positions to the left, so more than one removal can be needed.
  3. right - left + 1.
  4. "abba", or any string where the repeated character is not at the window's left edge.
  5. O(n) time (with constant-time average set operations), and O(k) space for the set.

References

  • Oracle, HashSet (Java SE 21 API). It states that add returns false if the element is already present and that add, remove, and contains take constant time when the hash function disperses elements properly.
  • Oracle, String (Java SE 21 API). It defines substring(beginIndex, endIndex) as inclusive at the start and exclusive at the end. The window argument and the O(n) cost are this course's own analysis; all Java code was compiled and run with JDK 21, except where a solution says it was reasoned out.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Type to search levels, phases, modules and lessons.