Intermediate DSA Questions — Strings, Arrays, Searching, and Stacks
Core algorithmic problems involving stack evaluations, binary search variations, matrix transformations, two-pointer strategies, and sliding window patterns.
01. Valid Parentheses
Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.
How It Works & Algorithmic Design
This uses a Stack Data Structure for LIFO (Last-In, First-Out) matching:
- Matching Map: Define a map of closing brackets to their corresponding opening brackets (
')': '(','}' : '{',']': '['). - Stack Operations: Iterate through character
chin strings:- If
chis an opening bracket ((,{,[), push it onto the stack. - If
chis a closing bracket, check the stack top. If the stack is empty orstack.pop()does not equal the matching opening bracket, the string is invalid (return false).
- If
- Final Check: After processing all characters, return
trueifstack.length === 0(all opened brackets were closed).
Why This Approach Works Best
Parentheses nesting follows a LIFO sequence. A stack processes nesting levels naturally in $O(n)$ time complexity and $O(n)$ auxiliary space.
"()[]{}"Push '(' -> Pop '(' on ')' | Push '[' -> Pop '[' on ']' | Push '{' -> Pop '{' on '}'trueAnswer: Valid Parentheses using Stack
function isValid(s) {
const stack = [];
const map = { ')': '(', '}': '{', ']': '[' };
for (const ch of s) {
if (ch in map) {
if (stack.pop() !== map[ch]) return false;
} else {
stack.push(ch);
}
}
return stack.length === 0;
}- Stack data structure handles nested structures naturally.
- Always check for empty stack conditions before popping.
- Runs in $O(n)$ time and $O(n)$ space.
02. Binary Search
Given a sorted array of integers nums and a target, write a function to search for target in nums in $O(\log n)$ time.
How It Works & Algorithmic Design
Binary Search uses Divide and Conquer on sorted arrays:
- Pointers: Set
left = 0,right = nums.length - 1. - Mid Calculation: Compute
mid = Math.floor((left + right) / 2)(orleft + Math.floor((right - left) / 2)to prevent overflow in fixed integer types). - Halving Condition:
- If
nums[mid] === target, returnmid. - If
nums[mid] < target, discard left half:left = mid + 1. - If
nums[mid] > target, discard right half:right = mid - 1.
- If
- Failure: Return
-1ifleft > right.
Why This Approach Works Best
Halving the search space each iteration reduces search time from linear $O(n)$ to logarithmic $O(\log n)$ time complexity.
nums = [-1, 0, 3, 5, 9, 12], target = 9left=0, right=5 -> mid=2 (nums[2]=3). 3 < 9 -> left=3.left=3, right=5 -> mid=4 (nums[4]=9). Match found!4Answer: Binary Search implementation
function search(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = left + Math.floor((right - left) / 2);
if (nums[mid] === target) return mid;
if (nums[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}- Binary search requires the input array to be pre-sorted.
- Use
left + (right - left) / 2to avoid integer overflow issues in languages like C++/Java. - Logarithmic time complexity $O(\log n)$ enables ultra-fast searching across large datasets.
03. Best Time to Buy and Sell Stock
Given an array prices where prices[i] is the stock price on day i, return the maximum profit you can achieve from a single transaction.
How It Works & Algorithmic Design
This uses a Single-Pass Greedy Running Minimum strategy:
- Variables: Track
minPrice = InfinityandmaxProfit = 0. - Iteration: Loop through each stock price in
prices:- If the current price is lower than
minPrice, updateminPrice = price. - Otherwise, calculate potential profit:
price - minPrice. If this profit exceedsmaxProfit, updatemaxProfit.
- If the current price is lower than
- Return: Return
maxProfitafter scanning all prices.
Why This Approach Works Best
Checking every pair of buying/selling days with nested loops takes $O(n^2)$ time. Tracking the lowest price seen so far allows maximum profit calculation in $O(n)$ time and $O(1)$ space.
prices = [7, 1, 5, 3, 6, 4]7 (min=7) -> 1 (min=1) -> 5 (profit=4) -> 3 -> 6 (profit=5, max=5) -> 45Answer: Maximum stock profit calculation
function maxProfit(prices) {
let minPrice = Infinity;
let maxProfit = 0;
for (const price of prices) {
if (price < minPrice) {
minPrice = price;
} else if (price - minPrice > maxProfit) {
maxProfit = price - minPrice;
}
}
return maxProfit;
}- Stock must be bought before it can be sold (left-to-right order dependency).
- Maintain running minimum price to evaluate maximum potential gain.
- Single pass achieves $O(n)$ time and $O(1)$ space.
04. Maximum Subarray (Kadane's Algorithm)
Given an integer array nums, find the contiguous subarray with the largest sum and return its sum.
How It Works & Algorithmic Design
Solves using Kadane's Dynamic Programming Algorithm:
- State Definition: At index $i$, decide whether to add
nums[i]to the existing subarray or start a brand new subarray starting atnums[i]. - Transition Formula:
currentSum = Math.max(nums[i], currentSum + nums[i]). - Global Track:
maxSum = Math.max(maxSum, currentSum). - Initialization: Set
currentSum = nums[0]andmaxSum = nums[0].
Why This Approach Works Best
Brute force checking all contiguous subarrays takes $O(n^2)$ time. Kadane's algorithm makes local optimal decisions at every step, reducing the runtime to optimal linear $O(n)$ time and $O(1)$ auxiliary space.
[-2, 1, -3, 4, -1, 2, 1, -5, 4]Subarray [4, -1, 2, 1] gives currentSum=6, which is the highest recorded.6Answer: Kadane's Algorithm implementation
function maxSubArray(nums) {
let currentSum = nums[0];
let maxSum = nums[0];
for (let i = 1; i < nums.length; i++) {
currentSum = Math.max(nums[i], currentSum + nums[i]);
maxSum = Math.max(maxSum, currentSum);
}
return maxSum;
}- Decides whether to extend current subarray or start fresh at each index.
- Works correctly even if all numbers in the array are negative.
- Optimal linear runtime of $O(n)$ time and $O(1)$ space.
05. Container With Most Water
Given an integer array height of length $n$, find two lines that together with the x-axis form a container that holds the most water.
How It Works & Algorithmic Design
Solves using Two Pointers Shrinking Boundary:
- Pointers: Position
left = 0andright = height.length - 1. - Area Calculation:
width = right - left. Area =Math.min(height[left], height[right]) * width. - Max Update:
maxWater = Math.max(maxWater, area). - Pointer Movement Strategy: To potentially find a taller boundary that compensates for reduced width, move the pointer that points to the shorter height (
if (height[left] < height[right]) left++ else right--).
Why This Approach Works Best
Evaluating all pairs of lines takes $O(n^2)$ time. Moving the shorter pointer guarantees that higher potential areas are checked while eliminating inferior choices in $O(n)$ time and $O(1)$ space.
height = [1, 8, 6, 2, 5, 4, 8, 3, 7]Index 1 (height 8) and Index 8 (height 7)49Answer: Container with most water using two pointers
function maxArea(height) {
let left = 0, right = height.length - 1;
let maxWater = 0;
while (left < right) {
const h = Math.min(height[left], height[right]);
const w = right - left;
maxWater = Math.max(maxWater, h * w);
if (height[left] < height[right]) left++;
else right--;
}
return maxWater;
}- Always move the pointer corresponding to the shorter boundary line.
- Width continuously decreases, so height must increase to improve area.
- Runs in linear $O(n)$ time and $O(1)$ space.
06. Longest Substring Without Repeating Characters
Given a string s, find the length of the longest substring without repeating characters.
How It Works & Algorithmic Design
Solves using the Variable Sliding Window with Hash Map Indexing:
- Pointers: Maintain a sliding window defined by left boundary
leftand right boundaryright. - Map Index Tracking: Use a Hash Map
charIndexMapto store the last seen index of each character. - Expansion & Contraction: Expand
rightacross strings:- If character
s[right]was seen inside the current window (charIndexMap[s[right]] >= left), updateleft = charIndexMap[s[right]] + 1to skip past the repeating character. - Update
charIndexMap[s[right]] = right. - Calculate window size (
right - left + 1) and updatemaxLength.
- If character
Why This Approach Works Best
Instead of resetting the window or using nested loops ($O(n^2)$), jumping left directly past previous duplicate positions ensures each character is processed efficiently in $O(n)$ time and $O(k)$ space.
s = "abcabcbb""abc" (len=3) -> 'a' repeats at idx 3 -> move left to idx 1 ("bca") -> len=33Answer: Sliding window unique substring search
function lengthOfLongestSubstring(s) {
const map = new Map();
let left = 0, maxLength = 0;
for (let right = 0; right < s.length; right++) {
if (map.has(s[right]) && map.get(s[right]) >= left) {
left = map.get(s[right]) + 1;
}
map.set(s[right], right);
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}- Store character indices to jump
leftpointer instantly. - Ensure the recorded index is $\ge left$ to avoid moving backwards.
- Linear time complexity $O(n)$ with $O(k)$ space for distinct characters.
07. Merge Intervals
Given an array of intervals where intervals[i] = [start_i, end_i], merge all overlapping intervals.
How It Works & Algorithmic Design
Solves using Interval Sorting and Sequential Merging:
- Sort: Sort intervals based on their start times (
interval[0]). This brings overlapping candidate intervals next to each other. - Iterate & Merge:
- Initialize
merged = [intervals[0]]. - For each subsequent
interval:- Get
lastMerged = merged[merged.length - 1]. - If
interval.start <= lastMerged.end, an overlap exists. Merge by updatinglastMerged.end = Math.max(lastMerged.end, interval.end). - Otherwise, no overlap; push
intervalintomerged.
- Get
- Initialize
- Return: Return
merged.
Why This Approach Works Best
Sorting takes $O(n \log n)$ time. Once sorted, a single $O(n)$ linear scan processes all merges cleanly.
[[1,3],[2,6],[8,10],[15,18]][1,3] and [2,6] overlap (2 <= 3) -> Merge to [1,6]. [8,10] and [15,18] do not overlap.[[1,6],[8,10],[15,18]]Answer: Merge overlapping intervals
function merge(intervals) {
if (intervals.length <= 1) return intervals;
intervals.sort((a, b) => a[0] - b[0]);
const result = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const last = result[result.length - 1];
const curr = intervals[i];
if (curr[0] <= last[1]) {
last[1] = Math.max(last[1], curr[1]);
} else {
result.push(curr);
}
}
return result;
}- Sorting by start time ensures overlapping candidates are adjacent.
- Use
Math.maxon end points to handle fully enclosed sub-intervals. - Time complexity is dominated by sorting: $O(n \log n)$.
08. Rotate Image (2D Matrix)
Given an $n \times n$ 2D matrix representing an image, rotate the image by 90 degrees clockwise in-place.
How It Works & Algorithmic Design
Mathematical transformation through Transpose then Reverse:
- Step 1: Transpose Matrix (Swap rows and columns across main diagonal):
- For every $i$ from $0$ to $n-1$ and $j$ from $i+1$ to $n-1$, swap
matrix[i][j]withmatrix[j][i].
- For every $i$ from $0$ to $n-1$ and $j$ from $i+1$ to $n-1$, swap
- Step 2: Reverse Each Row:
- For each row $i$, reverse elements horizontally (
leftandrightpointers swappingmatrix[i][left]andmatrix[i][right]).
- For each row $i$, reverse elements horizontally (
Why This Approach Works Best
Transpose + Reverse turns $90^\circ$ rotation into two clean, in-place matrix ops. Avoids $O(n^2)$ extra memory allocation, achieving true $O(1)$ space and $O(n^2)$ time complexity.
[[1,2,3],[4,5,6],[7,8,9]][[1,4,7],[2,5,8],[3,6,9]][[7,4,1],[8,5,2],[9,6,3]][[7,4,1],[8,5,2],[9,6,3]]Answer: In-place 90-degree matrix rotation
function rotate(matrix) {
const n = matrix.length;
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
for (let i = 0; i < n; i++) {
matrix[i].reverse();
}
}- Rotate clockwise = Transpose matrix + Reverse rows.
- Rotate counter-clockwise = Transpose matrix + Reverse columns.
- Performs in-place updates achieving $O(1)$ extra space and $O(n^2)$ time.
09. Group Anagrams
Given an array of strings strs, group the anagrams together in any order.
How It Works & Algorithmic Design
Solves using Hash Map Grouping with Canonical Key Signature:
- Key Signature: Two strings are anagrams if and only if their sorted character representations are identical (e.g.,
"eat"$\to$"aet"and"tea"$\to$"aet"). - Map Setup: Maintain a Hash Map where key =
sortedStringKeyand value =array of original strings. - Grouping: Loop through each string
sinstrs:- Compute
sortedKey = s.split('').sort().join(''). - Append
stomap[sortedKey].
- Compute
- Return: Return
Object.values(map).
Why This Approach Works Best
Sorted string keys group all anagram variants into matching buckets in $O(n \cdot k \log k)$ time, where $n$ is string count and $k$ is max string length.
["eat","tea","tan","ate","nat","bat"]"aet": ["eat","tea","ate"] | "ant": ["tan","nat"] | "abt": ["bat"][["eat","tea","ate"],["tan","nat"],["bat"]]Answer: Group anagrams using sorted key map
function groupAnagrams(strs) {
const map = {};
for (const s of strs) {
const key = s.split('').sort().join('');
if (!map[key]) map[key] = [];
map[key].push(s);
}
return Object.values(map);
}- Canonical representation (like sorted character strings) unifies identical frequency structures.
- Character count tuples ($26$-length arrays) can optimize key creation to $O(k)$ per word.
- Space complexity is $O(n \cdot k)$ to store string references.
10. Product of Array Except Self
Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i], without using division in $O(n)$ time.
How It Works & Algorithmic Design
Solves using Prefix and Suffix Product Accumulation:
- Left Products Pass: Create result array
res. Setres[0] = 1. For $i$ from $1$ to $n-1$, computeres[i] = res[i - 1] * nums[i - 1](stores product of all elements to the left of $i$). - Right Products Pass: Maintain running
right = 1. Loop backwards from $n-1$ down to $0$:res[i] = res[i] * right.- Update
right = right * nums[i].
- Return:
rescontains total prefix $\times$ suffix products for every index.
Why This Approach Works Best
Avoids division by zero crashes and achieves linear $O(n)$ time using $O(1)$ extra memory (excluding output array).
nums = [1, 2, 3, 4]res = [1, 1, 2, 6]res = [24, 12, 8, 6][24, 12, 8, 6]Answer: Product of array except self
function productExceptSelf(nums) {
const n = nums.length;
const res = new Array(n);
res[0] = 1;
for (let i = 1; i < n; i++) {
res[i] = res[i - 1] * nums[i - 1];
}
let right = 1;
for (let i = n - 1; i >= 0; i--) {
res[i] *= right;
right *= nums[i];
}
return res;
}- Combines prefix product and suffix product without using division.
- Reuses output array space to achieve true $O(1)$ auxiliary memory.
- Linear time complexity $O(n)$.