Practice Focus
Apply your knowledge with targeted practice strategies and problem-solving patterns
๐ฏ Recommended Practice Areas:
- Array Operations: Implement basic operations without using built-in methods
- Linked List Mastery: Build linked list with insert, delete, and search operations
- Stack & Queue: Create both using arrays and linked lists
- String Manipulation: Practice without built-in string functions
// Exercise 1: Implement array operations from scratch class MyArray{ private data: { [key: number]: T } = {}; private length: number = 0; get(index: number): T | undefined { return this.data[index]; } push(item: T): number { this.data[this.length] = item; this.length++; return this.length; } pop(): T | undefined { if (this.length === 0) return undefined; const lastItem = this.data[this.length - 1]; delete this.data[this.length - 1]; this.length--; return lastItem; } size(): number { return this.length; } } // Exercise 2: Reverse a string without built-in methods function reverseString(str: string): string { let result = ''; for (let i = str.length - 1; i >= 0; i--) { result += str[i]; } return result; } // Exercise 3: Check if parentheses are balanced using stack function isBalanced(str: string): boolean { const stack: string[] = []; const pairs: { [key: string]: string } = { ')': '(', '}': '{', ']': '[' }; for (const char of str) { if (char === '(' || char === '{' || char === '[') { stack.push(char); } else if (char === ')' || char === '}' || char === ']') { if (stack.length === 0 || stack.pop() !== pairs[char]) { return false; } } } return stack.length === 0; }
# Exercise 1: Implement array operations from scratch class MyArray: def __init__(self): self.data = {} self.length = 0 def get(self, index): return self.data.get(index) def push(self, item): self.data[self.length] = item self.length += 1 return self.length def pop(self): if self.length == 0: return None last_item = self.data[self.length - 1] del self.data[self.length - 1] self.length -= 1 return last_item def size(self): return self.length # Exercise 2: Reverse a string without built-in methods def reverse_string(s): result = '' for i in range(len(s) - 1, -1, -1): result += s[i] return result # Exercise 3: Check if parentheses are balanced using stack def is_balanced(s): stack = [] pairs = {')': '(', '}': '{', ']': '['} for char in s: if char in '({[': stack.append(char) elif char in ')}]': if not stack or stack.pop() != pairs[char]: return False return len(stack) == 0
(* Exercise 1: Implement basic list operations *) let rec list_length = function | [] -> 0 | _ :: tail -> 1 + list_length tail let rec list_append lst1 lst2 = match lst1 with | [] -> lst2 | head :: tail -> head :: list_append tail lst2 let rec list_reverse = function | [] -> [] | head :: tail -> list_append (list_reverse tail) [head] (* Exercise 2: Reverse a string without built-in methods *) let reverse_string s = let len = String.length s in let result = Bytes.create len in for i = 0 to len - 1 do Bytes.set result i s.[len - 1 - i] done; Bytes.to_string result (* Exercise 3: Check if parentheses are balanced using stack *) let is_balanced s = let stack = ref [] in let pairs = [(')', '('); ('}', '{'); (']', '[')] in let get_opening closing = List.assoc_opt closing pairs in let rec check i = if i >= String.length s then !stack = [] else let char = s.[i] in if char = '(' || char = '{' || char = '[' then ( stack := char :: !stack; check (i + 1) ) else if char = ')' || char = '}' || char = ']' then match !stack, get_opening char with | [], _ -> false | top :: rest, Some expected when top = expected -> stack := rest; check (i + 1) | _ -> false else check (i + 1) in check 0
๐ก Practice Tips:
- Start with simple implementations and gradually add complexity
- Test your implementations with edge cases (empty inputs, single elements)
- Time yourself to build coding speed
- Explain your code out loud to practice for interviews
๐ Key Patterns to Master:
- Array Traversal: Single pass, nested loops, sliding window
- String Processing: Character counting, pattern matching, palindromes
- Basic Recursion: Divide and conquer, tree-like problems
- Two-Pointer: Array problems, palindromes, pair finding
// Pattern 1: Array Traversal - Find maximum element function findMax(arr: number[]): number { if (arr.length === 0) throw new Error("Empty array"); let max = arr[0]; for (let i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } return max; } // Pattern 2: String Processing - Count character frequency function charFrequency(str: string): { [key: string]: number } { const freq: { [key: string]: number } = {}; for (const char of str) { freq[char] = (freq[char] || 0) + 1; } return freq; } // Pattern 3: Two-Pointer - Remove duplicates from sorted array function removeDuplicates(arr: number[]): number { if (arr.length <= 1) return arr.length; let writeIndex = 1; for (let readIndex = 1; readIndex < arr.length; readIndex++) { if (arr[readIndex] !== arr[readIndex - 1]) { arr[writeIndex] = arr[readIndex]; writeIndex++; } } return writeIndex; } // Pattern 4: Recursion - Calculate power function power(base: number, exponent: number): number { if (exponent === 0) return 1; // Base case if (exponent === 1) return base; // Base case return base * power(base, exponent - 1); // Recursive case }
# Pattern 1: Array Traversal - Find maximum element def find_max(arr): if not arr: raise ValueError("Empty array") max_val = arr[0] for num in arr[1:]: if num > max_val: max_val = num return max_val # Pattern 2: String Processing - Count character frequency def char_frequency(s): freq = {} for char in s: freq[char] = freq.get(char, 0) + 1 return freq # Pattern 3: Two-Pointer - Remove duplicates from sorted array def remove_duplicates(arr): if len(arr) <= 1: return len(arr) write_index = 1 for read_index in range(1, len(arr)): if arr[read_index] != arr[read_index - 1]: arr[write_index] = arr[read_index] write_index += 1 return write_index # Pattern 4: Recursion - Calculate power def power(base, exponent): if exponent == 0: return 1 # Base case if exponent == 1: return base # Base case return base * power(base, exponent - 1) # Recursive case
(* Pattern 1: Array Traversal - Find maximum element *) let find_max arr = if Array.length arr = 0 then failwith "Empty array" else let max_val = ref arr.(0) in for i = 1 to Array.length arr - 1 do if arr.(i) > !max_val then max_val := arr.(i) done; !max_val (* Pattern 2: String Processing - Count character frequency *) let char_frequency s = let freq = Hashtbl.create 16 in String.iter (fun char -> let count = try Hashtbl.find freq char with Not_found -> 0 in Hashtbl.replace freq char (count + 1) ) s; freq (* Pattern 3: Two-Pointer - Remove duplicates from sorted array *) let remove_duplicates arr = let len = Array.length arr in if len <= 1 then len else let write_index = ref 1 in for read_index = 1 to len - 1 do if arr.(read_index) <> arr.(read_index - 1) then ( arr.(!write_index) <- arr.(read_index); incr write_index ) done; !write_index (* Pattern 4: Recursion - Calculate power *) let rec power base exponent = if exponent = 0 then 1 (* Base case *) else if exponent = 1 then base (* Base case *) else base * power base (exponent - 1) (* Recursive case *)
๐ฏ Problem-Solving Strategy:
- Understand: Read the problem carefully and identify inputs/outputs
- Pattern Match: Does this remind you of a pattern you've seen?
- Plan: Write pseudocode or draw diagrams
- Implement: Code your solution step by step
- Test: Try edge cases and verify correctness
- Optimize: Can you improve time or space complexity?
// โ MISTAKE: Wrong loop condition function findMaxWrong(arr: number[]): number { let max = arr[0]; for (let i = 0; i <= arr.length; i++) { // BUG: <= instead of < if (arr[i] > max) { max = arr[i]; } } return max; // Will throw "index out of bounds" error } // โ CORRECT: Proper loop condition function findMaxCorrect(arr: number[]): number { let max = arr[0]; for (let i = 1; i < arr.length; i++) { // Start from 1, use < if (arr[i] > max) { max = arr[i]; } } return max; } // โ MISTAKE: Wrong substring indices function getMiddleWrong(str: string): string { const mid = Math.floor(str.length / 2); return str.substring(mid - 1, mid + 2); // BUG: Wrong range calculation } // โ CORRECT: Proper substring calculation function getMiddleCorrect(str: string): string { const mid = Math.floor(str.length / 2); if (str.length % 2 === 0) { return str.substring(mid - 1, mid + 1); // Even length } else { return str.substring(mid, mid + 1); // Odd length } }
// โ MISTAKE: No edge case handling function reverseArrayWrong(arr: number[]): number[] { const result: number[] = []; for (let i = arr.length - 1; i >= 0; i--) { result.push(arr[i]); } return result; // Works for normal cases, but what about empty array? } // โ CORRECT: Comprehensive edge case handling function reverseArrayCorrect(arr: number[] | null | undefined): number[] { // Handle null/undefined if (!arr) { return []; } // Handle empty array if (arr.length === 0) { return []; } // Handle single element if (arr.length === 1) { return [arr[0]]; } // Normal case const result: number[] = []; for (let i = arr.length - 1; i >= 0; i--) { result.push(arr[i]); } return result; }
// โ MISTAKE: Inefficient string concatenation in loop function joinNumbersWrong(numbers: number[]): string { let result = ""; for (let i = 0; i < numbers.length; i++) { result += numbers[i].toString(); // O(nยฒ) complexity! if (i < numbers.length - 1) { result += ", "; } } return result; } // โ CORRECT: Use array join for efficiency function joinNumbersCorrect(numbers: number[]): string { return numbers.map(n => n.toString()).join(", "); // O(n) complexity } // โ ALTERNATIVE: Use StringBuilder pattern function joinNumbersAlternative(numbers: number[]): string { const parts: string[] = []; for (let i = 0; i < numbers.length; i++) { parts.push(numbers[i].toString()); } return parts.join(", "); // O(n) complexity }
// โ MISTAKE: Removing elements while iterating forward function removeEvensWrong(arr: number[]): void { for (let i = 0; i < arr.length; i++) { if (arr[i] % 2 === 0) { arr.splice(i, 1); // BUG: Changes array length, skips elements } } } // โ CORRECT: Iterate backwards when removing function removeEvensCorrect(arr: number[]): void { for (let i = arr.length - 1; i >= 0; i--) { if (arr[i] % 2 === 0) { arr.splice(i, 1); // Safe: removing from end } } } // โ BETTER: Create new array instead of modifying function filterOdds(arr: number[]): number[] { return arr.filter(num => num % 2 !== 0); // Functional approach }
Before coding, write test cases for edge cases. This helps you think about potential issues.
Always ask: "What if this is empty? What if this is null? What are the boundaries?"
Walk through your code with specific examples, especially edge cases.
Add strategic print statements or use a debugger to see variable values at each step.
// ๐ฏ Strategy 1: Trace function entry/exit function binarySearch(arr: number[], target: number): number { console.log(`๐ binarySearch called with target: ${target}, array length: ${arr.length}`); let left = 0; let right = arr.length - 1; while (left <= right) { const mid = Math.floor((left + right) / 2); console.log(`๐ Checking mid=${mid}, value=${arr[mid]}, range=[${left}, ${right}]`); if (arr[mid] === target) { console.log(`โ Found target at index ${mid}`); return mid; } else if (arr[mid] < target) { console.log(`โฌ๏ธ Target is larger, searching right half`); left = mid + 1; } else { console.log(`โฌ๏ธ Target is smaller, searching left half`); right = mid - 1; } } console.log(`โ Target not found`); return -1; } // ๐ฏ Strategy 2: Conditional debugging function sortArray(arr: number[], debugMode = false): number[] { const debug = (message: string, data?: any) => { if (debugMode) { console.log(`๐ ${message}`, data || ''); } }; debug('Starting sort', arr); for (let i = 0; i < arr.length - 1; i++) { debug(`Pass ${i + 1}`); for (let j = 0; j < arr.length - i - 1; j++) { if (arr[j] > arr[j + 1]) { debug(`Swapping ${arr[j]} and ${arr[j + 1]}`); [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; debug('Array after swap', [...arr]); } } } debug('Sorting complete', arr); return arr; }
- Entry points: Start of functions to verify inputs
- Decision points: Before if/else statements and loops
- State changes: After variable modifications
- Exit points: Before return statements
- Add key variables to watch list
- Monitor array/object contents as they change
- Track loop counters and conditions
- Watch function parameters and return values
- Step Over: Execute current line, don't enter functions
- Step Into: Enter function calls to debug inside
- Step Out: Finish current function and return to caller
- Continue: Run until next breakpoint
// Code to trace: function bubbleSort(arr: number[]): number[] { for (let i = 0; i < arr.length - 1; i++) { for (let j = 0; j < arr.length - i - 1; j++) { if (arr[j] > arr[j + 1]) { [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; } } } return arr; } // Manual trace with arr = [3, 1, 4] /* Step-by-step execution: Initial: arr = [3, 1, 4], length = 3 Outer loop: i = 0 Inner loop: j = 0 arr[0] = 3, arr[1] = 1 3 > 1? YES โ swap arr = [1, 3, 4] Inner loop: j = 1 arr[1] = 3, arr[2] = 4 3 > 4? NO โ no swap arr = [1, 3, 4] Outer loop: i = 1 Inner loop: j = 0 arr[0] = 1, arr[1] = 3 1 > 3? NO โ no swap arr = [1, 3, 4] Final: arr = [1, 3, 4] โ */
Test with the simplest possible input first, then gradually increase complexity.
Comment out half the code to isolate where the problem occurs.
Write down what you expect at each step, then verify with debugging.
Empty inputs, single elements, boundary values, null/undefined.
Make sure you can trigger the bug reliably before trying to fix it.
Verify that variables contain the expected types and formats.
๐ Congratulations!
You've completed Level 1: Foundation! You now have a solid understanding of:
Big O notation & Algorithm vs Data Structure
Arrays, Linked Lists, Stacks & Queues
Linear Search, Recursion & Two-Pointer
Implementation & Problem-Solving Patterns
Keep practicing these fundamentals - they form the foundation for all advanced DSA concepts!