Basic Programming Concepts
TL;DR
Variables store data with a type and scope. Control flow structures (if/else, loops) direct program execution. Functions encapsulate logic and enable recursion for problems decomposable into subproblems. Iteration repeats logic over collections. Understanding these fundamentals is essential for algorithm development.
Core Concepts
Variables and Data Types
Variables are named storage locations that hold values. Each variable has a type (integer, string, boolean, etc.) that determines what values it can store and what operations are allowed.
Key characteristics:
- Name: Identifier for accessing the variable
- Type: Determines memory allocation and valid operations
- Scope: Defines where the variable is accessible (local, global)
- Lifetime: How long the variable exists in memory
- Value: The actual data stored
Scope matters in algorithms—global variables can lead to unexpected state changes, while local variables keep logic isolated and testable.
Control Flow Structures
Control flow determines the order in which statements execute:
Conditional statements (if/else, switch):
- Execute different code blocks based on conditions
- Enable decision-making logic
- Can be nested for complex branching
Loops execute code repeatedly:
- While loops: Continue while condition is true
- For loops: Iterate a fixed number of times
- Do-while loops: Execute at least once, then check condition
Functions and Abstraction
Functions are reusable blocks of code that accept inputs (parameters) and return outputs. Functions enable:
- Code reuse: Write once, call many times
- Abstraction: Hide complexity behind simple interfaces
- Modularity: Break problems into smaller pieces
- Testability: Test each function independently
Function signatures define parameters, return types, and behavior contracts. Parameters can be passed by value (copy) or reference (pointer), affecting performance in algorithms.
Recursion
Recursion occurs when a function calls itself to solve smaller instances of the same problem. Every recursive solution requires:
Base case: The simplest instance that returns directly without recursion Recursive case: The function calls itself with a simpler problem
Example: Factorial(n) = n * Factorial(n-1) when n > 1; returns 1 when n = 1.
Recursion enables elegant solutions for:
- Tree/graph traversals
- Divide-and-conquer algorithms
- Backtracking problems
- Natural problem decomposition
Each recursive call uses stack memory. Deep recursion causes stack overflow; iterative solutions may be more efficient.
Iteration Patterns
Iteration repeats logic over data collections or for fixed counts:
Index-based iteration: Loop through array indices, access elements Element-based iteration: Loop directly over values (foreach/for-in) While loops: Iterate while condition holds true
Understanding iteration is crucial for:
- Traversing data structures
- Searching and sorting
- Processing collections efficiently
- Building algorithms with predictable performance
Key Algorithms and Techniques
Linear Search
Iterate through elements sequentially to find a target. O(n) time complexity, useful for unsorted data.
Factorial Recursion
Classic recursive example: multiply n by factorial of n-1.
Nested Loops
Process multi-dimensional data or find pairs: O(n²) time when iterating through all pairs.
Loop Unrolling
Process multiple elements per iteration to reduce loop overhead (performance optimization).
Practical Example
- Python
- Java
- TypeScript
# Variables and basic control flow
age = 25
name = "Alice"
is_student = True
if age >= 18:
print(f"{name} is an adult")
else:
print(f"{name} is a minor")
# Function definition
def fibonacci(n):
"""Recursive function to compute nth Fibonacci number"""
if n <= 1:
return n # Base case
return fibonacci(n - 1) + fibonacci(n - 2) # Recursive case
# Iteration patterns
numbers = [1, 2, 3, 4, 5]
# Index-based iteration
for i in range(len(numbers)):
print(f"Index {i}: {numbers[i]}")
# Element-based iteration
for num in numbers:
print(f"Value: {num}")
# While loop
i = 0
while i < len(numbers):
print(numbers[i])
i += 1
# Result
print(f"Fibonacci(6) = {fibonacci(6)}") # Output: 8
public class BasicConcepts {
// Variables with types
static int age = 25;
static String name = "Alice";
static boolean isStudent = true;
// Function with parameters and return type
static int fibonacci(int n) {
if (n <= 1) {
return n; // Base case
}
return fibonacci(n - 1) + fibonacci(n - 2); // Recursion
}
public static void main(String[] args) {
// Conditional statement
if (age >= 18) {
System.out.println(name + " is an adult");
} else {
System.out.println(name + " is a minor");
}
// Array and iteration
int[] numbers = {1, 2, 3, 4, 5};
// For loop with index
for (int i = 0; i < numbers.length; i++) {
System.out.println("Index " + i + ": " + numbers[i]);
}
// Enhanced for loop
for (int num : numbers) {
System.out.println("Value: " + num);
}
// While loop
int i = 0;
while (i < numbers.length) {
System.out.println(numbers[i]);
i++;
}
System.out.println("Fibonacci(6) = " + fibonacci(6)); // 8
}
}
// Variables with explicit typing
let age: number = 25;
let name: string = "Alice";
let isStudent: boolean = true;
// Function with type annotations
function fibonacci(n: number): number {
if (n <= 1) {
return n; // Base case
}
return fibonacci(n - 1) + fibonacci(n - 2); // Recursion
}
// Conditional statement
if (age >= 18) {
console.log(`${name} is an adult`);
} else {
console.log(`${name} is a minor`);
}
// Array and iteration patterns
const numbers: number[] = [1, 2, 3, 4, 5];
// Index-based iteration
for (let i = 0; i < numbers.length; i++) {
console.log(`Index ${i}: ${numbers[i]}`);
}
// Element-based iteration
for (const num of numbers) {
console.log(`Value: ${num}`);
}
// Higher-order iteration
numbers.forEach((num, index) => {
console.log(`Index ${index}: ${num}`);
});
// While loop
let i = 0;
while (i < numbers.length) {
console.log(numbers[i]);
i++;
}
console.log(`Fibonacci(6) = ${fibonacci(6)}`); // 8
Common Pitfalls
Infinite loops: Forgetting to update loop condition or missing break statement causes program to hang. Always verify loop termination conditions.
Stack overflow from deep recursion: Deeply recursive functions exhaust stack memory. Consider iterative solutions or tail recursion optimization.
Off-by-one errors: Loop boundaries (i < n vs i <= n) and array indexing cause common bugs. Carefully verify loop ranges.
Scope confusion: Variables with same name in different scopes cause subtle bugs. Use descriptive names and understand variable lifetime.
Type mismatches: Assigning wrong types to variables causes runtime errors or unexpected behavior. Use type checking and explicit conversions.
Advanced Control Flow Patterns
Pattern 1: Early Exit
def find_user(users, target_id):
for user in users:
if user.id == target_id:
return user # Early exit
return None # Not found
Early exit is more efficient than collecting all matches.
Pattern 2: Nested Loops with Break
def find_pair_with_sum(numbers, target):
for i in range(len(numbers)):
for j in range(i + 1, len(numbers)):
if numbers[i] + numbers[j] == target:
return (i, j) # Found pair
# Could also use `break` on inner loop only
return None
Pattern 3: Recursion with Memoization
memo = {}
def fibonacci_fast(n):
if n in memo:
return memo[n] # Cache hit
if n <= 1:
return n
result = fibonacci_fast(n-1) + fibonacci_fast(n-2)
memo[n] = result
return result
Memoization transforms exponential recursion (2^n) into linear (n).
Scope and Lifetime Examples
Variable Scope Issues
x = 10 # Global scope
def modify_global():
global x # Must declare to modify global
x = 20
def local_scope():
x = 30 # Local x shadows global x
return x # Returns 30, global x unchanged
modify_global()
print(x) # 20 - global was modified
print(local_scope()) # 30 - local x
print(x) # Still 20 - local x didn't affect global
Parameter Passing: Value vs. Reference
Pass by Value (copy):
def modify_value(x):
x = x + 1
return x
a = 5
result = modify_value(a)
# a is still 5 (was copied, not modified)
Pass by Reference (object reference):
def modify_list(lst):
lst.append(99) # Modifies original list
numbers = [1, 2, 3]
modify_list(numbers)
# numbers is now [1, 2, 3, 99] (object was modified)
In Python, primitives are pass-by-value, objects are pass-by-reference.
Time Complexity Examples
# O(1) - Constant time
def get_first(lst):
return lst[0]
# O(n) - Linear time
def find_max(lst):
max_val = lst[0]
for num in lst[1:]:
if num > max_val:
max_val = num
return max_val
# O(n²) - Quadratic time
def bubble_sort(lst):
n = len(lst)
for i in range(n):
for j in range(n - i - 1):
if lst[j] > lst[j+1]:
lst[j], lst[j+1] = lst[j+1], lst[j]
return lst
# O(log n) - Logarithmic time (binary search)
def binary_search(lst, target):
left, right = 0, len(lst) - 1
while left <= right:
mid = (left + right) // 2
if lst[mid] == target:
return mid
elif lst[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
Testing Algorithms
def test_factorial():
assert factorial(0) == 1 # Edge case
assert factorial(1) == 1 # Edge case
assert factorial(5) == 120
assert factorial(10) == 3628800
def test_fibonacci():
assert fibonacci(0) == 0 # Edge case
assert fibonacci(1) == 1 # Edge case
assert fibonacci(6) == 8
assert fibonacci(10) == 55
def test_binary_search():
sorted_list = [1, 3, 5, 7, 9, 11]
assert binary_search(sorted_list, 7) == 3
assert binary_search(sorted_list, 1) == 0
assert binary_search(sorted_list, 11) == 5
assert binary_search(sorted_list, 8) == -1 # Not found
# Edge cases
assert binary_search([], 5) == -1 # Empty list
assert binary_search([5], 5) == 0 # Single element
assert binary_search([1], 2) == -1 # Single element, not found
def test_linear_search():
numbers = [5, 2, 8, 1, 9]
assert linear_search(numbers, 8) == 2
assert linear_search(numbers, 1) == 3
assert linear_search(numbers, 99) == -1 # Not found
Always test edge cases: n=0, n=1, duplicates, boundary values, empty inputs, single elements.
Algorithm Complexity Analysis
Measuring Performance
import time
def measure_time(func, *args):
start = time.perf_counter()
result = func(*args)
end = time.perf_counter()
return result, (end - start) * 1000 # milliseconds
# Linear search on list of 1M elements
arr = list(range(1_000_000))
result, time_ms = measure_time(linear_search, arr, 999_999)
print(f"Linear search: {time_ms:.2f}ms") # ~50ms for last element
# Binary search on same list
result, time_ms = measure_time(binary_search, arr, 999_999)
print(f"Binary search: {time_ms:.2f}ms") # ~0.05ms (1000x faster!)
Complexity is crucial: doubling input size should double linear O(n) time, but barely affect binary O(log n) search.
Scope and Lifetime Deep Dive
# Global scope
global_var = 100
def outer():
outer_var = 50 # Outer function scope
def inner():
inner_var = 25 # Inner function scope
# Can access all scopes
print(global_var) # 100
print(outer_var) # 50
print(inner_var) # 25
# Modify outer scope (with nonlocal keyword)
nonlocal outer_var
outer_var = 60
return inner
func = outer()
func() # Prints 100, 50, 25 and modifies outer_var
Scope rules: Local (function) → Enclosing (nested function) → Global → Built-in.
Sorting Algorithms Explained
Bubble Sort
def bubble_sort(arr):
"""Bubble sort: O(n²) time, O(1) space"""
n = len(arr)
for i in range(n):
swapped = False
for j in range(n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break # Already sorted
return arr
# Example: [3, 1, 4, 1, 5]
# Pass 1: [1, 3, 1, 4, 5]
# Pass 2: [1, 1, 3, 4, 5]
# Sorted!
Quicksort (Divide-and-Conquer)
def quicksort(arr):
"""Quicksort: O(n log n) average, O(n²) worst case"""
if len(arr) <= 1:
return arr
# Pick pivot (middle element)
pivot = arr[len(arr) // 2]
# Partition into three parts
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
# Recursively sort left and right
return quicksort(left) + middle + quicksort(right)
# Divide-and-conquer: breaks problem into smaller subproblems
Self-Check
-
What is the difference between a function parameter passed by value versus by reference, and how does this affect algorithm efficiency? (Value = copy; Reference = pointer; reference is faster for large objects)
-
Explain the base case and recursive case for the factorial function. Why are both necessary? (Base case stops recursion; recursive case breaks problem into smaller pieces. Without base case, infinite recursion. Without recursive case, can't solve larger problem)
-
When would you use a while loop instead of a for loop in an algorithm? (When iteration count is unknown, when condition is complex, when you need manual counter control)
-
What's the time complexity of the fibonacci recursive function without memoization? (O(2^n), exponential—each call spawns two more calls)
-
How does memoization improve algorithm performance? (Caches results, so duplicate computations are O(1) lookups instead of recomputing)
One Takeaway
Master these foundational concepts—variables, control flow, functions, recursion, and iteration—and you'll have the building blocks for any algorithm. Clean code with clear variable names, proper scoping, and well-structured functions makes algorithms easier to understand, test, and optimize. Practice writing simple, clean implementations before optimizing for performance.
References
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.
- McDowell, G. L. (2015). Cracking the Coding Interview (6th ed.). CareerCup.
- Sedgewick, R., & Wayne, K. (2011). Algorithms (4th ed.). Addison-Wesley.
- Bhargava, A. Y. (2016). Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People. Manning.
- Knuth, D. E. (1997). The Art of Computer Programming, Volume 1: Fundamental Algorithms (3rd ed.). Addison-Wesley.