š Ready for Level 2?
Bridge the gap between Foundation and Intermediate concepts
Level 1 to 2 Transition Guide
š Prerequisites Check
Level 1 Mastery Checklist
Before moving to Level 2, ensure you're comfortable with these fundamental concepts. Check off each item as you master it:
š§ Core Concepts
š Data Structures
ā” Algorithms
š¤ String Manipulation
0 of 15 prerequisites completed
š Bridge to Level 2
Intermediate Examples: From Simple to Complex
Let's bridge the gap between Level 1 and Level 2 with intermediate examples that gradually increase in complexity.
š From Arrays to Trees
You know arrays. Trees are just a way to organize data hierarchically instead of linearly.
Array to Tree Transition
TypeScript
Python
OCaml
// You already know this: Array with linear access const numbers = [1, 3, 5, 7, 9]; console.log(numbers[2]); // Access by index: O(1) // Now let's think hierarchically: Simple Tree Node class SimpleTreeNode { value: number; left: SimpleTreeNode | null = null; right: SimpleTreeNode | null = null; constructor(value: number) { this.value = value; } } // Building a tree is like organizing a family tree const root = new SimpleTreeNode(5); // Parent root.left = new SimpleTreeNode(3); // Left child root.right = new SimpleTreeNode(7); // Right child root.left.left = new SimpleTreeNode(1); // Grandchild root.left.right = new SimpleTreeNode(4); // Grandchild // Tree traversal is like visiting family members in order function visitInOrder(node: SimpleTreeNode | null): void { if (node === null) return; visitInOrder(node.left); // Visit left family first console.log(node.value); // Visit current person visitInOrder(node.right); // Visit right family last } // This prints: 1, 3, 4, 5, 7 (sorted order!) visitInOrder(root);
# You already know this: Array with linear access numbers = [1, 3, 5, 7, 9] print(numbers[2]) # Access by index: O(1) # Now let's think hierarchically: Simple Tree Node class SimpleTreeNode: def __init__(self, value): self.value = value self.left = None self.right = None # Building a tree is like organizing a family tree root = SimpleTreeNode(5) # Parent root.left = SimpleTreeNode(3) # Left child root.right = SimpleTreeNode(7) # Right child root.left.left = SimpleTreeNode(1) # Grandchild root.left.right = SimpleTreeNode(4) # Grandchild # Tree traversal is like visiting family members in order def visit_in_order(node): if node is None: return visit_in_order(node.left) # Visit left family first print(node.value) # Visit current person visit_in_order(node.right) # Visit right family last # This prints: 1, 3, 4, 5, 7 (sorted order!) visit_in_order(root)
(* You already know this: Array with linear access *) let numbers = [|1; 3; 5; 7; 9|] in Printf.printf "%d\n" numbers.(2);; (* Access by index: O(1) *) (* Now let's think hierarchically: Simple Tree *) type simple_tree = | Empty | Node of int * simple_tree * simple_tree (* Building a tree is like organizing a family tree *) let root = Node(5, (* Parent *) Node(3, (* Left child *) Node(1, Empty, Empty), (* Grandchild *) Node(4, Empty, Empty)), (* Grandchild *) Node(7, Empty, Empty)) (* Right child *) (* Tree traversal is like visiting family members in order *) let rec visit_in_order = function | Empty -> () | Node(value, left, right) -> visit_in_order left; (* Visit left family first *) Printf.printf "%d " value; (* Visit current person *) visit_in_order right (* Visit right family last *) (* This prints: 1 3 4 5 7 (sorted order!) *) let () = visit_in_order root
š” Key Insight: Trees are just arrays organized hierarchically. Instead of accessing by index, you navigate by relationships (parent, left child, right child).
š From Linear Search to Binary Search
You know linear search. Binary search is the same idea, but smarter - it eliminates half the possibilities each time.
Search Evolution
TypeScript
Python
OCaml
// You already know this: Linear Search (check every element) function linearSearch(arr: number[], target: number): number { for (let i = 0; i < arr.length; i++) { if (arr[i] === target) { return i; } } return -1; // Not found } // Time complexity: O(n) - might check every element // Now the smarter way: Binary Search (eliminate half each time) function binarySearch(arr: number[], target: number): number { let left = 0; let right = arr.length - 1; while (left <= right) { const mid = Math.floor((left + right) / 2); if (arr[mid] === target) { return mid; // Found it! } else if (arr[mid] < target) { left = mid + 1; // Target is in right half } else { right = mid - 1; // Target is in left half } } return -1; // Not found } // Time complexity: O(log n) - eliminates half each time // Example: Finding 7 in [1, 3, 5, 7, 9, 11, 13] const sortedArray = [1, 3, 5, 7, 9, 11, 13]; console.log("Linear search:", linearSearch(sortedArray, 7)); // Checks: 1,3,5,7 (4 steps) console.log("Binary search:", binarySearch(sortedArray, 7)); // Checks: 7 (1 step!)
# You already know this: Linear Search (check every element) def linear_search(arr, target): for i in range(len(arr)): if arr[i] == target: return i return -1 # Not found # Time complexity: O(n) - might check every element # Now the smarter way: Binary Search (eliminate half each time) def binary_search(arr, target): left = 0 right = len(arr) - 1 while left <= right: mid = (left + right) // 2 if arr[mid] == target: return mid # Found it! elif arr[mid] < target: left = mid + 1 # Target is in right half else: right = mid - 1 # Target is in left half return -1 # Not found # Time complexity: O(log n) - eliminates half each time # Example: Finding 7 in [1, 3, 5, 7, 9, 11, 13] sorted_array = [1, 3, 5, 7, 9, 11, 13] print("Linear search:", linear_search(sorted_array, 7)) # Checks: 1,3,5,7 (4 steps) print("Binary search:", binary_search(sorted_array, 7)) # Checks: 7 (1 step!)
(* You already know this: Linear Search (check every element) *) let rec linear_search arr target index = if index >= Array.length arr then -1 else if arr.(index) = target then index else linear_search arr target (index + 1) let linear_search_wrapper arr target = linear_search arr target 0 (* Time complexity: O(n) - might check every element *) (* Now the smarter way: Binary Search (eliminate half each time) *) let rec binary_search arr target left right = if left > right then -1 else let mid = (left + right) / 2 in if arr.(mid) = target then mid else if arr.(mid) < target then binary_search arr target (mid + 1) right (* Target is in right half *) else binary_search arr target left (mid - 1) (* Target is in left half *) let binary_search_wrapper arr target = binary_search arr target 0 (Array.length arr - 1) (* Time complexity: O(log n) - eliminates half each time *) (* Example: Finding 7 in [|1; 3; 5; 7; 9; 11; 13|] *) let sorted_array = [|1; 3; 5; 7; 9; 11; 13|] in Printf.printf "Linear search: %d\n" (linear_search_wrapper sorted_array 7); Printf.printf "Binary search: %d\n" (binary_search_wrapper sorted_array 7)
š” Key Insight: Binary search is linear search with a strategy. Instead of checking every element, you eliminate half the possibilities each time by using the sorted property.
š§® From Simple Recursion to Dynamic Programming
You know recursion. Dynamic Programming is recursion with memory - avoiding repeated calculations.
Recursion Evolution
TypeScript
Python
OCaml
// You already know this: Simple Recursion (Fibonacci) function fibonacciNaive(n: number): number { if (n <= 1) return n; return fibonacciNaive(n - 1) + fibonacciNaive(n - 2); } // Problem: Recalculates the same values many times! // fib(5) = fib(4) + fib(3) // fib(4) = fib(3) + fib(2) <- fib(3) calculated again! // Solution: Dynamic Programming (Recursion with Memory) function fibonacciMemo(n: number, memo: Map= new Map()): number { if (n <= 1) return n; // Check if we already calculated this if (memo.has(n)) { return memo.get(n)!; // Return stored result } // Calculate and store for future use const result = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo); memo.set(n, result); return result; } // Even better: Bottom-up approach (no recursion needed) function fibonacciDP(n: number): number { if (n <= 1) return n; const dp = [0, 1]; // Start with base cases for (let i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; // Build up from smaller problems } return dp[n]; } // Performance comparison for fib(40): console.time("Naive"); console.log(fibonacciNaive(40)); // Takes several seconds! console.timeEnd("Naive"); console.time("Memoized"); console.log(fibonacciMemo(40)); // Instant! console.timeEnd("Memoized"); console.time("DP"); console.log(fibonacciDP(40)); // Instant! console.timeEnd("DP");
# You already know this: Simple Recursion (Fibonacci) def fibonacci_naive(n): if n <= 1: return n return fibonacci_naive(n - 1) + fibonacci_naive(n - 2) # Problem: Recalculates the same values many times! # fib(5) = fib(4) + fib(3) # fib(4) = fib(3) + fib(2) <- fib(3) calculated again! # Solution: Dynamic Programming (Recursion with Memory) def fibonacci_memo(n, memo=None): if memo is None: memo = {} if n <= 1: return n # Check if we already calculated this if n in memo: return memo[n] # Return stored result # Calculate and store for future use result = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo) memo[n] = result return result # Even better: Bottom-up approach (no recursion needed) def fibonacci_dp(n): if n <= 1: return n dp = [0, 1] # Start with base cases for i in range(2, n + 1): dp.append(dp[i - 1] + dp[i - 2]) # Build up from smaller problems return dp[n] # Performance comparison for fib(40): import time start = time.time() print(f"Naive: {fibonacci_naive(35)}") # Using 35 instead of 40 (too slow!) print(f"Naive time: {time.time() - start:.4f} seconds") start = time.time() print(f"Memoized: {fibonacci_memo(40)}") print(f"Memoized time: {time.time() - start:.6f} seconds") start = time.time() print(f"DP: {fibonacci_dp(40)}") print(f"DP time: {time.time() - start:.6f} seconds")
(* You already know this: Simple Recursion (Fibonacci) *) let rec fibonacci_naive n = if n <= 1 then n else fibonacci_naive (n - 1) + fibonacci_naive (n - 2) (* Problem: Recalculates the same values many times! *) (* Solution: Dynamic Programming (Recursion with Memory) *) let fibonacci_memo n = let memo = Hashtbl.create 100 in let rec fib_helper n = if n <= 1 then n else try Hashtbl.find memo n (* Return stored result *) with Not_found -> let result = fib_helper (n - 1) + fib_helper (n - 2) in Hashtbl.add memo n result; (* Store for future use *) result in fib_helper n (* Even better: Bottom-up approach (no recursion needed) *) let fibonacci_dp n = if n <= 1 then n else let dp = Array.make (n + 1) 0 in dp.(1) <- 1; (* Base case *) for i = 2 to n do dp.(i) <- dp.(i - 1) + dp.(i - 2) (* Build up from smaller problems *) done; dp.(n) (* Performance comparison *) let time_function f x = let start = Sys.time () in let result = f x in let elapsed = Sys.time () -. start in (result, elapsed) let () = let (result1, time1) = time_function fibonacci_naive 35 in Printf.printf "Naive: %d (%.4f seconds)\n" result1 time1; let (result2, time2) = time_function fibonacci_memo 40 in Printf.printf "Memoized: %d (%.6f seconds)\n" result2 time2; let (result3, time3) = time_function fibonacci_dp 40 in Printf.printf "DP: %d (%.6f seconds)\n" result3 time3
š” Key Insight: Dynamic Programming is just smart recursion. Instead of recalculating the same subproblems, you remember the answers. It's like taking notes during a math exam!
šÆ What's Coming in Level 2
Level 2 Preview
Here's what you'll learn in Level 2, building on your Level 1 foundation:
Advanced Data Structures
Building on: Arrays and Linked Lists
You'll learn: Trees, Heaps, Hash Tables, and Graphs
Why it matters: These structures solve problems that arrays can't handle efficiently
Efficient Sorting & Searching
Building on: Basic sorting (bubble, selection, insertion)
You'll learn: Merge Sort, Quick Sort, Binary Search, Heap Sort
Why it matters: Handle large datasets efficiently (O(n log n) vs O(n²))
Dynamic Programming
Building on: Basic recursion
You'll learn: Memoization, Tabulation, Classic DP problems
Why it matters: Solve complex optimization problems efficiently
Advanced Techniques
Building on: Two-pointer technique
You'll learn: Sliding Window, Backtracking, Greedy Algorithms
Why it matters: Powerful problem-solving patterns for optimization