
Leetcode and DSA.
Great pain.
I’ve been doing this for quite long, but I’d like to improve my intuition. My general feel is that leetcode is getting a lot less important as Claude Code and other tools become more robust. Which is both good and bad.

I’ve done quite a lot of leetcode in the past, but none recently. Previously when I learned how to do leetcode with Python, I found it useful to do a lot of easy problems to remind myself of the syntax. Some of these snippets are from leetcode, some of it is from other sources, some of them I made up.
I’m going to write this in Python, and mayyybe I’ll come back and do it in C++. Probably not. I’m going to treat this page as the dumping ground for the first 150, standard problems. This is probably quite a boring page, unfortunately.
Python
Syntax
List basics
# Say you have some list
items = [5, 4, 3, 2, 1]
# This builds a list list [(1, 0), (2, 0), ...]
indexed = [(x, i) for i, x in enumerate(items)]
# This sort can be a bit slow because it needs to do a copy
sorted_indexed = sorted(indexed)
# This is inplace, but ruins the original
indexed.sort()
# You can also filter while doing list comprehension
no_three = [x for x in items if x != 3]
evens = [x for x in items if x % 2 == 0]
# Slicing is pretty straight forwards. The start is inclusive, the end is exclusive
sliced = items[1:3] # [4, 3]
sliced = items[:2] # [3, 2, 1]
sliced = items[-1] # the last element
sliced = items[::2] # 2nd element. I don't use this. Hard to read.
sliced = items[::-1] # Reversed. Again, I don't use this.
# Unpacking - this is a cool idea, but it costs O(n). so. doesn't make sense to use, most of the time.
first, *rest = items
first, *middle, last = items
# Functions
items.append(6)
items.extend([7, 8])
items.insert(0, 9) # insert 9 at index 0
last = items.pop()
items.pop(0) # at index
items.remove(3) # removes the first three
first_idx = items.index(3) # index of the first 3, O(n)
freq = items.count(3) # how many 3s
items.clear()
items = [5, 4, 3, 2, 1] # reset it...
cpy = items.copy()
# inplace reverse
items.reverse()
items = list(reversed(items)) # copy reverse
total = sum(items)
min_val, max_val = min(items), max(items)
three_exists = any(x == 3 for x in items)
everything_positive = all(x >= 0 for x in items)
# zipping
names = [str(x) for x in items]
for name, val in zip(names, items):
print(name, val)
# There's also map and filter functions, but list comprehension is preferred
val = list(filter(lambda x: x > 2, items))
val = list(map, lambda x: x * 2, items)
# can probably tell why since those are kinda grossASCII Letters
import string
# Letters
string.ascii_letters # 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
string.ascii_lowercase # 'abcdefghijklmnopqrstuvwxyz'
string.ascii_uppercase # 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
# Digits
string.digits # '0123456789'
# Alphanumeric (combine letters + digits)
string.ascii_letters + string.digits
# Punctuation
string.punctuation # '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
# Whitespace
string.whitespace # ' \t\n\r\x0b\x0c'
# Everything printable
string.printable # letters + digits + punctuation + whitespaceStrings
s = 'example string' # I think pep8 says single quotes are better
s2 = s + ' foo' # O(n + m), strings are immutable
# usually you'll want to do a join style thing with strings
s3 = " ".join(['one', 'two', 'three'])
# splitting
spl = s.split(' ')
s = s.split('e')
s = s.split()
s.replace('o', '0')
# Finds first index
# this is preferable to .index, because .index will throw an error
# if the item doesn't exist
# find just returns -1
f = s.find('e')
lowercase = s.lower()
uppercase = s.upper()
stripped = s.strip()
counts = s.count('e')
# strings are also kinda just lists so, you can do most of the list tricks above
# I won't dive into it
formatted = f'this is a {s} formatted string'
# Reversing is a pain, if you reverse using reversed() it'll
# return an iterator, and if you turn this iterator into a string
# it'll log like '<reversed object at 0x7f1231f5c250>' hahaha
# so you can use the [::-1] trick
reverse_string = formatted[::-1]
# maybe that's good in the future as well because it keeps it a list...
# You can left and right pad using rjust and ljust
# you can remember this as 'justify' in text alignment
v = "hhh"
a = v.rjust(5, '0') # hhh00
b = v.ljust(5, '0') # 00hhhCounters
# Usually this isn't necessary
from collections import Counter
items = list(range(10))
count = Counter(items)
# 2nd most common element
mc = count.most_common(2)
# O(n log k), but n is the index you're looking for. it maintains a heap and pops the stuff out
# adds these numbers in
count.update([5, 5, 1])
count += Counter([5, 5, 1])
# Be careful though, if you do this it duplicates the counter
# and wastes a bunch of space
count = count + Counter[(5, 5, 1)]
# Counts can intersect and union too
c1 = count1 & count2 # intersect; takes the minimum counts of the groups
c2 = count1 | count2 # maximum counts of the groupsDictionaries
d = {5: 'five', 4: 'four', 3: 'three'}
d = {}
five = d[5]
d[6] = 'six'
del d[5]
val = d.pop(4) # returns it, then pops it out
# get with a default
val = d.get(7, 'not found')
val = d.pop(4, 'not found')
d.keys()
d.values()
d.items()
# checks if its in the .keys(), useful for iterating
exist = 5 in d
d.update({6: 'six', 7: 'seven'})
d.clear()Default dictionaries
# Default dictionaries confused me really hard when I was coming from python
# particularly that if you touch them, it inserts the element
# This isn't always necessary
from collections import defaultdict
counts = defaultdict(int)
words = defaultdict(list)
# the big thing is that you don't need to insert
# updating a value inserts the default value, then updates it
counts[5] += 1
# If you feel like dodging the insert, you can get
counts.get(5, 0)
# only sets the key if its missing
counts.setdefault(5, 0)Heaps
# I heard there's a new min heap or max heap in python 3.14 or so
# but that's so new, I'm just going to ignore it
from heapq import * # not always necessary
items = [5, 4, 3, 2, 1]
heapify(items) # O(n)
heappush(items, 0)
x = heappop(items)
peek = items[0]
# you can use negatives to make a max heap
# if you want a lambda but remember your keys, use a tuple and put a
# custom value at the front
# also nlargest is pretty cool, here's a solution for kClosest
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
form = lambda x, y: x**2 + y**2
dist = [(form(x, y), (x, y)) for x, y in points]
return [(x, y) for _, (x, y) in nsmallest(k, dist)]Standard DSA Patterns
This section is quite boring. These are things you should just “know”.
Two Pointers
def fn(arr):
left = ans = 0
right = len(arr) - 1
while left < right:
# do some logic here with left and right
if CONDITION:
left += 1
else:
right -= 1
return ansTwo pointers, two inputs and exhuast both
def fn(arr1, arr2):
i = j = ans = 0
while i < len(arr1) and j < len(arr2):
# do some logic here
if CONDITION:
i += 1
else:
j += 1
while i < len(arr1):
# do logic
i += 1
while j < len(arr2):
# do logic
j += 1
return ans
Sliding window
def fn(arr):
left = ans = curr = 0
for right in range(len(arr)):
# do logic here to add arr[right] to curr
while WINDOW_CONDITION_BROKEN:
# remove arr[left] from curr
left += 1
# update ans
return ansPrefix sum
def fn(arr):
prefix = [arr[0]]
for i in range(1, len(arr)):
prefix.append(prefix[-1] + arr[i])
return prefixString building
# arr is a list of characters
def fn(arr):
ans = []
for c in arr:
ans.append(c)
return "".join(ans)Fast+slow
def fn(head):
slow = head
fast = head
ans = 0
while fast and fast.next:
# do logic
slow = slow.next
fast = fast.next.next
return ansFrequency maps
from collections import defaultdict
def fn(arr, k):
counts = defaultdict(int)
counts[0] = 1
ans = curr = 0
for num in arr:
# do logic to change curr
ans += counts[curr - k]
counts[curr] += 1
return ansMonotonic stack
def fn(arr):
stack = []
ans = 0
for num in arr:
# for monotonic decreasing, just flip the > to <
while stack and stack[-1] > num:
# do logic
stack.pop()
stack.append(num)
return ansRecursive DFS
def dfs(root):
if not root:
return
ans = 0
# do logic
dfs(root.left)
dfs(root.right)
return ansBFS
from collections import deque
def fn(root):
queue = deque([root])
ans = 0
while queue:
current_length = len(queue)
# do logic for current level
for _ in range(current_length):
node = queue.popleft()
# do logic
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return ansLooping recursive DFS
def fn(graph):
def dfs(node):
ans = 0
# do some logic
for neighbor in graph[node]:
if neighbor not in seen:
seen.add(neighbor)
ans += dfs(neighbor)
return ans
seen = {START_NODE}
return dfs(START_NODE)Iterative DFS
def fn(graph):
stack = [START_NODE]
seen = {START_NODE}
ans = 0
while stack:
node = stack.pop()
# do some logic
for neighbor in graph[node]:
if neighbor not in seen:
seen.add(neighbor)
stack.append(neighbor)
return ansGraph BFS
from collections import deque
def fn(graph):
queue = deque([START_NODE])
seen = {START_NODE}
ans = 0
while queue:
node = queue.popleft()
# do some logic
for neighbor in graph[node]:
if neighbor not in seen:
seen.add(neighbor)
queue.append(neighbor)
return ansTop K elements
import heapq
def fn(arr, k):
heap = []
for num in arr:
# do some logic to push onto heap according to problem's criteria
heapq.heappush(heap, (CRITERIA, num))
if len(heap) > k:
heapq.heappop(heap)
return [num for num in heap]Binary search - DSA 2 - Preventing Binary Search Confusion
class Solution:
def search(self, nums: List[int], target: int) -> int:
l, u = 0, len(nums)
while u - l > 1:
mid = (l + u) // 2
midv = nums[mid]
if midv <= target:
l = mid
else:
u = mid
return l if nums[l] == target else -1Backtracking
def backtrack(curr, OTHER_ARGUMENTS...):
if (BASE_CASE):
# modify the answer
return
ans = 0
for (ITERATE_OVER_INPUT):
# modify the current state
ans += backtrack(curr, OTHER_ARGUMENTS...)
# undo the modification of the current state
return ansDP
def fn(arr):
def dp(STATE):
if BASE_CASE:
return 0
if STATE in memo:
return memo[STATE]
ans = RECURRENCE_RELATION(STATE)
memo[STATE] = ans
return ans
memo = {}
return dp(STATE_FOR_WHOLE_INPUT)Trie
# note: using a class is only necessary if you want to store data at each node.
# otherwise, you can implement a trie using only hash maps.
class TrieNode:
def __init__(self):
# you can store data at nodes if you wish
self.data = None
self.children = {}
def fn(words):
root = TrieNode()
for word in words:
curr = root
for c in word:
if c not in curr.children:
curr.children[c] = TrieNode()
curr = curr.children[c]
# at this point, you have a full word at curr
# you can perform more logic here to give curr an attribute if you want
return rootDijkstra
from math import inf
from heapq import *
distances = [inf] * n
distances[source] = 0
heap = [(0, source)]
while heap:
curr_dist, node = heappop(heap)
if curr_dist > distances[node]:
continue
for nei, weight in graph[node]:
dist = curr_dist + weight
if dist < distances[nei]:
distances[nei] = dist
heappush(heap, (dist, nei))Reversing a linked list
Explaining how to reverse a list is terrible, but writing the code is easy. You need to remember that you need two variables, and you start with a temporary one
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
cur, prev = head, None
while cur:
temp = cur.next
cur.next = prev
prev = cur
cur = temp
return prevLook at that pattern. We go temp → cur.net → prev → cur → temp. That is, every line ends with the start of the next line. From there it’s obvious.
- We have to start with temp
- What are we going to forget when we move forwards; what’s temp for? Ah, cur.next
- What do we want cur.next to become? Ah, the previous
- What do we want previous to become? Ah, the current,
- Oh right we stored cur in the temp
It’s a bit cooked, but this method let me one-shot the problem even though I haven’t seen it in a year or thought about it for that long.
Boyer-Moore voting algorithm
This is for majority element. Its the obscure approach, and probably nobody will ask you this question while expecting this answer. It’s with space. I didn’t remember it at all while doing the problem.
class Solution:
def majorityElement(self, nums):
count = 0
candidate = None
for num in nums:
if count == 0:
candidate = num
count += 1 if num == candidate else -1
return candidateThis has the explanation. Quite funny, the opening paragraph simply says “If we had a way of counting instances of the majority element as , and instances of other elements as , summing them would make it obvious that the majority element is indeed the majority element”. I guess the non-intuitive thing is that this still holds even when other numbers are mixed in. Say your first number is your target, then of the array is random numbers, and the remainder is the target. You’ll be initially good, then bad, then the number at the end will make it okay again.
zyros