Introduction
Neetcode 150 is a popular collection of Leetcode problems designed to help programmers prepare for coding interviews. However, the tutorial videos may not always provide the most comprehensive explanations for full understanding. Therefore, I’ve decided to document my thought process while solving these problems, in the hope that it might help some of my readers.
Note: This post won't cover all the questions from Neetcode 150. Instead, I'll focus on the ones I found particularly tricky / challenging. I trust in your ability to know the basic logic behind each topic.
Sliding window
- Key factor:
- The use of unordered_map / array to store appearance or index of the chars;
- Use l and r
- l stays, indicating the start of the valid string[only change when certain condition met]
- r keep moving
- Compare arrays for finding permuation or same string but different order.
424.Longest Repeating Character Replacement
- The key for Longest Repeating is to count the appearance of each letter in current window
- That leads us to know the
maxCurrent
in the window - To make sure the window is valid, the entire current length is not over
k (current length - maxCurrent <= k)
. If not, movei
→ decrementvector[i]
by 1
Two ways to do:
vector
count
to keep track of numbers appearance in current window, we know the string only contains uppercase,vector<int> 26 fit
- Find the max appearance in the current window, to do so, have a function check through the vector to find the maxCurrent everytime
- To make sure the window is valid, the replacement is not over k
(current length - maxCurrent <= k)
. If not, movei
, decrementvector[i]
by 1 - Update Longest
Have
maxf
only update when a greater num appears.- Replace if statement by
(j - i + 1 - maxf > k)
- There is a risk when
maxf
contains outdated chars because it doesn’t decrease according toi
- However, it doesn’t hurt for not updating because we are finding the max appearces, the chars that couldn’t exceed the maxf will not be updated in
longest
anyway.
- Replace if statement by
Hint 1: Create variables
int characterReplacement(string s, int k) {
vector<int> m (26, 0); // Store the appearance of chars in current string.
int i = 0; // Begin of the current string, cornorstone of Sliding Window.
int longest = 0; // The final answer
int maxf = 0; // Max frequent
}
Hint 2: Update vector and maxf in loop
// Remeber so far we only move j, not i !
for(int j = 0; j < s.length(); ++j){
m[s[j] - 'A']++; // Find index or 26 letters and increase
maxf = max(maxf, m[s[j] - 'A']); // Check max
}
Hint 3: Move i
for(int j = 0; j < s.length(); ++j){
//...Skip
// If we remove max appearanc from currentLength and still find more than k non-max chars, shorten the i
while(i < s.length() && j - i + 1 - maxf > k){
m[s[i] - 'A']--; // Update current length frequency
i++;
}
longest = max(j - i + 1, longest);
}
Full Code
int characterReplacement(string s, int k) {
vector<int> m (26, 0);
int i = 0;
int longest = 0;
int maxf = 0;
for(int j = 0; j < s.length(); ++j){
m[s[j] - 'A']++;
maxf = max(maxf, m[s[j] - 'A']);
while(i < s.length() && j - i + 1 - maxf > k){
m[s[i] - 'A']--;
i++;
}
longest = max(j - i + 1, longest);
}
return longest;
}
425. Minimum Window Substring
- Mininum Window: Have two maps check currentWindow size, same thing as sliding window.
- The key is to
- Have a “form” to check if the window is valid
(form == 0)
, once valid, start moving it until theform != 0
. - Have an
arr[2] = {length, starting}
to update the answer.
- Have a “form” to check if the window is valid
Step:
- Create two maps
(dict, currentWindow)
, store target string intodict
- Create arr for
answer
,“form”
andi
- Increment in
currentWindow
, if (dict
also contain the letter and two numbers are the same), decrease“form”
; - While
form == 0
, update arr and removes[i]
fromcurrentWindow
. Same as step 3- if dict has it and
dict[i] > currentWindow[i]
, increase the form. (Means the letter in currentWindow is less than / not in requirement) - Return arr, or “”
- if dict has it and
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> dict;
unordered_map<char, int> currentWindow;
for(const char& c : t){
dict[c]++;
}
int i = 0;
int form = dict.size();
int arr [] = {-1, 0};
for(int j = 0; j < s.length(); ++j){
currentWindow[s[j]]++;
if(dict.count(s[j]) && dict[s[j]] == currentWindow[s[j]]){
form--;
}
while(i <= j && form == 0){
if(arr[0] == -1 || j - i + 1 < arr[0]){
arr[0] = j - i + 1;
arr[1] = i;
}
currentWindow[s[i]]--;
if(dict.count(s[i]) && dict[s[i]] > currentWindow[s[i]]){
++form;
}
++i;
}
}
return arr[0] == -1? "" : s.substr(arr[1], arr[0]);
}
};
426. Sliding Window Maximum
- The key for Sliding Window Maximum is if a num is
maxCurrent
, we don’t need any number before it. And if the incoming number is greater than deque back, we can smash it
Step:
- Create a
deque
to store index - If
deque
not empty and incoming is greater, smash anything is smaller - Check if
dq
front is out of bound(i > dq.front())
if(j + 1 >= k)
push_back result and move the i. (The if statement checks ifj+1 < k
, which we haven’t insert enough elements to meet the window size, j is index, ex. If k = 3, j at least should be 2 in index)
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> sta; // store index
int i = 0;
vector<int> answer;
for(int j = 0; j < nums.size(); ++j){
while(!sta.empty() && nums[sta.back()] <= nums[j]){
sta.pop_back();
}
sta.push_back(j);
if(sta.front() < i){
sta.pop_front();
}
if(j - i + 1 >= k){
answer.push_back(nums[sta.front()]);
++i;
}
}
return answer;
}
};
Stack
155. Min Stack
The key of Min Stack is to use two stacks:
nums
andmin_stack
. Thenums
stack stores all the values, while themin_stack
only stores the current minimum values and updates when anew value <= stack.top
.You might wonder, “What about when the incoming value is greater than the top of the stack? Shouldn’t it be stored in
min_stack
too?” The answer is no. You might think that if a larger value should be stored somewhere at the bottom ofmin_stack
so that when all the smaller numbers are popped, this larger value will be needed. But that’s not true. Whenpop()
is called, larger numbers will be popped before smaller ones, so there’s no need to store them inmin_stack
.
There are two approaches to solving this:
Create nums and min_stack.
For
Push()
: push the value into nums and push into min_stack only if the condition is met.For
Pop()
: pop from min_stack only ifnums.top() == min_stack.top()
.
Consider a scenario where the input is something like // 2 2 2 2 2. We don’t want to store all these values in the stack. Instead, create a
pair<int, appearance>
to keep track of the numbers and their frequencies.
class MinStack {
public:
stack<int> sta;
stack<int> min_track;
MinStack() {
}
void push(int val) {
if(min_track.empty() || val <= min_track.top()){
min_track.push(val);
}
sta.push(val);
}
void pop() {
if(!min_track.empty() && min_track.top() == sta.top()){
min_track.pop();
}
sta.pop();
}
int top() {
return sta.top();
}
int getMin() {
return min_track.top();
}
};
150. Evaluate Reverse Polish Notation
- Polish Notation should be straightforward, the key is pushed back numbers to stack every time the calculation is done!
- If we encounter a symbol, then we do the calculation from stack.
Step:
Create a stack
- Check if val is digit, if so, push to stack and continue; If
(token.size() > 1 || isdigit(token[0]))
- If we skip step 2, then val is an operator. Perform the operator and store it back to stack.
- Eventually the stack will have only one num (the answer), return
stack.top()
- Check if val is digit, if so, push to stack and continue; If
int evalRPN(vector<string>& tokens) {
stack<int> sta; // stores number;
for(string& s : tokens){
if(isdigit(s[0]) || (s[0] == '-' && s.length() > 1)){
sta.push(stoi(s));
continue;
}
int num_1 = sta.top(); sta.pop();
if(s == "+") sta.top() += num_1;
if(s == "-") sta.top() -= num_1;
if(s == "*") sta.top() *= num_1;
if(s == "/") sta.top() /= num_1;
}
return sta.top();
}
22. Generate Parentheses
- For generate parentheses We should know that we need “open” and “close” to both equal to n to push
- Whenever open == close, ex: () / (( )) // () (), we could only add an open bracket.
- Therefore we should visit open bracket first Only adding a close bracket if close < open, means we have extra open to be matched.
Step:
- Backtracking, we should create a helper function (vector answer, string temp, int n, int open, int close)
- If open and close both == n, push into answer
- If open < n, temp push an open bracket and helper(open + 1), finally remove temp.back
- If open > close, same thing, but push close bracket.
void helper(vector<string>& answer, string s, int left, int right){
if(left == 0 && right == 0){
answer.push_back(s);
return;
}
if(left == right){
helper(answer, s + "(", left - 1, right);
}else if(left < right){
if(left != 0) helper(answer, s + "(", left - 1, right);
if(right != 0) helper(answer, s + ")", left, right - 1);
}
}
vector<string> generateParenthesis(int n) {
vector<string> answer;
helper(answer, "", n, n);
return answer;
}
23. Daily Temperatures
- For Daily Temperatures, I’m correct with monolithic stack in the first place and obtained a solution by myself. The key is having a
pair {temperature, index}
in the stack. And pop only if currentTemp is higher[warmer]
, else push into stack
Step:
- Create a stack and vector for answer
- If
sta not empty && currentTemp > sta.top()
answer[sta.top().second]
= the gap in days- pop from stack
- Else, push into stack
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> sta;
vector<int> answer (temperatures.size(), 0);
for(int i = 0; i < temperatures.size(); ++i){
while(!sta.empty() && temperatures[i] > temperatures[sta.top()]){
answer[sta.top()] = (i - sta.top());
sta.pop();
}
sta.push(i);
}
return answer;
}
24. Car Fleet
- The key of Car Fleet that we are dealing with the time cars get to the destination based on position and speed. Then if two cars spend the same time getting to the final, they will meet. Or the car behind spends less time (faster) to reach, they will also meet.
Step:
- Create a
vector<position, double time>
and store all the values - Sort the vector based on position
- Make a fleet
counter
andmaxTime
- Start from the back where the car is the closest to destination, and
- if the time it spends is greater than
maxTime
- means the
currentTime
is not catching the previous carfleet, we should therefore increment the carfleet by one and updatemaxTime
- if the time it spends is greater than
class Solution {
public:
int carFleet(int target, vector<int>& position, vector<int>& speed) {
vector<pair<int, double>> cars(position.size());
//(destination - position[i] / speed[i])
for(int i = 0; i < position.size(); ++i){
double time = (double)(target - position[i]) / speed[i];
cars[i] = {position[i], time};
}
sort(cars.begin(), cars.end());
int carFleet = 0;
double maxTime = 0.0;
for(int i = cars.size() - 1; i >= 0; --i){
double time = cars[i].second;
if(time > maxTime){
maxTime = time;
carFleet++;
}
}
return carFleet;
}
};
84. Largest Rectangle in Histogram
The idea for Largest Rectangle is we should know how far a unit can expand, and we should maintain it in the stack before that happens. And until when we reach a smaller height, we reach the end point, then we should pop from the stack and calculate the
width
(the initial point to expanding end) andheight
(initial height).For the
currentHeight
, because we pop from stack until reaching a smaller unit wherecurrentHeight
cannot expand, we make that as our initial height for current.
Step:
- Create a
stack <start, height>
,maxArea
for answer - When
stack is not empty and currentHeight is smaller
, we pop the stack and updatemaxArea
and start position ofcurrentHeight
- Then push
{start position, currentHeight}
to stack - After the loop, there are elements left in the stack which means these are heights that can expand till the end. Therefore we should update the
maxArea
byheight * width
(width is endpoint - start)
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<pair<int, int>> sta; // <start, height>
int maxArea = 0;
for(int i = 0; i < heights.size(); ++i){
int start = i;
while(!sta.empty() && heights[i] < sta.top().second){
int width = i - sta.top().first;
int height = sta.top().second;
maxArea = max(maxArea, height * width);
start = sta.top().first;
sta.pop();
}
sta.push({start,heights[i]});
}
int n = heights.size();
while(!sta.empty()){
pair<int, int> p = sta.top();
sta.pop();
maxArea = max(maxArea, p.second * (n - p.first));
}
return maxArea;
}
};
Binary Search
74. Search a 2D Matrix
- The key of 2D Matrix is to recognize that not only is each row ordered, but each horizontal line is also ordered. This allows us to perform a binary search on the horizontal lines to find the target row, followed by a normal binary search within that row.
- Therefore we downgrade a 2D matrix to a 1D array for binary search.
Step:
- Create an
row
variable - Check if the
target > last element in each row
- if so, increase the
row
- else break
- if so, increase the
- Check edge case when target is way bigger than the entire matrix and therefore not in it, return false
- Create
left
andright
, perform binary search
bool searchMatrix(vector<vector<int>>& matrix, int target) {
// Look for the row we want, downgrade our searching range from matrix to an array
int row = 0;
while(row < matrix.size()){
if(target > matrix[row].back()){
++row;
}else{
break;
}
}
// Check edge case target exceed the last element in matrix
if(row >= matrix.size()){
return false;
}
vector<int>& arr = matrix[row]; //target row
// Simple binary search
int left = 0, right = arr.size() - 1;
while(left <= right){
int mid = (right - left) / 2 + left;
if(arr[mid] == target){
return true;
}else if(arr[mid] < target){
left = mid + 1;;
}else{
right = mid - 1;
}
}
return false;
}
875. Koko Eating Bananas
- The key of Koko Eating Bananas is that we should decide what
k
bananas Koko eats, it can eat anywhere from 1 to themax
in the piles, because anymore it will stop eating for one round. - Therefore do a binary search based on 1 to
max
Step:
- Create
low = 1
, high to bemaxPile
,answer
- Create an
hour
counting, put it long int for number floating, binary search through the array.- If the
hour
is within time, update the answer to be minimum of (mid, answer)
- If the
Hour
can be calculated as:hour += pile / mid + (pile % mid != 0)
Orhour += ceil((double) pile / mid)
Orhour += (pile + speed - 1) / speed
- Return
answer
;
int helper(vector<int>& piles, int speed){
int hour = 0;
for(int pile : piles){
hour += (pile + speed - 1) / speed; // clever solution
}
return hour;
};
int minEatingSpeed(vector<int>& piles, int h) {
// find max
int maxEat = 0;
for(int pile : piles){
maxEat = max(maxEat, pile);
}
int left = 1, right = maxEat;
int answer = maxEat;
while(left <= right){
int mid = (right - left) / 2 + left;
int time = helper(piles, mid);
if(time > h){
left = mid + 1;
}else{
answer = mid;
right = mid - 1;
}
}
return answer;
}
153. Find Minimum in Rotated Sorted Array
For Find Minimum in Rotated Array, I am right in the first place:
- in a normal situation:
left < mid < right
, if rotation happens, wheremid > right
: “4 5 6 7 1 2 3”,left = mid + 1
. - Else, it means
mid <= right
, which is a normal array or “6 7 1 2 3” where mid is the number. Setright = mid
;
- in a normal situation:
Also for the while condition, we do
left < right
, we don’t need to check left == right. Because if left even equals to right, we found the minimum value
Step:
- Create
left
andright
while(left < right)
,if (mid > right)
- left = mid + 1.
- Else right = mid;
- Return
nums[left]
ornums[right]
class Solution {
public:
int findMin(vector<int>& nums) {
//find the rotation point
// suppose: Left < right
// left < mid < right
// right < mid: 4 5 6 7 1 2 3, left = mid + 1
// right > mid: 8 2 3 4 5 6 7, right = mid
int left = 0, right = nums.size() - 1, mid = 0;
while(left < right){
mid = (right - left) / 2 + left;
if(nums[right] >= nums[mid]){
right = mid;
}else{
left = mid + 1;
}
}
return nums[left];
//binary search
}
};
33. Search in Rotated Sorted Array
- There are two ways to do Search in Rotated Sorted Array.
- One as we know, find the minimum rotation starter, and see if the target is a
left
orright
sorted array. - Another way is to do everything in one shot, which determines if the array is left sorted or right sorted. Here only explain the situation of second method which is harder
- One as we know, find the minimum rotation starter, and see if the target is a
Step:
- Create
left
andright
ptr while(left <= right)
,if(mid == target)
, returnmid
.- If
(left <= mid)
, it’s left sorted “//1 2 3 4 5 6 7 or //3 4 5 6 7 1 2”, we need to figure out if the target is on the left or right. - If
(target > nums[mid] || target < nums[left])
left = mid + 1
. - Else
right = mid - 1
;- Because
mid > left
, so iftarget > mid
, that meanstarget
is also >left
, we eliminate the entire left side, so we should go to right. - If
target <= mid
, it could be the second case wheremid
= 6, and we have nums on left and right meet the condition, so iftarget < left
, we go right.
- Because
- If
- Right side the same idea.
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while(left <= right){
int mid = (right - left) / 2 + left;
if(nums[mid] == target){
return mid;
}else if(nums[mid] > nums[right]){
if(target >= nums[left] && target < nums[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}else{
if(target <= nums[right] && target > nums[mid]){
left = mid + 1;
}else{
right = mid - 1;
}
}
//4 5 6 7 0 1 2
//3 4 5 6 7 0 1
//5 6 7 0 1 2 4
//6 7 0 1 3 4 5
}
return -1;
}
};
//0 1 2 3 4 5 6 7
//4 5 6 7 0 1 2 3
4. Median of Two Sorted Arrays
- There are three approaches for Median of Two Sorted Arrays:
- Merge the two arrays and perform a binary search. This takes
O(m + n)
time. - Start from the beginning of both
arr1
andarr2
. Since we are looking for the median (the middle value), we can compare values one by one and move the pointer forward until we reachlog(m+n)
comparisons. - Perform a binary search in the smaller array, which is the method I will explain.
- Merge the two arrays and perform a binary search. This takes
Steps:
Compare the two arrays and place the smaller array first. This is important because performing the search on the smaller array helps to eliminate the case where one array is empty.
Create
high
andlow
pointers for the binary search onarr1
.While low <= high
, create amid
pointer forarr1
and a correspondingmid
pointer forarr2
. Thetotal + 1
adjustment accounts for odd numbers, as we want to find the middle number.Define
left1
asarr1[i - 1]
andright1
asarr1[i]
(the middle value inarr1
). Defineleft2
asarr2[j - 1]
andright2
asarr2[j]
(the middle value inarr2
).By the nature of sorted arrays,
left1 < right1
andleft2 < right2
by default.- We should check if
left1 <= right2 and left2 <= right1
. - If both conditions are met, we’ve found the left sorted partition. Update the result based on whether the total length is odd or even, then exit the loop.
- We should check if
If left1 > right1
, it means the current partition is too large, so adjust by settingright = i - 1
. Otherwise,if left2 > right1
, it indicates thatarr1
is too small, so expand the search by settingleft = i + 1
.
class Solution {
public:
int p1 = 0, p2 = 0;
int mergeSort(vector<int>& nums1, vector<int>& nums2){
if(p1 < nums1.size() && p2 < nums2.size()){
return nums1[p1] < nums2[p2] ? nums1[p1++] : nums2[p2++];
}else if(p1 < nums1.size()){
return nums1[p1++];
}else{
return nums2[p2++];
}
return -1;
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size(), n = nums2.size();
if((m+n) % 2 == 0){
for(int i = 0; i < (m+n) / 2 - 1; ++i){
mergeSort(nums1, nums2);
}
return (double)(mergeSort(nums1, nums2) + mergeSort(nums1, nums2)) / 2;
}else{
for(int i = 0; i < (m+n) / 2; ++i){
mergeSort(nums1, nums2);
}
return mergeSort(nums1, nums2);
}
}
};