Question 1 · Function Returns and Operators · hard
Consider the function: 'def calculate(a, b): return a + b * 2'. If you call 'result = calculate(5, 3)', what value does result hold, and why does the multiplication occur before addition in the return statement?
The result is 11 because b * 2 (3*2=6) executes before addition due to operator precedence, then 5 + 6 = 11, demonstrating that order of operations applies inside function return statements
The result is 16 because (5 + 3) * 2 = 16, interpreting the expression as a left-to-right evaluation without considering standard mathematical operator precedence rules
The result is 5 because the function only processes parameter b, calculating b + 2 = 3 + 2 = 5, then ignoring parameter a entirely in the final result
The result is 13 because a student misreads the expression as b + a * 2, computing 3 + 5 * 2 = 3 + 10 = 13, mistakenly doubling parameter a instead of parameter b
Answer: A. The result is 11 because b * 2 (3*2=6) executes before addition due to operator precedence, then 5 + 6 = 11, demonstrating that order of operations applies inside function return statements
ExplanationFirst, the result is 11 because operator precedence rules apply everywhere in Python, including function returns. The expression a + b * 2 is evaluated as a + (b * 2) = 5 + (3 * 2) = 5 + 6 = 11. Then, since multiplication (*) has higher precedence than addition (+), the multiplication executes first. This is why understanding operator precedence is critical even within function definitions—it prevents silent bugs where developers might expect (a + b) * 2 = 16 instead of the correct 11.
Question 2 · If-Elif-Else Statements · hard
Consider this scenario: 'x = 10; if x > 5: print('greater'); elif x == 5: print('equal'); else: print('lesser')'. What output is produced, and explain how the conditional structure determines which code block executes?
The output is 'equal' because Python evaluates conditions in reverse order, checking elif before if, which causes the second condition to take precedence over the first condition
The output is 'greater equal' because both the if and elif conditions are evaluated independently, executing both print statements in sequence when either condition is true
The output is 'greater' because x=10 satisfies the first condition (10 > 5), so the first print statement executes and remaining conditions are skipped, demonstrating Python's if-elif-else flow control mechanism
The output is 'lesser' because the x value of 10 doesn't match the elif condition exactly, so Python skips to the else clause and prints the default result
Answer: C. The output is 'greater' because x=10 satisfies the first condition (10 > 5), so the first print statement executes and remaining conditions are skipped, demonstrating Python's if-elif-else flow control mechanism
ExplanationFirst, the output is 'greater' because when x=10, the condition x > 5 is true, so the first if block executes printing 'greater'. Once a condition is true and its block executes, Python skips all remaining elif and else blocks. Then, this is why understanding if-elif-else flow is critical: conditions are checked in order, and only the first matching block runs. This prevents unexpected behavior where multiple blocks might execute, and it's essential for writing correct decision-making logic in programs.
Question 3 · Dictionary Access · hard
Evaluate this Python code: 'my_dict = {'name': 'Alice', 'age': 15, 'city': 'Delhi'}; value = my_dict['age']'. What value is retrieved, and analyze how dictionaries provide key-value pair storage that differs fundamentally from lists?
The value 15 is retrieved because dictionaries use keys (like 'age') to access values instead of numeric indices, providing O(1) lookup time by key name and making data access more intuitive and readable than positional indexing
The value 'age' is retrieved because dictionaries return the key itself when accessed, treating keys as the primary data rather than using them for value lookup like traditional structures
The value 'Alice' is retrieved because dictionaries process keys in insertion order and 'name' is the first key, so accessing any key returns the first value stored in the dictionary
The value None is retrieved because 'age' is not an integer key like list indices, making dictionary access with string keys return undefined values in Python
Answer: A. The value 15 is retrieved because dictionaries use keys (like 'age') to access values instead of numeric indices, providing O(1) lookup time by key name and making data access more intuitive and readable than positional indexing
ExplanationFirst, the value 15 is retrieved because dictionaries use keys to map to values: my_dict['age'] accesses the value associated with the 'age' key, which is 15. Unlike lists that use numeric indices (0, 1, 2..), dictionaries use meaningful keys (strings, numbers, etc.) for access. Then, this provides O(1) average-case lookup time by key and makes code self-documenting—reading my_dict['age'] is clearer than my_list[1]. Dictionaries are superior for storing related data where the key has semantic meaning, making them essential for real-world data structures.
Question 4 · String Methods · hard
Analyze this code:
```python
text = 'HELLO'
lower_text = text.lower()
result = len(lower_text)
```
What is the value of result, and evaluate how string methods like lower() transform data while maintaining character count?
The result is 5 but with a different meaning because lower() changes each character's ASCII value, requiring len() to recount based on new numerical representations of the characters
The result is 10 because lower() doubles the string internally, creating 'hellohello' to represent the transformation, then len() counts 10 total characters from this doubled result
The result is 0 because lower() removes uppercase characters entirely, leaving an empty string in lower_text, making len() return 0 as there are no remaining characters
The result is 5 because lower() converts 'HELLO' to 'hello' (5 characters), and len() counts the characters, demonstrating that string methods transform content but preserve length unless characters are added or removed
Answer: D. The result is 5 because lower() converts 'HELLO' to 'hello' (5 characters), and len() counts the characters, demonstrating that string methods transform content but preserve length unless characters are added or removed
ExplanationFirst, the result is 5 because lower() transforms 'HELLO' to 'hello' without changing character count (still 5 characters), then len() counts these 5 characters. String methods like lower() modify content (uppercase to lowercase) but preserve string length unless explicitly designed to add/remove characters. Then, this demonstrates a critical principle: method chains don't alter structure unless specifically designed to do so. Understanding this prevents bugs where developers might expect length changes from case conversions.
Question 5 · Function Reusability · hard
Given: 'def calculate_average(numbers): total = sum(numbers); count = len(numbers); return total / count'. If you call 'avg = calculate_average([10, 20, 30])', what is the result, and evaluate why this function design supports code reusability?
The result is 20 because the function adds three numbers (10+20+30=60), then divides by 3 to get 20, but treating the result as an integer instead of a float with decimal representation
The result is 60 because the function only executes the sum() operation and ignores the division step, returning the total of all elements without performing the averaging calculation
The result is 20.0 because sum([10, 20, 30]) = 60 and len() = 3, so 60 / 3 = 20.0, demonstrating reusability since this function works for any list without modification, calculating accurate averages for different datasets
The result is 30 because the function processes the maximum value in the list instead of the average, since sum() selects the largest number before division
Answer: C. The result is 20.0 because sum([10, 20, 30]) = 60 and len() = 3, so 60 / 3 = 20.0, demonstrating reusability since this function works for any list without modification, calculating accurate averages for different datasets
ExplanationFirst, the result is 20.0 because sum([10, 20, 30]) = 60, len() = 3, and 60 / 3 = 20.0. This function is reusable because it accepts any list as input without modification—it works for [10, 20, 30], [5, 15], or any numeric sequence. Then, the function design encapsulates the averaging logic, allowing it to be called repeatedly with different data. This is why abstraction through functions is fundamental: it enables code reuse, reduces bugs (logic is tested once), and makes programs more maintainable.
Question 6 · Recursive Functions · hard
Given: 'def factorial(n): if n <= 1: return 1; else: return n * factorial(n-1)'. What is the result of 'factorial(5)', and predict how recursion unwinds through multiple function calls to compute the final answer?
The result is 5 because recursion only goes one level deep in Python, so factorial(n-1) is ignored and the function simply returns n directly without further recursion
The result is undefined because infinite recursion occurs when the function calls itself indefinitely without ever reaching the base case, causing a stack overflow
The result is 120 because factorial(5) calls factorial(4), which calls factorial(3), which calls factorial(2), which calls factorial(1) returning 1, then unwinds as 2*1=2, 3*2=6, 4*6=24, 5*24=120, demonstrating recursive decomposition and composition
The result is 10 because factorial sums the numbers (5+4+3+2+1=15) then divides by 1.5, misinterpreting the recursive multiplication as a different operation
Answer: C. The result is 120 because factorial(5) calls factorial(4), which calls factorial(3), which calls factorial(2), which calls factorial(1) returning 1, then unwinds as 2*1=2, 3*2=6, 4*6=24, 5*24=120, demonstrating recursive decomposition and composition
ExplanationThe result is 120 because of recursive decomposition: factorial(5) = 5 * factorial(4), which calls factorial(4) = 4 * factorial(3), continuing until factorial(1) = 1 (base case). Then the call stack unwinds: factorial(2) = 2 * 1 = 2, factorial(3) = 3 * 2 = 6, factorial(4) = 4 * 6 = 24, factorial(5) = 5 * 24 = 120.
Question 7 · Sorting Methods · hard
Analyze: 'numbers = [5, 2, 8, 1]; numbers.sort(); result = numbers'. What is the final result list, and evaluate how in-place sorting with sort() differs from creating a new sorted list with sorted()?
The result is [5, 2, 8, 1] because sort() doesn't modify lists; it only marks them conceptually as sorted without changing the actual element order
The result is [1, 2, 5, 8] but sort() returns this list while leaving numbers unchanged because sort() creates a new sorted list rather than modifying in-place
The result is [1, 2, 5, 8] because sort() modifies the original list in-place, rearranging elements without creating a new list object, making it memory-efficient for large datasets but destructive to the original order
The result cannot be determined because sort() requires explicit arguments, and calling it without parameters causes an error
Answer: C. The result is [1, 2, 5, 8] because sort() modifies the original list in-place, rearranging elements without creating a new list object, making it memory-efficient for large datasets but destructive to the original order
ExplanationFirst, the result is [1, 2, 5, 8] because sort() modifies numbers in-place, rearranging from [5, 2, 8, 1] to [1, 2, 5, 8]. The distinction from sorted() is critical: sort() returns None but modifies the original list, while sorted() returns a new sorted list without modifying the original. Then, sort() is memory-efficient for large lists (no copy overhead) but destructive—you lose the original order. sorted() is safer when you need both versions. This demonstrates trade-offs: memory efficiency versus data preservation. Finally, understanding which to use prevents data loss bugs.
Question 8 · Exception Types · hard
Consider: 'try: number = int('abc'); except ValueError: number = 0'. What is the final value of number, and evaluate how exception handling distinguishes between different error types for targeted recovery?
The final number is 'abc' because int('abc') silently fails and returns the original string instead of raising an exception
The final number is undefined because ValueError is not a valid exception type in Python, causing an error in the except clause itself
The final number remains uninitialized because the except clause cannot assign values, only logging or displaying error messages
The final number is 0 because int('abc') raises ValueError (the string 'abc' cannot be converted to integer), the except clause catches this specific exception type, and executes number = 0, demonstrating selective exception handling
Answer: D. The final number is 0 because int('abc') raises ValueError (the string 'abc' cannot be converted to integer), the except clause catches this specific exception type, and executes number = 0, demonstrating selective exception handling
ExplanationFirst, the final number is 0 because int('abc') raises ValueError—strings containing non-numeric characters cannot convert to integers. The except ValueError clause catches this specific exception type and executes number = 0. Then, selective exception handling is critical because different errors require different recovery strategies: ValueError suggests invalid input (set default), IOError suggests file access issues (retry or fail gracefully), ConnectionError suggests network issues (reconnect). Catching all exceptions with bare except is dangerous—it hides unexpected errors. Finally, specific exception handling demonstrates defensive programming: anticipating known failure modes and recovering appropriately.
Question 9 · String Algorithms · hard
Given: 'def is_palindrome(s): cleaned = s.lower().replace(' ', ''); return cleaned == cleaned[::-1]'. Analyze what this function does and evaluate how it demonstrates string manipulation and logical comparison for algorithm implementation?
This function only checks whether the string contains spaces, ignoring case and the actual palindrome logic entirely
The function returns cleaned[::-1] directly instead of comparing it to cleaned, so calling is_palindrome('hello') would return 'olleh' rather than a boolean True or False
The function checks if a string is a palindrome by converting to lowercase (s.lower()), removing spaces (replace(' ', '')), and comparing with the reversed version (cleaned[::-1]), returning True only if both are identical, demonstrating string method chaining and algorithmic thinking
The function requires the palindrome to be exactly the same as its reverse with capital letters, so case conversion makes it always return False
Answer: C. The function checks if a string is a palindrome by converting to lowercase (s.lower()), removing spaces (replace(' ', '')), and comparing with the reversed version (cleaned[::-1]), returning True only if both are identical, demonstrating string method chaining and algorithmic thinking
ExplanationThe function returns True for palindromes and False otherwise. For example, is_palindrome('race car') first lowercases and strips the space to get 'racecar', then compares 'racecar' == 'racecar'[::-1], which is 'racecar' == 'racecar', so the function returns True. For a non-palindrome like 'hello', cleaned is 'hello' and cleaned[::-1] is 'olleh', so 'hello' == 'olleh' is False. The function demonstrates method chaining: s.lower().replace(' ', '') applies two string methods in sequence to normalize the input before comparison. The comparison cleaned == cleaned[::-1] uses slice reversal ([::-1]) to reverse the string without writing an explicit loop, then checks equality directly against the original cleaned string. This combination of normalization (lowercasing, removing spaces) followed by reversal comparison is what makes the function correctly identify inputs such as 'racecar' or 'a man a plan a canal panama' as palindromes while rejecting strings that are not.
Question 10 · Nested Data Access · hard
Analyze: 'users = [{'name': 'Alice', 'age': 15}, {'name': 'Bob', 'age': 14}]; alice_age = users[0]['age']'. What is alice_age, and evaluate how nested indexing allows access to deeply nested data structures?
The alice_age is 15 because users[0] accesses the first dictionary {'name': 'Alice', 'age': 15}, then ['age'] accesses the value associated with 'age' key, demonstrating how chaining indexing operations navigates nested structures
The alice_age is 'Alice' because users[0] returns the first element, and ['age'] extracts the name field instead of the age
The alice_age is [15] as a list because accessing nested structures returns lists rather than single values
The alice_age is undefined because dictionary keys must be accessed with dot notation (users[0].age) rather than bracket notation in nested structures
Answer: A. The alice_age is 15 because users[0] accesses the first dictionary {'name': 'Alice', 'age': 15}, then ['age'] accesses the value associated with 'age' key, demonstrating how chaining indexing operations navigates nested structures
ExplanationFirst, the alice_age is 15 because nested indexing allows traversal: users[0] accesses the first dictionary, then ['age'] accesses the 'age' key within that dictionary. This demonstrates how lists of dictionaries create complex structures—users is a list of dictionaries, each representing a person with multiple attributes. Then, nested indexing is essential for real-world data: databases return lists of records (dictionaries), JSON APIs return nested structures, etc. Understanding nested access prevents IndexError and KeyError bugs. Finally, complex data manipulation (filtering, sorting, transforming) depends on comfortable navigation of nested structures.
Question 11 · Lexical Scoping · hard
Given: 'def outer(): x = 5; def inner(): print(x); inner(); outer()'. What is printed when this code executes, and evaluate how lexical scoping allows inner functions to access outer function variables?
Nothing is printed because inner() cannot access x from outer() scope due to scope isolation in nested functions.
An error occurs because x is not defined in inner's local scope, and accessing enclosing variables requires explicit parameter passing
The number 5 is printed because inner() accesses the variable x from the enclosing outer() scope (lexical scoping), demonstrating how Python resolves variable references by searching local scope, then enclosing scopes, then global scope
The string 'x' is printed because print(x) without quotes prints the variable name rather than its value.
Answer: C. The number 5 is printed because inner() accesses the variable x from the enclosing outer() scope (lexical scoping), demonstrating how Python resolves variable references by searching local scope, then enclosing scopes, then global scope
ExplanationThe number 5 is printed because of lexical scoping: when inner() references x, Python searches (1) local scope (not found), then (2) the enclosing scope (found x=5 in outer()), and uses that value. This demonstrates Python's scope resolution order (LEGB: Local, Enclosing, Global, Built-in), which is how lexical scoping lets inner functions read variables defined in their enclosing outer function without those variables being passed in as parameters.
Question 12 · Quicksort Partitioning · hard
Consider: 'numbers = [64, 34, 25, 12, 22, 11, 90]; pivot = numbers[0]; smaller = [x for x in numbers if x < pivot]; larger = [x for x in numbers if x >= pivot]'. What are the final smaller and larger lists, and explain how this demonstrates the partitioning step of quicksort?
Smaller becomes [11, 12, 22, 25, 34] (already sorted) and larger becomes [64, 90], because comprehensions automatically sort values while partitioning
The smaller list contains all elements and larger is empty because pivot selection causes all elements to compare as smaller than 64
Here, smaller is [34, 25, 12, 22, 11] and larger is [64, 90], because numbers[0]=64 is the pivot, smaller collects elements less than 64, and larger collects elements greater than or equal to 64 -- this is exactly quicksort's partitioning step, dividing elements around a pivot before recursively sorting each side
Both lists end up identical: smaller is [34, 25, 12, 22, 11] and larger is [64, 34, 25, 12, 22, 11, 90], since larger incorrectly includes every element rather than just those meeting the condition
Answer: C. Here, smaller is [34, 25, 12, 22, 11] and larger is [64, 90], because numbers[0]=64 is the pivot, smaller collects elements less than 64, and larger collects elements greater than or equal to 64 -- this is exactly quicksort's partitioning step, dividing elements around a pivot before recursively sorting each side
Explanationnumbers[0] = 64 is the pivot. The comprehension smaller = [x for x in numbers if x < pivot] scans [64, 34, 25, 12, 22, 11, 90] and keeps every value less than 64, giving [34, 25, 12, 22, 11]. The comprehension larger = [x for x in numbers if x >= pivot] keeps every value greater than or equal to 64, giving [64, 90]. This split -- every element sorted into "less than pivot" or "at least pivot" -- is exactly quicksort's partitioning step: each sublist would then be recursively partitioned around its own new pivot until the whole list is sorted.
Question 13 · Lambda Functions · hard
Analyze: 'data = [1, 2, 3, 4, 5]; result = list(map(lambda x: x * 2, data))'. What is the result list, and evaluate how map() with lambda functions enables functional programming patterns for data transformation?
The result is undefined because lambda functions require explicit names and cannot be used with map() directly in Python
The result is [1, 2, 3, 4, 5] because map() doesn't modify elements; it only iterates through them without applying transformations
The result is [2, 4, 6, 8, 10] as a map object (not a list) because map() returns an iterator that must be consumed, not automatically materialized
The result is [2, 4, 6, 8, 10] because map() applies the lambda function (x: x * 2) to each element, and list() converts the map object to a list, demonstrating functional transformation without explicit loop code
Answer: D. The result is [2, 4, 6, 8, 10] because map() applies the lambda function (x: x * 2) to each element, and list() converts the map object to a list, demonstrating functional transformation without explicit loop code
ExplanationFirst, the result is [2, 4, 6, 8, 10] because map(lambda x: x * 2, data) applies the lambda function to each element, creating a map object, and list() converts it to a list. Lambdas are anonymous functions: lambda x: x * 2 is equivalent to def f(x): return x * 2. Then, map() is a functional programming construct enabling data transformation without explicit loops. This is equivalent to the list comprehension [x * 2 for x in data] but demonstrates functional style. Understanding functional patterns (map, filter, reduce) is important for: processing streams, parallel operations, and expressing intent clearly. Finally, modern Python favors comprehensions over map, but both are valuable approaches.
Question 14 · Conditional Cascades · hard
Analyze: 'grades = [85, 90, 78, 92, 88]; average = sum(grades) / len(grades); if average >= 90: result = 'Excellent'; elif average >= 80: result = 'Good'; else: result = 'Needs Improvement''. What is the result value, and explain how this demonstrates multi-level conditional logic based on calculated values?
The result is 'Excellent' because the highest grade (92) is >=90, so the first condition is satisfied regardless of average
The result is 'Needs Improvement' because the lowest grade (78) fails to meet the 80 threshold, overriding the calculated average
The result is 'Good' because sum(grades)=433, len(grades)=5, average=433/5=86.6, which is >=80 but <90, matching the elif condition, demonstrating how calculated values drive conditional branching in grade evaluation systems
The result is undefined because the average is 86.6 (a decimal), which doesn't match any exact condition in the integer-based thresholds
Answer: C. The result is 'Good' because sum(grades)=433, len(grades)=5, average=433/5=86.6, which is >=80 but <90, matching the elif condition, demonstrating how calculated values drive conditional branching in grade evaluation systems
ExplanationThe result is 'Good' because average = (85+90+78+92+88)/5 = 433/5 = 86.6. This matches elif average >= 80 (86.6 >= 80 is true) and fails if average >= 90 (86.6 >= 90 is false), so 'Good' executes. This demonstrates decision-making based on computed values: calculate first, then use results for conditional logic. Real-world applications: grade assignment (this example), performance scoring, eligibility determination, risk assessment. Understanding the flow is critical: if-elif-else checks conditions in order, executing only the first match. If conditions overlap, order matters—this design is intentional: first check highest threshold, then lower ones, creating a cascade.
Question 15 · String Finding · hard
Consider: 'text = 'Python is great'; start = text.find('is'); result = text[start:start+2]'. What is the result string, and evaluate how find() enables substring location and slicing for string extraction?
The result is 'is great' because text.find('is') correctly returns 7, but mistaking the slice text[start:start+2] for text[start:] would return every character from index 7 to the end of the string instead of just two characters
The result is 'on' because find('is') returns 5 (an incorrect position), causing the slice to miss the target substring
The result is 'is' because text.find('is') returns the index 7 (where 'is' starts), and text[7:9] extracts characters at positions 7-8 ('is'), demonstrating find() + slicing for substring location and extraction without manual searching
The result is undefined because find() returns a boolean (True/False) not an index, preventing slice operations
Answer: C. The result is 'is' because text.find('is') returns the index 7 (where 'is' starts), and text[7:9] extracts characters at positions 7-8 ('is'), demonstrating find() + slicing for substring location and extraction without manual searching
ExplanationTracing the code: in 'Python is great', 'Python ' occupies indices 0-6, so text.find('is') scans the string and returns 7, the index where the substring 'is' begins. The slice text[7:9] (since start+2 = 9) then extracts the characters at positions 7 and 8, giving 'is' — slice end bounds are exclusive, so start+2 grabs exactly two characters from the starting index. This shows how find() locates a substring's starting position so a slice can extract it directly, instead of scanning the string character by character by hand.
Question 16 · Multiple Exception Handling · hard
Analyze: 'try: x = int(input('Enter a number: ')); y = 10 / x; except ZeroDivisionError: print('Cannot divide by zero'); except ValueError: print('Invalid number')'. What error handling occurs if a user enters '0' or 'abc', and evaluate how multiple except clauses enable differentiated error recovery?
Entering '0' triggers ZeroDivisionError and prints 'Cannot divide by zero', while entering 'abc' triggers ValueError and prints 'Invalid number', demonstrating how different exceptions represent distinct error types and multiple except clauses route to appropriate handling
Both inputs trigger the same exception, so only the first except clause executes regardless of the actual error type
Entering 'abc' succeeds because int() automatically converts any alphabetic string to a default value of 0 instead of raising an exception
Multiple except clauses are not allowed in a single try block; Python requires one generic except statement to handle every possible type of error
Answer: A. Entering '0' triggers ZeroDivisionError and prints 'Cannot divide by zero', while entering 'abc' triggers ValueError and prints 'Invalid number', demonstrating how different exceptions represent distinct error types and multiple except clauses route to appropriate handling
ExplanationEntering '0' triggers ZeroDivisionError and prints 'Cannot divide by zero' because int('0') successfully converts to 0, then 10 / 0 raises the exception during division. Entering 'abc' triggers ValueError and prints 'Invalid number' because int('abc') fails to convert the non-numeric string, so the error occurs before division is even attempted. The two except clauses let the program distinguish these failure types: the ValueError branch fires only when conversion fails, while the ZeroDivisionError branch fires only when conversion succeeds but the resulting value is zero — so each error type gets its own targeted response instead of one generic message.
Question 17 · Recursive vs Iterative Complexity · hard
Given that you have implemented a recursive function 'def fib(n): return n if n <= 1 else fib(n-1) + fib(n-2)' and you call fib(35), what happens when you analyze the time complexity and compare it with an iterative approach that uses a simple for loop with two variables tracking previous values?
The recursive version is actually faster at O(n log n) because the recursive tree naturally divides the problem into halves, similar to merge sort's divide-and-conquer approach
Both versions have identical O(n) time complexity because they both compute the same sequence of Fibonacci numbers, with the recursive version being slightly slower only due to function call overhead
The recursive version has O(2^n) time complexity making fib(35) require approximately 2^35 = 34 billion operations, while the iterative version completes in O(n) = 35 operations because it avoids redundant recalculations
The iterative version has O(n^2) complexity because the for loop must track two variables and swap them at each step, making it quadratically slower than the recursive O(n) approach
Answer: C. The recursive version has O(2^n) time complexity making fib(35) require approximately 2^35 = 34 billion operations, while the iterative version completes in O(n) = 35 operations because it avoids redundant recalculations
ExplanationThe naive recursive Fibonacci has O(2^n) time complexity because each call branches into two subcalls, creating an exponential tree. For fib(35), this means approximately 2^35 ≈ 34 billion operations. The iterative approach runs in O(n) = 35 steps because it computes each value exactly once using two tracking variables. This is why memoization or iteration is essential — the exponential blowup makes naive recursion impractical for n > 30.
Question 18 · Merge Sort Analysis · hard
Evaluate this merge sort implementation. If you trace through sorting 'arr = [38, 27, 43, 3]' from start to finish, which statement accurately describes what happens at each division level and the final merge?
First divide into [38, 27] and [43, 3], then [38], [27], [43], [3]. Merge pairs: [27, 38] and [3, 43]. Merge results: [3, 27, 38, 43]. Time complexity is O(n log n) because array halves at each level creating log n levels
The algorithm divides by picking every other element, creating [38, 43] and [27, 3], making it O(n^2) because the divisions are uneven
After dividing to single elements, each element is already sorted, so merging just concatenates them without comparing, resulting in O(n) time
The algorithm requires temporary arrays at each merge step, so worst case memory usage is O(n^2) making it impractical for large datasets
Answer: A. First divide into [38, 27] and [43, 3], then [38], [27], [43], [3]. Merge pairs: [27, 38] and [3, 43]. Merge results: [3, 27, 38, 43]. Time complexity is O(n log n) because array halves at each level creating log n levels
ExplanationFirst, merge sort divides the array in half recursively until single elements remain. For [38, 27, 43, 3]: divide into [38, 27] and [43, 3], then further divide to [38], [27], [43], [3]. Then, during merge, pairs are compared and sorted: [27, 38] and [3, 43]. Final merge compares from both sorted halves to produce [3, 27, 38, 43]. The algorithm creates log(n) division levels (log(4) = 2) and performs O(n) work at each level, resulting in O(n log n) time. Option B incorrectly describes division. Option C incorrectly claims merging needs no comparisons. Option D is incorrect about space; merge sort uses O(n) extra space, not O(n^2).
Question 19 · References vs Copies · hard
Consider implementing a simple debugging scenario. If you write 'x = [1, 2, 3]; y = x; y.append(4); print(x)' what output do you get and why does modifying y also modify x, and how would you create a true independent copy?
[1, 2, 3, 4] and this is correct behavior; you cannot create independent copies in Python, all lists share modifications
[1, 2, 3] because y = x creates a separate list with copied values, so appending to y doesn't affect x
[1, 2, 3, 4] but to prevent this you would need to use y = x.deepcopy() which is the only way to truly separate lists
[1, 2, 3, 4] because y = x creates a reference to the same list object, not a copy. To create independent copies use y = x.copy() or y = x[:] or y = list(x) which create new list objects
Answer: D. [1, 2, 3, 4] because y = x creates a reference to the same list object, not a copy. To create independent copies use y = x.copy() or y = x[:] or y = list(x) which create new list objects
ExplanationFirst, the output is [1, 2, 3, 4] because y = x makes y a reference to the same list object in memory, not a copy. When you append 4 to y, you're modifying the original list because x and y point to the same object. Then, to create independent copies, use x.copy(), x[:], or list(x), which create new list objects with copied elements. This distinction between reference assignment and object copying is critical for avoiding subtle bugs. Option B is false; assignment creates references, not copies. Option C uses non-existent deepcopy method at the list level (deepcopy exists in copy module for nested structures). Option D correctly identifies this behavior. Finally, understanding references versus copies is fundamental to debugging Python programs.
Question 20 · Memoization · hard
Consider this recursive Python function for computing Fibonacci numbers:
```python
def fibonacci(n):
return n if n <= 1 else fibonacci(n-1) + fibonacci(n-2)
```
Without optimization, computing fibonacci(6) calls fibonacci(2) many times, because the same subproblem shows up again and again in different branches of the recursion tree. A programmer fixes this by adding memoization: a dictionary cache is checked before computing fibonacci(k); if fibonacci(k) is already in the cache, the stored value is returned instead of being recomputed. Which statement correctly explains why this changes the algorithm's time complexity from O(2^n) to O(n)?
There are only n+1 distinct values to compute for fibonacci(n) — fibonacci(0) through fibonacci(n). Memoization computes fibonacci(2) exactly once and stores it; every other call to fibonacci(2) anywhere in the recursion tree becomes a cache lookup instead of a recomputation, so total work grows linearly with the n+1 unique subproblems rather than exponentially with the branching recursion tree.
Memoization works by converting the recursive calls into a simple loop, so fibonacci(2) is never actually computed — the cache treats it as a hardcoded base case and returns a fixed value immediately, the same way fibonacci(0) and fibonacci(1) are handled.
Because each branch of the recursion tree maintains its own independent copy of the cache, fibonacci(2) must still be computed once per branch that calls it; the speedup instead comes from each branch needing fewer additions once its own local cache is warmed up.
Memoization achieves O(n) time by discarding each cached value immediately after it is used, which keeps the algorithm's space complexity at O(1) while still avoiding repeated computation of fibonacci(2) within a single branch.
Answer: A. There are only n+1 distinct values to compute for fibonacci(n) — fibonacci(0) through fibonacci(n). Memoization computes fibonacci(2) exactly once and stores it; every other call to fibonacci(2) anywhere in the recursion tree becomes a cache lookup instead of a recomputation, so total work grows linearly with the n+1 unique subproblems rather than exponentially with the branching recursion tree.
ExplanationComputing fibonacci(n) only ever involves n+1 distinct subproblems: fibonacci(0), fibonacci(1), ..., fibonacci(n). The very first time fibonacci(2) is requested, it is computed and its result is stored in the cache; every later request for fibonacci(2) elsewhere in the recursion tree — and without memoization there can be exponentially many such requests, since the naive recursion tree branches into two calls at every level — is now answered with a single dictionary lookup instead of a fresh recomputation. Since each of the n+1 unique values is computed exactly once, total work scales linearly with n, giving O(n) time (in exchange for O(n) extra space to hold the cache).
The claim that memoization converts the recursion into a simple loop is wrong: that would be dynamic programming/iteration, a different technique, and memoization keeps the recursive calls intact. It's also wrong to treat fibonacci(2) as a hardcoded base case — only fibonacci(0) and fibonacci(1) are base cases. fibonacci(2) is genuinely computed once; it is cached, not hardcoded.
The claim that each branch keeps its own independent cache is wrong: the cache is shared across the whole computation, not duplicated per branch. If every branch kept its own private cache, the exponential redundancy across branches would never be eliminated, and there would be no speedup at all — the shared cache is precisely what removes the redundant recomputation.
The claim that cached values are discarded immediately after use is wrong: memoization must keep every computed value from fibonacci(0) through fibonacci(n) available for the rest of the computation, since any later call might still need it. That is why memoization costs O(n) space, not O(1); the O(n) time saving is bought with O(n) space, the classic time-space tradeoff, not a free optimization.