Γ·10 both sides: 840/1200 = 84/120
Γ·12 both sides: 84/120 = 7/10
7/10 = 0.7
Γ·100 both sides: 1260/4200 = 12.6/42
Simpler: divide by 420 β 3/10
3/10 = 0.3
Anchor: 50Β² = 2500, close to 2700.
Try 52: 52Β² = (50+2)Β² = 2500 + 200 + 4 = 2704. Off by only 4.
β2700 β 52
10% of 350 = 35
5% of 350 = 17.5
2% of 350 = 7
17% = 10% + 5% + 2% = 35 + 17.5 + 7 = 59.5
Population: ~4M inhabitants, ~1M households.
Demand: ~20% use taxi once/week β 200k trips/week β ~30k trips/day.
Supply: 1 taxi does ~12 trips/day β 30k/12 β 2,500 formal taxis. Add informal sector β ~4,000β5,000 total.
Population: ~37M. Working age (~15β60): ~55% = ~20M people.
Workforce with higher education: ~15% = ~3M. STEM graduates: ~20% of that = ~600k.
CS/IT specifically: ~25% of STEM = ~150k. Active developers (not all find dev jobs): ~60% = ~90,000β100,000.
Smartphone users: ~37M population, ~70% smartphone penetration = ~26M users.
WhatsApp adoption: Morocco is one of the highest globally β ~90% of smartphone users = ~23M active users.
Messages per user per day: average ~30 messages/day (mix of personal, family groups, professional).
23M Γ 30 = ~700 million messages/day.
Per stream: HD = ~3 GB/hour, 4K = ~7 GB/hour. Use ~3 GB as the average (mix of HD/SD).
Concurrent streams globally: Netflix has ~260M subscribers. Peak concurrency ~15% = ~40M simultaneous streams.
Average watch time: ~2h/subscriber/day. Total: 260M Γ 2h Γ 3 GB/h = ~1.5 exabytes/day.
Split 3 / 3 / 2. Weigh the two groups of 3.
If one side heavier β heavy ball is in that 3. If balanced β in the group of 2.
Second weighing identifies the ball in either case.
Answer: 2 weighings. Key: divide into 3 groups not 2 β logβ thinking.
Light rope A from both ends simultaneously, and light rope B from one end.
Rope A finishes in 30 min (burned from both ends). At that moment, light the other end of rope B.
Rope B had 30 min left. Burning from both ends now β it finishes in 15 more min. Total: 30 + 15 = 45 minutes.
The intuitive (wrong) answer is 0.10β¬. The correct answer is 0.05β¬.
Let ball = x. Bat = x + 1.00.
x + (x + 1.00) = 1.10 β 2x = 0.10 β x = 0.05.
Ball = 0.05β¬, Bat = 1.05β¬. Check: 1.05 - 0.05 = 1.00 β
Turn on switch 1 and wait 5 minutes.
Turn switch 1 OFF, turn switch 2 ON.
Enter the room: if the bulb is on β switch 2. If off but warm β switch 1. If off and cold β switch 3.
Image β static, read-only blueprint built at compile time. Like a class in OOP. Immutable, layered (OS + deps + app code).
Container β a running instance of an image. Like an object instantiated from a class. Has its own writable layer, process space, and network.
CI (Continuous Integration): every push triggers automated tests + build. Catches integration issues early. Tools: GitHub Actions, Jenkins, CircleCI.
git merge and git rebase? When would you use each?git merge creates a merge commit that joins two branch histories. Original history preserved.
git rebase replays your commits on top of the target branch, rewriting history to appear linear.
| merge | rebase | |
|---|---|---|
| History | Preserves divergence | Linear / cleaner |
| Commits | Adds a merge commit | Rewrites commits (new SHAs) |
| Use when | Team branches, PRs | Local cleanup before push |
git reset --hard do vs git revert? Which is safe on a shared branch?git reset --hard HEAD~1 β moves the branch pointer back, discards the commit. Destructive. Rewrites history.
git revert <SHA> β creates a new commit that undoes a previous commit. Non-destructive. History preserved.
reset --hard β safe only on local, unpublished commitsrevert β always safe, including on shared/main branches| Structure | Ordered | Duplicates | Lookup | Use case |
|---|---|---|---|---|
| List | β | β | O(n) | Ordered sequence, indexed access |
| Set | β | β | O(1) | Membership test, deduplication |
| Dict | β (3.7+) | Keys: β | O(1) | Keyβvalue mapping, frequency count |
# List β ordered, indexable, allows duplicates temps = [3, -5, 3, 0] # Set β fast membership, no order, no dupes visited = {"nodeA", "nodeB"} # Dict β keyβvalue, O(1) lookup freq = {"python": 3, "data": 2}
HashMap stores key β value pairs. HashSet stores only keys β internally it's a HashMap where the value is a dummy constant.
| HashMap | HashSet | |
|---|---|---|
| Stores | key + value | key only |
| Lookup | O(1) avg | O(1) avg |
| Use when | Count, cache, index | Visited, dedup, membership |
Both use a hash table. Hash function β bucket index. Collisions: chaining (linked list per bucket) or open addressing (Python uses open addressing with pseudo-random probing).
Stack β LIFO. push/pop from same end. Use: undo/redo, call stack, bracket validation.
Queue β FIFO. enqueue at back, dequeue from front. Use: BFS, task scheduling, Kafka.
from collections import deque class StackFromQueues: def __init__(self): self.q = deque() def push(self, x): self.q.append(x) for _ in range(len(self.q) - 1): self.q.append(self.q.popleft()) # rotate newest to front def pop(self): return self.q.popleft() # O(1) # push O(n), pop O(1)
| Operation | Array | Linked List |
|---|---|---|
| Index access | O(1) | O(n) |
| Search | O(n) | O(n) |
| Insert at head | O(n) shift | O(1) |
| Insert at tail | O(1) amortized | O(n) / O(1) with tail ptr |
| Memory | Contiguous, cache-friendly | Scattered, pointer overhead |
list is a dynamic array, not a linked list. Use collections.deque for O(1) head insertions.LangChain is an orchestration framework for LLM-powered apps. Direct API = single prompt β single response. LangChain adds: Chains (multi-step pipelines), Agents (LLM-driven tool selection), Memory (conversation history), Tools (external integrations), Prompt templates.
ReAct (Reasoning + Acting): the LLM alternates Thought β Action β Observation until it can produce a Final Answer.
Thought: "I need to find the population of Casablanca"
Action: search("Casablanca population 2024")
Observation: "4.27 million"
Final Answer: generated from accumulated observations
max_iterations to prevent infinite loopsRAG retrieves relevant docs at query time and injects them into the LLM prompt β solving the knowledge cutoff problem.
Index (offline): chunk β embed β store in vector DB (FAISS, Pinecone, Chroma)
Retrieve (online): embed query β cosine similarity β top-k chunks
Generate: LLM answers using retrieved chunks as context
| Chain | Agent | |
|---|---|---|
| Flow | Fixed at code time | LLM decides at runtime |
| Latency | Lower | Higher (multi-step) |
| Reliability | High | Variable |
| Use when | Known, repeatable task | Open-ended, multi-hop reasoning |
Example: data analyst agent β "Why did revenue drop in Q3?" β agent decides to: query DB, run correlation, search external news, then synthesize. A chain can't do this because the steps are unknown in advance.
TF-IDF: TF(t,d) Γ log(N / (1 + df(t))). Scores term importance by frequency Γ rarity.
BM25 adds: (1) TF saturation β diminishing returns on repeated terms. (2) Length normalization β penalizes long documents.
| Letter | Principle | One-liner |
|---|---|---|
| S | Single Responsibility | A class has only one reason to change |
| O | Open/Closed | Open for extension, closed for modification |
| L | Liskov Substitution | Subclasses must be usable wherever the parent is used |
| I | Interface Segregation | Don't force classes to implement interfaces they don't use |
| D | Dependency Inversion | Depend on abstractions, not concrete implementations |
# SRP violation β one class doing too much class Report: def generate(self): ... def save_to_db(self): ... # β different responsibility def send_email(self): ... # β different responsibility # SRP fix β one class per responsibility class ReportGenerator: def generate(self): ... class ReportRepository: def save(self, r): ... class ReportNotifier: def send(self, r): ...
| Property | Meaning | Violation example |
|---|---|---|
| Atomicity | All or nothing | Bank transfer: debit succeeds, credit crashes β money disappears |
| Consistency | DB stays in valid state | Order references a deleted customer β orphaned FK record |
| Isolation | Concurrent txns don't interfere | Dirty read: user sees another's uncommitted changes |
| Durability | Committed data survives crashes | Power cut after commit β data gone if not persisted to disk |
| SQL | NoSQL | |
|---|---|---|
| Schema | Fixed, enforced | Flexible / schema-less |
| Scaling | Vertical | Horizontal |
| Transactions | Full ACID | Eventual consistency |
| Examples | PostgreSQL, MySQL | MongoDB, Redis, Cassandra |
REST β resource-based endpoints returning fixed shapes. Problems: over-fetching (too many fields) and under-fetching (need multiple requests).
GraphQL β single endpoint, client defines exactly which fields it needs.
| REST | GraphQL | |
|---|---|---|
| Endpoints | Many (/users, /postsβ¦) | One (/graphql) |
| Over-fetching | Common | Eliminated |
| Type system | Optional (OpenAPI) | Built-in schema |
| Caching | Easy (HTTP) | Harder (POST requests) |
| Best for | Simple CRUD APIs | Complex nested data, mobile |
nums and an integer k, return the k most frequent elements. The result must be sorted in descending order of frequency. If two elements have the same frequency, sort them in ascending order of value.
Example: nums = [1,1,1,2,2,3], k = 2 β [1, 2]
Example: nums = [4,4,2,2,3], k = 2 β [2, 4] (tie in freq β ascending value)
Count frequencies naively with a loop, then sort all unique elements by frequency, take the first k.
def top_k_brute(nums, k): freq = {} for n in nums: freq[n] = freq.get(n, 0) + 1 # build manually # sort all unique elements by freq descending sorted_keys = sorted(freq.keys(), key=lambda x: -freq[x]) return sorted_keys[:k] # Time: O(n + u log u) u = unique elements Space: O(u)
Use collections.Counter to build the frequency map in O(n), then sort with a tuple key to handle ties β (-freq, value) sorts by frequency descending, then value ascending.
from collections import Counter def top_k_frequent(nums: list[int], k: int) -> list[int]: # Step 1: build frequency map β O(n) freq = Counter(nums) # freq = {1: 3, 2: 2, 3: 1} for [1,1,1,2,2,3] # Step 2: sort by (-freq, value) β handles ties cleanly sorted_elements = sorted(freq.keys(), key=lambda x: (-freq[x], x)) # Step 3: return top k return sorted_elements[:k] # Time: O(n + u log u) Space: O(u)
nums = [4,4,2,2,3], k = 2
freq = {4: 2, 2: 2, 3: 1}
sort key (-freq, value):
4 β (-2, 4)
2 β (-2, 2)
3 β (-1, 3)
sorted: [2, 4, 3] β 2 before 4 because (-2,2) < (-2,4)
return [:2] β [2, 4] β (tie broken by ascending value)
| Approach | Time | Space | Notes |
|---|---|---|---|
| Brute force (manual loop) | O(n + u log u) | O(u) | Same asymptotically but no tie handling |
| Counter + sorted (optimal) | O(n + u log u) | O(u) | Clean, handles ties, Pythonic |
| Heap (most_common) | O(n + u log k) | O(u) | Faster when k << u |
u = number of unique elements. In the worst case u = n (all elements distinct).
sorted() which is O(u log u). Can you do better when k is much smaller than u?Yes β use a min-heap of size k. Push elements one by one; if the heap exceeds k, pop the minimum. Final heap contains the top k.
import heapq def top_k_heap(nums, k): freq = Counter(nums) # heap of (freq, value) β min-heap on freq heap = [] for val, cnt in freq.items(): heapq.heappush(heap, (cnt, val)) if len(heap) > k: heapq.heappop(heap) # remove least frequent return [val for cnt, val in sorted(heap, reverse=True)] # Time: O(n + u log k) β better when k << u
| Method | Time | Best when |
|---|---|---|
| sorted() | O(u log u) | k close to u |
| Min-heap | O(u log k) | k << u |
Simply remove the minus sign from the sort key β (freq[x], x) instead of (-freq[x], x).
# Ascending frequency, ascending value on tie sorted_elements = sorted(freq.keys(), key=lambda x: (freq[x], x)) # Descending frequency, descending value on tie sorted_elements = sorted(freq.keys(), key=lambda x: (-freq[x], -x))
Counter actually do internally? What's the complexity of building it?Counter is a subclass of dict. It iterates through the list once and increments a count for each element β exactly equivalent to a manual hashmap loop.
Use the button below to reveal a different exercise. Rotate across mock sessions so candidates don't share answers.
O(n log n) is bigger β it grows faster than O(n).
For n = 1,000,000: O(n) = 1,000,000 operations. O(n log n) β 20,000,000 operations (logβ(10βΆ) β 20).
Yes, using a frequency hashmap (dictionary).
def is_anagram(s1: str, s2: str) -> bool: if len(s1) != len(s2): return False count = {} for ch in s1: count[ch] = count.get(ch, 0) + 1 # build freq map for ch in s2: count[ch] = count.get(ch, 0) - 1 # decrement if count[ch] < 0: return False return True # Time: O(n) Space: O(1) β bounded by 26 letters
| Approach | Time | Why |
|---|---|---|
| sorted() comparison | O(n log n) | Sorting is O(n log n) |
| Frequency hashmap | O(n) | Two linear passes, O(1) dict access |
O(log n) β because each step halves the search space.
After k steps, remaining elements = n / 2^k. When n / 2^k = 1, we've found the answer β k = logβ(n).
def binary_search(nums, target): left, right = 0, len(nums) - 1 while left <= right: mid = (left + right) // 2 if nums[mid] == target: return mid elif nums[mid] < target: left = mid + 1 else: right = mid - 1 return -1
Framework for optimization from O(nΒ²):
HashMap/HashSet β eliminate inner loop by looking up complements in O(1). Two Sum, Anagram, duplicates detection.
Sliding Window β when the problem involves a contiguous subarray/substring. Moves a window rather than re-scanning.
Two Pointers β when working on a sorted array. Start from both ends and converge.
Sort first β O(n log n) pre-processing sometimes unlocks an O(n) second pass. Better than O(nΒ²) overall.
Prefix sum / Memoization β avoid recomputing sub-results. Dynamic programming territory.
Space complexity = O(n) for the call stack in the worst case.
| Pattern | Typical Complexity | Key Problems |
|---|---|---|
| HashMap | O(n) | Two Sum, Anagram, Group Anagrams |
| Sliding Window | O(n) | Longest Substring, Max Subarray |
| Stack | O(n) | Valid Parentheses, Next Greater Element |
| BFS/DFS | O(n+e) | Number of Islands, Shortest Path |
| Binary Search | O(log n) | Sorted array search, rotated array |
| Heap / Priority Queue | O(n log k) | Top K Elements, K Closest Points |
| Sorting | O(n log n) | Merge Intervals, Meeting Rooms |
| Two Pointers | O(n) | Container With Most Water, 3Sum |
TF-IDF & BM25 β Excerpt
TF-IDF (Term FrequencyβInverse Document Frequency) is a numerical statistic that reflects how important a word is to a document in a corpus.
TF(t, d) = count(t in d) / total_terms(d)
IDF(t, D) = log( N / (1 + df(t)) )
TF-IDF(t, d, D) = TF(t, d) Γ IDF(t, D)
Where N is the total number of documents, and df(t) is the number of documents containing term t. The +1 in the denominator prevents division by zero for terms appearing in all documents.
BM25 (Best Match 25) improves on TF-IDF with two key mechanisms: term frequency saturation and document length normalization.
score(d, q) = Ξ£ IDF(qα΅’) Γ [ tf(qα΅’,d) Γ (k1+1) ] / [ tf(qα΅’,d) + k1 Γ (1 - b + b Γ |d|/avgdl) ]
IDF(qα΅’) = log( (N - df(qα΅’) + 0.5) / (df(qα΅’) + 0.5) + 1 )
k1 β [1.2, 2.0] β controls TF saturation
b β [0, 1] β controls length normalization (typically 0.75)
avgdl β average document length in the corpus
As tf grows, the numerator saturates: doubling term frequency no longer doubles the score. When b = 0, length normalization is disabled. When b = 1, full normalization is applied.
The log dampens the effect of rare terms. Without it, a term appearing in only 1 document out of 1,000,000 would get an IDF of 1,000,000 β completely dominating the score for any document that contains it, even once.
Problem 1 β TF saturation: in TF-IDF, if a term appears 100 times instead of 10, the score is 10Γ higher. But intuitively, a document mentioning "python" 100 times isn't 10Γ more relevant than one mentioning it 10 times. BM25 applies a saturation curve β score grows fast at first then plateaus as tf increases. Controlled by k1.
Problem 2 β Length normalization: a long document will naturally contain more term occurrences just because it's longer. TF-IDF doesn't account for this. BM25 penalizes long documents via the |d|/avgdl ratio, controlled by b. A term in a 50-word document is more meaningful than the same term in a 5,000-word document.
import math from collections import Counter def compute_tf(doc: list[str]) -> dict: """Term frequency: count(t) / total_terms""" counts = Counter(doc) total = len(doc) return {term: cnt / total for term, cnt in counts.items()} def compute_idf(corpus: list[list[str]]) -> dict: """IDF: log(N / (1 + df(t))) for each term""" N = len(corpus) df = {} for doc in corpus: for term in set(doc): # set β count doc once per term df[term] = df.get(term, 0) + 1 return {term: math.log(N / (1 + df[term])) for term in df} def tfidf_score(query: list[str], doc: list[str], idf: dict) -> float: """Score a single doc against a query""" tf = compute_tf(doc) return sum(tf.get(t, 0) * idf.get(t, 0) for t in query) def top_k_tfidf(query_str: str, corpus_strs: list[str], k: int = 3) -> list[tuple]: corpus = [doc.lower().split() for doc in corpus_strs] query = query_str.lower().split() idf = compute_idf(corpus) scores = [(i, tfidf_score(query, doc, idf)) for i, doc in enumerate(corpus)] scores.sort(key=lambda x: -x[1]) # descending score return scores[:k] # --- Demo --- corpus = [ "python is great for data science", "data science uses python and R", "java is used for backend development", "python python python everywhere" ] print(top_k_tfidf("python data", corpus)) # β [(1, 0.18), (0, 0.15), (3, 0.12)] (approx)
| Step | Time | Space |
|---|---|---|
| Build IDF (offline) | O(N Γ L) | O(V) |
| Score one doc | O(|query|) | O(|doc|) |
| Score all docs + sort | O(N Γ L + N log N) | O(N) |
N = corpus size, L = avg doc length, V = vocabulary size.
term β [doc_ids]. Query only hits documents that contain at least one query term β reduces N dramatically for large corpora.set(doc) in compute_idf?" β Answer: to count each document only once per term, regardless of how many times the term appears in that document. df(t) = number of documents containing t, not total occurrences.Only the scoring function changes β IDF computation and corpus indexing stay the same.
def bm25_score(query: list[str], doc: list[str], idf: dict, avgdl: float, k1: float = 1.5, b: float = 0.75) -> float: counts = Counter(doc) dl = len(doc) score = 0.0 for term in query: tf = counts.get(term, 0) idf_val = idf.get(term, 0) # BM25 saturation + length norm numerator = tf * (k1 + 1) denominator = tf + k1 * (1 - b + b * dl / avgdl) score += idf_val * (numerator / denominator) return score # avgdl computed once at index time: avgdl = sum(len(doc) for doc in corpus) / len(corpus)