Algorithms Deep Dive: Sorting, Searching, Dynamic Programming, and Graph Patterns
Algorithms are step-by-step procedures for solving computational problems — and recognizing which algorithm category a problem belongs to is the key to solving it efficiently under interview pressure.
What You’ll Learn
- Sorting algorithms: quicksort, mergesort, heapsort — implementations and when to use each
- Binary search patterns and variations
- Dynamic programming: top-down vs bottom-up, common patterns
- Graph algorithms: shortest path, MST, topological sort
- How to recognize algorithm patterns from problem descriptions
Why Algorithms Matter
Coding interviews exist because they test your ability to reason about efficiency, recognize problem patterns, and implement non-trivial logic under time pressure. Companies like Google, Meta, and Microsoft use algorithm questions because they correlate with strong engineering skills. Mastering the common algorithm patterns turns unfamiliar problems into recognizable templates.
DodaZIP uses sorting and compression algorithms that must balance speed and memory — choosing the right algorithm directly affects user experience.
Learning Path
flowchart LR
A[Data Structures Deep] --> B[Algorithms Deep<br/>You are here]
B --> C[System Design]
C --> D[Mock Interviews]
D --> E[Offer Negotiation]
style B fill:#f90,color:#fff
Sorting
Quicksort
def quicksort(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
pivot = partition(arr, low, high)
quicksort(arr, low, pivot - 1)
quicksort(arr, pivot + 1, high)
return arr
def partition(arr, low, high):
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
# Average: O(n log n), Worst: O(n²), Space: O(log n)Mergesort
def mergesort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = mergesort(arr[:mid])
right = mergesort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# Time: O(n log n), Space: O(n)Heapsort
def heapsort(arr):
n = len(arr)
# Build max heap
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# Extract elements
for i in range(n - 1, 0, -1):
arr[0], arr[i] = arr[i], arr[0]
heapify(arr, i, 0)
return arr
def heapify(arr, n, i):
largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[left] > arr[largest]:
largest = left
if right < n and arr[right] > arr[largest]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
# Time: O(n log n), Space: O(1)Sorting Cheatsheet
| Algorithm | Average | Worst | Space | Stable |
|---|---|---|---|---|
| Quicksort | O(n log n) | O(n²) | O(log n) | No |
| Mergesort | O(n log n) | O(n log n) | O(n) | Yes |
| Heapsort | O(n log n) | O(n log n) | O(1) | No |
| Bubble | O(n²) | O(n²) | O(1) | Yes |
| Insertion | O(n²) | O(n²) | O(1) | Yes |
Binary Search
Classic Binary Search
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1Binary Search Variations
# Find first occurrence (lower bound)
def first_occurrence(arr, target):
left, right = 0, len(arr) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] >= target:
right = mid - 1
else:
left = mid + 1
if arr[mid] == target:
result = mid
return result
# Find last occurrence (upper bound)
def last_occurrence(arr, target):
left, right = 0, len(arr) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] <= target:
left = mid + 1
else:
right = mid - 1
if arr[mid] == target:
result = mid
return result
# Search in rotated sorted array
def search_rotated(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
if nums[left] <= nums[mid]: # Left half is sorted
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else: # Right half is sorted
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1Dynamic Programming
Top-Down (Memoization)
from functools import lru_cache
# Fibonacci with memoization
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
# Time: O(n), Space: O(n)Bottom-Up (Tabulation)
def fib_bottom_up(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# Time: O(n), Space: O(n) → O(1) with space optimization
def fib_optimized(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
current = prev1 + prev2
prev2 = prev1
prev1 = current
return prev1Common DP Patterns
# 0/1 Knapsack
def knapsack(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(1, capacity + 1):
if weights[i - 1] <= w:
dp[i][w] = max(
values[i - 1] + dp[i - 1][w - weights[i - 1]],
dp[i - 1][w]
)
else:
dp[i][w] = dp[i - 1][w]
return dp[n][capacity]
# Longest Common Subsequence
def lcs(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]
# Coin Change (minimum coins)
def coin_change(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for a in range(coin, amount + 1):
dp[a] = min(dp[a], dp[a - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1DP Pattern Recognition
| Pattern | Example Problem | State Definition |
|---|---|---|
| Fibonacci-style | Climbing stairs | dp[n] = dp[n-1] + dp[n-2] |
| 0/1 Knapsack | Subset sum | dp[i][w] = max value with first i items, weight w |
| Unbounded Knapsack | Coin change | dp[a] = min coins to make amount a |
| LCS | Edit distance | dp[i][j] = LCS of first i chars and first j chars |
| LIS | Longest increasing subsequence | dp[i] = LIS ending at index i |
| Matrix | Path in grid | dp[i][j] = ways to reach (i,j) from start |
Graph Algorithms
Dijkstra (Shortest Path)
import heapq
def dijkstra(graph, start):
distances = {node: float('inf') for node in graph}
distances[start] = 0
pq = [(0, start)]
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
# Time: O((V + E) log V)Topological Sort
from collections import deque
def topological_sort(graph):
in_degree = {node: 0 for node in graph}
for node in graph:
for neighbor in graph[node]:
in_degree[neighbor] = in_degree.get(neighbor, 0) + 1
queue = deque([n for n, d in in_degree.items() if d == 0])
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in graph.get(node, []):
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return result if len(result) == len(graph) else []Common Algorithm Mistakes
1. Not Recognizing the Pattern
Spending 20 minutes on a DP problem when it’s actually a greedy problem.
Fix: Before coding, ask: “Can I use two pointers? Does this have optimal substructure?”
2. Off-by-One Errors
Binary search and DP array indices are common off-by-one sources.
Fix: Test with small examples. Check array bounds on every access.
3. Wrong Base Cases
DP solutions fail when base cases are wrong or missing.
Fix: Write base cases for: 0, 1, empty input, and minimal non-trivial input.
4. Forgetting to Sort
Many problems require sorted input (binary search, two-pointer, intervals).
Fix: “Do I need sorted input?” — if yes, sort first.
5. Infinite Loops
Graph algorithms with cycles (DFS, topological sort) can loop infinitely.
Fix: Use visited sets. For topological sort, track in-degrees correctly.
6. Suboptimal Algorithm
Dijkstra for unweighted graphs (use BFS). BFS for weighted graphs with negative edges (use Bellman-Ford).
Fix: Match the algorithm to the problem constraints.
7. Not Analyzing Complexity
Solving a problem but not understanding why it’s fast or slow.
Fix: Always state time and space complexity before and after coding.
Practice Questions
1. What is the difference between mergesort and quicksort?
Mergesort is O(n log n) guaranteed, stable, but uses O(n) space. Quicksort is O(n log n) average, O(n²) worst, in-place, and unstable.
2. What is dynamic programming and when do you use it?
DP solves problems with overlapping subproblems and optimal substructure by breaking them into simpler subproblems and storing results.
3. What is the difference between Dijkstra and BFS for shortest path?
BFS works on unweighted graphs (shortest path by edge count). Dijkstra works on weighted graphs with non-negative weights.
4. How do you recognize a DP problem?
It asks for optimal value (min, max, longest, shortest), has overlapping subproblems, and the brute-force is exponential.
5. What is topological sort and when is it used?
Ordering nodes in a directed acyclic graph so every edge goes from earlier to later. Used for dependency resolution, build systems, course scheduling.
Challenge: Implement a solution for the “Word Ladder” problem (LeetCode 127). Use BFS for shortest transformation path. Analyze time and space complexity.
FAQ
What’s Next
| Tutorial | What You’ll Learn |
|---|---|
| System Design Interview Prep | Designing scalable systems |
| Interview Questions Bank | Curated list of 50+ practice questions |
| Mock Interview Practice | Full-length interview simulations |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro