AlgoAtlas

Arrays And Hashing

Phase 1 22 solutions

Arrays & Hashing

mindmap
  root((Arrays & Hashing))
    Hash Map Lookup
      Two Sum LC1
      Group Anagrams LC49
      Valid Sudoku LC36
      Longest Consecutive Sequence LC128
      Subarray Sum Equals K LC560
    Frequency Counting
      Valid Anagram LC242
      Top K Frequent Elements LC347
      Majority Element LC169
      Majority Element II LC229
    Prefix Sum
      Product of Array Except Self LC238
      Range Sum Query 2D LC304
      Subarray Sum Equals K LC560
    Sorting
      Sort an Array LC912
      Sort Colors LC75
      Longest Common Prefix LC14
    Index Marking
      First Missing Positive LC41
    Design
      Design HashSet LC705
      Design HashMap LC706
    Encoding
      Encode and Decode Strings LC271

Overview

This section covers 22 problems focusing on array manipulation, hashing techniques, and frequency counting.

Problem List

# Problem LC # Difficulty Key Techniques
1 Concatenation of Array 1929 Easy Array
2 Contains Duplicate 217 Easy Hash Set
3 Valid Anagram 242 Easy Char Counting
4 Two Sum 1 Easy Hash Map
5 Longest Common Prefix 14 Easy String Scan
6 Group Anagrams 49 Medium Hash Map + Sort
7 Remove Element 27 Easy Two Pointers
8 Majority Element 169 Easy Boyer-Moore
9 Design HashSet 705 Easy Design
10 Design HashMap 706 Easy Design
11 Sort an Array 912 Medium Merge/Quick Sort
12 Sort Colors 75 Medium Dutch National Flag
13 Top K Frequent Elements 347 Medium Bucket Sort
14 Encode and Decode Strings 271 Medium Length Prefix
15 Range Sum Query 2D 304 Medium Prefix Sum 2D
16 Product of Array Except Self 238 Medium Prefix/Suffix
17 Valid Sudoku 36 Medium Hash Sets
18 Longest Consecutive Sequence 128 Medium Hash Set
19 Best Time to Buy/Sell Stock II 122 Medium Greedy
20 Majority Element II 229 Medium Boyer-Moore Extended
21 Subarray Sum Equals K 560 Medium Prefix Sum + Map
22 First Missing Positive 41 Hard Index Marking

Approach Flow Diagrams

Hash Map / Hash Set Pattern

flowchart TD
    A[Input Array] --> B{Need to track seen elements?}
    B -->|Yes| C[Use Hash Set]
    B -->|No| D{Need key-value mapping?}
    D -->|Yes| E[Use Hash Map]
    D -->|No| F[Array manipulation]
    
    C --> G[Contains Duplicate]
    C --> H[Longest Consecutive Sequence]
    C --> I[Valid Sudoku]
    
    E --> J[Two Sum]
    E --> K[Group Anagrams]
    E --> L[Subarray Sum Equals K]
    
    F --> M[Concatenation of Array]
    F --> N[Remove Element]

Frequency Counting Pattern

flowchart TD
    A[Input Data] --> B{Count frequencies}
    B --> C["Hash Map / Counter"]
    
    C --> D{What to find?}
    D -->|Top K elements| E[Top K Frequent Elements]
    D -->|Majority > n/2| F[Majority Element]
    D -->|Majority > n/3| G[Majority Element II]
    D -->|Anagram check| H[Valid Anagram]
    
    E --> E1[Approach 1: Sort by freq O-n log n-]
    E --> E2[Approach 2: Heap O-n log k-]
    E --> E3[Approach 3: Bucket Sort O-n-]
    
    F --> F1[Approach 1: Hash Map O-n-]
    F --> F2[Approach 2: Sort O-n log n-]
    F --> F3[Approach 3: Boyer-Moore O-n- O-1-]

Prefix Sum Pattern

flowchart TD
    A["Array/Matrix"] --> B{Repeated range queries?}
    B -->|1D Array| C[Prefix Sum Array]
    B -->|2D Matrix| D[2D Prefix Sum]
    B -->|Product queries| E[Prefix & Suffix Products]
    
    C --> C1[Subarray Sum Equals K]
    C1 --> C2[prefix_sum + hash_map = O-n-]
    
    D --> D1[Range Sum Query 2D]
    D1 --> D2[Precompute: O-mn-, Query: O-1-]
    
    E --> E1[Product of Array Except Self]
    E1 --> E2[Left pass × Right pass = O-n- O-1- space]

Sorting-Based Pattern

flowchart TD
    A[Unsorted Array] --> B{Which sort?}
    
    B -->|Divide & Conquer| C[Merge Sort O-n log n-]
    B -->|Partition| D[Quick Sort O-n log n- avg]
    B -->|Limited range 0,1,2| E[Dutch National Flag O-n-]
    B -->|Count occurrences| F["Counting/Bucket Sort O-n-"]
    
    C --> C1[Sort an Array - stable]
    D --> D1[Sort an Array - in-place]
    E --> E1[Sort Colors - one pass]
    F --> F1[Top K Frequent Elements]
    
    E1 --> G[Three pointers: low, mid, high]
    G --> G1["low: boundary of 0s"]
    G --> G2["mid: current element"]
    G --> G3["high: boundary of 2s"]

Design Pattern (HashSet / HashMap)

flowchart TD
    A[Design Problem] --> B{Data Structure}
    
    B -->|HashSet| C[MyHashSet]
    B -->|HashMap| D[MyHashMap]
    
    C --> C1[Approach 1: Boolean Array]
    C --> C2[Approach 2: Chaining - Linked List]
    
    D --> D1[Approach 1: Direct Array]
    D --> D2[Approach 2: Chaining - Linked List]
    
    C1 --> E["arr[key] = True/False"]
    C2 --> F["hash(key) % size -> bucket -> linked list"]
    
    D1 --> G["arr[key] = value"]
    D2 --> H["hash(key) % size -> bucket -> (key,val) pairs"]

Index Marking / Cyclic Sort Pattern

flowchart TD
    A["Array with values in [1, n]"] --> B{"Find missing/duplicate?"}
    
    B -->|Missing positive| C[First Missing Positive]
    
    C --> D[Approach 1: Hash Set O-n- space]
    C --> E[Approach 2: Index Marking O-1- space]
    
    E --> F["For each num, mark nums[num-1] as negative"]
    F --> G["First positive index i → answer is i+1"]
    
    style E fill:#90EE90

Encode/Decode Pattern

flowchart TD
    A["List of Strings"] --> B{Encode to single string}
    
    B --> C[Approach 1: Delimiter with Escaping]
    B --> D[Approach 2: Length Prefix]
    
    C --> C1["Join with special delimiter"]
    C --> C2["Must handle delimiter in strings"]
    
    D --> D1["For each str: len + '#' + str"]
    D --> D2["'5#Hello5#World'"]
    
    D2 --> E{Decode}
    E --> F["Read length → read that many chars → repeat"]
    
    style D fill:#90EE90

Greedy Pattern (Stock Problems)

flowchart TD
    A[Price Array] --> B{Multiple transactions allowed?}
    
    B -->|Yes - LC 122| C["Best Time to Buy/Sell Stock II"]
    
    C --> D[Approach 1: Recursion O-2^n-]
    C --> E[Approach 2: Greedy O-n-]
    
    E --> F["Sum all positive differences"]
    F --> G["if prices[i] > prices[i-1]: profit += diff"]
    
    style E fill:#90EE90

Complexity Summary

Problem Time Space Pattern
concatenation_of_array O(n) O(n) Array copy
contains_duplicate O(n) O(n) Hash set
valid_anagram O(n) O(1) Char counting
two_sum O(n) O(n) Hash map
longest_common_prefix O(n·k) O(1) String scan
group_anagrams O(n·k log k) O(n·k) Hash map + sort
remove_element O(n) O(1) Two pointers
majority_element O(n) O(1) Boyer-Moore
design_hashset O(1) avg O(n) Chaining
design_hashmap O(1) avg O(n) Chaining
sort_an_array O(n log n) O(n) Merge/Quick sort
sort_colors O(n) O(1) Dutch national flag
top_k_frequent_elements O(n) O(n) Bucket sort
encode_and_decode_strings O(n) O(n) Length prefix
range_sum_query_2d O(mn) pre / O(1) query O(mn) 2D prefix sum
product_of_array_except_self O(n) O(1) Prefix/suffix products
valid_sudoku O(1) O(1) Hash sets (fixed 9×9)
longest_consecutive_sequence O(n) O(n) Hash set
best_time_to_buy_sell_stock_ii O(n) O(1) Greedy
majority_element_ii O(n) O(1) Boyer-Moore extended
subarray_sum_equals_k O(n) O(n) Prefix sum + map
first_missing_positive O(n) O(1) Index marking

