عرض عملي: حل مشكلة باستخدام النافذة المنزلقة
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
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:
| Input | Longest valid substring | Answer |
|---|---|---|
"abcabcbb" | "abc" | 3 |
"aaaa" | "a" | 1 |
"" | none | 0 |
"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.
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:
- Look at the new character
c = s.charAt(right). - While
cis already in the set, removes.charAt(left)from the set and moveleftforward. This shrinks the window from the left untilcno longer repeats. - Add
cto the set. The window[left, right]is valid again. - 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:
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:
System.out.println(buggy("abc"));Output:
2Wrong. 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:
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:
"abc" -> 3 brute 3
"" -> 0 brute 0
"aaaa" -> 1 brute 1
"abcabcbb" -> 3 brute 3
"pwwkew" -> 3 brute 3
"abba" -> 2 brute 2The 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 problem | If 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.
| right | char | action | window | length |
|---|---|---|---|---|
| 0 | p | add | [0,0] p | 1 |
| 1 | w | add | [0,1] pw | 2 |
| 2 | w | repeat: remove p, remove w, then add w | [2,2] w | 1 |
| 3 | k | add | [2,3] wk | 2 |
| 4 | e | add | [2,4] wke | 3 |
| 5 | w | repeat: remove w, then add w | [3,5] kew | 3 |
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"?
أظهر الحل 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?
أظهر الحل 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?
أظهر الحل 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.
أظهر الحل 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?
أظهر الحل 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"?
أظهر الحل B3
B3.
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.
أظهر الحل C1
C1.
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.
أظهر الحل 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 withwhileuntil 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
Setfor 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
- What is a substring, and how is it different from a subsequence?
- Why does the window shrink with
whileand notif? - Which expression gives the length of window
[left, right]? - What test input would expose a bug where
ifreplaceswhile? - What is the time cost of the final solution?
أظهر الإجابات
- A substring is a block of consecutive characters; a subsequence may skip characters.
- The earlier copy of the repeated character may be several positions to the left, so more than one removal can be needed.
right - left + 1."abba", or any string where the repeated character is not at the window's left edge.- 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 thataddreturnsfalseif the element is already present and thatadd,remove, andcontainstake constant time when the hash function disperses elements properly. - Oracle,
String(Java SE 21 API). It definessubstring(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.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.