{"id":4777,"date":"2024-10-21T12:52:11","date_gmt":"2024-10-21T12:52:11","guid":{"rendered":"https:\/\/algocademy.com\/blog\/mastering-the-minimum-window-substring-problem-a-comprehensive-guide\/"},"modified":"2026-10-15T12:51:16","modified_gmt":"2026-10-15T12:51:16","slug":"mastering-the-minimum-window-substring-problem-a-comprehensive-guide","status":"publish","type":"post","link":"https:\/\/algocademy.com\/blog\/mastering-the-minimum-window-substring-problem-a-comprehensive-guide\/","title":{"rendered":"Minimum Window Substring: Sliding Window Explained in O(n)"},"content":{"rendered":"<p>Minimum Window Substring asks for the shortest part of <code>s<\/code> that contains every character of <code>t<\/code>, duplicates included. The O(n) solution is a sliding window with two pointers and a count of how many required characters are still satisfied: grow the right edge until the window is valid, then shrink the left edge as far as it stays valid, and record the best window each time.<\/p>\n<h2>The problem<\/h2>\n<blockquote><p>Given two strings <code>s<\/code> and <code>t<\/code>, return the minimum window substring of <code>s<\/code> such that every character in <code>t<\/code> (including duplicates) is included in the window. If there is no such substring, return the empty string.<\/p><\/blockquote>\n<p>This is LeetCode 76, rated Hard. Example: for <code>s = \"ADOBECODEBANC\"<\/code> and <code>t = \"ABC\"<\/code> the answer is <code>\"BANC\"<\/code>. Note that the window needs the right <em>counts<\/em>: for <code>t = \"AA\"<\/code> a window with one <code>A<\/code> is not enough. The window may contain extra characters, and any one valid window of minimum length is accepted when there are ties.<\/p>\n<h2>Why brute force is too slow<\/h2>\n<p>Checking every substring costs O(n^2) substrings, and checking each one against <code>t<\/code> costs more on top of that. The sliding window works because of one property: if a window is valid, making it bigger keeps it valid, and if it is invalid, making it smaller keeps it invalid. So each pointer only ever moves forward, and no substring needs to be examined twice.<\/p>\n<h2>The idea<\/h2>\n<ol>\n<li>Count the characters of <code>t<\/code> in a map <code>need<\/code>. Let <code>required<\/code> be the number of distinct characters in it.<\/li>\n<li>Keep a second map <code>window<\/code> with the counts inside <code>s[left..right]<\/code>, and a number <code>have<\/code>: how many distinct characters currently reach their required count.<\/li>\n<li>Move <code>right<\/code> one step at a time and add that character. If its count in the window just became equal to <code>need<\/code>, increase <code>have<\/code>.<\/li>\n<li>While <code>have == required<\/code>, the window is valid. Save it if it is the shortest so far, then remove <code>s[left]<\/code> and move <code>left<\/code> forward. If removing that character drops its count below <code>need<\/code>, decrease <code>have<\/code> and the loop ends.<\/li>\n<\/ol>\n<p>The <code>have<\/code> counter is what makes it O(n). Without it you have to re-check every character of <code>t<\/code> after each move.<\/p>\n<h2>Python solution<\/h2>\n<pre><code class=\"language-python\">from collections import Counter\n\ndef min_window(s, t):\n    if not s or not t:\n        return \"\"\n    need = Counter(t)\n    required = len(need)          # distinct characters to satisfy\n    have = 0                      # how many of them are satisfied now\n    window = {}\n    best_len, best_left = float(\"inf\"), 0\n    left = 0\n\n    for right, ch in enumerate(s):\n        window[ch] = window.get(ch, 0) + 1\n        if ch in need and window[ch] == need[ch]:\n            have += 1\n\n        while have == required:\n            if right - left + 1 &lt; best_len:\n                best_len, best_left = right - left + 1, left\n            out = s[left]\n            window[out] -= 1\n            if out in need and window[out] &lt; need[out]:\n                have -= 1\n            left += 1\n\n    return \"\" if best_len == float(\"inf\") else s[best_left:best_left + best_len]\n\nprint(min_window(\"ADOBECODEBANC\", \"ABC\"))  # BANC\nprint(min_window(\"a\", \"a\"))                # a\nprint(min_window(\"a\", \"aa\"))               # (empty string)\nprint(min_window(\"aa\", \"aa\"))              # aa\nprint(min_window(\"AABBC\", \"ABC\"))          # ABBC<\/code><\/pre>\n<p>Output when run: <code>BANC<\/code>, <code>a<\/code>, an empty line, <code>aa<\/code>, <code>ABBC<\/code>. The code was also checked against a brute-force version on 3,000 random short strings, and the result lengths always matched.<\/p>\n<h2>Trace on &#8220;ADOBECODEBANC&#8221;, t = &#8220;ABC&#8221;<\/h2>\n<p>Here <code>required = 3<\/code>. Each line shows the character added and the value of <code>have<\/code> afterwards; when the window is valid, the shrinking steps are listed.<\/p>\n<pre><code class=\"language-text\">add A  r=0   have=1\/3\nadd D  r=1   have=1\/3\nadd O  r=2   have=1\/3\nadd B  r=3   have=2\/3\nadd E  r=4   have=2\/3\nadd C  r=5   have=3\/3\n   valid [0:5]  = ADOBEC        (length 6, best so far)\nadd O  r=6   have=2\/3           (removing A broke the window)\nadd D  r=7   have=2\/3\nadd E  r=8   have=2\/3\nadd B  r=9   have=2\/3\nadd A  r=10  have=3\/3\n   valid [1:10] = DOBECODEBA\n   valid [2:10] = OBECODEBA\n   valid [3:10] = BECODEBA\n   valid [4:10] = ECODEBA\n   valid [5:10] = CODEBA        (length 6, not shorter)\nadd N  r=11  have=2\/3           (removing C broke the window)\nadd C  r=12  have=3\/3\n   valid [6:12] = ODEBANC\n   valid [7:12] = DEBANC\n   valid [8:12] = EBANC\n   valid [9:12] = BANC          (length 4, new best)<\/code><\/pre>\n<p>Notice that the window never goes backwards. Each character enters once and leaves at most once, which is the whole argument for linear time.<\/p>\n<h2>Complexity<\/h2>\n<ul>\n<li><strong>Time: O(|s| + |t|).<\/strong> Building <code>need<\/code> takes O(|t|). <code>right<\/code> moves |s| times and <code>left<\/code> moves at most |s| times in total, even though the <code>while<\/code> loop sits inside the <code>for<\/code> loop. Slicing the answer at the end is O(answer length).<\/li>\n<li><strong>Space: O(k)<\/strong>, where k is the number of distinct characters (at most the alphabet size, so O(1) for ASCII or lowercase letters).<\/li>\n<\/ul>\n<h2>A simpler version that is slower<\/h2>\n<p>A common first attempt skips <code>have<\/code> and re-checks the window against <code>need<\/code> after each move:<\/p>\n<pre><code class=\"language-python\">def is_valid(need, window):\n    return all(window[c] &gt;= n for c, n in need.items())<\/code><\/pre>\n<p>This is still correct (use <code>Counter()<\/code> for <code>window<\/code> so missing keys read as 0), but each check costs O(k), so the total is O(|s| * k). For a lowercase-only alphabet that is at most 26 times slower, which often passes, but interviewers will usually ask you to remove it. The <code>have<\/code> counter is the fix.<\/p>\n<h2>Common mistakes<\/h2>\n<ul>\n<li><strong>Ignoring duplicates in t.<\/strong> Tracking only a set of needed characters gives wrong answers for <code>t = \"AA\"<\/code>. Compare counts, not presence.<\/li>\n<li><strong>Updating <code>have<\/code> on every add.<\/strong> Increase it only when a count becomes <em>exactly<\/em> equal to the requirement. Extra copies of a character must not change it.<\/li>\n<li><strong>Counting characters not in t.<\/strong> Adding them to <code>window<\/code> is harmless, but the check <code>out in need<\/code> is required before touching <code>have<\/code>.<\/li>\n<li><strong>Recording the answer in the wrong place.<\/strong> Record inside the <code>while<\/code> loop, before you move <code>left<\/code>, not after it.<\/li>\n<li><strong>Forgetting the no-answer case.<\/strong> Return <code>\"\"<\/code> when no window was ever valid, including when <code>t<\/code> is longer than <code>s<\/code>.<\/li>\n<li><strong>Returning a copy too early.<\/strong> Slicing a string on every improvement can cost O(n) each time. Store the start and length, as above, and slice once.<\/li>\n<\/ul>\n<h2>What interviewers ask next<\/h2>\n<ul>\n<li><strong>Unicode or a large alphabet?<\/strong> Use a hash map as above; the array-of-26 trick only works for a known small alphabet.<\/li>\n<li><strong>Can you find the answer in a stream?<\/strong> The window only moves forward, so you only need to keep the characters inside it.<\/li>\n<li><strong>Related sliding window problems:<\/strong> longest substring without repeating characters, longest substring with at most K distinct characters, permutation in string, and find all anagrams in a string. They use the same expand-then-shrink loop with a different validity rule.<\/li>\n<\/ul>\n<h2>Practice<\/h2>\n<p>The pattern is easiest to learn by writing the pieces separately. Practice the counting part with the <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=hash-map&amp;solution=1\">Hash Map lesson<\/a> and the pointer part with <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=palindrome-substrings&amp;solution=1\">Palindrome Substrings<\/a> in the Two Pointers section. Then try <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=longest-subarray-without-reapeating&amp;solution=1\">Longest Subarray Without Repeating<\/a>, which is the simplest variable-size window, before coming back to this problem.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Solve Minimum Window Substring (LeetCode 76) in O(n) with a sliding window and character counts: Python code, a trace, common mistakes, follow-ups.<\/p>\n","protected":false},"author":1,"featured_media":4776,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23],"tags":[],"class_list":["post-4777","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-problem-solving"],"_links":{"self":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4777"}],"collection":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/comments?post=4777"}],"version-history":[{"count":2,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4777\/revisions"}],"predecessor-version":[{"id":8931,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4777\/revisions\/8931"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media\/4776"}],"wp:attachment":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media?parent=4777"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/categories?post=4777"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/tags?post=4777"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}