diff --git a/README.md b/README.md index 0ec23733f..65caeb5ca 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ If you want to uninstall algorithms, it is as simple as: - [merge_intervals](algorithms/arrays/merge_intervals.py) - [missing_ranges](algorithms/arrays/missing_ranges.py) - [plus_one](algorithms/arrays/plus_one.py) + - [remove_duplicates](algorithms/arrays/remove_duplicates.py) - [rotate](algorithms/arrays/rotate.py) - [summarize_ranges](algorithms/arrays/summarize_ranges.py) - [three_sum](algorithms/arrays/three_sum.py) diff --git a/algorithms/arrays/__init__.py b/algorithms/arrays/__init__.py index 9670db750..2632ca1c7 100644 --- a/algorithms/arrays/__init__.py +++ b/algorithms/arrays/__init__.py @@ -16,3 +16,4 @@ from .two_sum import * from .limit import * from .n_sum import * +from .remove_duplicates import * \ No newline at end of file diff --git a/algorithms/arrays/longest_non_repeat.py b/algorithms/arrays/longest_non_repeat.py index 786c03dfe..b25a952ca 100644 --- a/algorithms/arrays/longest_non_repeat.py +++ b/algorithms/arrays/longest_non_repeat.py @@ -88,4 +88,22 @@ def get_longest_non_repeat_v2(string): max_len = index - start + 1 sub_string = string[start: index + 1] used_char[char] = index - return max_len, sub_string \ No newline at end of file + return max_len, sub_string + +def get_longest_non_repeat_v3(string): + """ + Find the length of the longest substring + without repeating characters. + Uses window sliding approach. + Return max_len and the substring as a tuple + """ + longest_substring = '' + seen = set() + start_idx = 0 + for i in range(len(string)): + while string[i] in seen: + seen.remove(string[start_idx]) + start_idx += 1 + seen.add(string[i]) + longest_substring = max(longest_substring, string[start_idx: i+1], key=len) + return len(longest_substring), longest_substring diff --git a/algorithms/arrays/remove_duplicates.py b/algorithms/arrays/remove_duplicates.py new file mode 100644 index 000000000..1c0bc0a06 --- /dev/null +++ b/algorithms/arrays/remove_duplicates.py @@ -0,0 +1,18 @@ +""" +This algorithm removes any duplicates from an array and returns a new array with those duplicates +removed. + +For example: + +Input: [1, 1 ,1 ,2 ,2 ,3 ,4 ,4 ,"hey", "hey", "hello", True, True] +Output: [1, 2, 3, 4, 'hey', 'hello'] +""" + +def remove_duplicates(array): + new_array = [] + + for item in array: + if item not in new_array: + new_array.append(item) + + return new_array \ No newline at end of file diff --git a/algorithms/arrays/summarize_ranges.py b/algorithms/arrays/summarize_ranges.py index 8cfba68e1..58de7421a 100644 --- a/algorithms/arrays/summarize_ranges.py +++ b/algorithms/arrays/summarize_ranges.py @@ -6,22 +6,20 @@ """ -def summarize_ranges(array): - """ - :type array: List[int] - :rtype: List[] - """ +from typing import List + +def summarize_ranges(array: List[int]) -> List[str]: res = [] if len(array) == 1: return [str(array[0])] - i = 0 - while i < len(array): - num = array[i] - while i + 1 < len(array) and array[i + 1] - array[i] == 1: - i += 1 - if array[i] != num: - res.append((num, array[i])) + it = iter(array) + start = end = next(it) + for num in it: + if num - end == 1: + end = num else: - res.append((num, num)) - i += 1 - return res + res.append((start, end) if start != end else (start,)) + start = end = num + res.append((start, end) if start != end else (start,)) + return [f"{r[0]}-{r[1]}" if len(r) > 1 else str(r[0]) for r in res] + diff --git a/algorithms/graph/strongly_connected_components_kosaraju.py b/algorithms/graph/strongly_connected_components_kosaraju.py new file mode 100644 index 000000000..0c82a7af6 --- /dev/null +++ b/algorithms/graph/strongly_connected_components_kosaraju.py @@ -0,0 +1,81 @@ +""" +Implementing strongly connected components in a graph using Kosaraju's algorithm. +https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm +""" + + +class Kosaraju: + """ + Kosaraju's algorithm use depth first search approach to find strongly connected components in a directed graph. + Approach: + 1. Make a DFS call to keep track of finish time of each vertex. + 2. Tranpose the original graph. ie 1->2 transpose is 1<-2 + 3. Make another DFS call to calculate strongly connected components. + """ + + def dfs(self, i, V, adj, visited, stk): + visited[i] = 1 + + for x in adj[i]: + if visited[x] == -1: + self.dfs(x, V, adj, visited, stk) + + stk.append(i) + + def kosaraju(self, V, adj): + + stk, visited = [], [-1]*(V+1) + + for i in range(V): + if visited[i] == -1: + self.dfs(i, V, adj, visited, stk) + + stk.reverse() + res = stk.copy() + + ans, visited1 = 0, [-1]*(V+1) + + adj1 = [[] for x in range(V)] + + for i in range(len(adj)): + for x in adj[i]: + adj1[x].append(i) + + for i in range(len(res)): + if visited1[res[i]] == -1: + ans += 1 + self.dfs(res[i], V, adj1, visited1, stk) + + return ans + + +def main(): + """ + Let's look at the sample input. + + 6 7 #no of vertex, no of edges + 0 2 #directed edge 0->2 + 1 0 + 2 3 + 3 1 + 3 4 + 4 5 + 5 4 + + calculating no of strongly connected compnenets in a directed graph. + answer should be: 2 + 1st strong component: 0->2->3->1->0 + 2nd strongly connected component: 4->5->4 + """ + V, E = map(int, input().split()) + adj = [[] for x in range(V)] + + for i in range(E): + u, v = map(int, input().split()) + adj[u].append(v) + + print(Kosaraju().kosaraju(V, adj)) + + +if __name__ == '__main__': + main() diff --git a/algorithms/maths/polynomial.py b/algorithms/maths/polynomial.py index 07a0c3b8a..81faf2b7e 100644 --- a/algorithms/maths/polynomial.py +++ b/algorithms/maths/polynomial.py @@ -438,10 +438,7 @@ def __floordiv__(self, other: Union[int, float, Fraction, Monomial]): # def __truediv__(self, other: Union[int, float, Fraction, Monomial, Polynomial]) -> Polynomial: def __truediv__(self, other: Union[int, float, Fraction, Monomial]): """ - For Polynomials, only division by a monomial - is defined. - - TODO: Implement polynomial / polynomial. + For Polynomial division, no remainder is provided. Must use poly_long_division() to capture remainder """ if isinstance(other, int) or isinstance(other, float) or isinstance(other, Fraction): return self.__truediv__( Monomial({}, other) ) @@ -449,17 +446,11 @@ def __truediv__(self, other: Union[int, float, Fraction, Monomial]): poly_temp = reduce(lambda acc, val: acc + val, map(lambda x: x / other, [z for z in self.all_monomials()]), Polynomial([Monomial({}, 0)])) return poly_temp elif isinstance(other, Polynomial): - if Monomial({}, 0) in other.all_monomials(): - if len(other.all_monomials()) == 2: - temp_set = {x for x in other.all_monomials() if x != Monomial({}, 0)} - only = temp_set.pop() - return self.__truediv__(only) - elif len(other.all_monomials()) == 1: - temp_set = {x for x in other.all_monomials()} - only = temp_set.pop() - return self.__truediv__(only) - - raise ValueError('Can only divide a polynomial by an int, float, Fraction, or a Monomial.') + # Call long division + quotient, remainder = self.poly_long_division(other) + return quotient # Return just the quotient, remainder is ignored here + + raise ValueError('Can only divide a polynomial by an int, float, Fraction, Monomial, or Polynomial.') return @@ -526,7 +517,59 @@ def subs(self, substitutions: Union[int, float, Fraction, Dict[int, Union[int, f def __str__(self) -> str: """ - Get a string representation of - the polynomial. + Get a properly formatted string representation of the polynomial. + """ + sorted_monos = sorted(self.all_monomials(), key=lambda m: sorted(m.variables.items(), reverse=True), + reverse=True) + return ' + '.join(str(m) for m in sorted_monos if m.coeff != Fraction(0, 1)) + + def poly_long_division(self, other: 'Polynomial') -> tuple['Polynomial', 'Polynomial']: """ - return ' + '.join(str(m) for m in self.all_monomials() if m.coeff != Fraction(0, 1)) + Perform polynomial long division + Returns (quotient, remainder) + """ + if not isinstance(other, Polynomial): + raise ValueError("Can only divide by another Polynomial.") + + if len(other.all_monomials()) == 0: + raise ValueError("Cannot divide by zero polynomial.") + + quotient = Polynomial([]) + remainder = self.clone() + + divisor_monos = sorted(other.all_monomials(), key=lambda m: sorted(m.variables.items(), reverse=True), + reverse=True) + divisor_lead = divisor_monos[0] + + while remainder.all_monomials() and max(remainder.variables(), default=-1) >= max(other.variables(), + default=-1): + remainder_monos = sorted(remainder.all_monomials(), key=lambda m: sorted(m.variables.items(), reverse=True), + reverse=True) + remainder_lead = remainder_monos[0] + + if not all(remainder_lead.variables.get(var, 0) >= divisor_lead.variables.get(var, 0) for var in + divisor_lead.variables): + break + + lead_quotient = remainder_lead / divisor_lead + quotient = quotient + Polynomial([lead_quotient]) # Convert Monomial to Polynomial + + remainder = remainder - ( + Polynomial([lead_quotient]) * other) # Convert Monomial to Polynomial before multiplication + + return quotient, remainder + +dividend = Polynomial([ + Monomial({1: 3}, 4), # 4(a_1)^3 + Monomial({1: 2}, 3), # 3(a_1)^2 + Monomial({1: 1}, -2), # -2(a_1) + Monomial({}, 5) # +5 +]) + +divisor = Polynomial([ + Monomial({1: 1}, 2), # 2(a_1) + Monomial({}, -1) # -1 +]) + +quotient = dividend / divisor +print("Quotient:", quotient) diff --git a/algorithms/sort/bead_sort.py b/algorithms/sort/bead_sort.py new file mode 100644 index 000000000..0d949fa82 --- /dev/null +++ b/algorithms/sort/bead_sort.py @@ -0,0 +1,26 @@ +""" +Bead Sort (also known as Gravity Sort) is a natural sorting algorithm that simulates how beads would settle under gravity on an abacus. It is most useful for sorting positive integers, especially when the range of numbers isn't excessively large. However, it is not a comparison-based sort and is generally impractical for large inputs due to its reliance on physical modeling. +Time Complexity +- Best Case: O(n) if the numbers are already sorted +- Average Case: O(n^2) because each bead needs to be placed and then fall under gravity +- Worst Case: O(n^2) since each bead must "fall" individually +""" + +def bead_sort(arr): + if any(num < 0 for num in arr): + raise ValueError("Bead sort only works with non-negative integers.") + + max_num = max(arr) if arr else 0 + grid = [[0] * len(arr) for _ in range(max_num)] + + # Drop beads (place beads in columns) + for col, num in enumerate(arr): + for row in range(num): + grid[row][col] = 1 + + # Let the beads "fall" (count beads in each row) + for row in grid: + sum_beads = sum(row) + for col in range(len(arr)): + row[col] = 1 if col < sum_beads else 0 + diff --git a/tests/test_array.py b/tests/test_array.py index eaabfc710..b73ecb17b 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -9,6 +9,7 @@ missing_ranges, move_zeros, plus_one_v1, plus_one_v2, plus_one_v3, + remove_duplicates, rotate_v1, rotate_v2, rotate_v3, summarize_ranges, three_sum, @@ -298,6 +299,15 @@ def test_plus_one_v3(self): self.assertListEqual(plus_one_v3([9, 9, 9, 9]), [1, 0, 0, 0, 0]) +class TestRemoveDuplicate(unittest.TestCase): + + def test_remove_duplicates(self): + self.assertListEqual(remove_duplicates([1,1,1,2,2,2,3,3,4,4,5,6,7,7,7,8,8,9,10,10])) + self.assertListEqual(remove_duplicates(["hey", "hello", "hello", "car", "house", "house"])) + self.assertListEqual(remove_duplicates([True, True, False, True, False, None, None])) + self.assertListEqual(remove_duplicates([1,1,"hello", "hello", True, False, False])) + self.assertListEqual(remove_duplicates([1, "hello", True, False])) + class TestRotateArray(unittest.TestCase): diff --git a/tests/test_graph.py b/tests/test_graph.py index eb50dd4e1..540d29983 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -14,6 +14,7 @@ from algorithms.graph import cycle_detection from algorithms.graph import find_path from algorithms.graph import path_between_two_vertices_in_digraph +from algorithms.graph import strongly_connected_components_kosaraju import unittest @@ -349,3 +350,19 @@ def test_node_is_reachable(self): self.assertTrue(g.is_reachable(1, 3)) self.assertFalse(g.is_reachable(3, 1)) +class TestStronglyConnectedComponentsKosaraju(unittest.TestCase): + def test_kosaraju_algorithm(self): + V = 6 + adj = [ + [2], + [0], + [3], + [1, 4], + [5], + [4] + ] + + result = strongly_connected_components_kosaraju.Kosaraju().kosaraju(V, adj) + + # Expected result: 2 strongly connected components + self.assertEqual(result, 2) diff --git a/tests/test_polynomial.py b/tests/test_polynomial.py index 4ba2ba6b7..ab5674017 100644 --- a/tests/test_polynomial.py +++ b/tests/test_polynomial.py @@ -105,27 +105,6 @@ def test_polynomial_multiplication(self): ])) return - def test_polynomial_division(self): - - # Should raise a ValueError if the divisor is not a monomial - # or a polynomial with only one term. - self.assertRaises(ValueError, lambda x, y: x / y, self.p5, self.p3) - self.assertRaises(ValueError, lambda x, y: x / y, self.p6, self.p4) - - self.assertEqual(self.p3 / self.p2, Polynomial([ - Monomial({}, 1), - Monomial({1: 1, 2: -1}, 0.75) - ])) - self.assertEqual(self.p7 / self.m1, Polynomial([ - Monomial({1: -1, 2: -3}, 2), - Monomial({1: 0, 2: -4}, 1.5) - ])) - self.assertEqual(self.p7 / self.m1, Polynomial([ - Monomial({1: -1, 2: -3}, 2), - Monomial({2: -4}, 1.5) - ])) - return - def test_polynomial_variables(self): # The zero polynomial has no variables. @@ -172,4 +151,51 @@ def test_polynomial_clone(self): self.assertEqual(self.p5.clone(), Polynomial([ Monomial({1: -1, 3: 2}, 1) ])) - return \ No newline at end of file + return + + def test_polynomial_long_division(self): + """ + Test polynomial long division + """ + + # Dividend: 4a_1^3 + 3a_1^2 - 2a_1 + 5 + dividend = Polynomial([ + Monomial({1: 3}, 4), # 4(a_1)^3 + Monomial({1: 2}, 3), # 3(a_1)^2 + Monomial({1: 1}, -2), # -2(a_1) + Monomial({}, 5) # +5 + ]) + + # Divisor: 2a_1 - 1 + divisor = Polynomial([ + Monomial({1: 1}, 2), # 2(a_1) + Monomial({}, -1) # -1 + ]) + + # Expected Quotient: 2a_1^2 + (5/2)a_1 + 1/4 + expected_quotient = Polynomial([ + Monomial({1: 2}, 2), # 2(a_1)^2 + Monomial({1: 1}, Fraction(5, 2)), # (5/2)(a_1) + Monomial({}, Fraction(1, 4)) # +1/4 + ]) + + # Expected Remainder: 21/4 + expected_remainder = Polynomial([ + Monomial({}, Fraction(21, 4)) # 21/4 + ]) + + quotient_long_div, remainder_long_div = dividend.poly_long_division(divisor) + + quotient_truediv = dividend / divisor # Calls __truediv__, which returns only the quotient + + # Check if quotient from poly_long_division matches expected + self.assertEqual(quotient_long_div, expected_quotient) + + # Check if remainder from poly_long_division matches expected + self.assertEqual(remainder_long_div, expected_remainder) + + # Check if quotient from __truediv__ matches quotient from poly_long_division + self.assertEqual(quotient_truediv, quotient_long_div) + + return +