Selection statements can be nested, but excessive nesting reduces readability. Use guard clauses or Early returns where possible.
Executes a fixed number of times.
Executes until a condition becomes false.
Executes the body at least once, then checks the condition. Python does not have a native do-while; Emulate with:
```python def fib_iter(n): if n <= 1: return n a, b = 0, 1 for _ in range(2, n + 1): a, b = b, a + b return b ```def hanoi ( n , source , target , auxiliary ):
print ( f "Move disk 1 from { source } to { target } " )
hanoi(n - 1 , source, auxiliary, target)
print ( f "Move disk { n } from { source } to { target } " )
hanoi(n - 1 , auxiliary, target, source)
Complexity: T ( n ) = 2 T ( n − 1 ) + O ( 1 ) ⟹ T ( n ) = Θ ( 2 n ) T(n) = 2T(n-1) + O(1) \implies T(n) = \Theta(2^n) T ( n ) = 2 T ( n − 1 ) + O ( 1 ) ⟹ T ( n ) = Θ ( 2 n ) .
Proof. T ( n ) = 2 T ( n − 1 ) + 1 = 2 ( 2 T ( n − 2 ) + 1 ) + 1 = 4 T ( n − 2 ) + 3 = ⋯ = 2 n T ( 0 ) + ( 2 n − 1 ) = Θ ( 2 n ) T(n) = 2T(n-1) + 1 = 2(2T(n-2)+1)+1 = 4T(n-2)+3 = \cdots = 2^n T(0) + (2^n - 1) = \Theta(2^n) T ( n ) = 2 T ( n − 1 ) + 1 = 2 ( 2 T ( n − 2 ) + 1 ) + 1 = 4 T ( n − 2 ) + 3 = ⋯ = 2 n T ( 0 ) + ( 2 n − 1 ) = Θ ( 2 n ) . Minimum moves = 2 n − 1 2^n - 1 2 n − 1 . □ \square □
Feature Procedure Function Return value None Returns a value Purpose Perform an action (side effect) Compute a value Called as Statement Expression
def print_report ( data ): # Procedure
def calculate_average ( data ): # Function
return sum (data) / len (data)
By value: A copy of the argument is passed. Changes inside the function do not affect the original. (Default in Python for immutable types.)By reference: A reference to the original is passed. Changes affect the original. (Python passes object references; mutable objects like lists can be modified.) lst.append( 4 ) # Modifies the original list (by reference)
lst = [ 0 ] # Only rebinds the local variable (no effect outside)
print (my_list) # [1, 2, 3, 4]
Problem 1. Write a recursive function to compute the sum of digits of a positive integer. Prove Its correctness by induction.
Answer return n % 10 + sum_digits(n // 10 )
Correctness. By induction on the number of digits d d d .
Base case (d = 1 d = 1 d = 1 ): n < 10 n \lt 10 n < 10 Returns n n n . Sum of digits = n n n . ✓
Inductive step: Assume correct for all numbers with ≤ d \leq d ≤ d digits. For a ( d + 1 ) (d+1) ( d + 1 ) -digit number n n n : n m o d 10 n \bmod 10 n mod 10 gives the last digit, and n / / 10 n // 10 n //10 gives the remaining d d d digits. By the inductive Hypothesis, sum_digits(n // 10) correctly sums those d d d digits. Adding the last digit gives the Total sum. ✓
Termination: Variant function V ( n ) = n V(n) = n V ( n ) = n . Each call: V ( n / / 10 ) = ⌊ n / 10 ⌋ < n V(n // 10) = \lfloor n/10 \rfloor \lt n V ( n //10 ) = ⌊ n /10 ⌋ < n For n ≥ 10 n \geq 10 n ≥ 10 . ✓
Problem 2. Convert the following while loop to a for loop:
Answer for i in range ( 5 , 51 , 5 ):
Problem 3. Prove that the following function computes 2 n 2^n 2 n :
return 2 * power_of_two(n - 1 )
Answer By induction on n n n .
Base: n = 0 n = 0 n = 0 : returns 1 = 2 0 2^0 2 0 . ✓
Inductive step: Assume power_of_two(k) = 2^k for k ≤ n k \leq n k ≤ n . Then power_of_two(n+1) = 2 * power_of_two(n) = 2 * 2^n = 2^{n+1}. ✓
□ \square □
Problem 4. Explain the difference between iteration and recursion. When would you prefer one Over the other?
Answer Aspect Iteration Recursion Mechanism Loop constructs Function calls itself Memory O ( 1 ) O(1) O ( 1 ) extraO ( n ) O(n) O ( n ) stack framesOverhead Minimal Function call overhead per step Readability Better for simple loops Better for tree/divide-and-conquer Risk Infinite loop Stack overflow
Prefer iteration when: the problem has a natural loop structure, memory is constrained, or Performance is critical.
Prefer recursion when: the problem has a natural recursive structure (trees, divide-and-conquer), The depth is bounded (e.g., log n \log n log n ), or readability is paramount.
Problem 5. Write a function that uses recursion to check if a string is a palindrome. Prove Termination.
Answer return is_palindrome(s[ 1 : - 1 ])
Termination. Variant function: V ( s ) = l e n ( s ) V(s) = \mathrm{len}(s) V ( s ) = len ( s ) . Each recursive call: V ( s [ 1 : − 1 ] ) = l e n ( s ) − 2 < V ( s ) V(s[1:-1]) = \mathrm{len}(s) - 2 \lt V(s) V ( s [ 1 : − 1 ]) = len ( s ) − 2 < V ( s ) for l e n ( s ) ≥ 2 \mathrm{len}(s) \geq 2 len ( s ) ≥ 2 . Since V V V is a Non-negative integer that strictly decreases, the function must reach a base case. ✓
Problem 6. What is the output of the following code? Explain step by step.
Answer Output: 10
Explanation: Python passes the integer 10 by object reference. Inside modify``x = 20 rebinds The local parameter x to a new integer object 20. This does not affect the global xWhich Remains 10. Integers are immutable in Python, so there is no way to modify the original value Through the parameter.
Problem 7. Write a function gcd(a, b) using Euclid’s algorithm. Prove it terminates and Returns the GCD.
Answer Termination. Variant: V ( a , b ) = b V(a, b) = b V ( a , b ) = b . Each call: V ( a , b ) = b > a m o d b = V ( b , a m o d b ) V(a, b) = b \gt a \bmod b = V(b, a \bmod b) V ( a , b ) = b > a mod b = V ( b , a mod b ) (for b > 0 b \gt 0 b > 0 ). Since V V V is a non-negative integer that strictly decreases, the function reaches b = 0 b = 0 b = 0 . ✓
Correctness. We prove gcd ( a , b ) = gcd ( b , a m o d b ) \gcd(a, b) = \gcd(b, a \bmod b) g cd( a , b ) = g cd( b , a mod b ) .
Let d = gcd ( a , b ) d = \gcd(a, b) d = g cd( a , b ) . Then d ∣ a d | a d ∣ a and d ∣ b d | b d ∣ b So d ∣ ( a − q ⋅ b ) = a m o d b d | (a - q \cdot b) = a \bmod b d ∣ ( a − q ⋅ b ) = a mod b . Hence d ∣ gcd ( b , a m o d b ) d | \gcd(b, a \bmod b) d ∣ g cd( b , a mod b ) .
Conversely, let e = gcd ( b , a m o d b ) e = \gcd(b, a \bmod b) e = g cd( b , a mod b ) . Then e ∣ b e | b e ∣ b and e ∣ ( a m o d b ) e | (a \bmod b) e ∣ ( a mod b ) So e ∣ ( q ⋅ b + a m o d b ) = a e | (q \cdot b + a \bmod b) = a e ∣ ( q ⋅ b + a mod b ) = a . Hence e ∣ gcd ( a , b ) e | \gcd(a, b) e ∣ g cd( a , b ) .
Since d ∣ e d | e d ∣ e and e ∣ d e | d e ∣ d , d = e d = e d = e . ✓
Base case: gcd ( a , 0 ) = a \gcd(a, 0) = a g cd( a , 0 ) = a . ✓
Problem 8. A student writes the following recursive function. Identify the bug and fix it:
Answer Bugs:
No base case. Infinite recursion leading to stack overflow No guard against n < 0 n \lt 0 n < 0 Fixed version:
Or equivalently:
For revision on data structures that use recursion, see Trees .
Trace the execution of the following code and determine the output:
for j in range ( 1 , i + 1 ):
Trace:
Iteration ij rangeOutput Outer 1 1 range(1, 2) — j = 1* then newlineOuter 2 2 range(1, 3) — j = 1, 2** then newlineOuter 3 3 range(1, 4) — j = 1, 2, 3*** then newline
Output:
Write a function that repeatedly asks the user for an integer between 1 and 100 (inclusive) until Valid input is provided.
score = int ( input ( " Enter a score (1-100): " ))
print ( " Score must be between 1 and 100. " )
print ( " Invalid input. Please enter an integer. " )
This loop combines two validation checks: type validation (integer) and range validation (1-100). The loop only exits when both checks pass.
def multiplication_table ( n ):
for i in range ( 1 , n + 1 ):
for j in range ( 1 , n + 1 ):
print ( f " { i * j :4 } " , end = "" )
Output:
return n * factorial(n - 1 )
Call: factorial(4)
│ │ └── returns 2 * 1 = 2
Stack at deepest point (4 frames):
Frame nWaiting for 1 4 factorial(3)2 3 factorial(2)3 2 factorial(1)4 1 (base case, returns immediately)
return fib(n - 1 ) + fib(n - 2 )
Call: fib(5)
Note: fib(3) is computed twice, fib(2) is computed three times. This redundancy is why naive Recursive Fibonacci is O ( ϕ n ) O(\phi^n) O ( ϕ n ) — it recomputes the same subproblems repeatedly.
Off-by-one errors occur when a loop iterates one time too many or one time too few.
Error Code Fix Fencepost for i in range(1, n) — iterates 1 to n-1Use range(1, n + 1) if you need 1 to n Off-by-one in while while i < n vs while i <= nDecide whether the boundary is inclusive or exclusive Array indexing array[len(array)] — IndexErrorValid indices are 0 to len(array) - 1
An infinite loop occurs when the loop condition never becomes false.
## Bug: wrong increment direction
x = x + 1 # x increases, never reaches 0
# Bug: floating point comparison
x += 0.1 # May never exactly equal 1.0 due to rounding
Fix for floating point: Use a tolerance or integer counter:
Understanding variable scope is critical for correct programs.
# Pitfall: modifying a global variable
count = count + 1 # UnboundLocalError!
# This creates a local 'count' shadowing the global
count = count + 1 # Correct: references the global variable
# Pitfall: mutable default arguments
def add_item ( item , items = []):
print (add_item( 2 )) # [1, 2] — NOT [2]!
The default list [] is created once when the function is defined, not each time it is called. Fix:
def add_item ( item , items = None ):
Problem 1. Trace the following code and state the output:
Answer Trace each (i, j) pair:
ijCondition result changeresult0 0 i == j +1 1 0 1 else +3 4 0 2 else +3 7 1 0 i > j +2 9 1 1 i == j +1 10 1 2 else +3 13 2 0 i > j +2 15 2 1 i > j +2 17 2 2 i == j +1 18
Output: 18
Problem 2. Write a recursive function binary_search(arr, target, low, high) that returns the Index of target in a sorted array, or -1 if not found. Prove that it terminates.
Answer def binary_search ( arr , target , low , high ):
return binary_search(arr, target, low, mid - 1 )
return binary_search(arr, target, mid + 1 , high)
Termination proof. Variant function: V = h i g h − l o w + 1 V = high - low + 1 V = hi g h − l o w + 1 (the size of the search range).
Each recursive call either:
Returns (base case low > high), or Calls with mid - 1 as the new high: V ′ = ( m i d − 1 ) − l o w + 1 = m i d − l o w V' = (mid - 1) - low + 1 = mid - low V ′ = ( mi d − 1 ) − l o w + 1 = mi d − l o w . Since mid >= lowV ′ ≤ V − 1 V' \leq V - 1 V ′ ≤ V − 1 . Calls with mid + 1 as the new low: V ′ = h i g h − ( m i d + 1 ) + 1 = h i g h − m i d ‘ . S i n c e ‘ m i d < = h i g h ‘ V' = high - (mid + 1) + 1 = high - mid`. Since `mid \lt= high` V ′ = hi g h − ( mi d + 1 ) + 1 = hi g h − mi d ‘. S in ce ‘ mi d <= hi g h ‘ V’ \leq V - 1$. In both recursive cases, V V V strictly decreases. Since V V V is a non-negative integer, the function Must eventually reach the base case. ✓
Problem 3. The following function is intended to compute the sum of all even numbers from 1 to n. Find and fix the bug.
Answer Bug: range(1, n) excludes n. If n is even, it should be included in the sum.
Fix: Change to range(1, n + 1):
for i in range ( 1 , n + 1 ):
Alternative fix using a more efficient approach (only iterate over even numbers):
for i in range ( 2 , n + 1 , 2 ):
This halves the number of iterations.
Verification: For n = 6: sum = 2 + 4 + 6 = 12. Original code gives 2 + 4 = 6 (wrong). Fixed Code gives 2 + 4 + 6 = 12 (correct).
Problem 4. Write a function validate_password(password) that returns True if the password Meets all of the following rules, and False otherwise:
At least 8 characters long Contains at least one uppercase letter Contains at least one digit Contains at least one special character from !@#$%^&* Answer def validate_password ( password ):
specials = set ( " !@#$%^&* " )
return has_upper and has_digit and has_special
Alternative using Python built-ins:
def validate_password ( password ):
has_upper = any (c.isupper() for c in password)
has_digit = any (c.isdigit() for c in password)
has_special = any (c in " !@#$%^&* " for c in password)
return has_upper and has_digit and has_special
Both versions use early return for the length check and iterate through the password once, giving O ( n ) O(n) O ( n ) time complexity.
Problem 5. Explain the output of the following code. Why does the second call behave Unexpectedly?
def append_to ( element , target = []):
Answer Output:
Explanation: In Python, default arguments are evaluated once when the function is defined, Not each time the function is called. The list [] is created at definition time and shared across All calls that use the default.
First call: target is the default list []. After appending 1, it becomes [1].
Second call: target is the same list object [1]. After appending 2, it becomes [1, 2].
Fix: Use None as the default and create a new list inside the function:
def append_to ( element , target = None ):
Now each call that omits target gets a fresh empty list.
This topic explores fundamental concepts that shape our understanding of the world.
This topic covers the core concepts of programming constructs, including underlying theory, practical implementation, and key applications.
Key concepts include:
variables, data types, and control flow functions and procedures object-oriented programming error handling and debugging modular design Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.