Skip to content

Commit a212b2d

Browse files
authored
SOLN: Minimum window substring (#192)
1 parent 9f33899 commit a212b2d

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// DIFFICULTY: HARD
2+
//
3+
// Given two strings s and t of lengths m and n respectively, return the minimum window substring of s such that every
4+
// character in t (including duplicates) is included in the window. If there is no such substring, return the empty
5+
// string "".
6+
//
7+
// The testcases will be generated such that the answer is unique.
8+
//
9+
// @see {@link https://leetcode.com/problems/minimum-window-substring/}
10+
export { minWindow };
11+
12+
// SOLUTION:
13+
//
14+
// This can be solved using the sliding window technique with a bunch of extra bookkeeping. The right pointer will be
15+
// expanded until we have a valid window, then the left pointer will be shrunk to the minimum size window that still
16+
// satisfies the requirements.
17+
//
18+
// COMPLEXITY:
19+
//
20+
// Time complexity is O(m + n) because we iterate through both the source and the target strings.
21+
//
22+
// The want map is O(n) space complexity because we store character frequency of the target string. The got map is
23+
// O(m) space complexity because we store character frequency of the source string. Together it's O(m + n).
24+
function minWindow(s: string, t: string): string {
25+
// It is not possible to return a minimum window here; the target substring MUST be shorter than the source string.
26+
if (t.length > s.length) {
27+
return '';
28+
}
29+
30+
// First we need to create a frequency map, so we can easily check if a substring has all the characters in t. This
31+
// is required because the characters in t can be duplicated, and we require the same number of duplicates in the
32+
// source substring.
33+
const want = new Map<string, number>();
34+
for (const c of t) {
35+
const freq = want.get(c) || 0;
36+
want.set(c, freq + 1);
37+
}
38+
39+
// Define both pointers to start at the source string, and start by expanding the right pointer. Once a valid window
40+
// is discovered, we contract the left pointer.
41+
let left = 0;
42+
let right = 0;
43+
44+
// Define a map to keep track of the characters we have seen so far in the window. This will let us know when we have
45+
// a valid window.
46+
//
47+
// We also keep track of which characters we've 'gotten' so far. That is, if we have a requirement to contain X
48+
// number of 'a' characters, and we have seen X characters, then we have 'gotten' that character. When we have
49+
// 'gotten' all characters in the string, we have a valid window.
50+
const got = new Map<string, number>();
51+
let gotten = 0;
52+
53+
// Keep track of our minimum window substring. The left pointer should point to the start, and the size will tell us
54+
// the length of the window, allowing us to call s.substring() later.
55+
//
56+
// If we run through the algorithm and don't get a valid window, because minSize never got updated, then we return ''.
57+
let minLeft = 0;
58+
let minSize = Infinity;
59+
60+
// Begin by expanding the right pointer until we have a valid window.
61+
while (right < s.length) {
62+
// Update the frequency of the character that we have got.
63+
const c = s[right];
64+
const freq = got.get(c) || 0;
65+
got.set(c, freq + 1);
66+
67+
// If it turns out that we got exactly the frequency of character c that we wanted, then the requirement to contain
68+
// X number of character c has been satisfied.
69+
if (want.has(c) && want.get(c) === got.get(c)) {
70+
gotten++;
71+
}
72+
73+
// If we've gotten all the characters at exactly the right frequency, then we have a valid window. In that case,
74+
// we can begin shrinking the window to see if it stays valid.
75+
while (gotten === want.size) {
76+
const k = s[left];
77+
78+
// Calculate the size of the window. Note that left and right are INCLUSIVE indexes, so if we want the window
79+
// size of say [0, 1, 2, 3] with left = 0 and right = 3 (a window size of 4), we should do 3 - 0 + 1 = 4.
80+
//
81+
// So make sure to add 1 here to get the correct size.
82+
const size = right - left + 1;
83+
if (size < minSize) {
84+
minSize = size;
85+
minLeft = left;
86+
}
87+
88+
// Now try to contract the window by moving the left pointer. When we do this, we have to update the frequency
89+
// of characters we've gotten so far, and update the gotten count.
90+
//
91+
// Update the frequency of the character that we are about to lose. Then if we have got fewer characters than
92+
// wanted, we decrement the 'gotten' count because we no longer have a valid window.
93+
got.set(k, got.get(k)! - 1);
94+
if (want.has(k) && got.get(k)! < want.get(k)!) {
95+
gotten--;
96+
}
97+
98+
// After performing the bookkeeping, we can now shrink the window by moving the left pointer.
99+
left++;
100+
}
101+
102+
// Keep expanding the right window until we have a valid window.
103+
right++;
104+
}
105+
106+
// This means we didn't find any valid window.
107+
if (minSize === Infinity) {
108+
return '';
109+
}
110+
111+
return s.substring(minLeft, minLeft + minSize);
112+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { minWindow } from '../../src/sliding-window/minimum-window-substring';
2+
3+
describe('minimum window substring', () => {
4+
test('minWindow - test case 1', async () => {
5+
expect(minWindow('ADOBECODEBANC', 'ABC')).toStrictEqual('BANC');
6+
});
7+
});

0 commit comments

Comments
 (0)