Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
# 🧠 Python DSA — Data Structures & Algorithms

[![Tests](https://github.com/kelsonbrito50/python-dsa/actions/workflows/ci.yml/badge.svg)](https://github.com/kelsonbrito50/python-dsa/actions) ![Algorithms](https://img.shields.io/badge/algorithms-20+-blue) ![Tests](https://img.shields.io/badge/tests-40%2B%20passed-brightgreen)
[![Tests](https://github.com/kelsonbrito50/python-dsa/actions/workflows/ci.yml/badge.svg)](https://github.com/kelsonbrito50/python-dsa/actions) ![Algorithms](https://img.shields.io/badge/algorithms-30+-blue) ![Tests](https://img.shields.io/badge/tests-56%20passed-brightgreen)

My study notes and implementations while preparing for technical interviews.
Interview prep implementations with tests and complexity analysis.

## Implementations

| Category | Structure/Algorithm | Complexity | File |
|----------|-------------------|------------|------|
| Category | Algorithm | Complexity | File |
|----------|-----------|------------|------|
| 📦 Arrays | Two Sum (hash map + two-pointer) | O(n) | [`arrays/two_sum.py`](src/arrays/two_sum.py) |
| 📦 Arrays | Sliding Window (max sum, unique substr) | O(n) | [`arrays/sliding_window.py`](src/arrays/sliding_window.py) |
| 📦 Arrays | Kadane's Algorithm (max subarray) | O(n) | [`arrays/kadane.py`](src/arrays/kadane.py) |
| 📦 Arrays | Two Pointers (sorted sum, dedup, palindrome) | O(n) | [`arrays/two_pointers.py`](src/arrays/two_pointers.py) |
| 🔤 Strings | Anagram, Reverse Words, First Unique, LCP | O(n) | [`strings/manipulation.py`](src/strings/manipulation.py) |
| 🔗 Linked Lists | Singly (push, pop, find, reverse) | O(1)/O(n) | [`linked_lists/singly.py`](src/linked_lists/singly.py) |
| 📚 Stacks | Stack + Valid Parentheses | O(1) | [`stacks/stack.py`](src/stacks/stack.py) |
| 📬 Queues | Queue (deque-based) | O(1) | [`queues/queue.py`](src/queues/queue.py) |
| #️⃣ Hash Maps | Custom HashMap (open addressing) | O(1) avg | [`hashmaps/hashmap.py`](src/hashmaps/hashmap.py) |
| 🌳 Trees | BST (insert, search, in-order) | O(log n) | [`trees/bst.py`](src/trees/bst.py) |
| ⛰️ Heaps | Kth Largest, Top K Frequent, Merge K Sorted | O(n log k) | [`heaps/priority_queue.py`](src/heaps/priority_queue.py) |
| 🌐 Graphs | BFS, DFS iterative + recursive | O(V+E) | [`graphs/traversals.py`](src/graphs/traversals.py) |
| 📊 Sorting | Quick Sort (in-place + functional) | O(n log n) | [`sorting/quick_sort.py`](src/sorting/quick_sort.py) |
| 📊 Sorting | Merge Sort (divide & conquer) | O(n log n) | [`sorting/merge_sort.py`](src/sorting/merge_sort.py) |
| 📊 Sorting | Heap Sort (in-place) | O(n log n) | [`sorting/heap_sort.py`](src/sorting/heap_sort.py) |
| 🔍 Searching | Binary Search (iterative + recursive) | O(log n) | [`searching/binary_search.py`](src/searching/binary_search.py) |
| 🧮 Matrix | Rotate 90°, Spiral Order | O(n²) | [`matrix/operations.py`](src/matrix/operations.py) |
| 🔄 Dynamic Programming | Fibonacci (memo + bottom-up), Climbing Stairs | O(n) | [`dynamic_programming/fibonacci.py`](src/dynamic_programming/fibonacci.py) |
| 🔙 Backtracking | Permutations, Subsets (power set) | O(n!/2^n) | [`backtracking/permutations.py`](src/backtracking/permutations.py) |

## Running

Expand All @@ -30,11 +36,13 @@ python -m pytest tests/ -v

## What I Learned

- **Hash maps** solve most "find pair" problems in O(n) — always consider them first
- **Sliding window** eliminates nested loops for subarray problems
- **Two pointers** eliminates O(n²) brute force on sorted arrays — always ask "is it sorted?"
- **Sliding window** replaces nested loops for subarray/substring problems
- **Kadane's** is just a clever rolling max — once you see it, you can't unsee it
- **BFS = shortest path** in unweighted graphs, **DFS = explore everything**
- **Quick sort** is faster in practice than merge sort despite same Big O (cache locality)
- **Dynamic programming** = recursion + memoization — start top-down, optimize bottom-up
- **Backtracking** = DFS on a decision tree — add, recurse, undo
- **Heaps** solve any "top K" problem efficiently — `heapq` in Python is a min-heap
- **Valid parentheses** is the canonical stack problem — learn the pattern, solve 10 variants

## License
Expand Down
47 changes: 47 additions & 0 deletions src/arrays/two_pointers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Two Pointers — Efficient array traversal.

Time: O(n) — single pass
Space: O(1) — in-place
"""


def two_sum_sorted(nums: list[int], target: int) -> tuple[int, int]:
"""Find two indices in SORTED array that sum to target. O(n)."""
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == target:
return (left, right)
elif total < target:
left += 1
else:
right -= 1
return (-1, -1)


def remove_duplicates(nums: list[int]) -> int:
"""Remove duplicates in-place from sorted array. Returns new length. O(n)."""
if not nums:
return 0
write = 1
for read in range(1, len(nums)):
if nums[read] != nums[read - 1]:
nums[write] = nums[read]
write += 1
return write


def is_palindrome(s: str) -> bool:
"""Check if string is palindrome (ignoring non-alphanumeric). O(n)."""
left, right = 0, len(s) - 1
while left < right:
while left < right and not s[left].isalnum():
left += 1
while left < right and not s[right].isalnum():
right -= 1
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return True
Empty file added src/backtracking/__init__.py
Empty file.
38 changes: 38 additions & 0 deletions src/backtracking/permutations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
Backtracking — Permutations and Subsets.

Permutations: O(n!) time
Subsets: O(2^n) time
"""


def permutations(nums: list[int]) -> list[list[int]]:
"""Generate all permutations. O(n!)."""
result: list[list[int]] = []

def backtrack(path: list[int], remaining: list[int]) -> None:
if not remaining:
result.append(path[:])
return
for i in range(len(remaining)):
path.append(remaining[i])
backtrack(path, remaining[:i] + remaining[i + 1:])
path.pop()

backtrack([], nums)
return result


def subsets(nums: list[int]) -> list[list[int]]:
"""Generate all subsets (power set). O(2^n)."""
result: list[list[int]] = []

def backtrack(start: int, path: list[int]) -> None:
result.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1, path)
path.pop()

backtrack(0, [])
return result
Empty file.
38 changes: 38 additions & 0 deletions src/dynamic_programming/fibonacci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
Fibonacci — Classic DP problem.

Recursive: O(2^n) — DON'T
Memoized: O(n) time, O(n) space
Bottom-up: O(n) time, O(1) space ← optimal
"""


def fibonacci_memo(n: int, memo: dict[int, int] | None = None) -> int:
"""Top-down with memoization. O(n) time, O(n) space."""
if memo is None:
memo = {}
if n <= 1:
return n
if n not in memo:
memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
return memo[n]


def fibonacci_dp(n: int) -> int:
"""Bottom-up DP. O(n) time, O(1) space."""
if n <= 1:
return n
prev, curr = 0, 1
for _ in range(2, n + 1):
prev, curr = curr, prev + curr
return curr


def climbing_stairs(n: int) -> int:
"""Number of ways to climb n stairs (1 or 2 steps). O(n) time, O(1) space."""
if n <= 2:
return n
prev, curr = 1, 2
for _ in range(3, n + 1):
prev, curr = curr, prev + curr
return curr
Empty file added src/heaps/__init__.py
Empty file.
39 changes: 39 additions & 0 deletions src/heaps/priority_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Heap / Priority Queue — Top K problems.

Kth Largest: O(n log k)
Merge K Sorted: O(n log k)
"""

import heapq


def kth_largest(nums: list[int], k: int) -> int:
"""Find kth largest element. O(n log k) using min-heap."""
return heapq.nlargest(k, nums)[-1]


def top_k_frequent(nums: list[int], k: int) -> list[int]:
"""Find k most frequent elements. O(n log k)."""
from collections import Counter
count = Counter(nums)
return [x for x, _ in count.most_common(k)]


def merge_k_sorted(lists: list[list[int]]) -> list[int]:
"""Merge k sorted lists. O(n log k)."""
result: list[int] = []
heap: list[tuple[int, int, int]] = []

for i, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst[0], i, 0))

while heap:
val, list_idx, elem_idx = heapq.heappop(heap)
result.append(val)
if elem_idx + 1 < len(lists[list_idx]):
next_val = lists[list_idx][elem_idx + 1]
heapq.heappush(heap, (next_val, list_idx, elem_idx + 1))

return result
Empty file added src/matrix/__init__.py
Empty file.
44 changes: 44 additions & 0 deletions src/matrix/operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
Matrix Operations — 2D array problems.

Rotate: O(n²) time, O(1) space (in-place)
Spiral: O(m*n) time, O(1) space
"""


def rotate_90(matrix: list[list[int]]) -> None:
"""Rotate matrix 90° clockwise in-place. O(n²)."""
n = len(matrix)
# Transpose
for i in range(n):
for j in range(i + 1, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
# Reverse each row
for row in matrix:
row.reverse()


def spiral_order(matrix: list[list[int]]) -> list[int]:
"""Return elements in spiral order. O(m*n)."""
if not matrix:
return []
result: list[int] = []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1

while top <= bottom and left <= right:
for col in range(left, right + 1):
result.append(matrix[top][col])
top += 1
for row in range(top, bottom + 1):
result.append(matrix[row][right])
right -= 1
if top <= bottom:
for col in range(right, left - 1, -1):
result.append(matrix[bottom][col])
bottom -= 1
if left <= right:
for row in range(bottom, top - 1, -1):
result.append(matrix[row][left])
left += 1
return result
Empty file added src/strings/__init__.py
Empty file.
40 changes: 40 additions & 0 deletions src/strings/manipulation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
String Problems — Common interview patterns.

Anagram: O(n) with counter
Reverse Words: O(n)
"""

from collections import Counter


def is_anagram(s: str, t: str) -> bool:
"""Check if two strings are anagrams. O(n)."""
return Counter(s) == Counter(t)


def reverse_words(s: str) -> str:
"""Reverse words in a string. O(n)."""
return " ".join(s.split()[::-1])


def first_unique_char(s: str) -> int:
"""Find index of first non-repeating character. O(n)."""
count = Counter(s)
for i, char in enumerate(s):
if count[char] == 1:
return i
return -1


def longest_common_prefix(strs: list[str]) -> str:
"""Find longest common prefix. O(n*m)."""
if not strs:
return ""
prefix = strs[0]
for s in strs[1:]:
while not s.startswith(prefix):
prefix = prefix[:-1]
if not prefix:
return ""
return prefix
12 changes: 12 additions & 0 deletions tests/test_backtracking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from src.backtracking.permutations import permutations, subsets

def test_permutations():
result = permutations([1, 2, 3])
assert len(result) == 6
assert [1, 2, 3] in result

def test_subsets():
result = subsets([1, 2])
assert len(result) == 4
assert [] in result
assert [1, 2] in result
14 changes: 14 additions & 0 deletions tests/test_fibonacci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from src.dynamic_programming.fibonacci import fibonacci_memo, fibonacci_dp, climbing_stairs

def test_fib_memo():
assert fibonacci_memo(10) == 55
assert fibonacci_memo(0) == 0
assert fibonacci_memo(1) == 1

def test_fib_dp():
assert fibonacci_dp(10) == 55
assert fibonacci_dp(20) == 6765

def test_climbing_stairs():
assert climbing_stairs(3) == 3
assert climbing_stairs(5) == 8
10 changes: 10 additions & 0 deletions tests/test_heaps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from src.heaps.priority_queue import kth_largest, top_k_frequent, merge_k_sorted

def test_kth_largest():
assert kth_largest([3, 2, 1, 5, 6, 4], 2) == 5

def test_top_k():
assert set(top_k_frequent([1, 1, 1, 2, 2, 3], 2)) == {1, 2}

def test_merge_k():
assert merge_k_sorted([[1, 4, 5], [1, 3, 4], [2, 6]]) == [1, 1, 2, 3, 4, 4, 5, 6]
10 changes: 10 additions & 0 deletions tests/test_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from src.matrix.operations import rotate_90, spiral_order

def test_rotate():
m = [[1, 2], [3, 4]]
rotate_90(m)
assert m == [[3, 1], [4, 2]]

def test_spiral():
m = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
assert spiral_order(m) == [1, 2, 3, 6, 9, 8, 7, 4, 5]
8 changes: 1 addition & 7 deletions tests/test_sorting.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from src.sorting.quick_sort import quick_sort, quick_sort_inplace
from src.sorting.merge_sort import merge_sort, merge_sort_inplace
from src.sorting.merge_sort import merge_sort


def test_quick_sort():
Expand Down Expand Up @@ -30,9 +30,3 @@ def test_merge_sort_single():

def test_merge_sort_already_sorted():
assert merge_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]


def test_merge_sort_inplace():
arr = [5, 3, 8, 1, 2]
merge_sort_inplace(arr)
assert arr == [1, 2, 3, 5, 8]
15 changes: 15 additions & 0 deletions tests/test_strings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from src.strings.manipulation import is_anagram, reverse_words, first_unique_char, longest_common_prefix

def test_anagram():
assert is_anagram("anagram", "nagaram") is True
assert is_anagram("rat", "car") is False

def test_reverse_words():
assert reverse_words("the sky is blue") == "blue is sky the"

def test_first_unique():
assert first_unique_char("leetcode") == 0
assert first_unique_char("aabb") == -1

def test_common_prefix():
assert longest_common_prefix(["flower", "flow", "flight"]) == "fl"
Loading