diff --git a/README.md b/README.md index 1043381..446ad75 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/src/arrays/two_pointers.py b/src/arrays/two_pointers.py new file mode 100644 index 0000000..a2380dc --- /dev/null +++ b/src/arrays/two_pointers.py @@ -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 diff --git a/src/backtracking/__init__.py b/src/backtracking/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backtracking/permutations.py b/src/backtracking/permutations.py new file mode 100644 index 0000000..e531dfe --- /dev/null +++ b/src/backtracking/permutations.py @@ -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 diff --git a/src/dynamic_programming/__init__.py b/src/dynamic_programming/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/dynamic_programming/fibonacci.py b/src/dynamic_programming/fibonacci.py new file mode 100644 index 0000000..bf17e18 --- /dev/null +++ b/src/dynamic_programming/fibonacci.py @@ -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 diff --git a/src/heaps/__init__.py b/src/heaps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/heaps/priority_queue.py b/src/heaps/priority_queue.py new file mode 100644 index 0000000..ba68f40 --- /dev/null +++ b/src/heaps/priority_queue.py @@ -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 diff --git a/src/matrix/__init__.py b/src/matrix/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/matrix/operations.py b/src/matrix/operations.py new file mode 100644 index 0000000..4d9ed2c --- /dev/null +++ b/src/matrix/operations.py @@ -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 diff --git a/src/strings/__init__.py b/src/strings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/strings/manipulation.py b/src/strings/manipulation.py new file mode 100644 index 0000000..5d0216e --- /dev/null +++ b/src/strings/manipulation.py @@ -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 diff --git a/tests/test_backtracking.py b/tests/test_backtracking.py new file mode 100644 index 0000000..b97e76d --- /dev/null +++ b/tests/test_backtracking.py @@ -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 diff --git a/tests/test_fibonacci.py b/tests/test_fibonacci.py new file mode 100644 index 0000000..7df43c8 --- /dev/null +++ b/tests/test_fibonacci.py @@ -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 diff --git a/tests/test_heaps.py b/tests/test_heaps.py new file mode 100644 index 0000000..e0187de --- /dev/null +++ b/tests/test_heaps.py @@ -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] diff --git a/tests/test_matrix.py b/tests/test_matrix.py new file mode 100644 index 0000000..f227deb --- /dev/null +++ b/tests/test_matrix.py @@ -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] diff --git a/tests/test_sorting.py b/tests/test_sorting.py index ee313b1..77bfde0 100644 --- a/tests/test_sorting.py +++ b/tests/test_sorting.py @@ -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(): @@ -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] diff --git a/tests/test_strings.py b/tests/test_strings.py new file mode 100644 index 0000000..cc90e9f --- /dev/null +++ b/tests/test_strings.py @@ -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" diff --git a/tests/test_two_pointers.py b/tests/test_two_pointers.py new file mode 100644 index 0000000..48556ab --- /dev/null +++ b/tests/test_two_pointers.py @@ -0,0 +1,13 @@ +from src.arrays.two_pointers import two_sum_sorted, remove_duplicates, is_palindrome + +def test_two_sum_sorted(): + assert two_sum_sorted([2, 7, 11, 15], 9) == (0, 1) + assert two_sum_sorted([1, 3, 5, 7], 12) == (2, 3) + +def test_remove_duplicates(): + nums = [1, 1, 2, 3, 3] + assert remove_duplicates(nums) == 3 + +def test_palindrome(): + assert is_palindrome("A man, a plan, a canal: Panama") is True + assert is_palindrome("hello") is False