diff --git a/DIRECTORY.md b/DIRECTORY.md index caaa5692..c7bc792d 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -55,6 +55,10 @@ * [Test Max Profit Three](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/buy_sell_stock/test_max_profit_three.py) * [Test Max Profit Two](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/buy_sell_stock/test_max_profit_two.py) * [Test Max Profit With Fee](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/buy_sell_stock/test_max_profit_with_fee.py) + * Climb Stairs + * [Test Climb Stairs](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/climb_stairs/test_climb_stairs.py) + * Countingbits + * [Test Counting Bits](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/countingbits/test_counting_bits.py) * Domino Tromino Tiling * [Test Domino Tromino Tiling](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/domino_tromino_tiling/test_domino_tromino_tiling.py) * Duffle Bug Value @@ -69,6 +73,8 @@ * [Test Min Distance](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/min_distance/test_min_distance.py) * Min Path Sum * [Test Min Path Sum](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/min_path_sum/test_min_path_sum.py) + * Painthouse + * [Test Min Cost To Paint Houses](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/painthouse/test_min_cost_to_paint_houses.py) * Palindromic Substring * [Longest Palindromic Substring](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/palindromic_substring/longest_palindromic_substring.py) * [Test Longest Palindromic Substring](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/palindromic_substring/test_longest_palindromic_substring.py) @@ -138,12 +144,16 @@ * Spread Stones * [Test Minimum Moves To Spread Stones](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/greedy/spread_stones/test_minimum_moves_to_spread_stones.py) * Heap + * Kclosestelements + * [Test Find K Closest Elements](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/heap/kclosestelements/test_find_k_closest_elements.py) * Longest Happy String * [Test Longest Happy String](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/heap/longest_happy_string/test_longest_happy_string.py) * Maximal Score After K Operations * [Test Maximal Score](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/heap/maximal_score_after_k_operations/test_maximal_score.py) * Maximum Subsequence Score * [Test Max Subsequence Score](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/heap/maximum_subsequence_score/test_max_subsequence_score.py) + * Mergeksortedlists + * [Test Merge K Sorted Lists](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/heap/mergeksortedlists/test_merge_k_sorted_lists.py) * Min Cost Hire K Workers * [Test Min Cost To Hire Workers](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/heap/min_cost_hire_k_workers/test_min_cost_to_hire_workers.py) * Min Cost To Connect Sticks @@ -676,8 +686,6 @@ * [Battleship](https://github.com/BrianLusina/PythonSnips/blob/master/puzzles/battleship/battleship.py) * Beeramid * [Test Bearamid](https://github.com/BrianLusina/PythonSnips/blob/master/puzzles/beeramid/test_bearamid.py) - * Climb Stairs - * [Test Climb Stairs](https://github.com/BrianLusina/PythonSnips/blob/master/puzzles/climb_stairs/test_climb_stairs.py) * Find Missing Elem * [Test Find Missing Elem](https://github.com/BrianLusina/PythonSnips/blob/master/puzzles/find_missing_elem/test_find_missing_elem.py) * Hashmap diff --git a/puzzles/climb_stairs/README.md b/algorithms/dynamic_programming/climb_stairs/README.md similarity index 100% rename from puzzles/climb_stairs/README.md rename to algorithms/dynamic_programming/climb_stairs/README.md diff --git a/puzzles/climb_stairs/__init__.py b/algorithms/dynamic_programming/climb_stairs/__init__.py similarity index 100% rename from puzzles/climb_stairs/__init__.py rename to algorithms/dynamic_programming/climb_stairs/__init__.py diff --git a/puzzles/climb_stairs/test_climb_stairs.py b/algorithms/dynamic_programming/climb_stairs/test_climb_stairs.py similarity index 100% rename from puzzles/climb_stairs/test_climb_stairs.py rename to algorithms/dynamic_programming/climb_stairs/test_climb_stairs.py diff --git a/algorithms/dynamic_programming/countingbits/README.md b/algorithms/dynamic_programming/countingbits/README.md new file mode 100644 index 00000000..6d5dfc2c --- /dev/null +++ b/algorithms/dynamic_programming/countingbits/README.md @@ -0,0 +1,78 @@ +# Counting Bits + +Given an integer n, return an array ans of length n + 1 such that for each i (0 <= i <= n), ans[i] is the number of 1's +in the binary representation of i. + +## Examples + +```text +Example 1: + +Input: n = 2 +Output: [0,1,1] +Explanation: +0 --> 0 +1 --> 1 +2 --> 10 +``` + +``` +Example 2: + +Input: n = 5 +Output: [0,1,1,2,1,2] +Explanation: +0 --> 0 +1 --> 1 +2 --> 10 +3 --> 11 +4 --> 100 +5 --> 101 +``` + +## Constraints + +- 0 <= `n` <= 10^5 + +## Solution + +This solution uses a bottom-up dynamic programming approach to solve the problem. +The key to this problem lies in the fact that any binary number can be broken down into two parts: the least-significant +(rightmost bit), and the rest of the bits. The rest of the bits can be expressed as the binary number divided by 2 +(rounded down), or `i >> 1`. + +For example: +- 4 in binary = 100 +- rightmost bit = 0 +- rest of bits = 10, which is also (4 // 2) = 2 in binary. + +When the number is odd, +- 5 in binary = 101 +- rightmost bit = 1 +- rest of bits = 10, which is also (5 // 2) = 2 in binary. + +in the binary representation of i is that number plus 1 if the rightmost bit is 1. We can tell if the last significant +bit is 1 by checking if it is odd. + +As an example, we know that the number of 1's in 2 is 1. This information can be used to calculate the number of 1's in 4. +The number of 1's in 4 is the number of 1's in 2 plus 0, because 4 is even. + +This establishes a recurrence relationship between the number of 1's in the binary representation of i and the number of +1's in the binary representation of i // 2: if count[i] is the number of 1's in the binary representation of i, then +count[i] = count[i // 2] + (i % 2). + +With the recurrence relationship established, we can now solve the problem using a bottom-up dynamic programming approach. +We start with the base case dp[0] = 0, and then build up the solution for dp[i] for i from 1 to n. + +![Solution 1](./images/solutions/counting_bits_solution_1.png) +![Solution 2](./images/solutions/counting_bits_solution_2.png) +![Solution 3](./images/solutions/counting_bits_solution_3.png) +![Solution 4](./images/solutions/counting_bits_solution_4.png) +![Solution 5](./images/solutions/counting_bits_solution_5.png) +![Solution 6](./images/solutions/counting_bits_solution_6.png) +![Solution 7](./images/solutions/counting_bits_solution_7.png) +![Solution 8](./images/solutions/counting_bits_solution_8.png) +![Solution 9](./images/solutions/counting_bits_solution_9.png) +![Solution 10](./images/solutions/counting_bits_solution_10.png) +![Solution 11](./images/solutions/counting_bits_solution_11.png) +![Solution 12](./images/solutions/counting_bits_solution_12.png) diff --git a/algorithms/dynamic_programming/countingbits/__init__.py b/algorithms/dynamic_programming/countingbits/__init__.py new file mode 100644 index 00000000..3699563d --- /dev/null +++ b/algorithms/dynamic_programming/countingbits/__init__.py @@ -0,0 +1,20 @@ +from typing import List + + +def count_bits(n: int) -> List[int]: + """ + Counts the number of 1 bits in the given integer provided returning a list where count[i] stores the count of '1' + bits in the binary form of i. + Args: + n(int): integer + Returns: + list: count of 1 bits where each index contains the count of 1 bits + """ + dp = [0] * (n + 1) + + for i in range(1, n + 1): + # this can also be solved as which is faster in Python + # dp[i] = dp[i >> 1] + (i & 1) + dp[i] = dp[i // 2] + (i % 2) + + return dp diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_1.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_1.png new file mode 100644 index 00000000..0fcad317 Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_1.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_10.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_10.png new file mode 100644 index 00000000..5dacd825 Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_10.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_11.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_11.png new file mode 100644 index 00000000..7b5220db Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_11.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_12.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_12.png new file mode 100644 index 00000000..2eb72f21 Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_12.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_2.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_2.png new file mode 100644 index 00000000..b3b32438 Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_2.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_3.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_3.png new file mode 100644 index 00000000..f07938f4 Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_3.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_4.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_4.png new file mode 100644 index 00000000..19654c7b Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_4.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_5.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_5.png new file mode 100644 index 00000000..aa7c9832 Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_5.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_6.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_6.png new file mode 100644 index 00000000..e899c280 Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_6.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_7.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_7.png new file mode 100644 index 00000000..d7fd4b4f Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_7.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_8.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_8.png new file mode 100644 index 00000000..05307a38 Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_8.png differ diff --git a/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_9.png b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_9.png new file mode 100644 index 00000000..d525a73b Binary files /dev/null and b/algorithms/dynamic_programming/countingbits/images/solutions/counting_bits_solution_9.png differ diff --git a/algorithms/dynamic_programming/countingbits/test_counting_bits.py b/algorithms/dynamic_programming/countingbits/test_counting_bits.py new file mode 100644 index 00000000..73c5ffb9 --- /dev/null +++ b/algorithms/dynamic_programming/countingbits/test_counting_bits.py @@ -0,0 +1,24 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.dynamic_programming.countingbits import count_bits + +COUNTING_BITS_TEST_CASES = [ + (6, [0, 1, 1, 2, 1, 2, 2]), + (2, [0, 1, 1]), + (5, [0, 1, 1, 2, 1, 2]), + (0, [0]), + (7, [0, 1, 1, 2, 1, 2, 2, 3]), + (10, [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2]), +] + + +class CountingBitsTestCase(unittest.TestCase): + @parameterized.expand(COUNTING_BITS_TEST_CASES) + def test_counting_bits(self, n: int, expected: List[int]): + actual = count_bits(n) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/dynamic_programming/painthouse/README.md b/algorithms/dynamic_programming/painthouse/README.md new file mode 100644 index 00000000..50e27e10 --- /dev/null +++ b/algorithms/dynamic_programming/painthouse/README.md @@ -0,0 +1,107 @@ +# Paint House + +You are a renowned painter who is given a task to paint n houses in a row. You can paint each house with one of three colors: Red, Blue, or Green. The cost of painting each house with each color is different and given in a 2D array costs: + +- costs[i][0] = cost of painting house `i` Red +- costs[i][1] = cost of painting house `i` Blue +- costs[i][2] = cost of painting house `i` Green + +No two neighboring houses can have the same color. Return the minimum cost to paint all houses. + +## Constraints + +- 1 ≤ n ≤ 100 +- costs[i].length == 3 +- 1 ≤ costs[i][j] ≤ 1000 + +## Examples + +Example 1 + +```text +costs = [[8, 4, 15], [10, 7, 3], [6, 9, 12]] +Output: 13 + +Explanation: + +House 0: Blue (cost = 4) +House 1: Green (cost = 3) +House 2: Red (cost = 6) +Total = 4 + 3 + 6 = 13 + +``` + +Example 2 +```text +Input: + +costs = [[5, 8, 6], [19, 14, 13], [7, 5, 12], [14, 5, 9]] +Output: 30 + +Explanation: Red(5) → Green(13) → Red(7) → Blue(5) = 30 +``` + +## Solution + +The solution implements a space-optimized dynamic programming approach using three variables to track the minimum costs. + +Variable Initialization: + +prev_min_cost_red = 0: Minimum cost if the current house is painted red (color 0) +prev_min_cost_blue = 0: Minimum cost if the current house is painted blue (color 1) +prev_min_cost_green = 0: Minimum cost if the current house is painted green (color 2) + +These start at 0 because before painting any house, the cost is 0. + +Main Loop: The algorithm iterates through each house's costs using tuple unpacking: + +```python +for cost_red, cost_blue, cost_green in costs: + # +``` + +Where cost_red, cost_blue, cost_green represent the costs of painting the current house with red, blue, and green +respectively. + +State Transition: For each house, we simultaneously update all three variables: + +```python +prev_min_cost_red, prev_min_cost_blue, prev_min_cost_green = min(prev_min_cost_blue, prev_min_cost_green) + cost_red, +min(prev_min_cost_red, prev_min_cost_green) + cost_blue, min(prev_min_cost_red, prev_min_cost_blue) + cost_green +``` + +Breaking this down: + +- New `prev_min_cost_red` (cost if current house is red): `min(prev_min_cost_blue, prev_min_cost_green) + cost_red` + We take the minimum of the previous costs where the house was NOT red (either blue or green) + Add the cost of painting the current house red + +- New `prev_min_cost_blue` (cost if current house is blue): `min(prev_min_cost_red, prev_min_cost_green) + cost_blue` + We take the minimum of the previous costs where the house was NOT blue (either red or green) + Add the cost of painting the current house blue + +- New `prev_min_cost_green` (cost if current house is green): `min(prev_min_cost_red, prev_min_cost_blue) + cost_green` + We take the minimum of the previous costs where the house was NOT green (either red or blue) + Add the cost of painting the current house green + +The simultaneous assignment is crucial here - all three values are calculated using the old values before any updates occur. + +Final Result: After processing all houses, `prev_min_cost_red`, `prev_min_cost_blue`, and `prev_min_cost_green` contain +the minimum costs to paint all houses with the last house being red, blue, or green respectively. The answer is the +minimum among these three values: + +`return min(prev_min_cost_red, prev_min_cost_blue, prev_min_cost_green)` + +### Complexity Analysis + +#### Time Complexity: O(n) + +Where n is the number of houses (length of the costs array). The algorithm iterates through the costs array exactly once, +performing constant-time operations (comparisons and additions) for each house. + +#### Space Complexity: O(1) + +The algorithm uses only three variables (`prev_min_cost_red`, `prev_min_cost_blue`, `prev_min_cost_green`) to track the +minimum costs regardless of the input size. The space usage remains constant as it doesn't create any additional data +structures that scale with the input. The variables are reused and updated in each iteration rather than storing all +intermediate results. diff --git a/algorithms/dynamic_programming/painthouse/__init__.py b/algorithms/dynamic_programming/painthouse/__init__.py new file mode 100644 index 00000000..351d1317 --- /dev/null +++ b/algorithms/dynamic_programming/painthouse/__init__.py @@ -0,0 +1,43 @@ +from typing import List + + +def min_cost_to_paint_houses_alternate_colors(costs: List[List[int]]) -> int: + """ + Finds the minimum cost to paint houses in a street without repeating colors consecutively + Args: + costs(list): Costs of painting houses as a 2D list + Returns: + int: minimum cost of painting houses + """ + # If there are no costs of painting the houses, then the minimum cost is 0 + if not costs: + return 0 + + # initially, the cost of painting any house any color is 0 + # Initialize minimum costs for painting up to previous house with each color + # prev_min_cost_red: min cost if previous house painted red + # prev_min_cost_blue: min cost if previous house painted blue + # prev_min_cost_green: min cost if previous house painted green + prev_min_cost_red = 0 + prev_min_cost_blue = 0 + prev_min_cost_green = 0 + + # Iterate through each house cost + for current_cost in costs: + cost_red, cost_blue, cost_green = current_cost + + # Calculate minimum cost for current house with each color + # Current house painted red: add red cost to min of (prev blue, prev green) + # Current house painted blue: add blue cost to min of (prev red, prev green) + # Current house painted green: add green cost to min of (prev red, prev blue) + curr_min_cost_red = min(prev_min_cost_blue, prev_min_cost_green) + cost_red + curr_min_cost_blue = min(prev_min_cost_red, prev_min_cost_green) + cost_blue + curr_min_cost_green = min(prev_min_cost_red, prev_min_cost_blue) + cost_green + + # Update previous costs for next iteration + prev_min_cost_red = curr_min_cost_red + prev_min_cost_blue = curr_min_cost_blue + prev_min_cost_green = curr_min_cost_green + + # Return minimum cost among all three color options for the last house + return min(prev_min_cost_red, prev_min_cost_blue, prev_min_cost_green) diff --git a/algorithms/dynamic_programming/painthouse/test_min_cost_to_paint_houses.py b/algorithms/dynamic_programming/painthouse/test_min_cost_to_paint_houses.py new file mode 100644 index 00000000..b097d8f6 --- /dev/null +++ b/algorithms/dynamic_programming/painthouse/test_min_cost_to_paint_houses.py @@ -0,0 +1,20 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.dynamic_programming.painthouse import min_cost_to_paint_houses_alternate_colors + +MIN_COST_PAINT_HOUSE = [ + ([[8, 4, 15], [10, 7, 3], [6, 9, 12]], 13), + ([[5, 8, 6], [19, 14, 13], [7, 5, 12], [14, 5, 9]], 30), +] + + +class MinCostToPaintHouseTestCase(unittest.TestCase): + @parameterized.expand(MIN_COST_PAINT_HOUSE) + def test_min_cost_to_paint_houses(self, cost: List[List[int]], expected: int): + actual = min_cost_to_paint_houses_alternate_colors(cost) + self.assertEqual(expected, actual) + + +if __name__ == '__main__': + unittest.main() diff --git a/algorithms/heap/kclosestelements/README.md b/algorithms/heap/kclosestelements/README.md new file mode 100644 index 00000000..027bf90f --- /dev/null +++ b/algorithms/heap/kclosestelements/README.md @@ -0,0 +1,29 @@ +# Find K Closest Elements + +Given a sorted array nums, a target value target, and an integer k, find the k closest elements to target in the array, +where "closest" is the absolute difference between each element and target. Return these elements in an array, sorted in +ascending order. + +## Examples + +```text +nums = [-1, 0, 1, 4, 6] +target = 1 +k = 3 + +Output +[-1, 0, 1] + +Explanation +-1 is 2 away from 1, 0 is 1 away from 1, and 1 is 0 away from 1. All other elements are more than 2 away. +Since we need to return the elements in ascending order, the answer is [-1, 0, 1] +``` + +```text +nums = [5, 6, 7, 8, 9] +target = 10 +k = 2 + +Output: +[8, 9] +``` diff --git a/algorithms/heap/kclosestelements/__init__.py b/algorithms/heap/kclosestelements/__init__.py new file mode 100644 index 00000000..aa45f321 --- /dev/null +++ b/algorithms/heap/kclosestelements/__init__.py @@ -0,0 +1,18 @@ +from typing import List +import heapq + + +def k_closest(nums: List[int], k: int, target: int): + heap = [] + + for num in nums: + diff = abs(num - target) + + if len(heap) < k: + heapq.heappush(heap, (-diff, num)) + elif diff < -heap[0][0]: + heapq.heappushpop(heap, (-diff, num)) + + distances = [pair[1] for pair in heap] + distances.sort() + return distances diff --git a/algorithms/heap/kclosestelements/test_find_k_closest_elements.py b/algorithms/heap/kclosestelements/test_find_k_closest_elements.py new file mode 100644 index 00000000..198fda58 --- /dev/null +++ b/algorithms/heap/kclosestelements/test_find_k_closest_elements.py @@ -0,0 +1,26 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.heap.kclosestelements import k_closest + +K_CLOSEST_ELEMENTS_TEST_CASES = [ + ([1, 2, 3, 4, 5], 4, 3, [1, 2, 3, 4]), + ([1, 3, 5, 8, 10], 2, 5, [3, 5]), + ([3, 4, 7, 10, 15], 3, 8, [4, 7, 10]), + ([5, 6, 7, 8, 9], 3, 7, [6, 7, 8]), + ([1, 1, 1, 10, 10, 10], 1, 9, [10]), + ([1, 2, 3, 4, 5], 2, 6, [4, 5]), +] + + +class KClosestElementsTestCase(unittest.TestCase): + @parameterized.expand(K_CLOSEST_ELEMENTS_TEST_CASES) + def test_k_closest_elements( + self, nums: List[int], k: int, target: int, expected: List[int] + ): + actual = k_closest(nums, k, target) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/heap/mergeksortedlists/README.md b/algorithms/heap/mergeksortedlists/README.md new file mode 100644 index 00000000..450f6970 --- /dev/null +++ b/algorithms/heap/mergeksortedlists/README.md @@ -0,0 +1,18 @@ +# Merge K Sorted Lists + +Given k linked lists, each sorted in ascending order, in a list lists, write a function to merge the input lists into +one sorted linked list. + +## Examples + +```text +lists = [[3,4,6],[2,3,5],[-1,6]] +3 -> 4 -> 6 +2 -> 3 -> 5 +-1 -> 6 + +Output +[-1,2,3,3,4,5,6,6] + +-1 -> 2 -> 3 -> 3 -> 4 -> 5 -> 6 -> 6 +``` diff --git a/algorithms/heap/mergeksortedlists/__init__.py b/algorithms/heap/mergeksortedlists/__init__.py new file mode 100644 index 00000000..a5731bf1 --- /dev/null +++ b/algorithms/heap/mergeksortedlists/__init__.py @@ -0,0 +1,53 @@ +from typing import List +import heapq + + +def merge_k_lists(lists: List[List[int]]) -> List[int]: + """ + Merges k sorted lists into one list + Args: + lists(list): list of lists + Returns: + list: Combined list from the input list that is sorted + """ + # Validate that the input provided is not empty. If it is empty, return an empty list. Nothing more to do here + if not lists: + return [] + + # Check if any of the lists provided is empty. If we have an empty list, we return, as there is nothing more to do + has_non_empty = any(lst for lst in lists) + if not has_non_empty: + return [] + + # Initialize our heap. This will be used to keep the lowest value from each list at the root of the heap. Whenever + # a value is popped from the root, the next value is added to the list and if it is the lowest value it will be + # at the root of the heap + min_heap = [] + + # We will iterate through the list storing only the first values from each list, including the list index and the + # element index which we will use to iterate through a given list. The list index will be used to iterate through + # the provided lists + for idx, lst in enumerate(lists): + if lst: + value, list_index, element_index = lst[0], idx, 0 + heapq.heappush(min_heap, (value, list_index, element_index)) + + # Result will store the final output + result: List[int] = [] + + # While we still have elements in the heap + while min_heap: + # We get the top element from the heap + value, list_index, element_index = heapq.heappop(min_heap) + # Add the value to the result, at this point, we know this is the smallest value in the lists of lists + result.append(value) + + # We check if the element index is less than the current list it can be found in. This means that there are + # still other elements within this list + if element_index + 1 < len(lists[list_index]): + # We add the next value to the heap + next_value = lists[list_index][element_index + 1] + heapq.heappush(min_heap, (next_value, list_index, element_index + 1)) + + # Return the result + return result diff --git a/algorithms/heap/mergeksortedlists/test_merge_k_sorted_lists.py b/algorithms/heap/mergeksortedlists/test_merge_k_sorted_lists.py new file mode 100644 index 00000000..40ed0c80 --- /dev/null +++ b/algorithms/heap/mergeksortedlists/test_merge_k_sorted_lists.py @@ -0,0 +1,24 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.heap.mergeksortedlists import merge_k_lists + +MERGE_K_LISTS_TEST_CASES = [ + ([[3, 4, 6], [2, 3, 5], [-1, 6]], [-1, 2, 3, 3, 4, 5, 6, 6]), + ([[2, 4, 6], [1, 3, 5]], [1, 2, 3, 4, 5, 6]), + ([[1, 4, 5], [1, 3, 4], [2, 6]], [1, 1, 2, 3, 4, 4, 5, 6]), + ([], []), + ([[]], []), + ([[1, 2, 3]], [1, 2, 3]), +] + + +class MergeKListsTestCase(unittest.TestCase): + @parameterized.expand(MERGE_K_LISTS_TEST_CASES) + def test_merge_k_lists(self, lists: List[List[int]], expected: List[int]): + actual = merge_k_lists(lists) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main()