Everything you need to understand about data structures and algorithms, explained so simply that a 10-year-old could follow along. This is your starting point before diving into any specific topic.
You're a CS student (or self-learner) who wants to master DSA for competitive programming, LeetCode, and technical interviews. Maybe you've tried reading about Big-O or linked lists before and it didn't click. This page fixes that. We start from absolute zero and build up every concept with real-world analogies, visual diagrams, and the actual math behind why things work.
Imagine you just moved into a new house. You have a bunch of stuff -- books, clothes, food, tools. You could just throw everything into one giant pile in the middle of the room. But that would be a nightmare to find anything in, right?
So instead, you organize your stuff:
A data structure is exactly the same thing, but for computer data. It's a way of organizing information so you can find it, add to it, and work with it efficiently.
Just like you wouldn't use a bookshelf to store soup (that's what the fridge is for), different data structures are good for different jobs:
| Real World | Data Structure | Good For |
|---|---|---|
| Row of lockers | Array | Quick access by number |
| Treasure hunt clues | Linked List | Easy to insert/remove items |
| Stack of plates | Stack | Undo/redo, backtracking |
| Queue at a shop | Queue | First come, first served |
| Dictionary / phone book | Hash Map | Instant lookup by name |
| Family tree | Tree | Hierarchical data, fast search |
| Road map / social network | Graph | Connections between things |
There is no single "best" data structure. The right choice depends on what you need to do. A great programmer knows when to use which one -- and that's exactly what this page teaches you.
An algorithm is just a set of steps to solve a problem. That's it. You already use algorithms every day without realizing it.
Step 1: Boil water
Step 2: Put tea bag in cup
Step 3: Pour boiling water into cup
Step 4: Wait 3 minutes
Step 5: Remove tea bag
Step 6: Add milk/sugar if you want
That's an algorithm. A clear set of instructions that always produces the right result.
In programming, an algorithm is the same thing -- a step-by-step procedure to solve a problem. For example:
That last one? That's binary search -- one of the most important algorithms in computer science. You've been doing it since you were a kid looking up words.
Imagine you need to find the name "Sean" in a phone book with 1,000,000 names.
Bad algorithm: Start at page 1 and read every single name until you find "Sean". Could take up to 1,000,000 checks.
Good algorithm (binary search): Open to the middle. "Sean" comes after "M", so look in the second half. Open to the middle of that. Keep halving. Only takes about 20 checks to search 1,000,000 names.
Same problem. Same answer. But one approach is 50,000x faster. That's why algorithms matter.
Before you can understand data structures, you need to understand how a computer actually stores data. Don't worry -- it's simpler than you think.
Your computer's memory (RAM) is like a massive row of numbered mailboxes. Each mailbox:
When you create a variable in code like x = 42, the computer:
The key insight is: going to a specific mailbox by its number is instant. The computer doesn't have to walk past mailbox #0, #1, #2... to get to mailbox #500. It just jumps straight there. This is called random access, and it's why arrays are so fast at looking up items by index.
Every data structure is making a trade-off about how to use these mailboxes. Arrays put data in consecutive mailboxes (fast to access, hard to insert). Linked lists scatter data across random mailboxes but connect them with pointers (easy to insert, slow to access). Understanding this trade-off is the key to mastering data structures.
This is it. This is the single most important concept in all of data structures and algorithms. If you understand this deeply, everything else falls into place.
Imagine you're building an app. When you have 100 users, any code works fine. But what happens when you have 1,000,000 users? Or 1,000,000,000?
Bad code that takes 1 second for 100 users might take 3 hours for 1,000,000 users. Good code takes 1 second for both. That's the difference between a successful app and a broken one.
We don't measure speed in seconds because different computers run at different speeds. Instead, we count how many operations the algorithm needs as the input gets bigger.
Given a list of n numbers, find the biggest one.
Algorithm: Look at each number, keep track of the biggest.
For a list of 10 numbers: ~10 operations
For a list of 1,000 numbers: ~1,000 operations
For a list of 1,000,000 numbers: ~1,000,000 operations
The number of operations grows linearly with the input size. We call this O(n).
Big-O tells us: "As the input gets really big, how does the number of operations grow?"
| Big-O | Name | Analogy | Speed |
|---|---|---|---|
| O(1) | Constant | Knowing exactly which locker to open | Instant, no matter the size |
| O(log n) | Logarithmic | Looking up a word in a dictionary | Incredibly fast |
| O(n) | Linear | Reading every page of a book | Fast, grows steadily |
| O(n log n) | Linearithmic | Smart sorting (merge sort) | Pretty fast |
| O(n²) | Quadratic | Comparing every person with every other person | Slow for large inputs |
| O(2ⁿ) | Exponential | Trying every combination on a lock | Impossibly slow |
| O(n!) | Factorial | Trying every possible arrangement | Heat death of the universe |
This is where it gets real. Look at how many operations each complexity needs for different input sizes:
| n | O(1) | O(log n) | O(n) | O(n log n) | O(n²) | O(2ⁿ) |
|---|---|---|---|---|---|---|
| 10 | 1 | 3 | 10 | 33 | 100 | 1,024 |
| 100 | 1 | 7 | 100 | 664 | 10,000 | 1.27 x 10³⁰ |
| 1,000 | 1 | 10 | 1,000 | 9,966 | 1,000,000 | IMPOSSIBLE |
| 10,000 | 1 | 13 | 10,000 | 132,877 | 100,000,000 | IMPOSSIBLE |
| 100,000 | 1 | 17 | 100,000 | 1,660,964 | 10,000,000,000 | IMPOSSIBLE |
| 1,000,000 | 1 | 20 | 1,000,000 | 19,931,569 | 1,000,000,000,000 | IMPOSSIBLE |
For n = 1,000,000:
O(log n) needs just 20 operations. That's binary search.
O(n) needs 1 million operations. Fine, takes maybe 0.01 seconds.
O(n²) needs 1 trillion operations. That's roughly 16 minutes.
O(2ⁿ) is literally impossible -- more operations than atoms in the universe.
This is why understanding time complexity is life or death for your code.
No matter how big the input is, it always takes the same number of steps.
Accessing an array element by index: arr[5]
Whether the array has 10 elements or 10 billion, jumping to index 5 is instant. The computer calculates: address = start_address + 5 x element_size. One calculation, done.
Each step cuts the problem in half. This is the magic of binary search.
Start with n items. After step 1: n/2 items. After step 2: n/4. After step 3: n/8.
After k steps: n / 2ᵏ items remaining.
We stop when 1 item is left: n / 2ᵏ = 1
Solving: 2ᵏ = n, so k = log₂(n)
For n = 1,000,000,000 (1 billion): log₂(1,000,000,000) ≈ 30 steps
You can search through a BILLION items in just 30 steps. That's the power of logarithms.
You look at every element once. If the input doubles, the time doubles.
Finding the sum of all elements in an array. You must visit each element exactly once. No shortcut.
def find_sum(arr):
total = 0
for num in arr: # visits each element once = O(n)
total += num
return total
For every element, you look at every OTHER element. It's a nested loop.
Imagine 100 students in a class need to shake hands with every other student.
Student 1 shakes hands with 99 others.
Student 2 shakes hands with 99 others.
...
Total handshakes ≈ 100 x 100 = 10,000.
With 1,000 students: 1,000 x 1,000 = 1,000,000 handshakes.
With 1,000,000 students: 1,000,000 x 1,000,000 = 1,000,000,000,000. That's a TRILLION. Way too slow.
# This is O(n²) -- nested loop
for i in range(len(arr)):
for j in range(len(arr)): # for EACH i, we loop through ALL j
if arr[i] + arr[j] == target:
return [i, j]
The number of operations doubles every time you add one more element. This blows up incredibly fast.
Think of a combination lock with n switches (on/off).
1 switch: 2 combinations
2 switches: 4 combinations
3 switches: 8 combinations
10 switches: 1,024 combinations
20 switches: 1,048,576 (about 1 million)
30 switches: 1,073,741,824 (about 1 billion)
50 switches: 1,125,899,906,842,624 (over 1 quadrillion)
This is why brute-force solutions to problems like "find the best subset" are impractical for large inputs.
Big-O notation describes the upper bound on growth rate. Formally: f(n) = O(g(n)) means there exist constants c and n₀ such that f(n) ≤ c·g(n) for all n ≥ n₀.
In plain English: we only care about the fastest-growing term, and we drop constants.
Logarithms answer the question: "How many times do I need to divide n by 2 to get to 1?"
This is why binary search is so powerful. Even for 1 billion elements, you only need ~30 steps. Each step cuts the search space in half.
The best comparison-based sorting algorithms (merge sort, quick sort) are O(n log n). Here's the intuition:
Merge sort splits the array in half, sorts each half, then merges them.
How many times can you split? log₂(n) times (keep halving until you have individual elements).
How much work at each level? n operations (to merge all the pieces at that level).
Total: n work × log₂(n) levels = O(n log n)
A modern computer does roughly 100,000,000 (10⁸) simple operations per second. Use this to estimate if your solution is fast enough:
n ≤ 10: O(n!) is fine
n ≤ 20: O(2ⁿ) is fine
n ≤ 500: O(n³) is fine
n ≤ 10,000: O(n²) is fine
n ≤ 1,000,000: O(n log n) is fine
n ≤ 100,000,000: O(n) is fine
Any n: O(log n) or O(1) is fine
Space complexity is the same idea as time complexity, but for memory instead of time.
If your algorithm creates a new array of size n, that's O(n) space. If it only uses a few variables regardless of input size, that's O(1) space.
Often, you can make code faster by using more memory, or save memory by being slower. This is one of the most fundamental trade-offs in CS.
Brute force: O(n²) time, O(1) space -- check every pair
Hash map: O(n) time, O(n) space -- store seen numbers in a hash map
The hash map solution uses more memory but is MUCH faster. This is almost always worth it.
Now that you understand time complexity, let's learn each data structure. For each one, we'll cover: what it is, how it works internally, time complexity for every operation (and WHY), and when to use it.
For deep dives with code implementations, click through to each topic's dedicated page.
Analogy: A row of numbered lockers in a school hallway.
Elements are stored in consecutive memory locations. Because they're consecutive, the computer can calculate exactly where any element is:
This is why accessing by index is O(1) -- it's just one multiplication and one addition, no matter how big the array is.
| Operation | Time | Why |
|---|---|---|
| Access by index | O(1) | Direct address calculation |
| Search (unsorted) | O(n) | Must check each element |
| Insert at end | O(1)* | Just place at next spot (*amortized) |
| Insert at beginning/middle | O(n) | Must shift all elements after |
| Delete from middle | O(n) | Must shift elements to fill gap |
Deep dive: Arrays & Strings page
Analogy: A treasure hunt where each clue tells you where the next clue is.
Each element (called a node) stores two things: its value AND a pointer to the next node. Nodes can be scattered anywhere in memory -- they don't need to be consecutive.
| Operation | Time | Why |
|---|---|---|
| Access by index | O(n) | Must follow links from the start |
| Search | O(n) | Must follow links one by one |
| Insert at head | O(1) | Just create node, point to old head |
| Insert at position (given pointer) | O(1) | Just redirect pointers |
| Delete (given pointer) | O(1) | Just redirect pointers |
Deep dive: Linked Lists page
Analogy: A stack of plates. You can only add or remove from the top.
Three operations, ALL O(1):
Why O(1)? You always know exactly where the top is. No searching needed.
Use cases: Undo/redo, browser back button, matching parentheses, function call stack, DFS.
Deep dive: Stacks & Queues page
Analogy: A line at a shop. First person in line gets served first.
Two operations, both O(1):
Use cases: BFS, task scheduling, printer queue, message queues.
Analogy: A magical librarian who instantly knows exactly which shelf any book is on.
A hash map stores key-value pairs and can find any value by its key in O(1) average time. How?
Step 1: Take the key (e.g., "Sean")
Step 2: Run it through a hash function that converts it to a number (e.g., 7)
Step 3: Use that number as the index in an array: storage[7] = "Sean's data"
To find "Sean" later, just hash the key again → get 7 → go directly to storage[7]. Instant.
| Operation | Average | Worst | Why Worst Case? |
|---|---|---|---|
| Insert | O(1) | O(n) | Hash collisions (many keys map to same index) |
| Lookup | O(1) | O(n) | Same reason |
| Delete | O(1) | O(n) | Same reason |
Deep dive: Hash Maps & Sets page
Analogy: A family tree, or an org chart, or your file system (folders inside folders).
A Binary Search Tree (BST) has a special rule: left child < parent < right child. This means searching is like binary search:
Looking for 60 in the tree above:
Start at 50. 60 > 50, so go right.
At 70. 60 < 70, so go left.
At 60. Found it!
Each comparison eliminates HALF the tree (either the left or right subtree). Same math as binary search: O(log n) for a balanced tree.
Deep dive: Trees & BST page
Analogy: A tournament bracket where the winner always rises to the top.
A min-heap always keeps the smallest element at the root. A max-heap keeps the largest. Both support:
| Operation | Time | Why |
|---|---|---|
| Get min/max | O(1) | It's always at the root |
| Insert | O(log n) | Add at bottom, bubble up (tree height = log n) |
| Extract min/max | O(log n) | Remove root, bubble down replacement |
Use cases: Priority queues, finding Kth largest/smallest, Dijkstra's algorithm, merge K sorted lists.
Deep dive: Advanced Topics page
Analogy: A social network (people are nodes, friendships are edges) or a road map (cities are nodes, roads are edges).
Graphs are the most general data structure. Trees are actually a special type of graph (connected, no cycles). Linked lists are a special type of tree (each node has at most one child).
Two ways to explore graphs:
Deep dive: Graphs page
Analogy: The autocomplete feature that shows suggestions as you type each letter.
Each node represents a letter. Paths from root to leaf form words. Finding all words that start with "ca" means just following c → a and then listing everything below.
Deep dive: Advanced Topics page
Requirement: The data must be sorted.
How it works: Compare the target with the middle element. If target is smaller, search the left half. If larger, search the right half. Repeat.
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1 # target is in right half
else:
right = mid - 1 # target is in left half
return -1 # not found
Analogy: Russian nesting dolls. Open a doll, find a smaller doll inside. Keep opening until you reach the smallest doll (the base case).
Every recursive function needs:
# Factorial: 5! = 5 × 4 × 3 × 2 × 1
def factorial(n):
if n <= 1: # base case
return 1
return n * factorial(n - 1) # recursive case
Analogy: Reading a book from both ends simultaneously, moving inward.
Use two pointers (usually start and end) and move them toward each other based on conditions. Works on sorted arrays or when you need to find pairs.
Deep dive: LeetCode Patterns page
Analogy: A magnifying glass sliding across a page, looking at a fixed-width section at a time.
Maintain a "window" of elements and slide it across the array, adding/removing one element at a time instead of recalculating everything.
Deep dive: LeetCode Patterns page
Analogy: Instead of recalculating your monthly expenses every time someone asks, you write the total on a sticky note and just read it next time.
DP = solving a problem by breaking it into smaller overlapping subproblems and remembering (memoizing) the results so you never solve the same subproblem twice.
Deep dive: Dynamic Programming page
The most common sorting algorithms and when to use them:
| Algorithm | Time | Space | Key Idea |
|---|---|---|---|
| Bubble Sort | O(n²) | O(1) | Repeatedly swap adjacent elements. Simple but slow. |
| Merge Sort | O(n log n) | O(n) | Divide in half, sort each half, merge. Stable and consistent. |
| Quick Sort | O(n log n)* | O(log n) | Pick pivot, partition around it. Fast in practice. |
| Counting Sort | O(n + k) | O(k) | Count occurrences. Only works for integers in a known range. |
Deep dive: Sorting & Searching page
Analogy: Russian nesting dolls. Open a doll, find a smaller doll inside. Keep opening until you reach the smallest doll (the base case).
Every recursive function needs:
Each recursive call solves a smaller version of the same problem. Eventually you reach the base case, then the results "unwind" back up.
Process elements one at a time, moving forward or backward.
Python - Reverse a String
class Solution:
def reverse_string(self, s: list[str]) -> None:
def solve(left: int, right: int):
if left >= right:
return
s[left], s[right] = s[right], s[left]
solve(left + 1, right - 1)
solve(0, len(s) - 1)
Split problem into two halves, process each recursively.
Python - Fibonacci
class Solution:
def fib(self, n: int) -> int:
if n <= 1:
return n
return self.fib(n - 1) + self.fib(n - 2)
Add caching to avoid recalculating the same subproblems.
Python - Fibonacci with Memo
class Solution:
def fib(self, n: int) -> int:
cache = {}
def solve(x: int) -> int:
if x in cache:
return cache[x]
if x <= 1:
return x
cache[x] = solve(x - 1) + solve(x - 2)
return cache[x]
return solve(n)
Recursively process left and right subtrees.
Python - Binary Tree Inorder Traversal
class Solution:
def inorder_traversal(self, root) -> list[int]:
result = []
def solve(node):
if not node:
return
solve(node.left)
result.append(node.val)
solve(node.right)
solve(root)
return result
When writing recursive functions, ask: "What is the smallest version of this problem I can solve directly?" That's your base case. Then ask: "How do I reduce the current problem to a smaller one?" That's your recursive case.
Analogy: Exploring a maze. You try a path, if it hits a dead end, you backtrack to the last junction and try a different path.
Backtracking = Recursion + Choice + Undo Choice. You explore all possibilities by making a choice, recursing, then undoing that choice to try another.
Python
class Solution:
def subsets(self, nums: list[int]) -> list[list[int]]:
result = []
def backtrack(start: int, path: list[int]):
result.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1, path)
path.pop()
backtrack(0, [])
return result
Python
class Solution:
def permute(self, nums: list[int]) -> list[list[int]]:
result = []
used = [False] * len(nums)
def backtrack(path: list[int]):
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if not used[i]:
used[i] = True
path.append(nums[i])
backtrack(path)
path.pop()
used[i] = False
backtrack([])
return result
Python
class Solution:
def combination_sum(self, candidates: list[int], target: int) -> list[list[int]]:
result = []
def backtrack(start: int, path: list[int], remaining: int):
if remaining == 0:
result.append(path[:])
return
if remaining < 0:
return
for i in range(start, len(candidates)):
path.append(candidates[i])
backtrack(i, path, remaining - candidates[i])
path.pop()
backtrack(0, [], target)
return result
Python
class Solution:
def solve_n_queens(self, n: int) -> list[list[str]]:
result = []
cols = set()
diag1 = set()
diag2 = set()
def backtrack(row: int, path: list[str]):
if row == n:
result.append(path[:])
return
for col in range(n):
if col in cols or (row - col) in diag1 or (row + col) in diag2:
continue
cols.add(col)
diag1.add(row - col)
diag2.add(row + col)
path.append("." * col + "Q" + "." * (n - col - 1))
backtrack(row + 1, path)
path.pop()
cols.remove(col)
diag1.remove(row - col)
diag2.remove(row + col)
backtrack(0, [])
return result
Analogy: Instead of counting books on each shelf every time someone asks, you precompute cumulative totals and just subtract to get any range.
Precompute a running sum array so you can answer any "sum from index i to j" query in O(1).
Python
class Solution:
def range_sum(self, nums: list[int], queries: list[list[int]]) -> list[int]:
prefix = [0] * (len(nums) + 1)
for i in range(len(nums)):
prefix[i + 1] = prefix[i] + nums[i]
result = []
for l, r in queries:
result.append(prefix[r + 1] - prefix[l])
return result
Python
class Solution:
def subarray_sum(self, nums: list[int], k: int) -> int:
count = 0
prefix = 0
prefix_counts = {0: 1}
for num in nums:
prefix += num
count += prefix_counts.get(prefix - k, 0)
prefix_counts[prefix] = prefix_counts.get(prefix, 0) + 1
return count
Python
class Solution:
def max_subarray_len(self, nums: list[int], k: int) -> int:
prefix = 0
prefix_index = {0: -1}
max_len = 0
for i, num in enumerate(nums):
prefix += num
if prefix - k in prefix_index:
max_len = max(max_len, i - prefix_index[prefix - k])
if prefix not in prefix_index:
prefix_index[prefix] = i
return max_len
Python
class Solution:
def corp_flight_bookings(self, bookings: list[list[int]], n: int) -> list[int]:
diff = [0] * (n + 1)
for start, end, seats in bookings:
diff[start - 1] += seats
diff[end] -= seats
result = [0] * n
for i in range(n):
result[i] = result[i - 1] + diff[i] if i > 0 else diff[i]
return result
Analogy: Similar to prefix sum, but instead of "how much from the start," you want "how much from the end."
Useful when you need to quickly answer queries like "sum from index i to end" or when processing from right to left.
Python
class Solution:
def suffix_sum(self, nums: list[int]) -> list[int]:
n = len(nums)
suffix = [0] * n
suffix[n - 1] = nums[n - 1]
for i in range(n - 2, -1, -1):
suffix[i] = nums[i] + suffix[i + 1]
return suffix
Python - Product of Array Except Self
class Solution:
def product_except_self(self, nums: list[int]) -> list[int]:
n = len(nums)
result = [1] * n
prefix = 1
for i in range(n):
result[i] = prefix
prefix *= nums[i]
suffix = 1
for i in range(n - 1, -1, -1):
result[i] *= suffix
suffix *= nums[i]
return result
Python
class Solution:
def min_path_sum(self, grid: list[list[int]]) -> int:
m, n = len(grid), len(grid[0])
for j in range(n - 1, -1, -1):
for i in range(m - 1, -1, -1):
if i == m - 1 and j == n - 1:
continue
if i == m - 1:
grid[i][j] += grid[i][j + 1]
elif j == n - 1:
grid[i][j] += grid[i + 1][j]
else:
grid[i][j] += min(grid[i + 1][j], grid[i][j + 1])
return grid[0][0]
Python
class Solution:
def longest_prefix(self, s: str) -> str:
n = len(s)
for length in range(n - 1, 0, -1):
if s[:length] == s[n - length:]:
return s[:length]
return ""
Suffix sum is just prefix sum from the right side. Use it when you need to process from end to start or when queries ask about "from position i to end."
Analogy: A security guard walking along a street, counting how many people are on the street at each moment. When someone enters, count goes up; when someone leaves, count goes down.
Convert 2D/interval problems into sorted 1D events. Sweep left to right, maintaining state.
Python
class Solution:
def min_meeting_rooms(self, intervals: list[list[int]]) -> int:
events = []
for start, end in intervals:
events.append((start, 1))
events.append((end, -1))
events.sort()
max_rooms = 0
current = 0
for _, delta in events:
current += delta
max_rooms = max(max_rooms, current)
return max_rooms
Python
class MyCalendarThree:
def __init__(self):
self.events = collections.defaultdict(int)
def book(self, start: int, end: int) -> int:
self.events[start] += 1
self.events[end] -= 1
active = 0
max_overlap = 0
for time in sorted(self.events):
active += self.events[time]
max_overlap = max(max_overlap, active)
return max_overlap
Python
class Solution:
def is_rectangle_overlap(self, rec1: list[int], rec2: list[int]) -> bool:
x1, y1, x2, y2 = rec1
x3, y3, x4, y4 = rec2
return not (x2 <= x3 or x4 <= x1 or y2 <= y3 or y4 <= y1)
Python
class Solution:
def car_pooling(self, trips: list[list[int]], capacity: int) -> bool:
events = []
for num, start, end in trips:
events.append((start, num))
events.append((end, -num))
events.sort()
current = 0
for _, delta in events:
current += delta
if current > capacity:
return False
return True
Analogy: Instead of recalculating your monthly budget every time someone asks, you write it on a sticky note and just read it next time.
Memoization = Recursion + Cache. Store results of expensive function calls and return cached result when the same inputs occur again.
Python
class Solution:
def climb_stairs(self, n: int) -> int:
cache = {}
def solve(i: int) -> int:
if i in cache:
return cache[i]
if i <= 2:
return i
cache[i] = solve(i - 1) + solve(i - 2)
return cache[i]
return solve(n)
Python
class Solution:
def rob(self, nums: list[int]) -> int:
cache = {}
def solve(i: int) -> int:
if i in cache:
return cache[i]
if i < 0:
return 0
result = max(solve(i - 1), solve(i - 2) + nums[i])
cache[i] = result
return result
return solve(len(nums) - 1)
Python
class Solution:
def unique_paths(self, m: int, n: int) -> int:
cache = {}
def solve(i: int, j: int) -> int:
if (i, j) in cache:
return cache[(i, j)]
if i >= m or j >= n:
return 0
if i == m - 1 and j == n - 1:
return 1
cache[(i, j)] = solve(i + 1, j) + solve(i, j + 1)
return cache[(i, j)]
return solve(0, 0)
Python
class Solution:
def unique_paths_with_obstacles(self, grid: list[list[int]]) -> int:
N = len(grid)
M = len(grid[0])
if grid[N - 1][M - 1] == 1:
return 0
cache = {}
def solve(x: int, y: int) -> int:
key = str(x) + "," + str(y)
if key in cache:
return cache[key]
if x < 0 or x >= N or y < 0 or y >= M:
return 0
if grid[x][y] == 1:
return 0
if x == N - 1 and y == M - 1:
return 1
cache[key] = solve(x + 1, y) + solve(x, y + 1)
return cache[key]
solve()
return cache["0,0"] if "0,0" in cache else 0
Python
class Solution:
def coin_change(self, coins: list[int], amount: int) -> int:
cache = {}
def solve(remaining: int) -> int:
if remaining in cache:
return cache[remaining]
if remaining == 0:
return 0
if remaining < 0:
return float('inf')
min_coins = float('inf')
for coin in coins:
min_coins = min(min_coins, solve(remaining - coin) + 1)
cache[remaining] = min_coins
return min_coins
result = solve(amount)
return result if result != float('inf') else -1
Python
class Solution:
def longest_common_subsequence(self, text1: str, text2: str) -> int:
cache = {}
def solve(i: int, j: int) -> int:
if (i, j) in cache:
return cache[(i, j)]
if i == len(text1) or j == len(text2):
return 0
if text1[i] == text2[j]:
cache[(i, j)] = solve(i + 1, j + 1) + 1
else:
cache[(i, j)] = max(solve(i + 1, j), solve(i, j + 1))
return cache[(i, j)]
return solve(0, 0)
Memoization turns O(2^n) exponential problems into O(n) or O(n*m) polynomial problems. The key is identifying what subproblem to cache. Ask: "What parameters uniquely define this subproblem?"
Analogy: Instead of starting from the top and working down (like memoization), you start from the bottom (base cases) and build up to the final answer iteratively.
Tabulation = Bottom-Up DP. No recursion. Just fill a table from base cases to final answer.
Python
class Solution:
def climb_stairs(self, n: int) -> int:
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
Python
class Solution:
def rob(self, nums: list[int]) -> int:
if not nums:
return 0
if len(nums) == 1:
return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
return dp[-1]
Python
class Solution:
def unique_paths(self, m: int, n: int) -> int:
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]
Python
class Solution:
def longest_common_subsequence(self, text1: str, text2: str) -> int:
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]
Python
class Solution:
def coin_change(self, coins: list[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
Python
class Solution:
def knapsack(self, weights: list[int], values: list[int], capacity: int) -> int:
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],
dp[i - 1][w - weights[i - 1]] + values[i - 1]
)
else:
dp[i][w] = dp[i - 1][w]
return dp[n][capacity]
Python
class Solution:
def min_distance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
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] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
return dp[m][n]
Use memoization when you can't determine the order of computation easily. Use tabulation when you know the order and want to avoid recursion overhead. Both give the same time complexity, but tabulation often uses less memory (can drop old rows).
Quick syntax reference for the most common DSA operations in both Python and Java.
# Create array
arr = [1, 2, 3, 4, 5]
# Access - O(1)
arr[0] # first element
arr[-1] # last element
# Length - O(1)
len(arr)
# Append - O(1) amortized
arr.append(6)
# Insert at index - O(n)
arr.insert(0, 0) # insert 0 at beginning
# Remove at index - O(n)
arr.pop() # remove last
arr.pop(0) # remove first
# Slicing - O(k)
arr[1:4] # elements at index 1,2,3
arr[:3] # first 3 elements
arr[2:] # from index 2 to end
# Sort - O(n log n)
arr.sort()
arr.sort(reverse=True)
sorted_arr = sorted(arr)
# Search - O(n) for linear, O(log n) for binary
5 in arr # linear search
# Binary search requires sorted array
import bisect
bisect.bisect_left(arr, 3) # returns index
# 2D Array
matrix = [[1, 2], [3, 4]]
matrix[0][1] # row 0, col 1 = 2
// Create array
int[] arr = {1, 2, 3, 4, 5};
int[] arr2 = new int[10];
// Access - O(1)
arr[0]; // first element
arr[arr.length - 1]; // last element
// Length - O(1)
arr.length
// ArrayList - dynamic array
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // O(1) amortized
list.get(0); // O(1)
list.remove(0); // O(n)
list.size(); // O(1)
// Sort - O(n log n)
Arrays.sort(arr);
Arrays.sort(arr, Collections.reverseOrder());
// Binary search - O(log n)
Arrays.binarySearch(arr, 3);
// 2D Array
int[][] matrix = {{1, 2}, {3, 4}};
matrix[0][1]; // row 0, col 1 = 2
# List is same as array above
# List comprehension
squares = [x*x for x in range(10)]
evens = [x for x in arr if x % 2 == 0]
# List operations
arr.extend([6, 7]) # add multiple
arr.remove(3) # remove first occurrence
arr.index(5) # find index
arr.count(5) # count occurrences
// ArrayList
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(0, 5); // insert at index 0
list.get(0);
list.set(0, 10); // update
list.remove(0); // by index
list.remove(Integer.valueOf(5)); // by value
list.contains(5);
list.indexOf(5);
list.size();
// Iterate
for (int x : list) { ... }
for (int i = 0; i < list.size(); i++) { ... }
# Create
hashmap = {"a": 1, "b": 2}
from collections import defaultdict
dd = defaultdict(int) # default value 0
# Access - O(1)
hashmap["a"]
hashmap.get("c", 0) # default if not found
# Insert/Update - O(1)
hashmap["c"] = 3
# Delete - O(1)
del hashmap["a"]
hashmap.pop("b")
# Check existence
"a" in hashmap
# Iterate
for key in hashmap:
print(key, hashmap[key])
for key, val in hashmap.items():
print(key, val)
# Get all keys/values
hashmap.keys()
hashmap.values()
hashmap.items()
// Create
HashMap<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
// Access - O(1)
map.get("a");
map.getOrDefault("c", 0);
// Insert/Update - O(1)
map.put("c", 3);
map.putIfAbsent("d", 4);
// Delete - O(1)
map.remove("a");
map.remove("b", 2); // only if key=value
// Check existence
map.containsKey("a");
map.containsValue(1);
// Iterate
for (String key : map.keySet()) { ... }
for (Integer val : map.values()) { ... }
for (Map.Entry<String, Integer> e : map.entrySet()) {
e.getKey();
e.getValue();
}
// Get all
map.keySet();
map.values();
map.entrySet();
# Create
s = {1, 2, 3}
s = set() # empty set
# Add - O(1)
s.add(4)
s.update([5, 6])
# Remove - O(1)
s.remove(3) # throws if not found
s.discard(3) # no error if not found
s.pop() # remove and return arbitrary
# Check
5 in s
# Set operations
s1 | s2 # union
s1 & s2 # intersection
s1 - s2 # difference
s1 ^ s2 # symmetric difference
// Create
HashSet<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);
// Add - O(1)
set.add(4);
// Remove - O(1)
set.remove(3);
// Check
set.contains(5);
// Set operations
HashSet<Integer> set1 = new HashSet<>(Arrays.asList(1,2,3));
HashSet<Integer> set2 = new HashSet<>(Arrays.asList(2,3,4));
set1.addAll(set2); // union
set1.retainAll(set2); // intersection
set1.removeAll(set2); // difference
# Stack (LIFO) - use list
stack = [1, 2, 3]
stack.append(4) # push - O(1)
stack.pop() # pop - O(1)
stack[-1] # peek - O(1)
# Queue (FIFO) - use collections.deque
from collections import deque
queue = deque([1, 2])
queue.append(3) # enqueue - O(1)
queue.popleft() # dequeue - O(1)
queue[0] # peek - O(1)
// Stack (LIFO)
Stack<Integer> stack = new Stack<>();
stack.push(1); // O(1)
stack.pop(); // O(1)
stack.peek(); // O(1)
// Queue (FIFO)
Queue<Integer> queue = new LinkedList<>();
queue.add(1); // enqueue - O(1)
queue.remove(); // dequeue - O(1)
queue.peek(); // peek - O(1)
// Priority Queue (Heap)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
minHeap.add(5);
minHeap.poll(); // get min - O(log n)
minHeap.peek();
# Create
s = "hello"
# Access - O(1)
s[0]
# Length - O(1)
len(s)
# Find - O(n)
s.find("ll") # returns index or -1
s.index("ll") # throws if not found
# Slice - O(k)
s[1:4]
# Split/Join - O(n)
s.split(",")
",".join(["a", "b"])
# Replace - O(n)
s.replace("l", "x")
# Check
s.startswith("he")
s.endswith("lo")
s.isdigit()
s.isalpha()
# Convert
s.upper()
s.lower()
s.strip()
// Create
String s = "hello";
// Access - O(1)
s.charAt(0);
// Length - O(1)
s.length();
// Find - O(n)
s.indexOf("ll"); // returns index or -1
s.lastIndexOf("l");
// Substring - O(k)
s.substring(1, 4); // [1, 4)
// Split/Join
s.split(",");
String.join(",", Arrays.asList("a", "b"));
// Replace
s.replace("l", "x");
s.replaceAll("regex", "x");
// Check
s.startsWith("he");
s.endsWith("lo");
Character.isDigit(s.charAt(0));
Character.isLetter(s.charAt(0));
// Convert
s.toUpperCase();
s.toLowerCase();
s.trim();
// StringBuilder (mutable, efficient)
StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.toString();
# For loop
for i in range(10): # 0 to 9
for i in range(1, 10): # 1 to 9
for i in range(0, 10, 2): # 0,2,4,6,8
# For each
for x in arr:
print(x)
# Enumerate (index + value)
for i, val in enumerate(arr):
print(i, val)
# While
while condition:
...
# List comprehension
doubled = [x*2 for x in arr]
// For loop
for (int i = 0; i < 10; i++) { ... }
// Enhanced for
for (int x : arr) { ... }
// While
while (condition) { ... }
// Stream (Java 8+)
Arrays.stream(arr)
.filter(x -> x % 2 == 0)
.map(x -> x * 2)
.toArray();
import heapq
import collections
# Min Heap
heap = [3, 1, 4]
heapq.heapify(heap) # O(n)
heapq.heappush(heap, 2) # O(log n)
heapq.heappop(heap) # O(log n)
# Counter (frequency)
counter = collections.Counter(arr)
counter.most_common(3) # top 3
# Max/Min
max(arr)
min(arr)
max(arr, key=len)
# Sum/Product
sum(arr)
prod(arr) # Python 3.8+
# Reversed
reversed(arr)
reversed(range(10))
# Infinite
float('inf')
-float('inf')
import java.util.*;
// Min Heap
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// Max Heap
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// Counter - use HashMap
HashMap<Integer, Integer> counter = new HashMap<>();
for (int x : arr) {
counter.put(x, counter.getOrDefault(x, 0) + 1);
}
// Max/Min
Collections.max(list);
Collections.min(list);
Arrays.stream(arr).max().getAsInt();
// Sum
Arrays.stream(arr).sum();
// Reverse
Collections.reverse(list);
// For array: convert to list, reverse, convert back
// Infinity
Integer.MAX_VALUE;
Integer.MIN_VALUE;
Double.POSITIVE_INFINITY;
Double.NEGATIVE_INFINITY;
For LeetCode: Use Python for faster coding, Java for more structure. Both are equally accepted in interviews. Pick one and stick with it!
This is where most people get stuck -- not because they don't know data structures, but because they don't have a systematic approach to problem-solving.
After solving 100+ problems, you'll start recognizing patterns instantly. "Oh, this is a sliding window problem." "This needs a monotonic stack." That recognition comes from practice, not theory. It's why grinding LeetCode works -- you're training pattern recognition.
Follow this order. Each topic builds on the previous one.