# Fast input readingimport sysinput = sys.stdin.readline # Faster than input() for large inputs# Multiple test casest = int(input())for _ in range(t): # Process each test case n = int(input()) arr = list(map(int, input().split()))# Reading matrixn, m = map(int, input().split())matrix = []for _ in range(n): row = list(map(int, input().split())) matrix.append(row)
Python-Specific Syntax
# Multiple assignmenta, b = b, a # Swap without temp variablex, y, z = 1, 2, 3 # Multiple assignment# Conditional expressions (ternary operator)result = value_if_true if condition else value_if_falsemax_val = a if a > b else b# Walrus operator (Python 3.8+) - assignment in expressionsif (n := len(arr)) > 5: print(f"Array length {n} is greater than 5")# F-strings for formattingname, age = "Alice", 25print(f"Name: {name}, Age: {age}")
Built-in Data Structures
Lists - Dynamic Arrays
# List creation and basic operations - O(1) append, O(n) insert at beginningarr = [1, 2, 3, 4, 5]arr.append(6) # O(1) - add to endarr.pop() # O(1) - remove from endarr.insert(0, 0) # O(n) - insert at beginningarr.pop(0) # O(n) - remove from beginning# List comprehensions - Pythonic and efficientsquares = [x**2 for x in range(10)] # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]evens = [x for x in range(20) if x % 2 == 0] # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]# Nested list comprehension for 2D arraysmatrix = [[0 for _ in range(m)] for _ in range(n)] # n x m matrix# DON'T do: matrix = [[0] * m] * n # This creates references to same list!# List slicing - O(k) where k is slice lengtharr[1:4] # Elements at index 1, 2, 3arr[:3] # First 3 elementsarr[2:] # From index 2 to endarr[::-1] # Reverse the list - O(n)arr[::2] # Every 2nd element
Dictionaries - Hash Maps
# Dictionary operations - Average O(1) for get, set, deletefreq = {}freq['a'] = freq.get('a', 0) + 1 # Increment countfreq.setdefault('b', 0) # Set default if key doesn't exist# Dictionary comprehensionschar_count = {char: string.count(char) for char in set(string)}# Useful dictionary methodsdict1.update(dict2) # Merge dictionarieskeys = list(dict1.keys()) # Get all keysvalues = list(dict1.values()) # Get all valuesitems = list(dict1.items()) # Get key-value pairs# Defaultdict alternative (without collections)from collections import defaultdictdd = defaultdict(int) # Default value is 0dd = defaultdict(list) # Default value is empty listdd = defaultdict(set) # Default value is empty set
Sets - Hash Sets
# Set operations - Average O(1) for add, remove, containss1 = {1, 2, 3, 4}s2 = {3, 4, 5, 6}s1.add(5) # Add elements1.discard(1) # Remove if exists (no error if not found)s1.remove(2) # Remove (raises KeyError if not found)# Set operationsintersection = s1 & s2 # {3, 4, 5}union = s1 | s2 # {1, 2, 3, 4, 5, 6}difference = s1 - s2 # {1, 2}symmetric_diff = s1 ^ s2 # Elements in either but not both# Set comprehensionsunique_lengths = {len(word) for word in words}
Tuples - Immutable Sequences
# Tuples are immutable and hashable (can be dictionary keys)point = (3, 4)x, y = point # Unpacking# Named tuples for structured datafrom collections import namedtuplePoint = namedtuple('Point', ['x', 'y'])p = Point(3, 4)print(p.x, p.y) # Access by name
Collections Module
Deque - Double-ended Queue
from collections import deque# Efficient O(1) operations at both endsdq = deque([1, 2, 3])dq.appendleft(0) # Add to left - O(1)dq.append(4) # Add to right - O(1)dq.popleft() # Remove from left - O(1)dq.pop() # Remove from right - O(1)# Rotating elementsdq.rotate(1) # Rotate right by 1dq.rotate(-1) # Rotate left by 1# Use deque for sliding window problemsdef sliding_window_maximum(nums, k): dq = deque() # Store indices result = [] for i in range(len(nums)): # Remove elements outside window while dq and dq[0] <= i - k: dq.popleft() # Maintain decreasing order in deque while dq and nums[dq[-1]] <= nums[i]: dq.pop() dq.append(i) if i >= k - 1: result.append(nums[dq[0]]) return result
from collections import defaultdict# Automatically creates missing keysgraph = defaultdict(list) # Default value is empty listgraph[1].append(2) # No KeyError, creates empty list first# Group anagrams exampledef group_anagrams(strs): groups = defaultdict(list) for s in strs: key = ''.join(sorted(s)) # Use sorted string as key groups[key].append(s) return list(groups.values())
String Manipulation
Basic String Operations
s = "Hello World"# String methods - all return new strings (strings are immutable)s.lower() # "hello world"s.upper() # "HELLO WORLD"s.strip() # Remove whitespace from both endss.split() # Split by whitespace -> ['Hello', 'World']s.split(',') # Split by commas.replace('l', 'x') # "Hexxo Worxd"s.count('l') # Count occurrences -> 3# String formattingname, age = "Alice", 25formatted = f"Name: {name}, Age: {age}"formatted = "Name: {}, Age: {}".format(name, age)formatted = "Name: %s, Age: %d" % (name, age)# String checking methodss.isdigit() # True if all digitss.isalpha() # True if all letterss.isalnum() # True if all alphanumerics.startswith("He") # Trues.endswith("ld") # True
String Patterns for Interviews
# Palindrome check - O(n) time, O(1) spacedef is_palindrome(s): left, right = 0, len(s) - 1 while left < right: if s[left] != s[right]: return False left += 1 right -= 1 return True# Character frequency using dictionarydef char_frequency(s): freq = {} for char in s: freq[char] = freq.get(char, 0) + 1 return freq# String reversal (multiple ways)def reverse_string(s): return s[::-1] # Pythonic way return ''.join(reversed(s)) # Using built-in reversed# Anagram check using sorted stringsdef is_anagram(s1, s2): return sorted(s1) == sorted(s2)# Longest substring without repeating charactersdef length_of_longest_substring(s): char_set = set() left = max_length = 0 for right in range(len(s)): while s[right] in char_set: char_set.remove(s[left]) left += 1 char_set.add(s[right]) max_length = max(max_length, right - left + 1) return max_length
Regular Expressions
import re# Common regex patterns for interviewsemail_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'phone_pattern = r'^\d{3}-\d{3}-\d{4}$'# Regex functionsre.search(pattern, string) # Find first matchre.findall(pattern, string) # Find all matchesre.sub(pattern, replacement, string) # Replace matches# Example: Extract numbers from stringtext = "I have 10 apples and 5 oranges"numbers = re.findall(r'\d+', text) # ['10', '5']
List & Array Operations
Two Pointer Technique
# Classic two-pointer for sorted arraydef two_sum_sorted(nums, target): left, right = 0, len(nums) - 1 while left < right: current_sum = nums[left] + nums[right] if current_sum == target: return [left, right] elif current_sum < target: left += 1 else: right -= 1 return []# Remove duplicates from sorted array - O(n) time, O(1) spacedef remove_duplicates(nums): if not nums: return 0 write_index = 1 for read_index in range(1, len(nums)): if nums[read_index] != nums[read_index - 1]: nums[write_index] = nums[read_index] write_index += 1 return write_index
Sliding Window Patterns
# Fixed window sizedef max_sum_subarray(nums, k): if len(nums) < k: return 0 window_sum = sum(nums[:k]) max_sum = window_sum for i in range(k, len(nums)): window_sum += nums[i] - nums[i - k] max_sum = max(max_sum, window_sum) return max_sum# Variable window sizedef min_window_substring(s, t): if not s or not t: return "" dict_t = {} for char in t: dict_t[char] = dict_t.get(char, 0) + 1 required = len(dict_t) formed = 0 window_counts = {} l, r = 0, 0 ans = float("inf"), None, None while r < len(s): char = s[r] window_counts[char] = window_counts.get(char, 0) + 1 if char in dict_t and window_counts[char] == dict_t[char]: formed += 1 while l <= r and formed == required: if r - l + 1 < ans[0]: ans = (r - l + 1, l, r) char = s[l] window_counts[char] -= 1 if char in dict_t and window_counts[char] < dict_t[char]: formed -= 1 l += 1 r += 1 return "" if ans[0] == float("inf") else s[ans[1]:ans[2] + 1]
Array Manipulation
# Rotate array right by k steps - O(n) time, O(1) spacedef rotate_array(nums, k): k = k % len(nums) reverse(nums, 0, len(nums) - 1) reverse(nums, 0, k - 1) reverse(nums, k, len(nums) - 1)def reverse(nums, start, end): while start < end: nums[start], nums[end] = nums[end], nums[start] start += 1 end -= 1# Maximum subarray sum - Kadane's algorithmdef max_subarray(nums): max_sum = current_sum = nums[0] for num in nums[1:]: current_sum = max(num, current_sum + num) max_sum = max(max_sum, current_sum) return max_sum# Product of array except self - O(n) time, O(1) extra spacedef product_except_self(nums): result = [1] * len(nums) # Left pass for i in range(1, len(nums)): result[i] = result[i - 1] * nums[i - 1] # Right pass right = 1 for i in range(len(nums) - 1, -1, -1): result[i] *= right right *= nums[i] return result
# Map, filter, reducenumbers = [1, 2, 3, 4, 5]# Map - apply function to all elementssquares = list(map(lambda x: x**2, numbers)) # [1, 4, 9, 16, 25]squares = [x**2 for x in numbers] # List comprehension equivalent# Filter - select elements meeting conditionevens = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]evens = [x for x in numbers if x % 2 == 0] # List comprehension equivalent# Reduce - cumulative operationfrom functools import reduceproduct = reduce(lambda x, y: x * y, numbers) # 120# Zip - parallel iterationnames = ['Alice', 'Bob', 'Charlie']ages = [25, 30, 35]for name, age in zip(names, ages): print(f"{name}: {age}")# Enumerate - get index and valuefor i, value in enumerate(['a', 'b', 'c']): print(f"Index {i}: {value}")# Sorted with custom keywords = ['python', 'java', 'c++', 'go']sorted_by_length = sorted(words, key=len) # ['go', 'c++', 'java', 'python']sorted_reverse = sorted(words, reverse=True) # Reverse alphabetical
Memoization with Functools
from functools import lru_cache# LRU Cache for dynamic programming - O(1) lookup after computation@lru_cache(maxsize=None) # Unlimited cache sizedef fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2)# Cache infoprint(fibonacci.cache_info()) # Shows hits, misses, current size# Clear cachefibonacci.cache_clear()# Manual memoizationdef memoize(func): cache = {} def wrapper(*args): if args in cache: return cache[args] result = func(*args) cache[args] = result return result return wrapper---## Trees & Graphs### Binary Tree Implementation```pythonclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right# Tree traversals - all O(n) time, O(h) space where h is heightdef inorder_traversal(root): """Left -> Root -> Right""" result = [] def inorder(node): if node: inorder(node.left) result.append(node.val) inorder(node.right) inorder(root) return resultdef preorder_traversal(root): """Root -> Left -> Right""" result = [] def preorder(node): if node: result.append(node.val) preorder(node.left) preorder(node.right) preorder(root) return resultdef postorder_traversal(root): """Left -> Right -> Root""" result = [] def postorder(node): if node: postorder(node.left) postorder(node.right) result.append(node.val) postorder(root) return result# Level-order traversal (BFS) - O(n) time, O(w) space where w is max widthdef level_order_traversal(root): if not root: return [] result = [] queue = deque([root]) while queue: level_size = len(queue) level = [] for _ in range(level_size): node = queue.popleft() level.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) result.append(level) return result
Binary Search Tree Operations
# BST insertion - O(h) time where h is heightdef insert_bst(root, val): if not root: return TreeNode(val) if val < root.val: root.left = insert_bst(root.left, val) else: root.right = insert_bst(root.right, val) return root# BST search - O(h) timedef search_bst(root, val): if not root or root.val == val: return root if val < root.val: return search_bst(root.left, val) else: return search_bst(root.right, val)# Validate BST - O(n) timedef is_valid_bst(root, min_val=float('-inf'), max_val=float('inf')): if not root: return True if root.val <= min_val or root.val >= max_val: return False return (is_valid_bst(root.left, min_val, root.val) and is_valid_bst(root.right, root.val, max_val))# Lowest Common Ancestor in BST - O(h) timedef lca_bst(root, p, q): if not root: return None if p.val < root.val and q.val < root.val: return lca_bst(root.left, p, q) elif p.val > root.val and q.val > root.val: return lca_bst(root.right, p, q) else: return root
Graph Representations & Traversals
# Graph representation using adjacency listfrom collections import defaultdict, dequeclass Graph: def __init__(self): self.graph = defaultdict(list) def add_edge(self, u, v): self.graph[u].append(v) def add_undirected_edge(self, u, v): self.graph[u].append(v) self.graph[v].append(u)# DFS - O(V + E) time, O(V) spacedef dfs(graph, start, visited=None): if visited is None: visited = set() visited.add(start) print(start, end=' ') for neighbor in graph[start]: if neighbor not in visited: dfs(graph, neighbor, visited)# DFS iterative using stackdef dfs_iterative(graph, start): visited = set() stack = [start] while stack: node = stack.pop() if node not in visited: visited.add(node) print(node, end=' ') # Add neighbors to stack (reverse order for consistent traversal) for neighbor in reversed(graph[node]): if neighbor not in visited: stack.append(neighbor)# BFS - O(V + E) time, O(V) spacedef bfs(graph, start): visited = set([start]) queue = deque([start]) while queue: node = queue.popleft() print(node, end=' ') for neighbor in graph[node]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor)# Detect cycle in undirected graph using DFSdef has_cycle_undirected(graph, n): visited = [False] * n def dfs_cycle(node, parent): visited[node] = True for neighbor in graph[node]: if not visited[neighbor]: if dfs_cycle(neighbor, node): return True elif parent != neighbor: return True return False for i in range(n): if not visited[i]: if dfs_cycle(i, -1): return True return False# Topological sort using DFS - O(V + E) timedef topological_sort(graph, n): visited = [False] * n stack = [] def dfs_topo(node): visited[node] = True for neighbor in graph[node]: if not visited[neighbor]: dfs_topo(neighbor) stack.append(node) for i in range(n): if not visited[i]: dfs_topo(i) return stack[::-1] # Reverse to get correct order
Shortest Path Algorithms
import heapq# Dijkstra's algorithm - O((V + E) log V) timedef dijkstra(graph, start): distances = {node: float('inf') for node in graph} distances[start] = 0 pq = [(0, start)] # (distance, node) while pq: current_dist, current = heapq.heappop(pq) if current_dist > distances[current]: continue for neighbor, weight in graph[current]: distance = current_dist + weight if distance < distances[neighbor]: distances[neighbor] = distance heapq.heappush(pq, (distance, neighbor)) return distances# BFS for unweighted graph shortest path - O(V + E) timedef shortest_path_bfs(graph, start, end): if start == end: return 0 visited = set([start]) queue = deque([(start, 0)]) # (node, distance) while queue: node, dist = queue.popleft() for neighbor in graph[node]: if neighbor == end: return dist + 1 if neighbor not in visited: visited.add(neighbor) queue.append((neighbor, dist + 1)) return -1 # Path not found
Dynamic Programming
Classic DP Patterns
1. Fibonacci & Linear DP
# Bottom-up approach - O(n) time, O(1) spacedef fibonacci(n): if n <= 1: return n prev, curr = 0, 1 for _ in range(2, n + 1): prev, curr = curr, prev + curr return curr# Climbing stairs - O(n) time, O(1) spacedef climb_stairs(n): if n <= 2: return n prev2, prev1 = 1, 2 for _ in range(3, n + 1): current = prev1 + prev2 prev2, prev1 = prev1, current return prev1
2. Longest Increasing Subsequence
# O(n²) DP solutiondef length_of_lis(nums): if not nums: return 0 dp = [1] * len(nums) for i in range(1, len(nums)): for j in range(i): if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp)# O(n log n) solution using binary searchdef length_of_lis_optimized(nums): import bisect tails = [] for num in nums: pos = bisect.bisect_left(tails, num) if pos == len(tails): tails.append(num) else: tails[pos] = num return len(tails)
3. Knapsack Problems
# 0/1 Knapsack - O(n * W) time and spacedef knapsack_01(weights, values, capacity): n = len(weights) dp = [[0] * (capacity + 1) for _ in range(n + 1)] for i in range(1, n + 1): for w in range(capacity + 1): if weights[i-1] <= w: dp[i][w] = max( dp[i-1][w], # Don't include item dp[i-1][w - weights[i-1]] + values[i-1] # Include item ) else: dp[i][w] = dp[i-1][w] return dp[n][capacity]# Space-optimized knapsack - O(W) spacedef knapsack_optimized(weights, values, capacity): dp = [0] * (capacity + 1) for i in range(len(weights)): # Traverse backwards to avoid using updated values for w in range(capacity, weights[i] - 1, -1): dp[w] = max(dp[w], dp[w - weights[i]] + values[i]) return dp[capacity]# Unbounded knapsack (unlimited items) - O(n * W) timedef knapsack_unbounded(weights, values, capacity): dp = [0] * (capacity + 1) for w in range(1, capacity + 1): for i in range(len(weights)): if weights[i] <= w: dp[w] = max(dp[w], dp[w - weights[i]] + values[i]) return dp[capacity]
4. String DP Problems
# Longest Common Subsequence - O(m * n) time and spacedef longest_common_subsequence(text1, text2): m, n = len(text1), len(text2) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(1, m + 1): for j in range(1, n + 1): if text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) return dp[m][n]# Edit Distance (Levenshtein) - O(m * n) time and spacedef min_distance(word1, word2): m, n = len(word1), len(word2) dp = [[0] * (n + 1) for _ in range(m + 1)] # Initialize base cases for i in range(m + 1): dp[i][0] = i for j in range(n + 1): dp[0][j] = j for i in range(1, m + 1): for j in range(1, n + 1): if word1[i-1] == word2[j-1]: dp[i][j] = dp[i-1][j-1] else: dp[i][j] = min( dp[i-1][j] + 1, # Delete dp[i][j-1] + 1, # Insert dp[i-1][j-1] + 1 # Replace ) return dp[m][n]# Word Break - O(n²) time where n is string lengthdef word_break(s, word_dict): n = len(s) dp = [False] * (n + 1) dp[0] = True word_set = set(word_dict) for i in range(1, n + 1): for j in range(i): if dp[j] and s[j:i] in word_set: dp[i] = True break return dp[n]
5. Matrix DP
# Unique Paths - O(m * n) time and spacedef unique_paths(m, n): dp = [[1] * n for _ in range(m)] for i in range(1, m): for j in range(1, n): dp[i][j] = dp[i-1][j] + dp[i][j-1] return dp[m-1][n-1]# Space-optimized unique paths - O(n) spacedef unique_paths_optimized(m, n): dp = [1] * n for _ in range(1, m): for j in range(1, n): dp[j] += dp[j-1] return dp[n-1]# Maximum Path Sum in Triangle - O(n²) time, O(1) spacedef minimum_total(triangle): # Bottom-up approach, modify triangle in-place for row in range(len(triangle) - 2, -1, -1): for col in range(len(triangle[row])): triangle[row][col] += min( triangle[row + 1][col], triangle[row + 1][col + 1] ) return triangle[0][0]### DP Problem Recognition Patterns```python# 1. Optimization problems (min/max)# 2. Counting problems (number of ways)# 3. Decision problems (possible/impossible)# 4. String matching problems# 5. Game theory problems# Template for memoized recursiondef dp_memoized(problem_params): memo = {} def solve(state): if state in memo: return memo[state] if base_case(state): return base_result result = float('inf') # or 0, depending on problem for next_state in get_next_states(state): result = optimize(result, solve(next_state)) memo[state] = result return result return solve(initial_state)
Sorting & Searching
Built-in Sorting
# Python's Timsort - O(n log n) average/worst case, O(n) best casearr = [3, 1, 4, 1, 5, 9, 2, 6]arr.sort() # In-place sortingsorted_arr = sorted(arr) # Returns new sorted list# Custom sorting with key functionwords = ['apple', 'pie', 'Washington', 'book']words.sort(key=len) # Sort by lengthwords.sort(key=str.lower) # Case-insensitive sortwords.sort(reverse=True) # Descending order# Sort with multiple criteriastudents = [('Alice', 85), ('Bob', 90), ('Charlie', 85)]students.sort(key=lambda x: (-x[1], x[0])) # Sort by grade desc, then name asc# Sort 2D array/listintervals = [[1, 3], [2, 6], [8, 10], [15, 18]]intervals.sort(key=lambda x: x[0]) # Sort by start time
Binary Search Implementation
import bisect# Standard binary search - O(log n) timedef binary_search(arr, target): left, right = 0, len(arr) - 1 while left <= right: mid = left + (right - left) // 2 # Avoid overflow if arr[mid] == target: return mid elif arr[mid] < target: left = mid + 1 else: right = mid - 1 return -1# Find insertion position (leftmost)def search_insert(arr, target): left, right = 0, len(arr) while left < right: mid = left + (right - left) // 2 if arr[mid] < target: left = mid + 1 else: right = mid return left# Find first and last occurrencedef search_range(nums, target): def find_bound(is_left): left, right = 0, len(nums) while left < right: mid = left + (right - left) // 2 if nums[mid] == target: if is_left: right = mid else: left = mid + 1 elif nums[mid] < target: left = mid + 1 else: right = mid return left left_bound = find_bound(True) if left_bound >= len(nums) or nums[left_bound] != target: return [-1, -1] right_bound = find_bound(False) - 1 return [left_bound, right_bound]# Using bisect moduledef bisect_operations(arr, target): # Find insertion point for target (leftmost) left_pos = bisect.bisect_left(arr, target) # Find insertion point for target (rightmost) right_pos = bisect.bisect_right(arr, target) # Insert element maintaining sorted order bisect.insort(arr, target) return left_pos, right_pos
Heap Operations
import heapq# Min heap operations - all O(log n) except heapify which is O(n)heap = [3, 1, 4, 1, 5, 9, 2, 6]heapq.heapify(heap) # Convert list to heap in-placeheapq.heappush(heap, 0) # Add elementmin_element = heapq.heappop(heap) # Remove and return smallest# Peek at smallest element without removingsmallest = heap[0]# Push and pop in one operationheapq.heapreplace(heap, 7) # More efficient than separate push/pop# Get k largest/smallest elementsk_largest = heapq.nlargest(3, arr)k_smallest = heapq.nsmallest(3, arr)# Max heap using negative valuesmax_heap = [-x for x in arr]heapq.heapify(max_heap)# Custom comparison using tuples# Heap sorts by first element, then second, etc.task_heap = []heapq.heappush(task_heap, (priority, task_id, task_data))# Top K Frequent Elementsdef top_k_frequent(nums, k): count = {} for num in nums: count[num] = count.get(num, 0) + 1 # Use min heap to keep only k elements heap = [] for num, freq in count.items(): heapq.heappush(heap, (freq, num)) if len(heap) > k: heapq.heappop(heap) return [num for freq, num in heap]# Merge K Sorted Listsdef merge_k_sorted_lists(lists): heap = [] # Initialize heap with first element from each list for i, lst in enumerate(lists): if lst: heapq.heappush(heap, (lst[0], i, 0)) # (value, list_index, element_index) result = [] while heap: val, list_idx, elem_idx = heapq.heappop(heap) result.append(val) # Add next element from the same list if elem_idx + 1 < len(lists[list_idx]): next_val = lists[list_idx][elem_idx + 1] heapq.heappush(heap, (next_val, list_idx, elem_idx + 1)) return result
Mathematical Algorithms
Number Theory
import math# Greatest Common Divisor - O(log min(a, b))def gcd(a, b): while b: a, b = b, a % b return a# Using built-in (Python 3.5+)gcd_builtin = math.gcd(a, b)# Least Common Multipledef lcm(a, b): return abs(a * b) // gcd(a, b)# Extended Euclidean Algorithmdef extended_gcd(a, b): if a == 0: return b, 0, 1 gcd, x1, y1 = extended_gcd(b % a, a) x = y1 - (b // a) * x1 y = x1 return gcd, x, y# Fast Exponentiation - O(log n)def power(base, exp, mod=None): result = 1 base = base % mod if mod else base while exp > 0: if exp & 1: # If exp is odd result = (result * base) % mod if mod else result * base exp >>= 1 # exp = exp // 2 base = (base * base) % mod if mod else base * base return result# Prime checking - O(√n)def is_prime(n): if n < 2: return False if n == 2: return True if n % 2 == 0: return False for i in range(3, int(math.sqrt(n)) + 1, 2): if n % i == 0: return False return True# Sieve of Eratosthenes - O(n log log n)def sieve_of_eratosthenes(n): is_prime = [True] * (n + 1) is_prime[0] = is_prime[1] = False for i in range(2, int(math.sqrt(n)) + 1): if is_prime[i]: for j in range(i * i, n + 1, i): is_prime[j] = False return [i for i in range(n + 1) if is_prime[i]]
Bit Manipulation
# Basic bit operationsdef bit_operations(n): # Check if i-th bit is set def is_set(num, i): return (num & (1 << i)) != 0 # Set i-th bit def set_bit(num, i): return num | (1 << i) # Clear i-th bit def clear_bit(num, i): return num & ~(1 << i) # Toggle i-th bit def toggle_bit(num, i): return num ^ (1 << i) return is_set, set_bit, clear_bit, toggle_bit# Count set bits (Brian Kernighan's algorithm)def count_set_bits(n): count = 0 while n: n &= n - 1 # Remove rightmost set bit count += 1 return count# Built-in bit countcount_builtin = bin(n).count('1')# Check if power of 2def is_power_of_two(n): return n > 0 and (n & (n - 1)) == 0# Find single number (all others appear twice)def single_number(nums): result = 0 for num in nums: result ^= num # XOR cancels out pairs return result# Find two numbers that appear once (all others appear twice)def single_numbers_two(nums): xor = 0 for num in nums: xor ^= num # Find rightmost set bit rightmost_set_bit = xor & -xor num1 = num2 = 0 for num in nums: if num & rightmost_set_bit: num1 ^= num else: num2 ^= num return [num1, num2]# Generate all subsets using bit manipulationdef subsets_bit(nums): n = len(nums) result = [] for i in range(1 << n): # 2^n subsets subset = [] for j in range(n): if i & (1 << j): # If j-th bit is set subset.append(nums[j]) result.append(subset) return result
Combinatorics
import math# Factorialdef factorial(n): return math.factorial(n) # Built-in # Or iterative: result = 1; for i in range(1, n+1): result *= i# Combinations (nCr) - n choose rdef combinations(n, r): if r > n or r < 0: return 0 return math.factorial(n) // (math.factorial(r) * math.factorial(n - r))# Optimized combinations to avoid large factorialsdef combinations_optimized(n, r): if r > n - r: # Take advantage of symmetry r = n - r result = 1 for i in range(r): result = result * (n - i) // (i + 1) return result# Permutations (nPr)def permutations(n, r): return math.factorial(n) // math.factorial(n - r)# Pascal's Triangle for combinationsdef pascal_triangle(num_rows): triangle = [] for i in range(num_rows): row = [1] * (i + 1) for j in range(1, i): row[j] = triangle[i-1][j-1] + triangle[i-1][j] triangle.append(row) return triangle# Catalan Numbers - O(n²) using DPdef catalan_numbers(n): if n <= 1: return 1 catalan = [0] * (n + 1) catalan[0] = catalan[1] = 1 for i in range(2, n + 1): for j in range(i): catalan[i] += catalan[j] * catalan[i - 1 - j] return catalan[n]
Advanced Patterns
Union-Find (Disjoint Set)
class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n self.components = n def find(self, x): # Path compression if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return False # Union by rank if self.rank[root_x] < self.rank[root_y]: self.parent[root_x] = root_y elif self.rank[root_x] > self.rank[root_y]: self.parent[root_y] = root_x else: self.parent[root_y] = root_x self.rank[root_x] += 1 self.components -= 1 return True def connected(self, x, y): return self.find(x) == self.find(y) def get_components(self): return self.components# Example: Number of Connected Componentsdef count_components(n, edges): uf = UnionFind(n) for u, v in edges: uf.union(u, v) return uf.get_components()
Trie (Prefix Tree)
class TrieNode: def __init__(self): self.children = {} self.is_end_word = Falseclass Trie: def __init__(self): self.root = TrieNode() def insert(self, word): """Insert word into trie - O(m) where m is word length""" node = self.root for char in word: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] node.is_end_word = True def search(self, word): """Search for complete word - O(m)""" node = self._search_prefix(word) return node is not None and node.is_end_word def starts_with(self, prefix): """Check if any word starts with prefix - O(m)""" return self._search_prefix(prefix) is not None def _search_prefix(self, prefix): node = self.root for char in prefix: if char not in node.children: return None node = node.children[char] return node def get_all_words_with_prefix(self, prefix): """Get all words with given prefix""" node = self._search_prefix(prefix) if not node: return [] words = [] self._dfs_words(node, prefix, words) return words def _dfs_words(self, node, current_word, words): if node.is_end_word: words.append(current_word) for char, child_node in node.children.items(): self._dfs_words(child_node, current_word + char, words)# Word Search II using Triedef find_words(board, words): trie = Trie() for word in words: trie.insert(word) result = [] m, n = len(board), len(board[0]) def dfs(i, j, node, path): if node.is_end_word: result.append(path) node.is_end_word = False # Avoid duplicates if i < 0 or i >= m or j < 0 or j >= n: return char = board[i][j] if char not in node.children: return board[i][j] = '#' # Mark as visited for di, dj in [(0, 1), (1, 0), (0, -1), (-1, 0)]: dfs(i + di, j + dj, node.children[char], path + char) board[i][j] = char # Restore for i in range(m): for j in range(n): dfs(i, j, trie.root, "") return result
def backtrack_template(candidates, target): result = [] def backtrack(start, path, remaining): # Base case - solution found if is_solution(remaining): result.append(path[:]) # Make a copy return # Try each candidate for i in range(start, len(candidates)): candidate = candidates[i] # Skip invalid candidates if not is_valid(candidate, remaining): continue # Make choice path.append(candidate) # Recurse backtrack(i + 1, path, remaining - candidate) # Backtrack (undo choice) path.pop() backtrack(0, [], target) return result# N-Queens problemdef solve_n_queens(n): result = [] board = [['.' for _ in range(n)] for _ in range(n)] def is_safe(row, col): # Check column for i in range(row): if board[i][col] == 'Q': return False # Check diagonals for i, j in zip(range(row - 1, -1, -1), range(col - 1, -1, -1)): if board[i][j] == 'Q': return False for i, j in zip(range(row - 1, -1, -1), range(col + 1, n)): if board[i][j] == 'Q': return False return True def backtrack(row): if row == n: result.append([''.join(row) for row in board]) return for col in range(n): if is_safe(row, col): board[row][col] = 'Q' backtrack(row + 1) board[row][col] = '.' backtrack(0) return result
Interview Tips
Python-Specific Optimizations
# Use built-in functions when possiblesum(nums) # Instead of manual loopmax(nums) # Instead of manual comparisonany(condition for x in nums) # Instead of manual boolean logicall(condition for x in nums) # Check if all satisfy condition# List comprehensions are faster than loopssquares = [x**2 for x in range(10)] # Faster than append in loop# Use enumerate instead of range(len())for i, val in enumerate(nums): # Instead of for i in range(len(nums))# Use zip for parallel iterationfor x, y in zip(list1, list2): # Instead of index-based access# Set operations for membership testingif item in set_collection: # O(1) instead of O(n) for lists# Use collections.Counter for frequency countingfrom collections import Counterfreq = Counter(nums) # Instead of manual dictionary counting
Common Pitfalls to Avoid
Off-by-one errors: Carefully check loop bounds and array indices
Integer overflow: Consider using appropriate data types
Modifying list while iterating: Use reverse iteration or create new list
Shallow vs deep copy: Use copy.deepcopy() for nested structures
Mutable default arguments: Use None and initialize inside function
Memory-Efficient Techniques
# Use generators for large datasetsdef fibonacci_generator(): a, b = 0, 1 while True: yield a a, b = b, a + b# Use itertools for memory-efficient iterationimport itertoolsfor combo in itertools.combinations(large_list, 2): process(combo) # Process one at a time instead of generating all# Use slots for memory-efficient classesclass Point: __slots__ = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y