š 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