šŸš€ 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
Complete the checklist above to assess your readiness for Level 2
šŸŒ‰ 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

šŸš€ Ready to Start Level 2?

If you've completed the prerequisites checklist above, you're ready to dive into Level 2!