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 gross

ASCII 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 + whitespace

Strings

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') # 00hhh

Counters

# 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 groups

Dictionaries

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 ans

Two 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 ans

Prefix sum

def fn(arr):
    prefix = [arr[0]]
    for i in range(1, len(arr)):
        prefix.append(prefix[-1] + arr[i])
    
    return prefix

String 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 ans

Frequency 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 ans

Monotonic 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 ans

Recursive DFS

def dfs(root):
    if not root:
        return
    
    ans = 0
 
    # do logic
    dfs(root.left)
    dfs(root.right)
    return ans

BFS

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 ans

Looping 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 ans

Graph 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 ans

Top 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 -1

Backtracking

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 ans

DP

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 root

Dijkstra

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 prev

Look 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.

  1. We have to start with temp
  2. What are we going to forget when we move forwards; what’s temp for? Ah, cur.next
  3. What do we want cur.next to become? Ah, the previous
  4. What do we want previous to become? Ah, the current,
  5. 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 candidate

This 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.