Study Order (Recommended)

flowchart LR
    A[Easy] --> B[Medium] --> C[Hard]
    
    A --> A1[Concatenation]
    A --> A2[Contains Duplicate]
    A --> A3[Valid Anagram]
    A --> A4[Two Sum]
    A --> A5[Remove Element]
    A --> A6[Majority Element]
    
    B --> B1[Group Anagrams]
    B --> B2[Top K Frequent]
    B --> B3[Product Except Self]
    B --> B4[Longest Consecutive]
    B --> B5[Subarray Sum K]
    B --> B6[Sort Colors]
    
    C --> C1[First Missing Positive]

Arrays & Hashing — Pattern Notes

Core Intuition

Arrays + hashing problems are about trading space for time. A hash map/set turns O(n) lookups into O(1). The core patterns:

  • Frequency map: Count occurrences, then reason about counts.
  • Complement lookup: Store what you've seen; check if current element completes a pair/group.
  • Prefix sum: Precompute cumulative sums for O(1) range queries.
  • Index marking: Use the array itself as a hash map (mark visited by negating or swapping).
  • Bucketing: Group elements by a computed key (e.g., sorted tuple for anagrams).

Decision Flowchart

flowchart TD
    A["Array/Hashing Problem"] --> B{"Need O(1) range sum?"}
    B -- Yes --> C["Prefix Sum / 2D Prefix Sum"]
    B -- No --> D{Grouping by equivalence?}
    D -- Yes --> E["HashMap with canonical key<br/>e.g. sorted string, char count"]
    D -- No --> F{"Finding pair/complement?"}
    F -- Yes --> G["HashMap: store seen values<br/>check complement"]
    F -- No --> H{"Frequency / count based?"}
    H -- Yes --> I[Frequency array or Counter]
    H -- No --> J{"In-place, O(1) space?"}
    J -- Yes --> K["Index marking / cyclic sort"]
    J -- No --> L["HashSet for O(1) lookup"]

Per-Problem Notes

1. Concatenation of Array — LC 1929 | Easy

  • Key insight: Return nums + nums. That's it.
  • Tricky: Nothing — warm-up problem.
  • Common mistakes: Overcomplicating it.
  • Time: O(n) | Space: O(n)

2. Contains Duplicate — LC 217 | Easy

  • Key insight: HashSet — if element already in set, return True.
  • Tricky: Nothing. Alternative: sort and check adjacent.
  • Common mistakes: Using O(n²) nested loop.
  • Time: O(n) | Space: O(n)

3. Valid Anagram — LC 242 | Easy

  • Key insight: Count character frequencies. Two strings are anagrams if their frequency maps are equal.
  • Tricky: Unicode follow-up — use a dict instead of fixed-size array.
  • Common mistakes: Sorting both strings (O(n log n)) when O(n) is possible.
  • Time: O(n) | Space: O(1) (26 chars)

4. Two Sum — LC 1 | Easy

  • Key insight: Store {value: index} as you iterate. For each num, check if target - num is in map.
  • Tricky: Check complement before inserting current element (handles same-element pairs correctly).
  • Common mistakes: Inserting before checking — can use same element twice.
  • Time: O(n) | Space: O(n)

5. Longest Common Prefix — LC 14 | Easy

  • Key insight: Compare character by character across all strings. Stop at first mismatch or shortest string end.
  • Tricky: Sort strings and only compare first and last — they'll have the smallest common prefix.
  • Common mistakes: Not handling empty array or empty string inputs.
  • Time: O(n·m) | Space: O(1)

6. Group Anagrams — LC 49 | Medium

  • Key insight: Key = sorted string (or tuple of 26 char counts). Group words by key in a defaultdict.
  • Tricky: Char count tuple as key avoids O(k log k) sort → O(k) per word.
  • Common mistakes: Using sorted string key (fine for interviews, but char count is faster).
  • Time: O(n·k) | Space: O(n·k)

7. Remove Element — LC 27 | Easy

  • Key insight: Two-pointer in-place. Write pointer k tracks valid elements. Skip elements equal to val.
  • Tricky: Order doesn't matter — can swap with last element for O(1) deletion.
  • Common mistakes: Shifting elements (O(n²)). Forgetting to return k.
  • Time: O(n) | Space: O(1)

8. Majority Element — LC 169 | Easy

  • Key insight: Boyer-Moore Voting. Candidate + count. Increment if same, decrement if different. Reset at 0.
  • Tricky: Works because majority element appears > n/2 times — it always survives.
  • Common mistakes: Using sort (O(n log n)) or hashmap (O(n) space) when O(1) space is possible.
  • Time: O(n) | Space: O(1)

9. Design HashSet — LC 705 | Easy

  • Key insight: Use array of linked lists (chaining) or open addressing. Or just use a boolean array for small key range.
  • Tricky: Handle collisions properly if implementing real hashing.
  • Common mistakes: Using Python's built-in set (defeats the purpose).
  • Time: O(1) avg | Space: O(n)

10. Design HashMap — LC 706 | Easy

  • Key insight: Same as HashSet but store (key, value) pairs. Use chaining for collision resolution.
  • Tricky: Update existing key vs. insert new key.
  • Common mistakes: Not handling key updates in chained list.
  • Time: O(1) avg | Space: O(n)

11. Sort an Array — LC 912 | Medium

  • Key insight: Implement merge sort or heap sort. Quick sort has O(n²) worst case.
  • Tricky: Merge sort: careful with index bounds during merge. In-place merge is complex.
  • Common mistakes: Using quick sort without randomization → O(n²) on sorted input.
  • Time: O(n log n) | Space: O(n) merge sort

12. Sort Colors — LC 75 | Medium

  • Key insight: Dutch National Flag (3-way partition). Three pointers: low, mid, high. One pass.
  • Tricky: When swapping mid with high, don't increment mid (new element at mid is unprocessed).
  • Common mistakes: Incrementing mid after swapping with high — skips unprocessed element.
  • Time: O(n) | Space: O(1)

13. Top K Frequent Elements — LC 347 | Medium

  • Key insight: Bucket sort by frequency. Bucket index = frequency, size = n+1. Collect from high to low.
  • Tricky: Bucket sort is O(n) vs. heap's O(n log k). Both are valid.
  • Common mistakes: Using full sort O(n log n). Heap approach: use heapq.nlargest(k, count, key=count.get).
  • Time: O(n) bucket sort | Space: O(n)

14. Encode and Decode Strings — LC 271 | Medium

  • Key insight: Encode as len#string for each word. Decoder reads length, skips '#', reads exactly that many chars.
  • Tricky: Can't use a delimiter like ',' because strings may contain it. Length-prefix is delimiter-free.
  • Common mistakes: Using a simple delimiter that appears in strings.
  • Time: O(n) | Space: O(n)

15. Range Sum Query 2D — LC 304 | Medium

  • Key insight: 2D prefix sum. prefix[r][c] = sum of rectangle from (0,0) to (r,c). Query uses inclusion-exclusion.
  • Tricky: Formula: prefix[r2+1][c2+1] - prefix[r1][c2+1] - prefix[r2+1][c1] + prefix[r1][c1].
  • Common mistakes: Off-by-one in prefix array indexing. Forgetting the +1 offset for 1-indexed prefix array.
  • Time: O(n·m) build, O(1) query | Space: O(n·m)

16. Product of Array Except Self — LC 238 | Medium

  • Key insight: Two passes: left products then right products. No division needed.
  • Tricky: Do it in O(1) extra space by using output array for left pass, then multiply right pass in-place.
  • Common mistakes: Using division (fails with zeros). Using O(n) extra space for both passes.
  • Time: O(n) | Space: O(1) extra

