So, you've been to a few coding interviews, and you've realized something: everyone loves asking about arrays and strings. If you're like me, you've probably noticed that around 7 out of 10 interviews focus on arrays. The other 3? More arrays, but maybe they'll throw in a string problem or something more exotic like graphs or dynamic programming. Database stuff? Meh, rare.
Let's dive deep into the most common algorithms you need to ace these interviews, but I promise to keep it simple and fun. Each problem comes with the exact thought process to verbalize during a live coding round, runnable code in TypeScript, Python, Java, PHP, and Go, and a direct LeetCode link so you can practice the moment you finish reading.
Before You Start: The Live Coding Mindset
Before we dive into algorithms, internalize this: interviewers are evaluating how you think, not whether you remember the answer. The pattern that wins almost every live coding round looks like this:
- Restate the problem in your own words and confirm constraints (input size, edge cases, can the array be empty, are there duplicates, is it sorted, etc.).
- Walk through a small example by hand — on paper or by typing it out.
- Propose a brute-force solution first, state its complexity, then ask: "Should I optimize this, or is this acceptable?"
- Optimize by recognizing a pattern (hashmap, two pointers, sliding window, DP).
- Code carefully, narrate as you go.
- Test with edge cases: empty input, single element, all duplicates, negative numbers, the maximum size.
The candidates who get the offer aren't the ones who silently write the perfect solution — they're the ones who let the interviewer see how their mind works. Now let's get to the algorithms.
1. Arrays: The Big Kahuna of Interviews
If interviews had a mascot, it would be the humble array. Around 7 out of 10 problems involve them in some form. Master these patterns and you've solved the bulk of your interview prep.
Why are Arrays So Popular?
Arrays give O(1) random access, are easy to reason about, and let interviewers test optimization skills. The trick is rarely "find an answer" — it's "find the best answer."
Problem 1.1 — Two Sum
LeetCode #1 — leetcode.com/problems/two-sum
Given an array of integers
numsand an integertarget, return indices of the two numbers such that they add up totarget.
Thought process to verbalize:
"The brute force is two nested loops — O(n²). I can do better. For each number
x, I needtarget − x. If I keep a hashmap of values I've already seen, I can check that complement in O(1). One pass, O(n) time, O(n) space."
function twoSum(nums: number[], target: number): number[] {
const seen = new Map<number, number>(); // value -> index
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (seen.has(complement)) {
return [seen.get(complement)!, i];
}
seen.set(nums[i], i);
}
return []; // problem guarantees a solution, but be defensive
}
def two_sum(nums: list[int], target: int) -> list[int]:
seen: dict[int, int] = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> seen = new HashMap<>(); // value -> index
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (seen.containsKey(complement)) {
return new int[] { seen.get(complement), i };
}
seen.put(nums[i], i);
}
return new int[0];
}
function twoSum(array $nums, int $target): array
{
$seen = []; // value => index
foreach ($nums as $i => $num) {
$complement = $target - $num;
if (isset($seen[$complement])) {
return [$seen[$complement], $i];
}
$seen[$num] = $i;
}
return [];
}
func twoSum(nums []int, target int) []int {
seen := make(map[int]int) // value -> index
for i, num := range nums {
complement := target - num
if j, ok := seen[complement]; ok {
return []int{j, i}
}
seen[num] = i
}
return []int{}
}
Complexity: Time O(n), Space O(n).
Watch out for: Duplicates (nums = [3, 3], target = 6 — your hashmap must store the index before you check, not after). Negative numbers. Same element used twice — the problem forbids it, which is why we check the map before inserting the current value.
Problem 1.2 — Best Time to Buy and Sell Stock
LeetCode #121 — leetcode.com/problems/best-time-to-buy-and-sell-stock
You're given an array
priceswhereprices[i]is the price of a stock on dayi. Maximize profit from a single buy-sell pair (buy before sell).
Thought process:
"Track the minimum price seen so far. For each day, the best profit if I sold today is
today's price − min seen so far. I keep the running max of that."
function maxProfit(prices: number[]): number {
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;
}
def max_profit(prices: list[int]) -> int:
min_price = float("inf")
max_profit = 0
for price in prices:
if price < min_price:
min_price = price
elif price - min_price > max_profit:
max_profit = price - min_price
return max_profit
public int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE;
int maxProfit = 0;
for (int price : prices) {
if (price < minPrice) {
minPrice = price;
} else if (price - minPrice > maxProfit) {
maxProfit = price - minPrice;
}
}
return maxProfit;
}
function maxProfit(array $prices): int
{
$minPrice = PHP_INT_MAX;
$maxProfit = 0;
foreach ($prices as $price) {
if ($price < $minPrice) {
$minPrice = $price;
} elseif ($price - $minPrice > $maxProfit) {
$maxProfit = $price - $minPrice;
}
}
return $maxProfit;
}
func maxProfit(prices []int) int {
minPrice := math.MaxInt
maxProfit := 0
for _, price := range prices {
if price < minPrice {
minPrice = price
} else if price-minPrice > maxProfit {
maxProfit = price - minPrice
}
}
return maxProfit
}
Complexity: Time O(n), Space O(1).
Common follow-up: "What if you can buy and sell multiple times?" — That's LeetCode #122, and the answer is to sum every positive difference between consecutive days.
Problem 1.3 — Maximum Subarray (Kadane's Algorithm)
LeetCode #53 — leetcode.com/problems/maximum-subarray
Find the contiguous subarray with the largest sum.
Thought process:
"At every index, I have a choice: extend the previous subarray, or start a new one from here. I extend if the running sum is still positive — otherwise I start fresh. That's Kadane's algorithm."
function maxSubArray(nums: number[]): number {
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;
}
def max_sub_array(nums: list[int]) -> int:
current_sum = max_sum = nums[0]
for num in nums[1:]:
current_sum = max(num, current_sum + num)
max_sum = max(max_sum, current_sum)
return max_sum
public int maxSubArray(int[] nums) {
int currentSum = nums[0];
int maxSum = nums[0];
for (int i = 1; i < nums.length; i++) {
currentSum = Math.max(nums[i], currentSum + nums[i]);
maxSum = Math.max(maxSum, currentSum);
}
return maxSum;
}
function maxSubArray(array $nums): int
{
$currentSum = $nums[0];
$maxSum = $nums[0];
for ($i = 1; $i < count($nums); $i++) {
$currentSum = max($nums[$i], $currentSum + $nums[$i]);
$maxSum = max($maxSum, $currentSum);
}
return $maxSum;
}
func maxSubArray(nums []int) int {
currentSum, maxSum := nums[0], nums[0]
for i := 1; i < len(nums); i++ {
currentSum = max(nums[i], currentSum+nums[i])
maxSum = max(maxSum, currentSum)
}
return maxSum
}
// Go 1.21+ has built-in max(); earlier versions need a tiny helper.
Complexity: Time O(n), Space O(1).
Edge case to mention: All negative numbers — the answer is the largest single element. The algorithm handles this correctly because max(nums[i], currentSum + nums[i]) will pick nums[i] whenever extending makes things worse.
Pro Tips for Arrays
- Always ask if the array is sorted. A sorted array unlocks two pointers and binary search.
- The two-pointer pattern crushes problems like "find a pair that sums to X in a sorted array" in O(n) time.
- Prefix sums (
prefix[i] = nums[0] + … + nums[i]) turn many subarray-sum problems into O(1) range queries — see LeetCode #560 — Subarray Sum Equals K.
2. Strings: The Array's Fancy Cousin
Strings are arrays of characters with a few extra concerns: immutability in some languages (Java, Python, JavaScript), encoding (ASCII vs Unicode), and pattern matching. Most string problems reduce to array problems once you internalize this.
Problem 2.1 — Reverse String (in place)
LeetCode #344 — leetcode.com/problems/reverse-string
Reverse
sin place. You must use O(1) extra memory.
Thought process:
"Two pointers, one at each end, swap and walk inward until they meet."
function reverseString(s: string[]): void {
let left = 0;
let right = s.length - 1;
while (left < right) {
[s[left], s[right]] = [s[right], s[left]];
left++;
right--;
}
}
def reverse_string(s: list[str]) -> None:
left, right = 0, len(s) - 1
while left < right:
s[left], s[right] = s[right], s[left]
left += 1
right -= 1
public void reverseString(char[] s) {
int left = 0;
int right = s.length - 1;
while (left < right) {
char tmp = s[left];
s[left] = s[right];
s[right] = tmp;
left++;
right--;
}
}
function reverseString(array &$s): void
{
$left = 0;
$right = count($s) - 1;
while ($left < $right) {
[$s[$left], $s[$right]] = [$s[$right], $s[$left]];
$left++;
$right--;
}
}
func reverseString(s []byte) {
left, right := 0, len(s)-1
for left < right {
s[left], s[right] = s[right], s[left]
left++
right--
}
}
Complexity: Time O(n), Space O(1).
Gotcha: In real code, s[::-1] in Python or [...s].reverse().join("") in JS works in one line — but the interviewer wants to see you understand the algorithm, not the language shortcut. Mention the shortcut to show awareness, then implement the manual version.
Problem 2.2 — Valid Anagram
LeetCode #242 — leetcode.com/problems/valid-anagram
Return
trueiftis an anagram ofs.
Two approaches — show you know both:
Approach A — Sorting: O(n log n) time, O(1) extra space if sorting in place. Simple, works.
Approach B — Frequency counter: O(n) time, O(1) space (alphabet is bounded — 26 lowercase letters or 128 ASCII).
function isAnagram(s: string, t: string): boolean {
if (s.length !== t.length) return false;
const counts = new Map<string, number>();
for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
for (const ch of t) {
const remaining = counts.get(ch);
if (!remaining) return false;
counts.set(ch, remaining - 1);
}
return true;
}
from collections import Counter
def is_anagram(s: str, t: str) -> bool:
return Counter(s) == Counter(t)
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
int[] counts = new int[26];
for (int i = 0; i < s.length(); i++) {
counts[s.charAt(i) - 'a']++;
counts[t.charAt(i) - 'a']--;
}
for (int c : counts) if (c != 0) return false;
return true;
}
function isAnagram(string $s, string $t): bool
{
if (strlen($s) !== strlen($t)) return false;
return count_chars($s, 1) === count_chars($t, 1);
}
func isAnagram(s, t string) bool {
if len(s) != len(t) {
return false
}
var counts [26]int
for i := 0; i < len(s); i++ {
counts[s[i]-'a']++
counts[t[i]-'a']--
}
for _, c := range counts {
if c != 0 {
return false
}
}
return true
}
Common follow-up: "What if the string contains Unicode characters?" — Stick with the hashmap; don't assume a 26-element array. The Java/Go bounded-array trick relies on lowercase ASCII; the TS/Python/PHP Map/Counter versions handle Unicode out of the box.
Problem 2.3 — Longest Palindromic Substring
LeetCode #5 — leetcode.com/problems/longest-palindromic-substring
Return the longest palindromic substring in
s.
Thought process — "expand around centers":
"A palindrome is symmetric around its center. There are 2n−1 possible centers — n single-char centers (odd-length palindromes) and n−1 between-char centers (even-length). For each center, expand outward while the characters match. Track the longest one. O(n²) time, O(1) space."
function longestPalindrome(s: string): string {
if (s.length < 2) return s;
let start = 0;
let maxLen = 1;
const expand = (left: number, right: number): void => {
while (left >= 0 && right < s.length && s[left] === s[right]) {
if (right - left + 1 > maxLen) {
start = left;
maxLen = right - left + 1;
}
left--;
right++;
}
};
for (let i = 0; i < s.length; i++) {
expand(i, i); // odd-length palindrome
expand(i, i + 1); // even-length palindrome
}
return s.substring(start, start + maxLen);
}
def longest_palindrome(s: str) -> str:
if len(s) < 2:
return s
start, max_len = 0, 1
def expand(left: int, right: int) -> None:
nonlocal start, max_len
while left >= 0 and right < len(s) and s[left] == s[right]:
if right - left + 1 > max_len:
start = left
max_len = right - left + 1
left -= 1
right += 1
for i in range(len(s)):
expand(i, i) # odd
expand(i, i + 1) # even
return s[start:start + max_len]
public String longestPalindrome(String s) {
if (s.length() < 2) return s;
int[] best = {0, 1}; // {start, maxLen}
for (int i = 0; i < s.length(); i++) {
expand(s, i, i, best);
expand(s, i, i + 1, best);
}
return s.substring(best[0], best[0] + best[1]);
}
private void expand(String s, int left, int right, int[] best) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
if (right - left + 1 > best[1]) {
best[0] = left;
best[1] = right - left + 1;
}
left--;
right++;
}
}
function longestPalindrome(string $s): string
{
if (strlen($s) < 2) return $s;
$start = 0;
$maxLen = 1;
$expand = function (int $left, int $right) use ($s, &$start, &$maxLen): void {
while ($left >= 0 && $right < strlen($s) && $s[$left] === $s[$right]) {
if ($right - $left + 1 > $maxLen) {
$start = $left;
$maxLen = $right - $left + 1;
}
$left--;
$right++;
}
};
for ($i = 0; $i < strlen($s); $i++) {
$expand($i, $i);
$expand($i, $i + 1);
}
return substr($s, $start, $maxLen);
}
func longestPalindrome(s string) string {
if len(s) < 2 {
return s
}
start, maxLen := 0, 1
expand := func(left, right int) {
for left >= 0 && right < len(s) && s[left] == s[right] {
if right-left+1 > maxLen {
start = left
maxLen = right - left + 1
}
left--
right++
}
}
for i := 0; i < len(s); i++ {
expand(i, i)
expand(i, i+1)
}
return s[start : start+maxLen]
}
Complexity: Time O(n²), Space O(1).
Worth mentioning: There's an O(n) algorithm called Manacher's, but no interviewer expects it. If asked, say you know it exists but the expand-around-center solution is the realistic interview answer.
3. Hashmaps: The Unsung Hero
Hashmaps give you O(1) average lookups, inserts, and deletes. Whenever a problem mentions "find," "count," "duplicate," or "frequency" — a hashmap is your first instinct.
Problem 3.1 — Contains Duplicate
LeetCode #217 — leetcode.com/problems/contains-duplicate
Return
trueif any value appears at least twice.
function containsDuplicate(nums: number[]): boolean {
const seen = new Set<number>();
for (const num of nums) {
if (seen.has(num)) return true;
seen.add(num);
}
return false;
}
// One-liner alternative:
const containsDuplicateShort = (nums: number[]) =>
new Set(nums).size !== nums.length;
def contains_duplicate(nums: list[int]) -> bool:
return len(set(nums)) != len(nums)
public boolean containsDuplicate(int[] nums) {
Set<Integer> seen = new HashSet<>();
for (int num : nums) {
if (!seen.add(num)) return true; // add() returns false if already present
}
return false;
}
function containsDuplicate(array $nums): bool
{
return count(array_unique($nums)) !== count($nums);
}
func containsDuplicate(nums []int) bool {
seen := make(map[int]struct{})
for _, num := range nums {
if _, ok := seen[num]; ok {
return true
}
seen[num] = struct{}{}
}
return false
}
Complexity: Time O(n), Space O(n).
Problem 3.2 — Group Anagrams
LeetCode #49 — leetcode.com/problems/group-anagrams
Group strings that are anagrams of each other.
Thought process:
"Two strings are anagrams iff their sorted forms are equal. Use the sorted form as a hashmap key, group strings under it."
function groupAnagrams(strs: string[]): string[][] {
const groups = new Map<string, string[]>();
for (const str of strs) {
const key = str.split("").sort().join("");
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(str);
}
return Array.from(groups.values());
}
from collections import defaultdict
def group_anagrams(strs: list[str]) -> list[list[str]]:
groups: dict[str, list[str]] = defaultdict(list)
for s in strs:
key = "".join(sorted(s))
groups[key].append(s)
return list(groups.values())
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> groups = new HashMap<>();
for (String str : strs) {
char[] chars = str.toCharArray();
Arrays.sort(chars);
String key = new String(chars);
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(str);
}
return new ArrayList<>(groups.values());
}
function groupAnagrams(array $strs): array
{
$groups = [];
foreach ($strs as $str) {
$chars = str_split($str);
sort($chars);
$key = implode("", $chars);
$groups[$key][] = $str;
}
return array_values($groups);
}
func groupAnagrams(strs []string) [][]string {
groups := make(map[string][]string)
for _, str := range strs {
chars := []byte(str)
sort.Slice(chars, func(i, j int) bool { return chars[i] < chars[j] })
key := string(chars)
groups[key] = append(groups[key], str)
}
result := make([][]string, 0, len(groups))
for _, g := range groups {
result = append(result, g)
}
return result
}
Complexity: Time O(n · k log k) where k is the average string length. Space O(n · k).
Optimization to mention: Instead of sorting, use a 26-element character-count tuple as the key. That brings the time down to O(n · k).
4. Sliding Window: The Ninja Technique
The sliding window pattern shines on problems asking about contiguous subarrays or substrings — find the longest, the largest sum, the count, etc. Two flavors:
- Fixed-size window — you know
kupfront. - Variable-size window — you grow and shrink the window based on a condition.
Problem 4.1 — Maximum Average Subarray I (Fixed Window)
LeetCode #643 — leetcode.com/problems/maximum-average-subarray-i
Find the maximum average value of any contiguous subarray of length
k.
Thought process:
"Compute the sum of the first window, then slide: subtract the element leaving on the left, add the new element on the right. O(n)."
function findMaxAverage(nums: number[], k: number): number {
let windowSum = 0;
for (let i = 0; i < k; i++) windowSum += nums[i];
let maxSum = windowSum;
for (let i = k; i < nums.length; i++) {
windowSum += nums[i] - nums[i - k];
maxSum = Math.max(maxSum, windowSum);
}
return maxSum / k;
}
def find_max_average(nums: list[int], k: int) -> float:
window_sum = sum(nums[:k])
max_sum = window_sum
for i in range(k, len(nums)):
window_sum += nums[i] - nums[i - k]
max_sum = max(max_sum, window_sum)
return max_sum / k
public double findMaxAverage(int[] nums, int k) {
int windowSum = 0;
for (int i = 0; i < k; i++) windowSum += nums[i];
int maxSum = windowSum;
for (int i = k; i < nums.length; i++) {
windowSum += nums[i] - nums[i - k];
maxSum = Math.max(maxSum, windowSum);
}
return (double) maxSum / k;
}
function findMaxAverage(array $nums, int $k): float
{
$windowSum = array_sum(array_slice($nums, 0, $k));
$maxSum = $windowSum;
for ($i = $k; $i < count($nums); $i++) {
$windowSum += $nums[$i] - $nums[$i - $k];
$maxSum = max($maxSum, $windowSum);
}
return $maxSum / $k;
}
func findMaxAverage(nums []int, k int) float64 {
windowSum := 0
for i := 0; i < k; i++ {
windowSum += nums[i]
}
maxSum := windowSum
for i := k; i < len(nums); i++ {
windowSum += nums[i] - nums[i-k]
if windowSum > maxSum {
maxSum = windowSum
}
}
return float64(maxSum) / float64(k)
}
Complexity: Time O(n), Space O(1).
Problem 4.2 — Longest Substring Without Repeating Characters (Variable Window)
LeetCode #3 — leetcode.com/problems/longest-substring-without-repeating-characters
Find the length of the longest substring without repeating characters.
Thought process — verbalize this carefully:
"I'll use a sliding window with two pointers,
leftandright. I extendrightand add characters to a set. When I hit a duplicate, I moveleftforward — removing characters from the set — until the duplicate is gone. The window is always free of duplicates, so I track its maximum size."
function lengthOfLongestSubstring(s: string): number {
const seen = new Set<string>();
let left = 0;
let maxLen = 0;
for (let right = 0; right < s.length; right++) {
while (seen.has(s[right])) {
seen.delete(s[left]);
left++;
}
seen.add(s[right]);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
def length_of_longest_substring(s: str) -> int:
seen: set[str] = set()
left = 0
max_len = 0
for right, ch in enumerate(s):
while ch in seen:
seen.remove(s[left])
left += 1
seen.add(ch)
max_len = max(max_len, right - left + 1)
return max_len
public int lengthOfLongestSubstring(String s) {
Set<Character> seen = new HashSet<>();
int left = 0;
int maxLen = 0;
for (int right = 0; right < s.length(); right++) {
while (seen.contains(s.charAt(right))) {
seen.remove(s.charAt(left));
left++;
}
seen.add(s.charAt(right));
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
function lengthOfLongestSubstring(string $s): int
{
$seen = [];
$left = 0;
$maxLen = 0;
for ($right = 0; $right < strlen($s); $right++) {
while (isset($seen[$s[$right]])) {
unset($seen[$s[$left]]);
$left++;
}
$seen[$s[$right]] = true;
$maxLen = max($maxLen, $right - $left + 1);
}
return $maxLen;
}
func lengthOfLongestSubstring(s string) int {
seen := make(map[byte]bool)
left, maxLen := 0, 0
for right := 0; right < len(s); right++ {
for seen[s[right]] {
delete(seen, s[left])
left++
}
seen[s[right]] = true
if right-left+1 > maxLen {
maxLen = right - left + 1
}
}
return maxLen
}
Complexity: Time O(n) — each character enters and leaves the window at most once. Space O(min(n, alphabet size)).
Common variation: LeetCode #424 — Longest Repeating Character Replacement. Same pattern, different invariant — the window is valid as long as windowSize − mostFrequentChar ≤ k.
5. Dynamic Programming: When Things Get Real
DP scares people, but it's just recursion + caching. The recipe is always the same:
- Define the state — what does
dp[i](ordp[i][j]) represent? - Write the recurrence — how does
dp[i]depend on smaller subproblems? - Identify the base case.
- Decide on direction — top-down (memoization) or bottom-up (tabulation).
- Optimize space if only the last few states are needed.
Problem 5.1 — Climbing Stairs
LeetCode #70 — leetcode.com/problems/climbing-stairs
You can climb 1 or 2 steps at a time. How many distinct ways to reach step
n?
Thought process:
"To reach step
n, my last move was either a 1-step fromn−1or a 2-step fromn−2. Soways(n) = ways(n−1) + ways(n−2)— that's Fibonacci. Base cases:ways(1) = 1,ways(2) = 2. I only need the last two values, so O(1) space."
function climbStairs(n: number): number {
if (n <= 2) return n;
let prev2 = 1;
let prev1 = 2;
for (let i = 3; i <= n; i++) {
const curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
def climb_stairs(n: int) -> int:
if n <= 2:
return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
prev2, prev1 = prev1, prev1 + prev2
return prev1
public int climbStairs(int n) {
if (n <= 2) return n;
int prev2 = 1;
int prev1 = 2;
for (int i = 3; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
function climbStairs(int $n): int
{
if ($n <= 2) return $n;
$prev2 = 1;
$prev1 = 2;
for ($i = 3; $i <= $n; $i++) {
$curr = $prev1 + $prev2;
$prev2 = $prev1;
$prev1 = $curr;
}
return $prev1;
}
func climbStairs(n int) int {
if n <= 2 {
return n
}
prev2, prev1 := 1, 2
for i := 3; i <= n; i++ {
prev2, prev1 = prev1, prev1+prev2
}
return prev1
}
Complexity: Time O(n), Space O(1).
Why this matters: This is the simplest DP problem. If you can articulate the recurrence and the space optimization here, you've shown the interviewer the entire DP toolkit.
Problem 5.2 — House Robber
LeetCode #198 — leetcode.com/problems/house-robber
Houses on a street, each with money. You can't rob two adjacent houses. Maximize the total.
Thought process:
"At house
i, I have two choices: rob it (addnums[i]to the best I could do up toi−2) or skip it (carry over the best up toi−1). Sodp[i] = max(dp[i−1], dp[i−2] + nums[i])."
function rob(nums: number[]): number {
let prev2 = 0; // max loot up to i-2
let prev1 = 0; // max loot up to i-1
for (const num of nums) {
const curr = Math.max(prev1, prev2 + num);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
def rob(nums: list[int]) -> int:
prev2 = prev1 = 0
for num in nums:
prev2, prev1 = prev1, max(prev1, prev2 + num)
return prev1
public int rob(int[] nums) {
int prev2 = 0;
int prev1 = 0;
for (int num : nums) {
int curr = Math.max(prev1, prev2 + num);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
function rob(array $nums): int
{
$prev2 = 0;
$prev1 = 0;
foreach ($nums as $num) {
$curr = max($prev1, $prev2 + $num);
$prev2 = $prev1;
$prev1 = $curr;
}
return $prev1;
}
func rob(nums []int) int {
prev2, prev1 := 0, 0
for _, num := range nums {
curr := max(prev1, prev2+num)
prev2 = prev1
prev1 = curr
}
return prev1
}
Complexity: Time O(n), Space O(1).
Common follow-up: LeetCode #213 — House Robber II (houses in a circle). Trick: run the linear solution twice — once excluding the first house, once excluding the last — and take the max.
Problem 5.3 — Longest Common Subsequence
LeetCode #1143 — leetcode.com/problems/longest-common-subsequence
Given two strings, find the length of their longest common subsequence (not contiguous).
Thought process — 2D DP:
"Define
dp[i][j]= LCS length oftext1[0..i]andtext2[0..j]. If the characters match,dp[i][j] = dp[i−1][j−1] + 1. Otherwise,dp[i][j] = max(dp[i−1][j], dp[i][j−1])— drop one character from either string and take the best."
function longestCommonSubsequence(text1: string, text2: string): number {
const m = text1.length;
const n = text2.length;
const dp: number[][] = Array.from({ length: m + 1 }, () =>
new Array(n + 1).fill(0),
);
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
def longest_common_subsequence(text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
function longestCommonSubsequence(string $text1, string $text2): int
{
$m = strlen($text1);
$n = strlen($text2);
$dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
for ($i = 1; $i <= $m; $i++) {
for ($j = 1; $j <= $n; $j++) {
if ($text1[$i - 1] === $text2[$j - 1]) {
$dp[$i][$j] = $dp[$i - 1][$j - 1] + 1;
} else {
$dp[$i][$j] = max($dp[$i - 1][$j], $dp[$i][$j - 1]);
}
}
}
return $dp[$m][$n];
}
func longestCommonSubsequence(text1, text2 string) int {
m, n := len(text1), len(text2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if text1[i-1] == text2[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
}
}
}
return dp[m][n]
}
Complexity: Time O(m·n), Space O(m·n) — can be optimized to O(min(m, n)) by keeping only two rows.
6. Graphs: The Brain Teaser
Graph problems test your ability to think in connections. The two essential traversals — BFS (queue, finds shortest paths in unweighted graphs) and DFS (stack/recursion, great for connected components and cycle detection) — solve a huge fraction of graph questions.
Problem 6.1 — Number of Islands
LeetCode #200 — leetcode.com/problems/number-of-islands
Given an
m × ngrid of'1'(land) and'0'(water), count the number of islands.
Thought process:
"Iterate over the grid. When I find unvisited land, that's a new island. I run BFS from there to flood-fill the whole connected component, marking it visited. Count++ each time I trigger a flood-fill."
function numIslands(grid: string[][]): number {
if (!grid.length) return 0;
const rows = grid.length;
const cols = grid[0].length;
const dirs: [number, number][] = [[1, 0], [-1, 0], [0, 1], [0, -1]];
let count = 0;
const bfs = (sr: number, sc: number): void => {
const queue: [number, number][] = [[sr, sc]];
let head = 0; // index pointer instead of shift() — O(1) dequeue
grid[sr][sc] = "0";
while (head < queue.length) {
const [r, c] = queue[head++];
for (const [dr, dc] of dirs) {
const nr = r + dr;
const nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === "1") {
grid[nr][nc] = "0";
queue.push([nr, nc]);
}
}
}
};
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === "1") {
count++;
bfs(r, c);
}
}
}
return count;
}
from collections import deque
def num_islands(grid: list[list[str]]) -> int:
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def bfs(r: int, c: int) -> None:
queue = deque([(r, c)])
grid[r][c] = "0"
while queue:
row, col = queue.popleft()
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = row + dr, col + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == "1":
grid[nr][nc] = "0"
queue.append((nr, nc))
for r in range(rows):
for c in range(cols):
if grid[r][c] == "1":
count += 1
bfs(r, c)
return count
public int numIslands(char[][] grid) {
if (grid.length == 0) return 0;
int rows = grid.length;
int cols = grid[0].length;
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int count = 0;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] != '1') continue;
count++;
Deque<int[]> queue = new ArrayDeque<>();
queue.offer(new int[]{r, c});
grid[r][c] = '0';
while (!queue.isEmpty()) {
int[] cell = queue.poll();
for (int[] d : dirs) {
int nr = cell[0] + d[0];
int nc = cell[1] + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == '1') {
grid[nr][nc] = '0';
queue.offer(new int[]{nr, nc});
}
}
}
}
}
return count;
}
function numIslands(array $grid): int
{
if (empty($grid)) return 0;
$rows = count($grid);
$cols = count($grid[0]);
$dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
$count = 0;
for ($r = 0; $r < $rows; $r++) {
for ($c = 0; $c < $cols; $c++) {
if ($grid[$r][$c] !== '1') continue;
$count++;
$queue = [[$r, $c]];
$head = 0;
$grid[$r][$c] = '0';
while ($head < count($queue)) {
[$row, $col] = $queue[$head++];
foreach ($dirs as [$dr, $dc]) {
$nr = $row + $dr;
$nc = $col + $dc;
if ($nr >= 0 && $nr < $rows && $nc >= 0 && $nc < $cols && $grid[$nr][$nc] === '1') {
$grid[$nr][$nc] = '0';
$queue[] = [$nr, $nc];
}
}
}
}
}
return $count;
}
func numIslands(grid [][]byte) int {
if len(grid) == 0 {
return 0
}
rows, cols := len(grid), len(grid[0])
dirs := [4][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
count := 0
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if grid[r][c] != '1' {
continue
}
count++
queue := [][2]int{{r, c}}
grid[r][c] = '0'
for len(queue) > 0 {
cell := queue[0]
queue = queue[1:]
for _, d := range dirs {
nr, nc := cell[0]+d[0], cell[1]+d[1]
if nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == '1' {
grid[nr][nc] = '0'
queue = append(queue, [2]int{nr, nc})
}
}
}
}
}
return count
}
Complexity: Time O(m·n), Space O(m·n) worst case (queue holds the largest island at peak).
Problem 6.2 — Course Schedule (Cycle Detection / Topological Sort)
LeetCode #207 — leetcode.com/problems/course-schedule
Given
numCoursesand prerequisite pairs[a, b](must takebbeforea), return whether you can finish all courses.
Thought process:
"Build a directed graph from prerequisites. The question reduces to: does this graph have a cycle? If yes, impossible. I'll use Kahn's algorithm — compute in-degrees, start with nodes that have in-degree 0, peel them off, and decrement their neighbors. If I process all
numCoursesnodes, no cycle."
function canFinish(numCourses: number, prerequisites: number[][]): boolean {
const graph: number[][] = Array.from({ length: numCourses }, () => []);
const inDegree: number[] = new Array(numCourses).fill(0);
for (const [course, prereq] of prerequisites) {
graph[prereq].push(course);
inDegree[course]++;
}
const queue: number[] = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) queue.push(i);
}
let processed = 0;
let head = 0;
while (head < queue.length) {
const course = queue[head++];
processed++;
for (const next of graph[course]) {
if (--inDegree[next] === 0) queue.push(next);
}
}
return processed === numCourses;
}
from collections import defaultdict, deque
def can_finish(num_courses: int, prerequisites: list[list[int]]) -> bool:
graph = defaultdict(list)
in_degree = [0] * num_courses
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
queue = deque(i for i in range(num_courses) if in_degree[i] == 0)
processed = 0
while queue:
course = queue.popleft()
processed += 1
for nxt in graph[course]:
in_degree[nxt] -= 1
if in_degree[nxt] == 0:
queue.append(nxt)
return processed == num_courses
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> graph = new ArrayList<>();
int[] inDegree = new int[numCourses];
for (int i = 0; i < numCourses; i++) graph.add(new ArrayList<>());
for (int[] p : prerequisites) {
graph.get(p[1]).add(p[0]);
inDegree[p[0]]++;
}
Deque<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) queue.offer(i);
}
int processed = 0;
while (!queue.isEmpty()) {
int course = queue.poll();
processed++;
for (int next : graph.get(course)) {
if (--inDegree[next] == 0) queue.offer(next);
}
}
return processed == numCourses;
}
function canFinish(int $numCourses, array $prerequisites): bool
{
$graph = array_fill(0, $numCourses, []);
$inDegree = array_fill(0, $numCourses, 0);
foreach ($prerequisites as [$course, $prereq]) {
$graph[$prereq][] = $course;
$inDegree[$course]++;
}
$queue = [];
for ($i = 0; $i < $numCourses; $i++) {
if ($inDegree[$i] === 0) $queue[] = $i;
}
$processed = 0;
$head = 0;
while ($head < count($queue)) {
$course = $queue[$head++];
$processed++;
foreach ($graph[$course] as $next) {
if (--$inDegree[$next] === 0) $queue[] = $next;
}
}
return $processed === $numCourses;
}
func canFinish(numCourses int, prerequisites [][]int) bool {
graph := make([][]int, numCourses)
inDegree := make([]int, numCourses)
for _, p := range prerequisites {
graph[p[1]] = append(graph[p[1]], p[0])
inDegree[p[0]]++
}
queue := []int{}
for i := 0; i < numCourses; i++ {
if inDegree[i] == 0 {
queue = append(queue, i)
}
}
processed := 0
for len(queue) > 0 {
course := queue[0]
queue = queue[1:]
processed++
for _, next := range graph[course] {
inDegree[next]--
if inDegree[next] == 0 {
queue = append(queue, next)
}
}
}
return processed == numCourses
}
Complexity: Time O(V + E), Space O(V + E).
Common follow-up: LeetCode #210 — Course Schedule II asks for the actual order — same algorithm, just collect the processed courses into a list.
Quick Reference: Pattern Recognition Cheat Sheet
| Signal in the problem | Likely pattern |
|---|---|
| "Find a pair / triplet that sums to X" | Hashmap (unsorted) or two pointers (sorted) |
| "Contiguous subarray / substring with property X" | Sliding window |
| "Longest / shortest / count of X" + overlapping subproblems | Dynamic programming |
| "Number of ways to…" | DP (often) or combinatorics |
| "Connected components in a grid" | DFS/BFS flood fill |
| "Order of tasks with dependencies" | Topological sort |
| "Shortest path, unweighted" | BFS |
| "Shortest path, weighted, non-negative" | Dijkstra's |
| "Find k-th largest / smallest" | Heap (priority queue) |
| "Already-sorted input" | Two pointers or binary search |
Complexity Quick Reference
| Operation | Array | Hashmap | Heap | BST (balanced) |
|---|---|---|---|---|
| Access by index | O(1) | — | — | O(log n) |
| Search | O(n) | O(1) avg | O(n) | O(log n) |
| Insert | O(n) (middle) | O(1) avg | O(log n) | O(log n) |
| Delete | O(n) (middle) | O(1) avg | O(log n) | O(log n) |
| Min / max | O(n) | O(n) | O(1) | O(log n) |
Language-Specific Gotchas Worth Knowing
A handful of language-quirks that bite candidates more often than algorithms do. Mention the relevant one in your interview to score points.
TypeScript / JavaScript
Array.prototype.shift()is O(n), not O(1). For BFS on large inputs, use an index pointer (head++) over the array — orDequefrom a library.arr.sort()sorts lexicographically ([10, 2, 1]becomes[1, 10, 2]). Always pass a comparator:arr.sort((a, b) => a - b).Set.has()is O(1);array.includes()is O(n). Don't confuse them.Math.max(...arr)throwsRangeErrorfor arrays larger than ~100k elements. Use a reduce loop.- Strings are immutable:
s[i] = "x"silently does nothing. Convert to array first.
Python
- Default mutable arguments are evaluated once:
def f(x=[])shares the same list across all calls. Usedef f(x=None)and initialise inside. list.pop(0)is O(n). Usecollections.dequefor queues.Counteris the cleanest way to count anything iterable — don't reach for a dict + manual increment.dictpreserves insertion order since 3.7;setdoes not.- For numeric ranges,
range(n)is lazy and O(1) memory;list(range(n))materialises and uses O(n).
Java
int[]does not have anArrays.contains(...)like JS. Use aSet<Integer>for fast lookups.Arrays.asList(arr)on anint[]gives you aList<int[]>(one element!), notList<Integer>. UseArrays.stream(arr).boxed().toList().Stringconcatenation in a loop is O(n²) due to immutability. UseStringBuilderfor hot paths.HashMap.computeIfAbsentis the cleanest way to build a map of lists — no null check + put dance.- Auto-boxing penalties matter: prefer
int[]overList<Integer>in tight loops.
PHP
- The
===operator is strict;==does loose type comparison and is a foot-gun. Always===in interview code. - Arrays in PHP are both numeric and associative —
array_uniqueworks on both, butcountcounts all entries. str_split($s)returns an array of single-character strings. For Unicode, usemb_str_split.usort($arr, fn ($a, $b) => $a - $b)is a clean sort comparator; avoid signed-overflow on huge integers (use<=>spaceship).array_shiftandarray_unshiftare O(n) — they re-index the whole array. Use indexed pointers in BFS-style code.
Go
len()is O(1) on slices, strings, and maps — call it freely in loops.slice[1:]to dequeue is O(1), but it doesn't free the underlying array; for long-lived processes, copy the tail.- Maps don't have a "contains" method — the
value, ok := m[key]two-value form is idiomatic. - Strings are byte slices internally; iterating with
rangegives you runes (Unicode code points).len(s)returns bytes, not characters. - Sorting a slice:
sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }). There's no built-inint.comparelike Java.
Wrapping It All Up
When it comes to live coding interviews, arrays and strings are your bread and butter. Hashmaps turn brute-force O(n²) solutions into O(n) almost daily. Sliding window and two pointers are the optimization patterns that make interviewers nod approvingly. Dynamic programming is intimidating until you realise every DP problem follows the same five-step recipe. Graphs are about thinking in connections — and BFS plus DFS cover most of what you'll see.
But the meta-skill that wins interviews is communication: restate the problem, walk a small example, propose a brute force, optimise, narrate as you code, test edge cases. The candidates who get the offer aren't the ones who silently write the perfect solution — they're the ones who let the interviewer see how their mind works.
Pick five of the LeetCode problems linked below. Solve each one twice — once on paper, once in your IDE. Time yourself. After a week, you'll start spotting patterns before you finish reading the problem statement.
Good luck, and go get that job. 🚀
Recommended LeetCode practice list (in order)
- Two Sum — #1
- Best Time to Buy and Sell Stock — #121
- Contains Duplicate — #217
- Valid Anagram — #242
- Maximum Subarray — #53
- Longest Substring Without Repeating Characters — #3
- Group Anagrams — #49
- Climbing Stairs — #70
- House Robber — #198
- Number of Islands — #200
- Longest Palindromic Substring — #5
- Longest Common Subsequence — #1143
- Course Schedule — #207
- Subarray Sum Equals K — #560
- Longest Repeating Character Replacement — #424