17. Valid Sudoku — LC 36 | Medium

  • Key insight: Track seen values per row, column, and 3×3 box. Box index = (r//3)*3 + c//3.
  • Tricky: Use sets of sets, or encode as (val, row), (val, col), (val, box) in one set.
  • Common mistakes: Wrong box index formula. Not skipping '.' cells.
  • Time: O(81) = O(1) | Space: O(81) = O(1)

18. Longest Consecutive Sequence — LC 128 | Medium

  • Key insight: HashSet of all numbers. For each number that is a sequence start (num-1 not in set), count streak.
  • Tricky: Only start counting from sequence beginnings to avoid O(n²).
  • Common mistakes: Not checking num-1 not in set → O(n²) for each element.
  • Time: O(n) | Space: O(n)

19. Best Time to Buy/Sell Stock II — LC 122 | Medium

  • Key insight: Greedy — sum all positive differences between consecutive days. Capture every upward move.
  • Tricky: You can hold at most one stock, but this greedy is equivalent to buying/selling every day it goes up.
  • Common mistakes: Trying DP when greedy is O(n) and simpler.
  • Time: O(n) | Space: O(1)

20. Majority Element II — LC 229 | Medium

  • Key insight: Boyer-Moore for 2 candidates (at most 2 elements can appear > n/3 times). Two-pass: find candidates, then verify counts.
  • Tricky: Must verify candidates in second pass — not all candidates are majority elements.
  • Common mistakes: Not verifying candidates. Forgetting there can be 0, 1, or 2 results.
  • Time: O(n) | Space: O(1)

21. Subarray Sum Equals K — LC 560 | Medium

  • Key insight: Prefix sum + hashmap. count[prefix_sum] tracks how many times each prefix sum occurred. For each index, check prefix - k in map.
  • Tricky: Initialize count[0] = 1 for subarrays starting at index 0.
  • Common mistakes: Forgetting count[0] = 1. Sliding window doesn't work here (negative numbers).
  • Time: O(n) | Space: O(n)

22. First Missing Positive — LC 41 | Hard

  • Key insight: Index marking. Place each number x in index x-1 (cyclic sort). Then scan for first index where nums[i] != i+1.
  • Tricky: Answer is always in range [1, n+1]. Ignore numbers outside [1, n].
  • Common mistakes: Using extra space (O(n) set) when O(1) is required. Not handling duplicates in cyclic sort.
  • Time: O(n) | Space: O(1)

Edge Cases to Watch For

  • Empty array: Return 0, [], or handle before main logic.
  • Single element: Many problems have trivial single-element answers.
  • Negative numbers: Prefix sum, subarray problems — sliding window won't work with negatives.
  • Duplicates: Two Sum (same element twice), Group Anagrams, Consecutive Sequence.
  • Integer overflow: Product of array, sum problems — use Python (no overflow) or long in Java.
  • All same elements: Majority element, sort colors, consecutive sequence.
  • Zeros in array: Product except self (can't divide by zero), subarray sum.
  • Unicode strings: Valid anagram follow-up — use dict not fixed array.
# Problem LC # Difficulty Key Techniques
1 Concatenation of Array 1929 Easy Array
2 Contains Duplicate 217 Easy Hash Set
3 Valid Anagram 242 Easy Char Counting
4 Two Sum 1 Easy Hash Map
5 Longest Common Prefix 14 Easy String Scan
6 Group Anagrams 49 Medium Hash Map + Sort
7 Remove Element 27 Easy Two Pointers
8 Majority Element 169 Easy Boyer-Moore
9 Design HashSet 705 Easy Design
10 Design HashMap 706 Easy Design
11 Sort an Array 912 Medium Merge/Quick Sort
12 Sort Colors 75 Medium Dutch National Flag
13 Top K Frequent Elements 347 Medium Bucket Sort
14 Encode and Decode Strings 271 Medium Length Prefix
15 Range Sum Query 2D 304 Medium Prefix Sum 2D
16 Product of Array Except Self 238 Medium Prefix/Suffix
17 Valid Sudoku 36 Medium Hash Sets
18 Longest Consecutive Sequence 128 Medium Hash Set
19 Best Time to Buy/Sell Stock II 122 Medium Greedy
20 Majority Element II 229 Medium Boyer-Moore Extended
21 Subarray Sum Equals K 560 Medium Prefix Sum + Map
22 First Missing Positive 41 Hard Index Marking
Best Time To Buy And Sell Stock Ii ▼ expand
"""
# LC 122: Best Time to Buy and Sell Stock II

You are given an integer array `prices` where `prices[i]` is the price of a
given stock on the i-th day. On each day, you may decide to buy and/or sell the
stock. You can only hold at most one share of the stock at any time. However,
you can buy it then immediately sell it on the same day.

Find and return the maximum profit you can achieve.

## Examples

```text
Input:  prices = [7,1,5,3,6,4]
Output: 7
Explanation: Buy on day 2 (price=1), sell on day 3 (price=5), profit=4.
             Buy on day 4 (price=3), sell on day 5 (price=6), profit=3.
             Total profit = 7.

Input:  prices = [1,2,3,4,5]
Output: 4

Input:  prices = [7,6,4,3,1]
Output: 0
```

## Constraints
- 1 <= prices.length <= 3 * 10^4
- 0 <= prices[i] <= 10^4
"""
from typing import List


class Solution:
    def maxProfit_brute(self, prices: List[int]) -> int:
        """Recursion — O(2^n) time, O(n) stack space."""
        # Recursive DFS: at each day decide to buy, sell, or skip
        # Exponential without memoization — illustrates the decision tree
        def dfs(i: int, holding: bool) -> int:
            if i == len(prices):
                return 0
            # skip
            best = dfs(i + 1, holding)
            if holding:
                best = max(best, prices[i] + dfs(i + 1, False))
            else:
                best = max(best, -prices[i] + dfs(i + 1, True))
            return best
        return dfs(0, False)

    def maxProfit(self, prices: List[int]) -> int:
        """Greedy — O(n) time, O(1) space."""
        # Greedy: capture every upward price movement as profit
        # Equivalent to buying and selling on every consecutive rising day
        return sum(max(prices[i] - prices[i - 1], 0) for i in range(1, len(prices)))


if __name__ == "__main__":
    s = Solution()

    assert s.maxProfit([7, 1, 5, 3, 6, 4]) == 7
    assert s.maxProfit([1, 2, 3, 4, 5]) == 4
    assert s.maxProfit([7, 6, 4, 3, 1]) == 0
    assert s.maxProfit([1]) == 0

    print("All tests passed.")
Concatenation Of Array ▼ expand
"""
# LC 1929: Concatenation of Array

Given an integer array `nums` of length `n`, return an array `ans` of length `2n`
where `ans[i] == nums[i]` and `ans[i + n] == nums[i]` for all `0 <= i < n`.

## Examples

```text
Input:  nums = [1,2,1]
Output: [1,2,1,1,2,1]

Input:  nums = [1,3,2,1]
Output: [1,3,2,1,1,3,2,1]
```

## Constraints
- n == nums.length
- 1 <= n <= 1000
- 1 <= nums[i] <= 1000
"""
from typing import List


class Solution:
    def getConcatenation_brute(self, nums: List[int]) -> List[int]:
        # Append all elements twice using a loop
        # O(n) time | O(n) space
        ans = []
        for _ in range(2):
            for n in nums:
                ans.append(n)
        return ans

    def getConcatenation(self, nums: List[int]) -> List[int]:
        # Python list concatenation creates a new list with both copies
        # O(n) time | O(n) space
        return nums + nums


if __name__ == "__main__":
    s = Solution()
    assert s.getConcatenation([1, 2, 1]) == [1, 2, 1, 1, 2, 1]
    assert s.getConcatenation([1, 3, 2, 1]) == [1, 3, 2, 1, 1, 3, 2, 1]
    print("All tests passed.")
Contains Duplicate ▼ expand
"""
# LC 217: Contains Duplicate

Given an integer array `nums`, return `true` if any value appears **at least twice**,
and return `false` if every element is distinct.

## Examples

```text
Input:  nums = [1,2,3,1]
Output: true

Input:  nums = [1,2,3,4]
Output: false

Input:  nums = [1,1,1,3,3,4,3,2,4,2]
Output: true
```

## Constraints
- 1 <= nums.length <= 10^5
- -10^9 <= nums[i] <= 10^9
"""
from typing import List


class Solution:
    def containsDuplicate_brute(self, nums: List[int]) -> bool:
        # Compare every pair — O(n^2); fine for small inputs
        # O(n^2) time | O(1) space
        for i in range(len(nums)):
            for j in range(i + 1, len(nums)):
                if nums[i] == nums[j]:
                    return True
        return False

    def containsDuplicate_sort(self, nums: List[int]) -> bool:
        # After sorting, duplicates are adjacent — one linear scan suffices
        # O(n log n) time | O(1) space
        nums.sort()
        for i in range(1, len(nums)):
            if nums[i] == nums[i - 1]:
                return True
        return False

    def containsDuplicate(self, nums: List[int]) -> bool:
        # A set collapses duplicates; if lengths differ, a duplicate existed
        # O(n) time | O(n) space
        return len(nums) != len(set(nums))


if __name__ == "__main__":
    s = Solution()
    assert s.containsDuplicate([1, 2, 3, 1]) == True
    assert s.containsDuplicate([1, 2, 3, 4]) == False
    assert s.containsDuplicate([1, 1, 1, 3, 3, 4, 3, 2, 4, 2]) == True
    print("All tests passed.")
Design Hashmap ▼ expand
"""
# LC 706: Design HashMap

Design a HashMap without using any built-in hash table libraries.

Implement `MyHashMap`:
- `put(key, value)` — Inserts a `(key, value)` pair. Updates value if key exists.
- `get(key)` — Returns the value mapped to `key`, or `-1` if not present.
- `remove(key)` — Removes the `key` and its value if present.

## Examples

```text
Input:
  ["MyHashMap","put","put","get","get","put","get","remove","get"]
  [[],[1,1],[2,2],[1],[3],[2,1],[2],[2],[2]]
Output:
  [null,null,null,1,-1,null,1,null,-1]
```

## Constraints
- 0 <= key, value <= 10^6
- At most 10^4 calls will be made to put, get, and remove.
"""


# Approach 1: Array-based — O(1) time | O(10^6) space
class MyHashMap:
    def __init__(self):
        self.data = [-1] * (10**6 + 1)

    def put(self, key: int, value: int) -> None:
        self.data[key] = value

    def get(self, key: int) -> int:
        return self.data[key]

    def remove(self, key: int) -> None:
        self.data[key] = -1


# Approach 2: Chaining with lists of (key, value) pairs — O(1) avg time | O(n) space
class MyHashMapChaining:
    def __init__(self):
        self.size = 1000
        self.buckets = [[] for _ in range(self.size)]

    def _hash(self, key: int) -> int:
        return key % self.size

    def put(self, key: int, value: int) -> None:
        h = self._hash(key)
        for i, (k, v) in enumerate(self.buckets[h]):
            if k == key:
                self.buckets[h][i] = (key, value)
                return
        self.buckets[h].append((key, value))

    def get(self, key: int) -> int:
        for k, v in self.buckets[self._hash(key)]:
            if k == key:
                return v
        return -1

    def remove(self, key: int) -> None:
        h = self._hash(key)
        self.buckets[h] = [(k, v) for k, v in self.buckets[h] if k != key]


if __name__ == "__main__":
    hm = MyHashMap()
    hm.put(1, 1)
    hm.put(2, 2)
    assert hm.get(1) == 1
    assert hm.get(3) == -1
    hm.put(2, 1)
    assert hm.get(2) == 1
    hm.remove(2)
    assert hm.get(2) == -1
    print("All tests passed.")
Design Hashset ▼ expand
"""
# LC 705: Design HashSet

Design a HashSet without using any built-in hash table libraries.

Implement `MyHashSet`:
- `add(key)` — Inserts the value `key` into the HashSet.
- `remove(key)` — Removes the value `key` in the HashSet. If `key` does not exist, do nothing.
- `contains(key)` — Returns whether the value `key` exists in the HashSet or not.

## Examples

```text
Input:
  ["MyHashSet","add","add","contains","contains","add","contains","remove","contains"]
  [[],[1],[2],[1],[3],[2],[2],[2],[2]]
Output:
  [null,null,null,true,false,null,true,null,false]
```

## Constraints
- 0 <= key <= 10^6
- At most 10^4 calls will be made to add, remove, and contains.
"""


# Approach 1: Boolean array — O(1) time | O(10^6) space
class MyHashSet:
    def __init__(self):
        self.data = [False] * (10**6 + 1)

    def add(self, key: int) -> None:
        self.data[key] = True

    def remove(self, key: int) -> None:
        self.data[key] = False

    def contains(self, key: int) -> bool:
        return self.data[key]


# Approach 2: Chaining with lists — O(1) avg time | O(n) space
class MyHashSetChaining:
    def __init__(self):
        self.size = 1000
        self.buckets = [[] for _ in range(self.size)]

    def _hash(self, key: int) -> int:
        return key % self.size

    def add(self, key: int) -> None:
        h = self._hash(key)
        if key not in self.buckets[h]:
            self.buckets[h].append(key)

    def remove(self, key: int) -> None:
        h = self._hash(key)
        if key in self.buckets[h]:
            self.buckets[h].remove(key)

    def contains(self, key: int) -> bool:
        return key in self.buckets[self._hash(key)]


if __name__ == "__main__":
    hs = MyHashSet()
    hs.add(1)
    hs.add(2)
    assert hs.contains(1) == True
    assert hs.contains(3) == False
    hs.add(2)
    assert hs.contains(2) == True
    hs.remove(2)
    assert hs.contains(2) == False
    print("All tests passed.")
Encode And Decode Strings ▼ expand
"""
# LC 271: Encode and Decode Strings

Design an algorithm to encode a list of strings to a single string. The encoded
string is then sent over the network and is decoded back to the original list of
strings.

## Examples

```text
Input:  strs = ["Hello","World"]
Encoded: "5#Hello5#World"
Output: ["Hello","World"]

Input:  strs = [""]
Output: [""]
```

## Constraints
- 0 <= strs.length <= 200
- 0 <= strs[i].length <= 200
- strs[i] contains any possible characters out of 256 valid ASCII characters
"""
from typing import List


class Codec:
    def encode(self, strs: List[str]) -> str:
        """Delimiter with escaping — O(n) time."""
        # Prefix each string with its length and '#' as delimiter
        # This handles strings containing any character including '#'
        return "".join(f"{len(s)}#{s}" for s in strs)

    def decode(self, s: str) -> List[str]:
        """Length-prefix decoding — O(n) time."""
        # Read length prefix up to '#', then extract exactly that many chars
        # Advance i past the extracted string to process the next one
        result, i = [], 0
        while i < len(s):
            j = s.index("#", i)
            length = int(s[i:j])
            result.append(s[j + 1: j + 1 + length])
            i = j + 1 + length
        return result


if __name__ == "__main__":
    codec = Codec()

    for strs in [
        ["Hello", "World"],
        [""],
        [],
        ["a#b", "c#d"],
        ["with spaces", "and\nnewlines"],
    ]:
        assert codec.decode(codec.encode(strs)) == strs, strs

    print("All tests passed.")
First Missing Positive ▼ expand
"""
# LC 41: First Missing Positive

Given an unsorted integer array `nums`, return the smallest missing positive integer.
You must implement an algorithm that runs in O(n) time and uses O(1) auxiliary space.

## Examples

```text
Input:  nums = [1,2,0]
Output: 3

Input:  nums = [3,4,-1,1]
Output: 2

Input:  nums = [7,8,9,11,12]
Output: 1
```

## Constraints
- 1 <= nums.length <= 10^5
- -2^31 <= nums[i] <= 2^31 - 1
"""
from typing import List


class Solution:
    def firstMissingPositive_hashset(self, nums: List[int]) -> int:
        """Hash set — O(n) time, O(n) space."""
        # Store all numbers in a set, then probe 1, 2, 3... for the first gap
        s = set(nums)
        i = 1
        while i in s:
            i += 1
        return i

    def firstMissingPositive(self, nums: List[int]) -> int:
        """Index marking (cyclic sort idea) — O(n) time, O(1) space."""
        # Cyclic sort: place each number at index nums[i]-1 if in range [1,n]
        # After rearranging, the first index where nums[i] != i+1 is the answer
        # If all positions are correct, the answer is n+1
        n = len(nums)
        # Place each number in its correct index position (nums[i] == i+1)
        for i in range(n):
            while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
                nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]
        for i in range(n):
            if nums[i] != i + 1:
                return i + 1
        return n + 1


if __name__ == "__main__":
    s = Solution()

    assert s.firstMissingPositive([1, 2, 0]) == 3
    assert s.firstMissingPositive([3, 4, -1, 1]) == 2
    assert s.firstMissingPositive([7, 8, 9, 11, 12]) == 1
    assert s.firstMissingPositive([1]) == 2
    assert s.firstMissingPositive([1, 2, 3]) == 4

    print("All tests passed.")
Group Anagrams ▼ expand
"""
# LC 49: Group Anagrams

Given an array of strings `strs`, group the anagrams together. You can return
the answer in any order.

## Examples

```text
Input:  strs = ["eat","tea","tan","ate","nat","bat"]
Output: [["bat"],["nat","tan"],["ate","eat","tea"]]

Input:  strs = [""]
Output: [[""]]

Input:  strs = ["a"]
Output: [["a"]]
```

## Constraints
- 1 <= strs.length <= 10^4
- 0 <= strs[i].length <= 100
- strs[i] consists of lowercase English letters.
"""
from typing import List
from collections import defaultdict


class Solution:
    def groupAnagrams_sort(self, strs: List[str]) -> List[List[str]]:
        # Sorted string is a canonical key — all anagrams share the same key
        # O(n * k log k) time | O(n * k) space
        groups = defaultdict(list)
        for s in strs:
            groups[tuple(sorted(s))].append(s)
        return list(groups.values())

    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        # Character-frequency tuple is a canonical key without sorting cost
        # Build a 26-element count array per word, then use it as dict key
        # O(n * k) time | O(n * k) space
        groups = defaultdict(list)
        for s in strs:
            count = [0] * 26
            for c in s:
                count[ord(c) - ord('a')] += 1
            groups[tuple(count)].append(s)
        return list(groups.values())


if __name__ == "__main__":
    s = Solution()
    result = s.groupAnagrams(["eat", "tea", "tan", "ate", "nat", "bat"])
    assert sorted(sorted(g) for g in result) == [["ate", "eat", "tea"], ["bat"], ["nat", "tan"]]
    assert s.groupAnagrams([""]) == [[""]]
    assert s.groupAnagrams(["a"]) == [["a"]]
    print("All tests passed.")
Longest Common Prefix ▼ expand
"""
# LC 14: Longest Common Prefix

Write a function to find the longest common prefix string amongst an array of strings.
If there is no common prefix, return an empty string `""`.

## Examples

```text
Input:  strs = ["flower","flow","flight"]
Output: "fl"

Input:  strs = ["dog","racecar","car"]
Output: ""
```

## Constraints
- 1 <= strs.length <= 200
- 0 <= strs[i].length <= 200
- strs[i] consists of only lowercase English letters.
"""
from typing import List


class Solution:
    def longestCommonPrefix_vertical(self, strs: List[str]) -> str:
        # Check each character position across all strings simultaneously
        # Stop as soon as any string diverges or runs out of characters
        # O(S) time where S = sum of all characters | O(1) space
        if not strs:
            return ""
        for i, ch in enumerate(strs[0]):
            for s in strs[1:]:
                if i >= len(s) or s[i] != ch:
                    return strs[0][:i]
        return strs[0]

    def longestCommonPrefix(self, strs: List[str]) -> str:
        # After sorting, only the first and last strings need to be compared
        # The common prefix of extremes is the common prefix of all strings
        # O(n log n * m) time | O(1) space
        strs.sort()
        first, last = strs[0], strs[-1]
        i = 0
        while i < len(first) and i < len(last) and first[i] == last[i]:
            i += 1
        return first[:i]


if __name__ == "__main__":
    s = Solution()
    assert s.longestCommonPrefix(["flower", "flow", "flight"]) == "fl"
    assert s.longestCommonPrefix(["dog", "racecar", "car"]) == ""
    assert s.longestCommonPrefix(["interview", "interact", "interface"]) == "inter"
    print("All tests passed.")
Longest Consecutive Sequence ▼ expand
"""
# LC 128: Longest Consecutive Sequence

Given an unsorted array of integers `nums`, return the length of the longest
consecutive elements sequence. You must write an algorithm that runs in O(n) time.

## Examples

```text
Input:  nums = [100,4,200,1,3,2]
Output: 4
Explanation: [1,2,3,4]

Input:  nums = [0,3,7,2,5,8,4,6,0,1]
Output: 9
```

## Constraints
- 0 <= nums.length <= 10^5
- -10^9 <= nums[i] <= 10^9
"""
from typing import List


class Solution:
    def longestConsecutive_sort(self, nums: List[int]) -> int:
        """Sorting — O(n log n) time, O(1) extra space."""
        # After deduplication and sorting, consecutive elements differ by 1
        if not nums:
            return 0
        nums = sorted(set(nums))
        best = cur = 1
        for i in range(1, len(nums)):
            if nums[i] == nums[i - 1] + 1:
                cur += 1
                best = max(best, cur)
            else:
                cur = 1
        return best

    def longestConsecutive(self, nums: List[int]) -> int:
        """Hash set — O(n) time, O(n) space."""
        # Only start counting from the beginning of a sequence (n-1 not in set)
        # This ensures each sequence is counted exactly once — O(n) total
        num_set = set(nums)
        best = 0
        for n in num_set:
            if n - 1 not in num_set:
                cur = 1
                while n + cur in num_set:
                    cur += 1
                best = max(best, cur)
        return best


if __name__ == "__main__":
    s = Solution()

    assert s.longestConsecutive([100, 4, 200, 1, 3, 2]) == 4
    assert s.longestConsecutive([0, 3, 7, 2, 5, 8, 4, 6, 0, 1]) == 9
    assert s.longestConsecutive([]) == 0
    assert s.longestConsecutive([1]) == 1

    print("All tests passed.")
Majority Element ▼ expand
"""
# LC 169: Majority Element

Given an array `nums` of size `n`, return the majority element — the element that
appears more than `⌊n / 2⌋` times. The majority element always exists.

## Examples

```text
Input:  nums = [3,2,3]
Output: 3

Input:  nums = [2,2,1,1,1,2,2]
Output: 2
```

## Constraints
- n == nums.length
- 1 <= n <= 5 * 10^4
- -10^9 <= nums[i] <= 10^9
- The majority element always exists.
"""
from typing import List
from collections import Counter


class Solution:
    def majorityElement_map(self, nums: List[int]) -> int:
        # Count occurrences and return the element with the highest count
        # O(n) time | O(n) space
        count = Counter(nums)
        return max(count, key=count.get)

    def majorityElement_sort(self, nums: List[int]) -> int:
        # The majority element always occupies the middle index after sorting
        # O(n log n) time | O(1) space
        nums.sort()
        return nums[len(nums) // 2]

    def majorityElement(self, nums: List[int]) -> int:
        # Boyer-Moore: maintain a candidate and a net vote count
        # When count hits 0, the current element becomes the new candidate
        # Majority element's votes always outlast all opposition combined
        # O(n) time | O(1) space — Boyer-Moore Voting
        candidate, count = None, 0
        for n in nums:
            if count == 0:
                candidate = n
            count += 1 if n == candidate else -1
        return candidate


if __name__ == "__main__":
    s = Solution()
    assert s.majorityElement([3, 2, 3]) == 3
    assert s.majorityElement([2, 2, 1, 1, 1, 2, 2]) == 2
    assert s.majorityElement([1]) == 1
    print("All tests passed.")
Majority Element Ii ▼ expand
"""
# LC 229: Majority Element II

Given an integer array of size `n`, find all elements that appear more than
⌊n/3⌋ times.

## Examples

```text
Input:  nums = [3,2,3]
Output: [3]

Input:  nums = [1]
Output: [1]

Input:  nums = [1,2]
Output: [1,2]
```

## Constraints
- 1 <= nums.length <= 5 * 10^4
- -10^9 <= nums[i] <= 10^9
"""
from typing import List
from collections import Counter


class Solution:
    def majorityElement_hashmap(self, nums: List[int]) -> List[int]:
        """Hash map counting — O(n) time, O(n) space."""
        # Count all elements; return those appearing more than n//3 times
        threshold = len(nums) // 3
        return [num for num, cnt in Counter(nums).items() if cnt > threshold]

    def majorityElement(self, nums: List[int]) -> List[int]:
        """Boyer-Moore extended (at most 2 candidates) — O(n) time, O(1) space."""
        # At most 2 elements can appear more than n/3 times
        # Maintain two candidates with vote counts; cancel out different pairs
        # Final verification pass confirms candidates actually exceed threshold
        cand1, cand2, cnt1, cnt2 = 0, 1, 0, 0
        for n in nums:
            if n == cand1:
                cnt1 += 1
            elif n == cand2:
                cnt2 += 1
            elif cnt1 == 0:
                cand1, cnt1 = n, 1
            elif cnt2 == 0:
                cand2, cnt2 = n, 1
            else:
                cnt1 -= 1
                cnt2 -= 1
        threshold = len(nums) // 3
        return [c for c in (cand1, cand2) if nums.count(c) > threshold]


if __name__ == "__main__":
    s = Solution()

    assert sorted(s.majorityElement([3, 2, 3])) == [3]
    assert sorted(s.majorityElement([1])) == [1]
    assert sorted(s.majorityElement([1, 2])) == [1, 2]
    assert sorted(s.majorityElement([1, 1, 1, 3, 3, 2, 2, 2])) == [1, 2]

    print("All tests passed.")
Product Of Array Except Self ▼ expand
"""
# LC 238: Product of Array Except Self

Given an integer array `nums`, return an array `answer` such that `answer[i]`
is equal to the product of all the elements of `nums` except `nums[i]`.

The product of any prefix or suffix of `nums` is guaranteed to fit in a 32-bit
integer. You must write an algorithm that runs in O(n) time and without using
the division operation.

## Examples

```text
Input:  nums = [1,2,3,4]
Output: [24,12,8,6]

Input:  nums = [-1,1,0,-3,3]
Output: [0,0,9,0,0]
```

## Constraints
- 2 <= nums.length <= 10^5
- -30 <= nums[i] <= 30
- The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer
"""
from typing import List


class Solution:
    def productExceptSelf_brute(self, nums: List[int]) -> List[int]:
        """Brute force — O(n^2) time, O(1) extra space."""
        # For each position, multiply all other elements — O(n^2)
        n = len(nums)
        answer = []
        for i in range(n):
            p = 1
            for j in range(n):
                if j != i:
                    p *= nums[j]
            answer.append(p)
        return answer

    def productExceptSelf_prefix_suffix(self, nums: List[int]) -> List[int]:
        """Prefix and suffix arrays — O(n) time, O(n) space."""
        # prefix[i] = product of all elements to the LEFT of i
        # suffix[i] = product of all elements to the RIGHT of i
        # answer[i] = prefix[i] * suffix[i] — no division needed
        n = len(nums)
        prefix = [1] * n
        suffix = [1] * n
        for i in range(1, n):
            prefix[i] = prefix[i - 1] * nums[i - 1]
        for i in range(n - 2, -1, -1):
            suffix[i] = suffix[i + 1] * nums[i + 1]
        return [prefix[i] * suffix[i] for i in range(n)]

    def productExceptSelf(self, nums: List[int]) -> List[int]:
        """Single output array — O(n) time, O(1) extra space."""
        # First pass: fill answer[i] with running left-product up to i-1
        # Second pass: multiply in running right-product from i+1 onward
        # This avoids the O(n) extra arrays by reusing the output array
        n = len(nums)
        answer = [1] * n
        prefix = 1
        for i in range(n):
            answer[i] = prefix
            prefix *= nums[i]
        suffix = 1
        for i in range(n - 1, -1, -1):
            answer[i] *= suffix
            suffix *= nums[i]
        return answer


if __name__ == "__main__":
    s = Solution()

    assert s.productExceptSelf([1, 2, 3, 4]) == [24, 12, 8, 6]
    assert s.productExceptSelf([-1, 1, 0, -3, 3]) == [0, 0, 9, 0, 0]
    assert s.productExceptSelf([1, 1]) == [1, 1]

    print("All tests passed.")
Range Sum Query 2d Immutable ▼ expand
"""
# LC 304: Range Sum Query 2D - Immutable

Given a 2D matrix `matrix`, handle multiple queries of the following type:
Calculate the sum of the elements of `matrix` inside the rectangle defined by
its upper left corner `(row1, col1)` and lower right corner `(row2, col2)`.

Implement the `NumMatrix` class with efficient repeated queries.

## Examples

```text
matrix = [
  [3, 0, 1, 4, 2],
  [5, 6, 3, 2, 1],
  [1, 2, 0, 1, 5],
  [4, 1, 0, 1, 7],
  [1, 0, 3, 0, 5]
]
sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12
```

## Constraints
- m == matrix.length
- n == matrix[i].length
- 1 <= m, n <= 200
- -10^4 <= matrix[i][j] <= 10^4
- 0 <= row1 <= row2 < m
- 0 <= col1 <= col2 < n
- At most 10^4 calls will be made to sumRegion
"""
from typing import List


class NumMatrixBrute:
    """Brute force — O(m*n) per query, O(1) extra space."""

    def __init__(self, matrix: List[List[int]]) -> None:
        self.matrix = matrix

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        return sum(
            self.matrix[r][c]
            for r in range(row1, row2 + 1)
            for c in range(col1, col2 + 1)
        )


class NumMatrix:
    """2D prefix sum — O(m*n) build, O(1) per query."""

    def __init__(self, matrix: List[List[int]]) -> None:
        m, n = len(matrix), len(matrix[0])
        self.prefix = [[0] * (n + 1) for _ in range(m + 1)]
        for r in range(1, m + 1):
            for c in range(1, n + 1):
                self.prefix[r][c] = (
                    matrix[r - 1][c - 1]
                    + self.prefix[r - 1][c]
                    + self.prefix[r][c - 1]
                    - self.prefix[r - 1][c - 1]
                )

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        p = self.prefix
        return (
            p[row2 + 1][col2 + 1]
            - p[row1][col2 + 1]
            - p[row2 + 1][col1]
            + p[row1][col1]
        )


if __name__ == "__main__":
    matrix = [
        [3, 0, 1, 4, 2],
        [5, 6, 3, 2, 1],
        [1, 2, 0, 1, 5],
        [4, 1, 0, 1, 7],
        [1, 0, 3, 0, 5],
    ]
    nm = NumMatrix(matrix)
    assert nm.sumRegion(2, 1, 4, 3) == 8
    assert nm.sumRegion(1, 1, 2, 2) == 11
    assert nm.sumRegion(1, 2, 2, 4) == 12

    print("All tests passed.")
Remove Element ▼ expand
"""
# LC 27: Remove Element

Given an integer array `nums` and an integer `val`, remove all occurrences of `val`
in `nums` **in-place**. The order of the elements may be changed. Return the number
of elements in `nums` which are not equal to `val`.

## Examples

```text
Input:  nums = [3,2,2,3], val = 3
Output: 2, nums = [2,2,_,_]

Input:  nums = [0,1,2,2,3,0,4,2], val = 2
Output: 5, nums = [0,1,4,0,3,_,_,_]
```

## Constraints
- 0 <= nums.length <= 100
- 0 <= nums[i] <= 50
- 0 <= val <= 100
"""
from typing import List


class Solution:
    def removeElement_brute(self, nums: List[int], val: int) -> int:
        # Shift all elements left when val found — O(n^2) due to shifting
        # O(n^2) time | O(1) space
        i = 0
        while i < len(nums):
            if nums[i] == val:
                for j in range(i, len(nums) - 1):
                    nums[j] = nums[j + 1]
                nums.pop()
            else:
                i += 1
        return len(nums)

    def removeElement(self, nums: List[int], val: int) -> int:
        # k tracks the write position for non-val elements
        # Only advance k when we copy a valid element — O(n) single pass
        # O(n) time | O(1) space
        k = 0
        for i in range(len(nums)):
            if nums[i] != val:
                nums[k] = nums[i]
                k += 1
        return k


if __name__ == "__main__":
    s = Solution()
    nums = [3, 2, 2, 3]
    assert s.removeElement(nums, 3) == 2 and nums[:2] == [2, 2]

    nums = [0, 1, 2, 2, 3, 0, 4, 2]
    assert s.removeElement(nums, 2) == 5 and sorted(nums[:5]) == [0, 0, 1, 3, 4]
    print("All tests passed.")
Sort An Array ▼ expand
"""
# LC 912: Sort an Array

Given an array of integers `nums`, sort the array in ascending order and return it.
You must solve the problem **without using any built-in functions** in O(n log n)
time complexity and with the smallest space complexity possible.

## Examples

```text
Input:  nums = [5,2,3,1]
Output: [1,2,3,5]

Input:  nums = [5,1,1,2,0,0]
Output: [0,0,1,1,2,5]
```

## Constraints
- 1 <= nums.length <= 5 * 10^4
- -5 * 10^4 <= nums[i] <= 5 * 10^4
"""
from typing import List
import random


class Solution:
    # Approach 1: Merge Sort — O(n log n) time | O(n) space
    def sortArray_merge(self, nums: List[int]) -> List[int]:
        # Divide array in half recursively until single elements, then merge
        if len(nums) <= 1:
            return nums
        mid = len(nums) // 2
        left = self.sortArray_merge(nums[:mid])
        right = self.sortArray_merge(nums[mid:])
        return self._merge(left, right)

    def _merge(self, left, right):
        # Merge two sorted halves by comparing front elements one at a time
        result, i, j = [], 0, 0
        while i < len(left) and j < len(right):
            if left[i] <= right[j]:
                result.append(left[i]); i += 1
            else:
                result.append(right[j]); j += 1
        return result + left[i:] + right[j:]

    # Approach 2: Quick Sort — O(n log n) avg | O(log n) space
    def sortArray_quick(self, nums: List[int]) -> List[int]:
        # Randomized pivot avoids worst-case O(n^2) on sorted input
        self._quick(nums, 0, len(nums) - 1)
        return nums

    def _quick(self, nums, lo, hi):
        # Partition around pivot, then recursively sort both halves
        if lo >= hi:
            return
        pivot_idx = self._partition(nums, lo, hi)
        self._quick(nums, lo, pivot_idx - 1)
        self._quick(nums, pivot_idx + 1, hi)

    def _partition(self, nums, lo, hi):
        # Randomly swap pivot to end, then move all <= pivot to the left
        rand = random.randint(lo, hi)
        nums[rand], nums[hi] = nums[hi], nums[rand]
        pivot, i = nums[hi], lo - 1
        for j in range(lo, hi):
            if nums[j] <= pivot:
                i += 1
                nums[i], nums[j] = nums[j], nums[i]
        nums[i + 1], nums[hi] = nums[hi], nums[i + 1]
        return i + 1

    # Approach 3: Heap Sort — O(n log n) time | O(1) space
    def sortArray(self, nums: List[int]) -> List[int]:
        # Heap sort: build max-heap, then repeatedly extract max to end
        n = len(nums)
        for i in range(n // 2 - 1, -1, -1):
            self._heapify(nums, n, i)
        for i in range(n - 1, 0, -1):
            nums[0], nums[i] = nums[i], nums[0]
            self._heapify(nums, i, 0)
        return nums

    def _heapify(self, nums, n, i):
        # Sift down: swap with largest child to maintain heap property
        largest, l, r = i, 2 * i + 1, 2 * i + 2
        if l < n and nums[l] > nums[largest]:
            largest = l
        if r < n and nums[r] > nums[largest]:
            largest = r
        if largest != i:
            nums[i], nums[largest] = nums[largest], nums[i]
            self._heapify(nums, n, largest)


if __name__ == "__main__":
    s = Solution()
    assert s.sortArray([5, 2, 3, 1]) == [1, 2, 3, 5]
    assert s.sortArray([5, 1, 1, 2, 0, 0]) == [0, 0, 1, 1, 2, 5]
    assert s.sortArray([-4, 0, 7, 4, 9, -5, -1, 0, -7, -1]) == sorted([-4, 0, 7, 4, 9, -5, -1, 0, -7, -1])
    print("All tests passed.")
Sort Colors ▼ expand
"""
# LC 75: Sort Colors

Given an array `nums` with `n` objects colored red, white, or blue, sort them
in-place so that objects of the same color are adjacent, with the colors in the
order red, white, and blue. Use integers 0, 1, and 2 to represent red, white,
and blue respectively.

You must solve this without using the library's sort function.

## Examples

```text
Input:  nums = [2,0,2,1,1,0]
Output: [0,0,1,1,2,2]

Input:  nums = [2,0,1]
Output: [0,1,2]
```

## Constraints
- n == nums.length
- 1 <= n <= 300
- nums[i] is either 0, 1, or 2
"""
from typing import List


class Solution:
    def sortColors_counting(self, nums: List[int]) -> None:
        """Counting sort — O(n) time, O(1) space, two-pass."""
        # Count 0s, 1s, 2s then overwrite array in order — two passes
        count = [0, 0, 0]
        for n in nums:
            count[n] += 1
        i = 0
        for val, cnt in enumerate(count):
            for _ in range(cnt):
                nums[i] = val
                i += 1

    def sortColors(self, nums: List[int]) -> None:
        """Dutch National Flag — O(n) time, O(1) space, one-pass."""
        # Dutch National Flag: three regions — [0..lo-1]=0, [lo..mid-1]=1, [hi+1..n-1]=2
        # If mid is 0: swap with lo (both advance), if 1: advance mid, if 2: swap with hi
        # hi shrinks but mid doesn't advance after swap with hi (unknown value)
        lo, mid, hi = 0, 0, len(nums) - 1
        while mid <= hi:
            if nums[mid] == 0:
                nums[lo], nums[mid] = nums[mid], nums[lo]
                lo += 1
                mid += 1
            elif nums[mid] == 1:
                mid += 1
            else:
                nums[mid], nums[hi] = nums[hi], nums[mid]
                hi -= 1


if __name__ == "__main__":
    s = Solution()

    nums = [2, 0, 2, 1, 1, 0]
    s.sortColors(nums)
    assert nums == [0, 0, 1, 1, 2, 2], nums

    nums = [2, 0, 1]
    s.sortColors(nums)
    assert nums == [0, 1, 2], nums

    nums = [0]
    s.sortColors(nums)
    assert nums == [0], nums

    print("All tests passed.")
Subarray Sum Equals K ▼ expand
"""
# LC 560: Subarray Sum Equals K

Given an array of integers `nums` and an integer `k`, return the total number
of subarrays whose sum equals to `k`.

A subarray is a contiguous non-empty sequence of elements within an array.

## Examples

```text
Input:  nums = [1,1,1], k = 2
Output: 2

Input:  nums = [1,2,3], k = 3
Output: 2
```

## Constraints
- 1 <= nums.length <= 2 * 10^4
- -1000 <= nums[i] <= 1000
- -10^7 <= k <= 10^7
"""
from typing import List
from collections import defaultdict


class Solution:
    def subarraySum_brute(self, nums: List[int], k: int) -> int:
        """Brute force — O(n^2) time, O(1) space."""
        # Try all subarrays starting at i — O(n^2)
        count = 0
        for i in range(len(nums)):
            total = 0
            for j in range(i, len(nums)):
                total += nums[j]
                if total == k:
                    count += 1
        return count

    def subarraySum(self, nums: List[int], k: int) -> int:
        """Prefix sum with hash map — O(n) time, O(n) space."""
        # prefix[j] - prefix[i] == k  =>  prefix[i] == prefix[j] - k
        # Store how many times each prefix sum has occurred
        # freq[0]=1 handles subarrays starting from index 0
        count = 0
        prefix = 0
        freq: dict = defaultdict(int)
        freq[0] = 1
        for n in nums:
            prefix += n
            count += freq[prefix - k]
            freq[prefix] += 1
        return count


if __name__ == "__main__":
    s = Solution()

    assert s.subarraySum([1, 1, 1], 2) == 2
    assert s.subarraySum([1, 2, 3], 3) == 2
    assert s.subarraySum([1], 0) == 0
    assert s.subarraySum([-1, -1, 1], 0) == 1

    print("All tests passed.")
Top K Frequent Elements ▼ expand
"""
# LC 347: Top K Frequent Elements

Given an integer array `nums` and an integer `k`, return the `k` most frequent
elements. You may return the answer in any order.

## Examples

```text
Input:  nums = [1,1,1,2,2,3], k = 2
Output: [1,2]

Input:  nums = [1], k = 1
Output: [1]
```

## Constraints
- 1 <= nums.length <= 10^5
- -10^4 <= nums[i] <= 10^4
- k is in the range [1, the number of unique elements in the array]
- The answer is guaranteed to be unique
"""
from typing import List
from collections import Counter
import heapq


class Solution:
    def topKFrequent_sort(self, nums: List[int], k: int) -> List[int]:
        """Sort by frequency — O(n log n) time, O(n) space."""
        # Count frequencies, then sort by descending frequency and take top k
        count = Counter(nums)
        return sorted(count, key=lambda x: -count[x])[:k]

    def topKFrequent_heap(self, nums: List[int], k: int) -> List[int]:
        """Min-heap of size k — O(n log k) time, O(n) space."""
        # heapq.nlargest keeps a min-heap of size k — O(n log k)
        count = Counter(nums)
        return heapq.nlargest(k, count, key=count.get)

    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        """Bucket sort — O(n) time, O(n) space."""
        # Bucket sort: index = frequency, so we can iterate buckets in reverse
        # Collect elements from highest-frequency bucket down until we have k
        count = Counter(nums)
        buckets: List[List[int]] = [[] for _ in range(len(nums) + 1)]
        for num, freq in count.items():
            buckets[freq].append(num)
        result: List[int] = []
        for freq in range(len(buckets) - 1, 0, -1):
            for num in buckets[freq]:
                result.append(num)
                if len(result) == k:
                    return result
        return result


if __name__ == "__main__":
    s = Solution()

    assert sorted(s.topKFrequent([1, 1, 1, 2, 2, 3], 2)) == [1, 2]
    assert s.topKFrequent([1], 1) == [1]
    assert sorted(s.topKFrequent([1, 2], 2)) == [1, 2]

    print("All tests passed.")
Two Sum ▼ expand
"""
# LC 1: Two Sum

Given an array of integers `nums` and an integer `target`, return *indices* of the
two numbers such that they add up to `target`. You may assume exactly one solution
exists, and you may not use the same element twice.

## Examples

```text
Input:  nums = [2,7,11,15], target = 9
Output: [0,1]

Input:  nums = [3,2,4], target = 6
Output: [1,2]

Input:  nums = [3,3], target = 6
Output: [0,1]
```

## Constraints
- 2 <= nums.length <= 10^4
- -10^9 <= nums[i] <= 10^9
- -10^9 <= target <= 10^9
- Only one valid answer exists.
"""
from typing import List


class Solution:
    def twoSum_brute(self, nums: List[int], target: int) -> List[int]:
        # Try every pair (i, j) — O(n^2) because we check all combinations
        # O(n^2) time | O(1) space
        for i in range(len(nums)):
            for j in range(i + 1, len(nums)):
                if nums[i] + nums[j] == target:
                    return [i, j]
        return []

    def twoSum(self, nums: List[int], target: int) -> List[int]:
        # Use a hash map to remember each number's index as we scan
        # For each number, compute what complement is needed to reach target
        # If complement already seen, we found our pair in O(1) lookup
        # O(n) time | O(n) space
        seen = {}
        for i, n in enumerate(nums):
            diff = target - n
            if diff in seen:
                return [seen[diff], i]
            seen[n] = i
        return []


if __name__ == "__main__":
    s = Solution()
    assert s.twoSum([2, 7, 11, 15], 9) == [0, 1]
    assert s.twoSum([3, 2, 4], 6) == [1, 2]
    assert s.twoSum([3, 3], 6) == [0, 1]
    print("All tests passed.")
Valid Anagram ▼ expand
"""
# LC 242: Valid Anagram

Given two strings `s` and `t`, return `true` if `t` is an anagram of `s`,
and `false` otherwise.

## Examples

```text
Input:  s = "anagram", t = "nagaram"
Output: true

Input:  s = "rat", t = "car"
Output: false
```

## Constraints
- 1 <= s.length, t.length <= 5 * 10^4
- s and t consist of lowercase English letters
"""


class Solution:
    def isAnagram_sort(self, s: str, t: str) -> bool:
        # Sorting both strings makes them identical iff they're anagrams
        # O(n log n) time | O(1) space
        return sorted(s) == sorted(t)

    def isAnagram(self, s: str, t: str) -> bool:
        # Early exit if lengths differ — anagrams must have same length
        # Increment count for s's char, decrement for t's char simultaneously
        # All zeros at the end means every character balanced out
        # O(n) time | O(1) space — at most 26 keys
        if len(s) != len(t):
            return False
        count = [0] * 26
        for a, b in zip(s, t):
            count[ord(a) - ord('a')] += 1
            count[ord(b) - ord('a')] -= 1
        return all(c == 0 for c in count)


if __name__ == "__main__":
    s = Solution()
    assert s.isAnagram("anagram", "nagaram") == True
    assert s.isAnagram("rat", "car") == False
    assert s.isAnagram("a", "ab") == False
    print("All tests passed.")
Valid Sudoku ▼ expand
"""
# LC 36: Valid Sudoku

Determine if a 9 x 9 Sudoku board is valid. Only the filled cells need to be
validated according to the following rules:
1. Each row must contain the digits 1-9 without repetition.
2. Each column must contain the digits 1-9 without repetition.
3. Each of the nine 3 x 3 sub-boxes must contain the digits 1-9 without repetition.

Note: A Sudoku board (partially filled) could be valid but is not necessarily solvable.

## Examples

```text
Input: board =
[["5","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
Output: true

Input: board with "8" appearing twice in the first row -> false
```

## Constraints
- board.length == 9
- board[i].length == 9
- board[i][j] is a digit 1-9 or '.'
"""
from typing import List
from collections import defaultdict


class Solution:
    def isValidSudoku_brute(self, board: List[List[str]]) -> bool:
        """Check rows, cols, boxes separately — O(1) time/space."""
        # Check each row, column, and 3x3 box for duplicates separately
        for i in range(9):
            row = [x for x in board[i] if x != "."]
            col = [board[r][i] for r in range(9) if board[r][i] != "."]
            if len(row) != len(set(row)) or len(col) != len(set(col)):
                return False
        for br in range(3):
            for bc in range(3):
                box = [
                    board[br * 3 + r][bc * 3 + c]
                    for r in range(3)
                    for c in range(3)
                    if board[br * 3 + r][bc * 3 + c] != "."
                ]
                if len(box) != len(set(box)):
                    return False
        return True

    def isValidSudoku(self, board: List[List[str]]) -> bool:
        """Single pass with hash sets — O(1) time/space."""
        # Single pass: track seen values per row, column, and box simultaneously
        # Box index is (r//3, c//3) — maps each cell to its 3x3 region
        # If a value already exists in any of the three sets, board is invalid
        rows = defaultdict(set)
        cols = defaultdict(set)
        boxes = defaultdict(set)
        for r in range(9):
            for c in range(9):
                val = board[r][c]
                if val == ".":
                    continue
                box = (r // 3, c // 3)
                if val in rows[r] or val in cols[c] or val in boxes[box]:
                    return False
                rows[r].add(val)
                cols[c].add(val)
                boxes[box].add(val)
        return True


if __name__ == "__main__":
    s = Solution()

    valid_board = [
        ["5", "3", ".", ".", "7", ".", ".", ".", "."],
        ["6", ".", ".", "1", "9", "5", ".", ".", "."],
        [".", "9", "8", ".", ".", ".", ".", "6", "."],
        ["8", ".", ".", ".", "6", ".", ".", ".", "3"],
        ["4", ".", ".", "8", ".", "3", ".", ".", "1"],
        ["7", ".", ".", ".", "2", ".", ".", ".", "6"],
        [".", "6", ".", ".", ".", ".", "2", "8", "."],
        [".", ".", ".", "4", "1", "9", ".", ".", "5"],
        [".", ".", ".", ".", "8", ".", ".", "7", "9"],
    ]
    assert s.isValidSudoku(valid_board) is True

    invalid_board = [row[:] for row in valid_board]
    invalid_board[0][1] = "5"  # duplicate 5 in row 0
    assert s.isValidSudoku(invalid_board) is False

    print("All tests passed.")

Hash Map

Trade space for time with complement / prefix lookups

2
[0]
7
[1]
11
[2]
15
[3]
HashMap: seen = {
empty
}
Press "Next Step" to walk through Two Sum hash map lookup.