├── 50 Days of DSA in Python ├── Day 24 │ ├── Rotate_Array.py │ ├── Container_with_Most_Water.py │ └── Readme.md ├── Day 18 │ ├── Max_Subarray_Dynamic_Programming_Approach.py │ ├── Max_Subarray_Brute_Force_Approach.py │ ├── Max_Product_Subarray_Brute_Force_Approach.py │ └── Max_Product_Subarray_Dynamic_Programming_Approach.py ├── Day 25 │ ├── Two_Sum.py │ ├── Isomorphic_Strings.py │ └── Readme.md ├── Day 27 │ ├── Group_Anagrams.py │ ├── Longest_Unique_Substring.py │ └── Readme.md ├── Day 52 │ ├── DFS_(Depth-First Search)_Recursive_Approach.py │ ├── BFS_(Breadth-First Search).py │ └── DFS_(Depth-First Search)_Iterative_Approach.py ├── Day 51 │ ├── Min_Priority_Queue.py │ └── Max_Heap.py ├── Day 35 │ ├── Find_Duplicate.py │ └── Add_2_Numbers.py ├── Day 26 │ ├── Palindrome.py │ ├── Non-Repeating_Character.py │ └── Readme.md ├── Day 54 │ ├── Number_of_Provinces.py │ └── Find_if_Path_Exists_in_Graph.py ├── Day 30 │ ├── Insertion_Sort.py │ └── Bubble_Sort.py ├── Day 53 │ ├── Number_of_Connected_Components.py │ └── Topological_Sort.py ├── Day 34 │ ├── Cycle Detection.py │ ├── Reverse_SSL.py │ └── Readme.md ├── Day 12 │ ├── Target_Sum.py │ └── Partition_Equal_Subset_Sum.py ├── Day 20 │ ├── Jump_Game_1.py │ └── Minimum_Number_of_Arrows_to_Burst_Balloons.py ├── Day 33 │ ├── Delete_Duplicates.py │ └── Construct_SSL.py ├── Day 28 │ ├── Binary_Search.py │ └── Search_in_Rotated_Sorted_Array.py ├── Day 36 │ ├── DLL_Remove_All.py │ └── DLL_Remove_Insert.py ├── Day 23 │ ├── Jump_Game_2.py │ ├── Gas_Stations.py │ └── Readme.md ├── Day 31 │ ├── Selection_Sort.py │ └── Merge_Sort.py ├── Day 09 │ ├── Fibonacci_with_Recursion.py │ ├── Fibonacci_with_Tabulation.py │ ├── Fibonacci_with_Memorization.py │ ├── Fibonacci_with_Space_Optimization.py │ └── Claimbing_Stairs_with_Recursion.py ├── Day 21 │ ├── Boats_to_Save_People.py │ └── Two_City_Scheduling.py ├── Day 22 │ ├── Largest_Number.py │ └── Task_Scheduler.py ├── Day 40 │ ├── Pre-order_Traversal_(Iterative).py │ └── In-order_Traversal_(Iterative).py ├── Day 47 │ ├── Convert_Sorted_Array_to_BST.py │ └── Validate_BST.py ├── Day 32 │ ├── Quick_Sort.py │ └── Radix_Sort.py ├── Day 55 │ ├── Numbers_with_Sam_Consecutive_Differences.py │ └── Number_of_Islands.py ├── Day 41 │ ├── Post-order_Traversal_(Iterative).py │ └── Path_Sum_2.py ├── Day 46 │ ├── Invert_Tree.py │ └── Diameter_of_Tree.py ├── Day 43 │ ├── Left_View_of_Binary_Tree.py │ ├── Right_View_of_Binary_Tree.py │ └── Level_Order_Traversal_(Breadth-First Search).py ├── Day 15 │ ├── Longest_Palindromic_Subsequence.py │ ├── Longest_Palindromic_Substring.py │ ├── Palindromic_Substring_Tabulation_Approach.py │ ├── Palindromic_Substring_Memorization_Approach.py │ └── Palindromic_Substring_Recursive_Approach.py ├── Day 50 │ ├── N-ary_Tree_Level_Order_Traversal.py │ └── Serialize_and_Deserialize_Binary_Tree.py ├── Day 19 │ ├── Non_Overlaping_Intervals.py │ └── Fractional_Knapsack.py ├── Day 29 │ ├── Search_in_2D_Array.py │ └── Find_First_and_Last_Position.py ├── Day 37 │ ├── Construct_Stack.py │ └── Reverse_Polish_Notation.py ├── Day 16 │ ├── Palindrome_Partitioning_2_Tabulation_Approach_A.py │ ├── Palindrome_Partitioning_2_Tabulation_Approach_B.py │ ├── Palindrome_Partitioning_2_Recursive_Approach.py │ ├── Palindrome_Partitioning.py │ └── Palindrome_Partitioning_2_Memorization_Approach.py ├── Day 38 │ ├── Construct_Queue.py │ └── Implement_Queue_with_Stack.py ├── Day 17 │ ├── Matrix_Chain_Multiplication_Tabulation_Approach.py │ ├── Word_Break.py │ ├── Word_Break_Tabulation_Approach.py │ ├── Matrix_Chain_Multiplication_Recursive_Approach.py │ ├── Word_Break_Recursive_Approach.py │ ├── Matrix_Chain_Multiplication_Memorization_Approach.py │ └── Word_Break_Memorization_Approach.py ├── Day 42 │ ├── In-order_and_Post-order_Traversal.py │ └── Construct_Binary_Tree_from_Pre-order_and_In-order_Traversal.py ├── Day 13 │ ├── LCS_Recursice_Approach.py │ ├── LCS_Tabulation_Approach.py │ ├── LCS_Space_Optimised_Tabulation_Approach.py │ ├── LCS_Memorization_Approach.py │ ├── Edit_Distance_Tabulation_Approach.py │ ├── Edit_Distance_Recursive_Approach.py │ ├── Edit_Distance_Space_Optimised_Tabulation_Approach.py │ └── Edit_Distance_Memorization_Approach.py ├── Day 45 │ ├── Sum_Root_to_Leaf_Numbers.py │ └── Vertical_Order_Traversal.py ├── Day 14 │ ├── LIS_Recursive_Approach.py │ ├── LIS_Memorization_Approach.py │ ├── LIS_Tabulation_1D_Array_Approach.py │ ├── LIS_Tabulation_2D_Array_Approach.py │ ├── LIS_Binary_Search_Approach.py │ ├── Max_Length_of_Pair_Chain.py │ └── Russian_Doll_Envelopes.py ├── Day 48 │ ├── Unique_BST_2.py │ └── Lowest_Common_Ancestor_of_BST.py ├── Day 49 │ ├── Unique_BST_1.py │ └── Lowest_Common_Ancestor_of_Binary_Tree.py ├── Day 02 │ ├── Josephus_Problem_Method_3.py │ ├── K-th_Symbol_Grammar.py │ ├── Josephus_Problem_Method_2.py │ └── Josephus_Problem_Method_1.py ├── Day 44 │ ├── Zigzag_(Spiral)_Traversal.py │ └── Level_Order_Traversal_2.py ├── Day 10 │ ├── Tribonacci.py │ ├── Minimun_Cost_Climbing_Stairs_Tabulation_Approach.py │ ├── Minimun_Cost_Climbing_Stairs_Recursive_Approach.py │ └── Minimun_Cost_Climbing_Stairs_Memorization_Approach.py ├── Day 03 │ ├── Power_Sum.py │ └── Tower_Of_Hanoi.py ├── Day 39 │ ├── Traversa_Techniques.py │ └── Construct_BST.py ├── Day 11 │ ├── 1_Knapsack_Tabulation_Approach.py │ ├── 1_Knapsack_Space_Optimised_Tabulation_Approach.py │ ├── 1_Knapsack_Recursive_Approach.py │ ├── 1_Unbounded_Knapsack.py │ └── 1_Knapsack_Memorization_Approach .py ├── Day 04 │ ├── Permutations_Question_1.py │ └── Permutations_Question_2.py ├── Day 01 │ ├── Array_Question_2.py │ └── Array_Question_1.py ├── Day 06 │ ├── Combination_Question_1.py │ ├── Combination_Question_1_with_Optimization.py │ └── Combination_Sum_Question_1.py ├── Day 05 │ ├── Subsets_Question_1.py │ └── Subsets_Question_2.py ├── Day 07 │ ├── Combination_Sum_Question_3.py │ └── Combination_Sum_Question_2.py └── Day 08 │ └── N_Queen.py └── LICENSE /50 Days of DSA in Python/Day 24/Rotate_Array.py: -------------------------------------------------------------------------------- 1 | def rotate(nums, k): 2 | """ 3 | Rotate the array to the right by k steps. 4 | 5 | Args: 6 | nums (list): The list of integers to rotate. 7 | k (int): The number of positions to rotate. 8 | 9 | Returns: 10 | None: The function modifies the list in-place. 11 | """ 12 | n = len(nums) 13 | k = k % n # Handle cases where k is greater than the length of the array 14 | nums[:] = nums[-k:] + nums[:-k] 15 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 18/Max_Subarray_Dynamic_Programming_Approach.py: -------------------------------------------------------------------------------- 1 | def max_subarray_kadane(nums): 2 | """ 3 | Find the maximum subarray sum using Kadane's Algorithm. 4 | 5 | Args: 6 | nums (list): List of integers. 7 | 8 | Returns: 9 | int: Maximum subarray sum. 10 | """ 11 | max_sum = float('-inf') 12 | current_sum = 0 13 | 14 | for num in nums: 15 | current_sum = max(num, current_sum + num) 16 | max_sum = max(max_sum, current_sum) 17 | 18 | return max_sum 19 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 25/Two_Sum.py: -------------------------------------------------------------------------------- 1 | def twoSum(nums, target): 2 | """ 3 | Find two numbers in the list that add up to the target using a hash table. 4 | 5 | Args: 6 | nums (list): List of integers. 7 | target (int): The target sum. 8 | 9 | Returns: 10 | list: Indices of the two numbers. 11 | """ 12 | num_dict = {} 13 | for i, num in enumerate(nums): 14 | complement = target - num 15 | if complement in num_dict: 16 | return [num_dict[complement], i] 17 | num_dict[num] = i 18 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 27/Group_Anagrams.py: -------------------------------------------------------------------------------- 1 | def groupAnagrams(strs): 2 | """ 3 | Group the anagrams in the given list of strings. 4 | 5 | Args: 6 | strs (list): A list of strings. 7 | 8 | Returns: 9 | list: A list of lists containing grouped anagrams. 10 | """ 11 | anagrams = {} 12 | 13 | for s in strs: 14 | sorted_str = ''.join(sorted(s)) 15 | if sorted_str not in anagrams: 16 | anagrams[sorted_str] = [] 17 | anagrams[sorted_str].append(s) 18 | 19 | return list(anagrams.values()) 20 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 52/DFS_(Depth-First Search)_Recursive_Approach.py: -------------------------------------------------------------------------------- 1 | def dfs(graph, node, visited=None): 2 | if visited is None: 3 | visited = set() 4 | 5 | visited.add(node) 6 | print(node, end=" ") # Visit the node (you can process it here) 7 | 8 | for neighbor in graph[node]: 9 | if neighbor not in visited: 10 | dfs(graph, neighbor, visited) 11 | 12 | # Example: 13 | graph = { 14 | 0: [1, 2], 15 | 1: [0, 3, 4], 16 | 2: [0], 17 | 3: [1], 18 | 4: [1] 19 | } 20 | dfs(graph, 0) # Output: 0 1 3 4 2 21 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 18/Max_Subarray_Brute_Force_Approach.py: -------------------------------------------------------------------------------- 1 | def max_subarray_brute_force(nums): 2 | """ 3 | Find the maximum subarray sum using brute force. 4 | 5 | Args: 6 | nums (list): List of integers. 7 | 8 | Returns: 9 | int: Maximum subarray sum. 10 | """ 11 | max_sum = float('-inf') 12 | n = len(nums) 13 | 14 | for i in range(n): 15 | current_sum = 0 16 | for j in range(i, n): 17 | current_sum += nums[j] 18 | max_sum = max(max_sum, current_sum) 19 | 20 | return max_sum 21 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 51/Min_Priority_Queue.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | 3 | class MinPriorityQueue: 4 | def __init__(self): 5 | self.heap = [] 6 | 7 | def insert(self, val): 8 | heapq.heappush(self.heap, val) 9 | 10 | def extract_min(self): 11 | if len(self.heap) == 0: 12 | return None 13 | return heapq.heappop(self.heap) 14 | 15 | # Example: 16 | pq = MinPriorityQueue() 17 | pq.insert(10) 18 | pq.insert(5) 19 | pq.insert(15) 20 | pq.insert(2) 21 | 22 | print("Min value:", pq.extract_min()) # Output: 2 23 | print("Min value:", pq.extract_min()) # Output: 5 24 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 35/Find_Duplicate.py: -------------------------------------------------------------------------------- 1 | def has_duplicate(head): 2 | """ 3 | Check if the singly linked list contains any duplicates. 4 | 5 | Args: 6 | head (ListNode): The head node of the singly linked list. 7 | 8 | Returns: 9 | bool: True if there are duplicates, False otherwise. 10 | """ 11 | seen = set() 12 | current = head 13 | 14 | while current: 15 | if current.val in seen: 16 | return True # Duplicate found 17 | seen.add(current.val) 18 | current = current.next 19 | 20 | return False # No duplicates 21 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 26/Palindrome.py: -------------------------------------------------------------------------------- 1 | def isPalindrome(s): 2 | """ 3 | Check if the string is a palindrome using two pointers. 4 | 5 | Args: 6 | s (str): The input string. 7 | 8 | Returns: 9 | bool: True if the string is a palindrome, otherwise False. 10 | """ 11 | s = ''.join(char.lower() for char in s if char.isalnum()) # Remove non-alphanumeric characters and lowercase 12 | left, right = 0, len(s) - 1 13 | 14 | while left < right: 15 | if s[left] != s[right]: 16 | return False 17 | left += 1 18 | right -= 1 19 | 20 | return True 21 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 54/Number_of_Provinces.py: -------------------------------------------------------------------------------- 1 | def find_provinces(graph): 2 | visited = set() 3 | 4 | def dfs(city): 5 | visited.add(city) 6 | for neighbor in graph[city]: 7 | if neighbor not in visited: 8 | dfs(neighbor) 9 | 10 | count = 0 11 | for city in range(len(graph)): 12 | if city not in visited: 13 | dfs(city) 14 | count += 1 15 | 16 | return count 17 | 18 | # Example: 19 | graph = [ 20 | [1, 0, 0, 1], 21 | [1, 1, 0, 0], 22 | [0, 0, 1, 1], 23 | [1, 0, 1, 1] 24 | ] 25 | print(find_provinces(graph)) # Output: 2 26 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 18/Max_Product_Subarray_Brute_Force_Approach.py: -------------------------------------------------------------------------------- 1 | def max_product_subarray_brute_force(nums): 2 | """ 3 | Find the maximum product subarray using brute force. 4 | 5 | Args: 6 | nums (list): List of integers. 7 | 8 | Returns: 9 | int: Maximum product subarray. 10 | """ 11 | max_product = float('-inf') 12 | n = len(nums) 13 | 14 | for i in range(n): 15 | current_product = 1 16 | for j in range(i, n): 17 | current_product *= nums[j] 18 | max_product = max(max_product, current_product) 19 | 20 | return max_product 21 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 30/Insertion_Sort.py: -------------------------------------------------------------------------------- 1 | def insertionSort(nums): 2 | """ 3 | Sort the list using Insertion Sort. 4 | 5 | Args: 6 | nums (list): The list of integers to be sorted. 7 | 8 | Returns: 9 | list: The sorted list. 10 | """ 11 | for i in range(1, len(nums)): 12 | key = nums[i] 13 | j = i - 1 14 | 15 | # Move elements of nums[0..i-1], that are greater than key, to one position ahead 16 | while j >= 0 and key < nums[j]: 17 | nums[j + 1] = nums[j] 18 | j -= 1 19 | 20 | nums[j + 1] = key 21 | 22 | return nums 23 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 53/Number_of_Connected_Components.py: -------------------------------------------------------------------------------- 1 | def count_connected_components(graph): 2 | visited = set() 3 | 4 | def dfs(node): 5 | visited.add(node) 6 | for neighbor in graph[node]: 7 | if neighbor not in visited: 8 | dfs(neighbor) 9 | 10 | count = 0 11 | for node in graph: 12 | if node not in visited: 13 | dfs(node) 14 | count += 1 15 | 16 | return count 17 | 18 | # Example: 19 | graph = { 20 | 0: [1, 2], 21 | 1: [0, 2], 22 | 2: [0, 1], 23 | 3: [4], 24 | 4: [3], 25 | 5: [] 26 | } 27 | print(count_connected_components(graph)) # Output: 3 28 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 26/Non-Repeating_Character.py: -------------------------------------------------------------------------------- 1 | def firstUniqChar(s): 2 | """ 3 | Find the first non-repeating character in the string using a hash table. 4 | 5 | Args: 6 | s (str): The input string. 7 | 8 | Returns: 9 | int: The index of the first non-repeating character, or -1 if none exists. 10 | """ 11 | freq = {} 12 | 13 | # Count the frequency of each character 14 | for char in s: 15 | freq[char] = freq.get(char, 0) + 1 16 | 17 | # Find the first character with frequency 1 18 | for i, char in enumerate(s): 19 | if freq[char] == 1: 20 | return i 21 | 22 | return -1 23 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 52/BFS_(Breadth-First Search).py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | def bfs(graph, start): 4 | visited = set() 5 | queue = deque([start]) 6 | visited.add(start) 7 | 8 | while queue: 9 | node = queue.popleft() 10 | print(node, end=" ") # Visit the node (you can process it here) 11 | 12 | for neighbor in graph[node]: 13 | if neighbor not in visited: 14 | visited.add(neighbor) 15 | queue.append(neighbor) 16 | 17 | # Example: 18 | graph = { 19 | 0: [1, 2], 20 | 1: [0, 3, 4], 21 | 2: [0], 22 | 3: [1], 23 | 4: [1] 24 | } 25 | bfs(graph, 0) # Output: 0 1 2 3 4 26 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 34/Cycle Detection.py: -------------------------------------------------------------------------------- 1 | def has_cycle(head): 2 | """ 3 | Detect if there is a cycle in the given singly linked list. 4 | 5 | Args: 6 | head (ListNode): The head node of the singly linked list. 7 | 8 | Returns: 9 | bool: True if there is a cycle, False otherwise. 10 | """ 11 | slow = head 12 | fast = head 13 | 14 | while fast and fast.next: 15 | slow = slow.next # Move slow pointer by 1 step 16 | fast = fast.next.next # Move fast pointer by 2 steps 17 | 18 | if slow == fast: # Cycle detected 19 | return True 20 | 21 | return False # No cycle 22 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 12/Target_Sum.py: -------------------------------------------------------------------------------- 1 | def findTargetSumWays(nums, target): 2 | n = len(nums) 3 | summation = sum(nums) 4 | dp = [[None]*(2*summation+1) for _ in range(n)] 5 | 6 | def helper(index,sum_nums): 7 | #base case 8 | if index<0: 9 | if sum_nums==target:return 1 10 | else:return 0 11 | if dp[index][sum_nums+summation]!=None:return dp[index][sum_nums+summation] 12 | 13 | negative = helper(index-1,sum_nums+-1*nums[index]) 14 | positive = helper(index-1,sum_nums+nums[index]) 15 | dp[index][sum_nums+summation] = negative+positive 16 | return dp[index][sum_nums+summation] 17 | return helper(n-1,0) 18 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 34/Reverse_SSL.py: -------------------------------------------------------------------------------- 1 | def reverse_linked_list(head): 2 | """ 3 | Reverse the given singly linked list. 4 | 5 | Args: 6 | head (ListNode): The head node of the singly linked list. 7 | 8 | Returns: 9 | ListNode: The new head node after the list has been reversed. 10 | """ 11 | prev = None 12 | current = head 13 | 14 | while current: 15 | next_node = current.next # Store the next node 16 | current.next = prev # Reverse the current node's pointer 17 | prev = current # Move prev and current one step forward 18 | current = next_node 19 | 20 | return prev # New head of the reversed list 21 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 27/Longest_Unique_Substring.py: -------------------------------------------------------------------------------- 1 | def lengthOfLongestSubstring(s): 2 | """ 3 | Find the length of the longest substring without repeating characters using sliding window. 4 | 5 | Args: 6 | s (str): The input string. 7 | 8 | Returns: 9 | int: The length of the longest substring without repeating characters. 10 | """ 11 | char_set = set() 12 | left = 0 13 | max_length = 0 14 | 15 | for right in range(len(s)): 16 | while s[right] in char_set: 17 | char_set.remove(s[left]) 18 | left += 1 19 | char_set.add(s[right]) 20 | max_length = max(max_length, right - left + 1) 21 | 22 | return max_length 23 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 54/Find_if_Path_Exists_in_Graph.py: -------------------------------------------------------------------------------- 1 | def path_exists(graph, start, target): 2 | visited = set() 3 | 4 | def dfs(node): 5 | if node == target: 6 | return True 7 | visited.add(node) 8 | for neighbor in graph[node]: 9 | if neighbor not in visited: 10 | if dfs(neighbor): 11 | return True 12 | return False 13 | 14 | return dfs(start) 15 | 16 | # Example: 17 | graph = { 18 | 0: [1, 2], 19 | 1: [2], 20 | 2: [0, 3], 21 | 3: [] 22 | } 23 | print(path_exists(graph, 0, 3)) # Output: True 24 | print(path_exists(graph, 1, 3)) # Output: True 25 | print(path_exists(graph, 0, 4)) # Output: False 26 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 20/Jump_Game_1.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def can_jump(nums: List[int]) -> bool: 4 | """ 5 | Determines if it is possible to reach the last index. 6 | 7 | Args: 8 | nums (list): List of integers representing jump distances. 9 | 10 | Returns: 11 | bool: True if reachable, False otherwise. 12 | """ 13 | max_reach = 0 14 | n = len(nums) 15 | 16 | for i in range(n): 17 | if i > max_reach: 18 | return False # Can't move past this point 19 | max_reach = max(max_reach, i + nums[i]) 20 | if max_reach >= n - 1: 21 | return True # Can reach or exceed the last index 22 | 23 | return False 24 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 33/Delete_Duplicates.py: -------------------------------------------------------------------------------- 1 | def delete_duplicates(head): 2 | """ 3 | Remove all duplicate nodes from the linked list. 4 | 5 | Args: 6 | head (ListNode): The head node of the singly linked list. 7 | 8 | Returns: 9 | ListNode: The head node after duplicates have been removed. 10 | """ 11 | current = head 12 | seen = set() 13 | 14 | while current: 15 | if current.value in seen: 16 | # Remove the node by changing the pointer of the previous node 17 | prev.next = current.next 18 | else: 19 | seen.add(current.value) 20 | prev = current 21 | current = current.next 22 | 23 | return head 24 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 28/Binary_Search.py: -------------------------------------------------------------------------------- 1 | def binarySearch(nums, target): 2 | """ 3 | Perform binary search to find the target value in a sorted array. 4 | 5 | Args: 6 | nums (list): A sorted list of integers. 7 | target (int): The target value to search for. 8 | 9 | Returns: 10 | int: The index of the target value, or -1 if not found. 11 | """ 12 | left, right = 0, len(nums) - 1 13 | 14 | while left <= right: 15 | mid = left + (right - left) // 2 16 | if nums[mid] == target: 17 | return mid 18 | elif nums[mid] < target: 19 | left = mid + 1 20 | else: 21 | right = mid - 1 22 | 23 | return -1 24 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 52/DFS_(Depth-First Search)_Iterative_Approach.py: -------------------------------------------------------------------------------- 1 | def dfs_iterative(graph, start): 2 | visited = set() 3 | stack = [start] 4 | 5 | while stack: 6 | node = stack.pop() 7 | if node not in visited: 8 | visited.add(node) 9 | print(node, end=" ") # Visit the node (you can process it here) 10 | 11 | for neighbor in reversed(graph[node]): # Reversed to maintain order of exploration 12 | if neighbor not in visited: 13 | stack.append(neighbor) 14 | 15 | # Example: 16 | graph = { 17 | 0: [1, 2], 18 | 1: [0, 3, 4], 19 | 2: [0], 20 | 3: [1], 21 | 4: [1] 22 | } 23 | dfs_iterative(graph, 0) # Output: 0 2 1 4 3 24 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 53/Topological_Sort.py: -------------------------------------------------------------------------------- 1 | def topological_sort(graph): 2 | visited = set() 3 | stack = [] 4 | 5 | def dfs(node): 6 | visited.add(node) 7 | for neighbor in graph[node]: 8 | if neighbor not in visited: 9 | dfs(neighbor) 10 | stack.append(node) # Add node to stack after processing all its neighbors 11 | 12 | for node in graph: 13 | if node not in visited: 14 | dfs(node) 15 | 16 | return stack[::-1] # Reverse the stack to get the correct order 17 | 18 | # Example: 19 | graph = { 20 | 'A': ['B', 'C'], 21 | 'B': ['D'], 22 | 'C': ['D'], 23 | 'D': [] 24 | } 25 | print(topological_sort(graph)) # Output: ['A', 'C', 'B', 'D'] 26 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 36/DLL_Remove_All.py: -------------------------------------------------------------------------------- 1 | class DoublyLinkedList: 2 | def __init__(self): 3 | self.head = None 4 | 5 | def remove_all(self, value): 6 | """ 7 | Remove all nodes with the given 'value' from the DLL. 8 | """ 9 | current = self.head 10 | while current: 11 | next_node = current.next # Store next node to avoid losing reference 12 | if current.value == value: 13 | if current.prev: 14 | current.prev.next = current.next 15 | if current.next: 16 | current.next.prev = current.prev 17 | if current == self.head: 18 | self.head = current.next 19 | current = next_node 20 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 23/Jump_Game_2.py: -------------------------------------------------------------------------------- 1 | def jump(nums): 2 | """ 3 | Calculate the minimum number of jumps to reach the last index. 4 | 5 | Args: 6 | nums (list): A list of non-negative integers representing maximum jumps. 7 | 8 | Returns: 9 | int: The minimum number of jumps required to reach the last index. 10 | """ 11 | n = len(nums) 12 | if n <= 1: 13 | return 0 14 | 15 | jumps = 0 16 | current_end = 0 17 | farthest = 0 18 | 19 | for i in range(n): 20 | farthest = max(farthest, i + nums[i]) 21 | 22 | if i == current_end: 23 | jumps += 1 24 | current_end = farthest 25 | if current_end >= n - 1: 26 | break 27 | 28 | return jumps 29 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 31/Selection_Sort.py: -------------------------------------------------------------------------------- 1 | def selectionSort(nums): 2 | """ 3 | Sort the list using Selection Sort. 4 | 5 | Args: 6 | nums (list): The list of integers to be sorted. 7 | 8 | Returns: 9 | list: The sorted list. 10 | """ 11 | n = len(nums) 12 | 13 | # Traverse through all elements in the list 14 | for i in range(n): 15 | min_idx = i 16 | 17 | # Find the minimum element in the unsorted part 18 | for j in range(i + 1, n): 19 | if nums[j] < nums[min_idx]: 20 | min_idx = j 21 | 22 | # Swap the found minimum element with the first element of unsorted part 23 | nums[i], nums[min_idx] = nums[min_idx], nums[i] 24 | 25 | return nums 26 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 09/Fibonacci_with_Recursion.py: -------------------------------------------------------------------------------- 1 | def fibonacci(n): 2 | """ 3 | Calculate the Fibonacci number at the nth position using recursion. 4 | 5 | Parameters: 6 | n (int): The position in the Fibonacci sequence to compute. 7 | 8 | Returns: 9 | int: The Fibonacci number at position n. 10 | 11 | Time Complexity: O(2^n) 12 | - Due to the exponential growth of recursive calls. 13 | Space Complexity: O(n) 14 | - The space required for the recursion stack is proportional to the depth of the recursion tree. 15 | """ 16 | # Base cases: Return n if it's 0 or 1. 17 | if n <= 1: 18 | return n 19 | 20 | # Recursive case: Compute Fibonacci(n-1) + Fibonacci(n-2). 21 | return fibonacci(n - 1) + fibonacci(n - 2) 22 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 21/Boats_to_Save_People.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def num_rescue_boats(people: List[int], limit: int) -> int: 4 | """ 5 | Finds the minimum number of boats needed to save people. 6 | 7 | Args: 8 | people (list): List of people's weights. 9 | limit (int): Maximum weight a boat can carry. 10 | 11 | Returns: 12 | int: Minimum number of boats required. 13 | """ 14 | people.sort() 15 | left, right = 0, len(people) - 1 16 | boats = 0 17 | 18 | while left <= right: 19 | if people[left] + people[right] <= limit: 20 | left += 1 # Take the lighter person as well 21 | right -= 1 # Always take the heavier person 22 | boats += 1 23 | 24 | return boats 25 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 18/Max_Product_Subarray_Dynamic_Programming_Approach.py: -------------------------------------------------------------------------------- 1 | def max_product_subarray(nums): 2 | """ 3 | Find the maximum product subarray using dynamic programming. 4 | 5 | Args: 6 | nums (list): List of integers. 7 | 8 | Returns: 9 | int: Maximum product subarray. 10 | """ 11 | max_product = nums[0] 12 | min_product = nums[0] 13 | result = nums[0] 14 | 15 | for i in range(1, len(nums)): 16 | if nums[i] < 0: 17 | max_product, min_product = min_product, max_product 18 | 19 | max_product = max(nums[i], max_product * nums[i]) 20 | min_product = min(nums[i], min_product * nums[i]) 21 | 22 | result = max(result, max_product) 23 | 24 | return result 25 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 22/Largest_Number.py: -------------------------------------------------------------------------------- 1 | from functools import cmp_to_key 2 | 3 | def largestNumber(nums): 4 | """ 5 | Arrange numbers to form the largest possible number. 6 | 7 | Args: 8 | nums (list): A list of non-negative integers. 9 | 10 | Returns: 11 | str: The largest number formed by concatenating the integers. 12 | """ 13 | def compare(x, y): 14 | if x + y > y + x: 15 | return -1 16 | elif x + y < y + x: 17 | return 1 18 | return 0 19 | 20 | # Convert all integers to strings for custom sorting 21 | nums = list(map(str, nums)) 22 | nums.sort(key=cmp_to_key(compare)) 23 | 24 | # If the largest number is 0, return "0" 25 | return ''.join(nums) if nums[0] != '0' else '0' 26 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 21/Two_City_Scheduling.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def two_city_scheduling(costs: List[List[int]]) -> int: 4 | """ 5 | Minimizes the cost of sending people to two cities. 6 | 7 | Args: 8 | costs (list): List of [costA, costB] for each person. 9 | 10 | Returns: 11 | int: Minimum total cost. 12 | """ 13 | # Sort by cost difference between City A and City B 14 | costs.sort(key=lambda x: x[0] - x[1]) 15 | 16 | total_cost = 0 17 | n = len(costs) // 2 18 | 19 | # First n people go to City A, rest to City B 20 | for i in range(n): 21 | total_cost += costs[i][0] # City A 22 | for i in range(n, 2 * n): 23 | total_cost += costs[i][1] # City B 24 | 25 | return total_cost 26 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 40/Pre-order_Traversal_(Iterative).py: -------------------------------------------------------------------------------- 1 | class BinaryTree: 2 | def __init__(self, key): 3 | self.value = key 4 | self.left = None 5 | self.right = None 6 | 7 | def iterative_preorder(self): 8 | """ 9 | Iterative Pre-order Traversal: Node → Left → Right 10 | """ 11 | if not self: 12 | return [] 13 | 14 | stack = [self] 15 | result = [] 16 | 17 | while stack: 18 | node = stack.pop() 19 | result.append(node.value) 20 | 21 | # Push right first, so left is processed first 22 | if node.right: 23 | stack.append(node.right) 24 | if node.left: 25 | stack.append(node.left) 26 | 27 | return result 28 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 47/Convert_Sorted_Array_to_BST.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | def sorted_array_to_bst(nums): 8 | """ 9 | Convert a sorted array to a height-balanced BST. 10 | """ 11 | if not nums: 12 | return None 13 | 14 | # Find the middle element to be the root 15 | mid = len(nums) // 2 16 | root = TreeNode(nums[mid]) 17 | 18 | # Recursively build the left and right subtrees 19 | root.left = sorted_array_to_bst(nums[:mid]) 20 | root.right = sorted_array_to_bst(nums[mid+1:]) 21 | 22 | return root 23 | 24 | # Example: 25 | nums = [-10, -3, 0, 5, 9] 26 | root = sorted_array_to_bst(nums) 27 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 32/Quick_Sort.py: -------------------------------------------------------------------------------- 1 | def quickSort(nums): 2 | """ 3 | Sort the list using Quick Sort. 4 | 5 | Args: 6 | nums (list): The list of integers to be sorted. 7 | 8 | Returns: 9 | list: The sorted list. 10 | """ 11 | if len(nums) <= 1: 12 | return nums 13 | 14 | # Select the pivot (last element in the array) 15 | pivot = nums[-1] 16 | left = [] 17 | right = [] 18 | 19 | # Partition the list into left (smaller than pivot) and right (greater than pivot) 20 | for num in nums[:-1]: 21 | if num < pivot: 22 | left.append(num) 23 | else: 24 | right.append(num) 25 | 26 | # Recursively apply quickSort to the left and right subarrays 27 | return quickSort(left) + [pivot] + quickSort(right) 28 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 20/Minimum_Number_of_Arrows_to_Burst_Balloons.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def min_arrows_to_burst_balloons(points: List[List[int]]) -> int: 4 | """ 5 | Finds the minimum number of arrows required to burst all balloons. 6 | 7 | Args: 8 | points (list): List of balloon intervals [x_start, x_end]. 9 | 10 | Returns: 11 | int: Minimum number of arrows needed. 12 | """ 13 | if not points: 14 | return 0 15 | 16 | # Sort balloons by end position 17 | points.sort(key=lambda x: x[1]) 18 | 19 | arrows = 1 20 | last_arrow_pos = points[0][1] 21 | 22 | for start, end in points[1:]: 23 | if start > last_arrow_pos: 24 | arrows += 1 25 | last_arrow_pos = end 26 | 27 | return arrows 28 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 55/Numbers_with_Sam_Consecutive_Differences.py: -------------------------------------------------------------------------------- 1 | def nums_same_consec_diff(n, k): 2 | result = [] 3 | 4 | def backtrack(num, length): 5 | if length == n: 6 | result.append(num) 7 | return 8 | 9 | last_digit = num % 10 10 | # Add the next possible digits based on the difference k 11 | if last_digit + k < 10: 12 | backtrack(num * 10 + last_digit + k, length + 1) 13 | if last_digit - k >= 0 and k != 0: 14 | backtrack(num * 10 + last_digit - k, length + 1) 15 | 16 | # Try starting from all digits from 1 to 9 (since the number cannot start with 0) 17 | for i in range(1, 10): 18 | backtrack(i, 1) 19 | 20 | return result 21 | 22 | # Example: 23 | print(nums_same_consec_diff(3, 7)) # Output: [181, 292, 707, 818, 929] 24 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 41/Post-order_Traversal_(Iterative).py: -------------------------------------------------------------------------------- 1 | class BinaryTree: 2 | def __init__(self, key): 3 | self.value = key 4 | self.left = None 5 | self.right = None 6 | 7 | def iterative_postorder(self): 8 | """ 9 | Iterative Post-order Traversal: Left → Right → Node 10 | """ 11 | if not self: 12 | return [] 13 | 14 | stack1 = [self] 15 | stack2 = [] 16 | result = [] 17 | 18 | while stack1: 19 | node = stack1.pop() 20 | stack2.append(node) 21 | 22 | if node.left: 23 | stack1.append(node.left) 24 | if node.right: 25 | stack1.append(node.right) 26 | 27 | while stack2: 28 | result.append(stack2.pop().value) 29 | 30 | return result 31 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 23/Gas_Stations.py: -------------------------------------------------------------------------------- 1 | def canCompleteCircuit(gas, cost): 2 | """ 3 | Determine if a trip around the circuit is possible starting from a gas station. 4 | 5 | Args: 6 | gas (list): A list of gas available at each station. 7 | cost (list): A list of gas required to reach the next station. 8 | 9 | Returns: 10 | int: The index of the starting station, or -1 if not possible. 11 | """ 12 | total_gas = 0 13 | current_gas = 0 14 | start_index = 0 15 | 16 | for i in range(len(gas)): 17 | total_gas += gas[i] - cost[i] 18 | current_gas += gas[i] - cost[i] 19 | 20 | # If the current gas is negative, reset the start index 21 | if current_gas < 0: 22 | start_index = i + 1 23 | current_gas = 0 24 | 25 | return start_index if total_gas >= 0 else -1 26 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 25/Isomorphic_Strings.py: -------------------------------------------------------------------------------- 1 | def isIsomorphic(s, t): 2 | """ 3 | Check if two strings are isomorphic using hash tables. 4 | 5 | Args: 6 | s (str): The first input string. 7 | t (str): The second input string. 8 | 9 | Returns: 10 | bool: True if the strings are isomorphic, otherwise False. 11 | """ 12 | if len(s) != len(t): 13 | return False 14 | 15 | s_map = {} 16 | t_map = {} 17 | 18 | for char_s, char_t in zip(s, t): 19 | if char_s in s_map: 20 | if s_map[char_s] != char_t: 21 | return False 22 | else: 23 | s_map[char_s] = char_t 24 | 25 | if char_t in t_map: 26 | if t_map[char_t] != char_s: 27 | return False 28 | else: 29 | t_map[char_t] = char_s 30 | 31 | return True 32 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 24/Container_with_Most_Water.py: -------------------------------------------------------------------------------- 1 | def maxArea(height): 2 | """ 3 | Calculate the maximum area that can be formed between two lines. 4 | 5 | Args: 6 | height (list): A list of integers representing the heights of lines. 7 | 8 | Returns: 9 | int: The maximum area formed by any two lines. 10 | """ 11 | left = 0 12 | right = len(height) - 1 13 | max_area = 0 14 | 15 | while left < right: 16 | # Calculate the area between the current pair of lines 17 | width = right - left 18 | current_area = min(height[left], height[right]) * width 19 | max_area = max(max_area, current_area) 20 | 21 | # Move the pointer pointing to the shorter line 22 | if height[left] < height[right]: 23 | left += 1 24 | else: 25 | right -= 1 26 | 27 | return max_area 28 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 09/Fibonacci_with_Tabulation.py: -------------------------------------------------------------------------------- 1 | # Solution 3: Fibonacci using Tabulation 2 | 3 | def fibonacci(n): 4 | """ 5 | Calculate the Fibonacci number at the nth position using the tabulation approach. 6 | 7 | Parameters: 8 | n (int): The position in the Fibonacci sequence to compute. 9 | 10 | Returns: 11 | int: The Fibonacci number at position n. 12 | 13 | Time Complexity: O(n) 14 | - A single loop runs from 2 to n. 15 | Space Complexity: O(n) 16 | - Space is used to store the entire DP table. 17 | """ 18 | # Initialize the DP table to store Fibonacci numbers. 19 | dp = [0] * (n + 1) 20 | 21 | # Base cases 22 | if n > 0: 23 | dp[1] = 1 24 | 25 | # Fill the DP table iteratively. 26 | for i in range(2, n + 1): 27 | dp[i] = dp[i - 1] + dp[i - 2] 28 | 29 | return dp[n] 30 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 40/In-order_Traversal_(Iterative).py: -------------------------------------------------------------------------------- 1 | class BinaryTree: 2 | def __init__(self, key): 3 | self.value = key 4 | self.left = None 5 | self.right = None 6 | 7 | def iterative_inorder(self): 8 | """ 9 | Iterative In-order Traversal: Left → Node → Right 10 | """ 11 | stack = [] 12 | result = [] 13 | current = self 14 | 15 | while stack or current: 16 | # Reach the leftmost node of the current node 17 | while current: 18 | stack.append(current) 19 | current = current.left 20 | 21 | # Current is None, so we pop from stack 22 | current = stack.pop() 23 | result.append(current.value) 24 | 25 | # Now, visit the right subtree 26 | current = current.right 27 | 28 | return result 29 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 46/Invert_Tree.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | def invert_tree(root): 8 | """ 9 | Invert a binary tree recursively. 10 | """ 11 | if not root: 12 | return None 13 | 14 | # Swap the left and right children 15 | root.left, root.right = root.right, root.left 16 | 17 | # Recursively invert the left and right subtrees 18 | invert_tree(root.left) 19 | invert_tree(root.right) 20 | 21 | return root 22 | 23 | # Example: 24 | root = TreeNode(4) 25 | root.left = TreeNode(2) 26 | root.right = TreeNode(7) 27 | root.left.left = TreeNode(1) 28 | root.left.right = TreeNode(3) 29 | root.right.left = TreeNode(6) 30 | root.right.right = TreeNode(9) 31 | 32 | inverted_root = invert_tree(root) 33 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 43/Left_View_of_Binary_Tree.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from logging import root 3 | 4 | def left_view(root): 5 | """ 6 | Return the left view of a binary tree. 7 | """ 8 | if not root: 9 | return [] 10 | 11 | result = [] 12 | queue = deque([root]) 13 | 14 | while queue: 15 | level_length = len(queue) 16 | 17 | for i in range(level_length): 18 | node = queue.popleft() 19 | 20 | # If it is the first node of the level, add it to the result 21 | if i == 0: 22 | result.append(node.value) 23 | 24 | if node.left: 25 | queue.append(node.left) 26 | if node.right: 27 | queue.append(node.right) 28 | 29 | return result 30 | 31 | # Example: 32 | print("Left View:", left_view(root)) -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 47/Validate_BST.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | def is_valid_bst(root, low=float('-inf'), high=float('inf')): 8 | """ 9 | Validate if a binary tree is a valid BST. 10 | """ 11 | if not root: 12 | return True 13 | 14 | # Check if current node value is within the valid range 15 | if not (low < root.value < high): 16 | return False 17 | 18 | # Recursively check the left and right subtrees with updated bounds 19 | return is_valid_bst(root.left, low, root.value) and is_valid_bst(root.right, root.value, high) 20 | 21 | # Example: 22 | root = TreeNode(2) 23 | root.left = TreeNode(1) 24 | root.right = TreeNode(3) 25 | 26 | print("Is the tree a valid BST?", is_valid_bst(root)) # Output: True 27 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 15/Longest_Palindromic_Subsequence.py: -------------------------------------------------------------------------------- 1 | def longest_palindrome_subseq(s): 2 | """ 3 | This function calculates the longest palindromic subsequence in the given string using dynamic programming. 4 | 5 | Args: 6 | s (str): The input string to find the longest palindromic subsequence. 7 | 8 | Returns: 9 | int: The length of the longest palindromic subsequence in the input string. 10 | """ 11 | rev_s = s[::-1] 12 | n = len(s) 13 | 14 | dp = [[-1] * (n + 1) for _ in range(n + 1)] 15 | for i in range(n + 1): 16 | dp[i][0] = 0 17 | dp[0][i] = 0 18 | 19 | for i in range(1, n + 1): 20 | for j in range(1, n + 1): 21 | if s[i - 1] == rev_s[j - 1]: 22 | dp[i][j] = 1 + dp[i - 1][j - 1] 23 | else: 24 | dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]) 25 | 26 | return dp[n][n] 27 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 50/N-ary_Tree_Level_Order_Traversal.py: -------------------------------------------------------------------------------- 1 | class Node: 2 | def __init__(self, val=None, children=None): 3 | self.val = val 4 | self.children = children if children is not None else [] 5 | 6 | def level_order(root): 7 | """ 8 | Perform level order traversal on an N-ary tree. 9 | """ 10 | if not root: 11 | return [] 12 | 13 | result = [] 14 | queue = [root] 15 | 16 | while queue: 17 | level = [] 18 | for _ in range(len(queue)): 19 | node = queue.pop(0) 20 | level.append(node.val) 21 | queue.extend(node.children) 22 | result.append(level) 23 | 24 | return result 25 | 26 | # Example: 27 | root = Node(1) 28 | root.children = [Node(2), Node(3)] 29 | root.children[0].children = [Node(4)] 30 | 31 | print("Level Order Traversal:", level_order(root)) # Output: [[1], [2, 3], [4]] 32 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 19/Non_Overlaping_Intervals.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def non_overlapping_intervals(intervals: List[List[int]]) -> int: 4 | """ 5 | Finds the minimum number of intervals to remove to eliminate overlaps. 6 | 7 | Args: 8 | intervals (list): List of intervals [start, end]. 9 | 10 | Returns: 11 | int: Minimum number of intervals to remove. 12 | """ 13 | if not intervals: 14 | return 0 15 | 16 | # Sort by ending times 17 | intervals.sort(key=lambda x: x[1]) 18 | 19 | count = 0 20 | prev_end = intervals[0][1] 21 | 22 | for i in range(1, len(intervals)): 23 | if intervals[i][0] < prev_end: 24 | # Overlapping interval, remove it 25 | count += 1 26 | else: 27 | # Update previous end time 28 | prev_end = intervals[i][1] 29 | 30 | return count 31 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 43/Right_View_of_Binary_Tree.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from logging import root 3 | 4 | def right_view(root): 5 | """ 6 | Return the right view of a binary tree. 7 | """ 8 | if not root: 9 | return [] 10 | 11 | result = [] 12 | queue = deque([root]) 13 | 14 | while queue: 15 | level_length = len(queue) 16 | 17 | for i in range(level_length): 18 | node = queue.popleft() 19 | 20 | # If it is the last node of the level, add it to the result 21 | if i == level_length - 1: 22 | result.append(node.value) 23 | 24 | if node.left: 25 | queue.append(node.left) 26 | if node.right: 27 | queue.append(node.right) 28 | 29 | return result 30 | 31 | # Example: 32 | print("Right View:", right_view(root)) -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 15/Longest_Palindromic_Substring.py: -------------------------------------------------------------------------------- 1 | def longest_palindrome(s): 2 | """ 3 | This function finds the longest palindromic substring in the given string using dynamic programming. 4 | 5 | Args: 6 | s (str): The input string to find the longest palindromic substring. 7 | 8 | Returns: 9 | str: The longest palindromic substring in the input string. 10 | """ 11 | n = len(s) 12 | dp = [[-1] * n for _ in range(n)] 13 | longest = '' 14 | 15 | for l in range(1, n + 1): 16 | for i in range(0, n - l + 1): 17 | j = i + l - 1 18 | if i == j: 19 | dp[i][j] = True 20 | elif s[i] == s[j] and (j == i + 1 or dp[i + 1][j - 1]): 21 | dp[i][j] = True 22 | else: 23 | dp[i][j] = False 24 | if dp[i][j]: 25 | longest = s[i:j + 1] 26 | 27 | return longest 28 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 29/Search_in_2D_Array.py: -------------------------------------------------------------------------------- 1 | def searchMatrix(matrix, target): 2 | """ 3 | Search for the target value in a 2D matrix. 4 | 5 | Args: 6 | matrix (list of list): A 2D matrix where each row is sorted. 7 | target (int): The target value to search for. 8 | 9 | Returns: 10 | bool: True if the target is found in the matrix, False otherwise. 11 | """ 12 | if not matrix: 13 | return False 14 | 15 | rows, cols = len(matrix), len(matrix[0]) 16 | left, right = 0, rows * cols - 1 17 | 18 | while left <= right: 19 | mid = left + (right - left) // 2 20 | mid_value = matrix[mid // cols][mid % cols] 21 | 22 | if mid_value == target: 23 | return True 24 | elif mid_value < target: 25 | left = mid + 1 26 | else: 27 | right = mid - 1 28 | 29 | return False 30 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 15/Palindromic_Substring_Tabulation_Approach.py: -------------------------------------------------------------------------------- 1 | def count_substrings(s): 2 | """ 3 | This function counts the number of palindromic substrings in the given string using dynamic programming. 4 | 5 | Args: 6 | s (str): The input string to check for palindromic substrings. 7 | 8 | Returns: 9 | int: The number of palindromic substrings in the input string. 10 | """ 11 | result = 0 12 | n = len(s) 13 | 14 | dp = [[0] * n for _ in range(n)] 15 | 16 | for length in range(1, n + 1): 17 | for i in range(n - length + 1): 18 | j = i + length - 1 19 | if i == j: 20 | dp[i][j] = True 21 | result += 1 22 | elif s[i] == s[j] and (j == i + 1 or dp[i + 1][j - 1]): 23 | dp[i][j] = True 24 | result += 1 25 | else: 26 | dp[i][j] = False 27 | return result 28 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 37/Construct_Stack.py: -------------------------------------------------------------------------------- 1 | class Stack: 2 | def __init__(self): 3 | self.stack = [] 4 | 5 | def push(self, item): 6 | """ 7 | Push an item onto the stack. 8 | """ 9 | self.stack.append(item) 10 | 11 | def pop(self): 12 | """ 13 | Remove and return the top item from the stack. Raises an error if the stack is empty. 14 | """ 15 | if not self.is_empty(): 16 | return self.stack.pop() 17 | raise IndexError("pop from empty stack") 18 | 19 | def peek(self): 20 | """ 21 | Return the top item without removing it. 22 | """ 23 | if not self.is_empty(): 24 | return self.stack[-1] 25 | raise IndexError("peek from empty stack") 26 | 27 | def is_empty(self): 28 | """ 29 | Check if the stack is empty. 30 | """ 31 | return len(self.stack) == 0 32 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 16/Palindrome_Partitioning_2_Tabulation_Approach_A.py: -------------------------------------------------------------------------------- 1 | def min_cut(s): 2 | """ 3 | Find the minimum cuts needed to partition a string into palindromic substrings 4 | using tabulation (Bottom-Up Dynamic Programming). 5 | 6 | Args: 7 | s (str): The input string. 8 | 9 | Returns: 10 | int: The minimum number of cuts required. 11 | """ 12 | n = len(s) 13 | dp = [[0] * n for _ in range(n)] 14 | 15 | for length in range(1, n + 1): 16 | for i in range(n - length + 1): 17 | j = i + length - 1 18 | if i == j: 19 | dp[i][j] = 0 20 | elif s[i] == s[j] and (dp[i + 1][j - 1] == 0): 21 | dp[i][j] = 0 22 | else: 23 | dp[i][j] = j - i 24 | for k in range(i, j): 25 | dp[i][j] = min(dp[i][j], 1 + dp[i][k] + dp[k + 1][j]) 26 | 27 | return dp[0][n - 1] 28 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 38/Construct_Queue.py: -------------------------------------------------------------------------------- 1 | class Queue: 2 | def __init__(self): 3 | self.queue = [] 4 | 5 | def enqueue(self, item): 6 | """ 7 | Add an item to the rear of the queue. 8 | """ 9 | self.queue.append(item) 10 | 11 | def dequeue(self): 12 | """ 13 | Remove and return the front item from the queue. Raises an error if the queue is empty. 14 | """ 15 | if not self.is_empty(): 16 | return self.queue.pop(0) 17 | raise IndexError("dequeue from empty queue") 18 | 19 | def front(self): 20 | """ 21 | Return the front item without removing it. 22 | """ 23 | if not self.is_empty(): 24 | return self.queue[0] 25 | raise IndexError("front from empty queue") 26 | 27 | def is_empty(self): 28 | """ 29 | Check if the queue is empty. 30 | """ 31 | return len(self.queue) == 0 32 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 09/Fibonacci_with_Memorization.py: -------------------------------------------------------------------------------- 1 | # Solution 2: Fibonacci using Memoization 2 | 3 | # Initialize a dictionary to store precomputed Fibonacci values. 4 | memo = {0: 0, 1: 1} 5 | 6 | def fibonacci(n): 7 | """ 8 | Calculate the Fibonacci number at the nth position using memoization. 9 | 10 | Parameters: 11 | n (int): The position in the Fibonacci sequence to compute. 12 | 13 | Returns: 14 | int: The Fibonacci number at position n. 15 | 16 | Time Complexity: O(n) 17 | - Each Fibonacci number from 0 to n is computed once and stored. 18 | Space Complexity: O(n) 19 | - Space is used to store the memoization dictionary and recursion stack. 20 | """ 21 | # Check if the result is already computed. 22 | if n in memo: 23 | return memo[n] 24 | 25 | # Compute and store the Fibonacci value in the dictionary. 26 | memo[n] = fibonacci(n - 1) + fibonacci(n - 2) 27 | 28 | return memo[n] 29 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 30/Bubble_Sort.py: -------------------------------------------------------------------------------- 1 | def bubbleSort(nums): 2 | """ 3 | Sort the list using Bubble Sort. 4 | 5 | Args: 6 | nums (list): The list of integers to be sorted. 7 | 8 | Returns: 9 | list: The sorted list. 10 | """ 11 | n = len(nums) 12 | 13 | # Traverse through all elements in the list 14 | for i in range(n): 15 | # Flag to optimize: if no two elements were swapped in an inner loop, break early 16 | swapped = False 17 | 18 | # Last i elements are already sorted 19 | for j in range(0, n - i - 1): 20 | # Swap if the element found is greater than the next element 21 | if nums[j] > nums[j + 1]: 22 | nums[j], nums[j + 1] = nums[j + 1], nums[j] 23 | swapped = True 24 | 25 | # If no two elements were swapped, the list is sorted 26 | if not swapped: 27 | break 28 | 29 | return nums 30 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 17/Matrix_Chain_Multiplication_Tabulation_Approach.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Matrix Chain Multiplication - Tabulation Approach 4 | """ 5 | 6 | from typing import List 7 | 8 | 9 | def matrix_multiplication(n: int, arr: List[int]) -> int: 10 | """ 11 | Computes the minimum cost of multiplying matrices using tabulation. 12 | 13 | Args: 14 | n (int): Number of matrices. 15 | arr (List[int]): Dimensions of matrices. 16 | 17 | Returns: 18 | int: Minimum number of scalar multiplications required. 19 | """ 20 | dp = [[0] * n for _ in range(n)] 21 | 22 | for length in range(2, n): # Length of matrix chain segment 23 | for i in range(1, n - length + 1): 24 | j = i + length - 1 25 | dp[i][j] = float('inf') 26 | 27 | for k in range(i, j): 28 | cost = dp[i][k] + dp[k + 1][j] + arr[i - 1] * arr[k] * arr[j] 29 | dp[i][j] = min(dp[i][j], cost) 30 | 31 | return dp[1][n - 1] 32 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 42/In-order_and_Post-order_Traversal.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | def inorder_traversal(self): 8 | """ 9 | In-order Traversal (Left → Root → Right) 10 | """ 11 | result = [] 12 | if self.left: 13 | result.extend(self.left.inorder_traversal()) 14 | result.append(self.value) 15 | if self.right: 16 | result.extend(self.right.inorder_traversal()) 17 | return result 18 | 19 | def postorder_traversal(self): 20 | """ 21 | Post-order Traversal (Left → Right → Root) 22 | """ 23 | result = [] 24 | if self.left: 25 | result.extend(self.left.postorder_traversal()) 26 | if self.right: 27 | result.extend(self.right.postorder_traversal()) 28 | result.append(self.value) 29 | return result 30 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 13/LCS_Recursice_Approach.py: -------------------------------------------------------------------------------- 1 | def longestCommonSubsequence(text1, text2): 2 | """ 3 | Calculate the length of the Longest Common Subsequence (LCS) using recursion. 4 | 5 | Args: 6 | text1 (str): The first input string. 7 | text2 (str): The second input string. 8 | 9 | Returns: 10 | int: The length of the Longest Common Subsequence (LCS). 11 | """ 12 | n = len(text1) 13 | m = len(text2) 14 | 15 | def helper(index1, index2): 16 | # Base case: If either string has been fully processed 17 | if index1 >= n or index2 >= m: 18 | return 0 19 | 20 | # If characters match, include them in the LCS 21 | if text1[index1] == text2[index2]: 22 | return 1 + helper(index1 + 1, index2 + 1) 23 | 24 | # Otherwise, explore both possibilities (skip character from either text1 or text2) 25 | return max(helper(index1 + 1, index2), helper(index1, index2 + 1)) 26 | 27 | return helper(0, 0) 28 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 45/Sum_Root_to_Leaf_Numbers.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | def sum_root_to_leaf(root, current_sum=0): 8 | """ 9 | Calculate the sum of all numbers formed from root to leaf. 10 | """ 11 | if not root: 12 | return 0 13 | 14 | # Update the current sum by appending the current node's value 15 | current_sum = current_sum * 10 + root.value 16 | 17 | # If the node is a leaf, return the current sum 18 | if not root.left and not root.right: 19 | return current_sum 20 | 21 | # Recursively calculate the sum for the left and right subtrees 22 | return sum_root_to_leaf(root.left, current_sum) + sum_root_to_leaf(root.right, current_sum) 23 | 24 | # Example: 25 | root = TreeNode(1) 26 | root.left = TreeNode(2) 27 | root.right = TreeNode(3) 28 | 29 | print("Sum Root to Leaf Numbers:", sum_root_to_leaf(root)) 30 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 14/LIS_Recursive_Approach.py: -------------------------------------------------------------------------------- 1 | def lengthOfLIS(nums): 2 | """ 3 | Calculate the length of the longest increasing subsequence using recursion. 4 | 5 | Args: 6 | nums (list[int]): A list of integers representing the sequence. 7 | 8 | Returns: 9 | int: The length of the longest increasing subsequence. 10 | """ 11 | n = len(nums) 12 | 13 | def helper(curr, prev): 14 | # Base case: if the current index exceeds the array length 15 | if curr > n - 1: 16 | return 0 17 | 18 | # Exclude the current element from the subsequence 19 | exclude = helper(curr + 1, prev) 20 | 21 | # Include the current element if it forms an increasing subsequence 22 | include = 0 23 | if prev == -1 or nums[curr] > nums[prev]: 24 | include = 1 + helper(curr + 1, curr) 25 | 26 | # Return the maximum of including or excluding the current element 27 | return max(include, exclude) 28 | 29 | return helper(0, -1) 30 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 19/Fractional_Knapsack.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | def fractional_knapsack(items: List[Tuple[int, int]], capacity: int) -> float: 4 | """ 5 | Solves the Fractional Knapsack problem using a greedy approach. 6 | 7 | Args: 8 | items (list): List of tuples where each tuple (value, weight). 9 | capacity (int): Maximum weight the knapsack can hold. 10 | 11 | Returns: 12 | float: Maximum total value possible. 13 | """ 14 | # Sort items by value-to-weight ratio in descending order 15 | items.sort(key=lambda item: item[0] / item[1], reverse=True) 16 | 17 | total_value = 0.0 18 | 19 | for value, weight in items: 20 | if capacity >= weight: 21 | # Take the full item 22 | capacity -= weight 23 | total_value += value 24 | else: 25 | # Take the fraction of the item 26 | total_value += (value / weight) * capacity 27 | break 28 | 29 | return total_value 30 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 55/Number_of_Islands.py: -------------------------------------------------------------------------------- 1 | def num_islands(grid): 2 | if not grid: 3 | return 0 4 | 5 | def dfs(i, j): 6 | # Check boundaries and water 7 | if i < 0 or i >= len(grid) or j < 0 or j >= len(grid[0]) or grid[i][j] == '0': 8 | return 9 | # Mark the land as visited 10 | grid[i][j] = '0' 11 | # Explore all 4 directions (up, down, left, right) 12 | dfs(i+1, j) 13 | dfs(i-1, j) 14 | dfs(i, j+1) 15 | dfs(i, j-1) 16 | 17 | count = 0 18 | for i in range(len(grid)): 19 | for j in range(len(grid[0])): 20 | if grid[i][j] == '1': # Found an unvisited land cell 21 | dfs(i, j) # Perform DFS to mark all connected land cells 22 | count += 1 # Increment the island count 23 | 24 | return count 25 | 26 | # Example: 27 | grid = [ 28 | ['1', '1', '1', '1', '0'], 29 | ['1', '1', '0', '1', '0'], 30 | ['1', '1', '0', '0', '0'], 31 | ['0', '0', '0', '0', '0'] 32 | ] 33 | print(num_islands(grid)) # Output: 1 34 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 28/Search_in_Rotated_Sorted_Array.py: -------------------------------------------------------------------------------- 1 | def search(nums, target): 2 | """ 3 | Search for the target value in a rotated sorted array. 4 | 5 | Args: 6 | nums (list): A rotated sorted list of integers. 7 | target (int): The target value to search for. 8 | 9 | Returns: 10 | int: The index of the target value, or -1 if not found. 11 | """ 12 | left, right = 0, len(nums) - 1 13 | 14 | while left <= right: 15 | mid = left + (right - left) // 2 16 | if nums[mid] == target: 17 | return mid 18 | 19 | # Determine which half is sorted 20 | if nums[left] <= nums[mid]: 21 | # Left half is sorted 22 | if nums[left] <= target < nums[mid]: 23 | right = mid - 1 24 | else: 25 | left = mid + 1 26 | else: 27 | # Right half is sorted 28 | if nums[mid] < target <= nums[right]: 29 | left = mid + 1 30 | else: 31 | right = mid - 1 32 | 33 | return -1 34 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 13/LCS_Tabulation_Approach.py: -------------------------------------------------------------------------------- 1 | def longestCommonSubsequence(text1, text2): 2 | """ 3 | Calculate the length of the Longest Common Subsequence (LCS) using tabulation. 4 | 5 | Args: 6 | text1 (str): The first input string. 7 | text2 (str): The second input string. 8 | 9 | Returns: 10 | int: The length of the Longest Common Subsequence (LCS). 11 | """ 12 | n = len(text1) 13 | m = len(text2) 14 | 15 | # Initialize a DP table with 0s 16 | dp = [[0] * (m + 1) for _ in range(n + 1)] 17 | 18 | # Fill the DP table iteratively 19 | for i in range(1, n + 1): 20 | for j in range(1, m + 1): 21 | # If characters match, include them in the LCS 22 | if text1[i - 1] == text2[j - 1]: 23 | dp[i][j] = 1 + dp[i - 1][j - 1] 24 | # If characters don't match, take the maximum of excluding one character from either string 25 | else: 26 | dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) 27 | 28 | # The bottom-right cell contains the length of the LCS 29 | return dp[n][m] 30 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 13/LCS_Space_Optimised_Tabulation_Approach.py: -------------------------------------------------------------------------------- 1 | def longestCommonSubsequence(text1, text2): 2 | """ 3 | Calculate the length of the Longest Common Subsequence (LCS) using space optimized tabulation. 4 | 5 | Args: 6 | text1 (str): The first input string. 7 | text2 (str): The second input string. 8 | 9 | Returns: 10 | int: The length of the Longest Common Subsequence (LCS). 11 | """ 12 | n = len(text1) 13 | m = len(text2) 14 | 15 | # Only store two rows: current and previous 16 | prev = [0] * (m + 1) 17 | curr = [0] * (m + 1) 18 | 19 | # Fill the DP table iteratively, using only the current and previous rows 20 | for i in range(1, n + 1): 21 | for j in range(1, m + 1): 22 | if text1[i - 1] == text2[j - 1]: 23 | curr[j] = 1 + prev[j - 1] 24 | else: 25 | curr[j] = max(prev[j], curr[j - 1]) 26 | # Move the current row to the previous row for the next iteration 27 | prev = curr[:] 28 | 29 | # The result is stored in the last cell of the current row 30 | return curr[m] 31 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 09/Fibonacci_with_Space_Optimization.py: -------------------------------------------------------------------------------- 1 | # Solution 4: Fibonacci using Space Optimization 2 | 3 | def fibonacci(n): 4 | """ 5 | Calculate the Fibonacci number at the nth position using a space-optimized approach. 6 | 7 | Parameters: 8 | n (int): The position in the Fibonacci sequence to compute. 9 | 10 | Returns: 11 | int: The Fibonacci number at position n. 12 | 13 | Time Complexity: O(n) 14 | - A single loop runs from 1 to n-1. 15 | Space Complexity: O(1) 16 | - Only constant space is used for variables. 17 | """ 18 | # Handle base cases directly 19 | if n <= 1: 20 | return n 21 | 22 | # Initialize variables to store the last two Fibonacci numbers 23 | prev = 0 # Represents F(n-2) 24 | curr = 1 # Represents F(n-1) 25 | 26 | # Calculate Fibonacci numbers iteratively 27 | for _ in range(2, n + 1): 28 | # Compute the next Fibonacci number 29 | next_num = prev + curr 30 | 31 | # Update variables 32 | prev = curr 33 | curr = next_num 34 | 35 | # Return the nth Fibonacci number 36 | return curr 37 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 17/Word_Break.py: -------------------------------------------------------------------------------- 1 | """ 2 | Word Break Problem - Normal DP Approach 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | def word_break(s: str, word_dict: List[str]) -> bool: 9 | """ 10 | Determines if the string `s` can be segmented into space-separated 11 | words from the given `word_dict` using dynamic programming. 12 | 13 | Args: 14 | s (str): Input string. 15 | word_dict (List[str]): List of valid words. 16 | 17 | Returns: 18 | bool: True if `s` can be segmented, otherwise False. 19 | """ 20 | word_set = set(word_dict) # Convert list to set for optimized lookup 21 | n = len(s) 22 | dp = [[False] * n for _ in range(n)] 23 | 24 | for length in range(1, n + 1): # Length of substring 25 | for start in range(n - length + 1): 26 | end = start + length - 1 27 | 28 | if s[start:end + 1] in word_set: 29 | dp[start][end] = True 30 | else: 31 | for mid in range(start, end): 32 | dp[start][end] = dp[start][end] or (dp[start][mid] and dp[mid + 1][end]) 33 | 34 | return dp[0][n - 1] 35 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 48/Unique_BST_2.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | def generate_trees(n): 8 | """ 9 | Generate all unique BSTs that store values 1 to n. 10 | """ 11 | def build_tree(start, end): 12 | if start > end: 13 | return [None] 14 | 15 | trees = [] 16 | for i in range(start, end + 1): 17 | # Generate all left and right subtrees 18 | left_trees = build_tree(start, i - 1) 19 | right_trees = build_tree(i + 1, end) 20 | 21 | # Combine them to form trees with root i 22 | for left in left_trees: 23 | for right in right_trees: 24 | root = TreeNode(i) 25 | root.left = left 26 | root.right = right 27 | trees.append(root) 28 | 29 | return trees 30 | 31 | return build_tree(1, n) 32 | 33 | # Example: 34 | trees = generate_trees(3) 35 | print(f"Number of unique BSTs: {len(trees)}") # Output: 5 36 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 49/Unique_BST_1.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | def generate_trees(n): 8 | """ 9 | Generate all unique BSTs that store values 1 to n. 10 | """ 11 | def build_tree(start, end): 12 | if start > end: 13 | return [None] 14 | 15 | trees = [] 16 | for i in range(start, end + 1): 17 | # Generate all left and right subtrees 18 | left_trees = build_tree(start, i - 1) 19 | right_trees = build_tree(i + 1, end) 20 | 21 | # Combine them to form trees with root i 22 | for left in left_trees: 23 | for right in right_trees: 24 | root = TreeNode(i) 25 | root.left = left 26 | root.right = right 27 | trees.append(root) 28 | 29 | return trees 30 | 31 | return build_tree(1, n) 32 | 33 | # Example: 34 | trees = generate_trees(3) 35 | print(f"Number of unique BSTs: {len(trees)}") # Output: 5 36 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 37/Reverse_Polish_Notation.py: -------------------------------------------------------------------------------- 1 | def evaluate_rpn(expression): 2 | """ 3 | Evaluate an expression in Reverse Polish Notation (RPN). 4 | 5 | Args: 6 | expression (List[str]): List of tokens representing an RPN expression. 7 | 8 | Returns: 9 | int: The result of the RPN expression. 10 | """ 11 | stack = [] 12 | 13 | for token in expression: 14 | if token in ('+', '-', '*', '/'): 15 | # Pop the top two elements from the stack 16 | b = stack.pop() 17 | a = stack.pop() 18 | 19 | # Perform the operation based on the operator 20 | if token == '+': 21 | stack.append(a + b) 22 | elif token == '-': 23 | stack.append(a - b) 24 | elif token == '*': 25 | stack.append(a * b) 26 | elif token == '/': 27 | stack.append(int(a / b)) # Integer division 28 | else: 29 | # If the token is a number, push it to the stack 30 | stack.append(int(token)) 31 | 32 | return stack[0] # The result is the only element remaining in the stack 33 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 49/Lowest_Common_Ancestor_of_Binary_Tree.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | def lowest_common_ancestor(root, p, q): 8 | """ 9 | Find the lowest common ancestor of two nodes in a binary tree. 10 | """ 11 | if not root: 12 | return None 13 | 14 | if root == p or root == q: 15 | return root 16 | 17 | left = lowest_common_ancestor(root.left, p, q) 18 | right = lowest_common_ancestor(root.right, p, q) 19 | 20 | if left and right: 21 | return root 22 | 23 | return left if left else right 24 | 25 | # Example: 26 | root = TreeNode(3) 27 | root.left = TreeNode(5) 28 | root.right = TreeNode(1) 29 | root.left.left = TreeNode(6) 30 | root.left.right = TreeNode(2) 31 | root.right.left = TreeNode(0) 32 | root.right.right = TreeNode(8) 33 | root.left.right.left = TreeNode(7) 34 | root.left.right.right = TreeNode(4) 35 | 36 | p = root.left # Node 5 37 | q = root.right # Node 1 38 | 39 | print("LCA:", lowest_common_ancestor(root, p, q).value) # Output: 3 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 rohanmistry231 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 41/Path_Sum_2.py: -------------------------------------------------------------------------------- 1 | class BinaryTree: 2 | def __init__(self, key): 3 | self.value = key 4 | self.left = None 5 | self.right = None 6 | 7 | def path_sum_2(self, target_sum): 8 | """ 9 | Find all root-to-leaf paths where the sum equals target_sum. 10 | """ 11 | result = [] 12 | self._dfs(self, target_sum, [], result) 13 | return result 14 | 15 | def _dfs(self, node, remaining_sum, current_path, result): 16 | if not node: 17 | return 18 | 19 | # Include the current node in the path 20 | current_path.append(node.value) 21 | 22 | # If it's a leaf node and the sum matches, add path to result 23 | if not node.left and not node.right and remaining_sum == node.value: 24 | result.append(list(current_path)) 25 | else: 26 | # Continue to explore left and right subtrees 27 | self._dfs(node.left, remaining_sum - node.value, current_path, result) 28 | self._dfs(node.right, remaining_sum - node.value, current_path, result) 29 | 30 | # Backtrack 31 | current_path.pop() 32 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 17/Word_Break_Tabulation_Approach.py: -------------------------------------------------------------------------------- 1 | """ 2 | Word Break Problem - Tabulation (Bottom-Up DP) Approach 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | def word_break(s: str, word_dict: List[str]) -> bool: 9 | """ 10 | Determines if the string `s` can be segmented into space-separated 11 | words from the given `word_dict` using a bottom-up DP approach. 12 | 13 | Args: 14 | s (str): Input string. 15 | word_dict (List[str]): List of valid words. 16 | 17 | Returns: 18 | bool: True if `s` can be segmented, otherwise False. 19 | """ 20 | word_set = set(word_dict) # Convert list to set for faster lookups 21 | n = len(s) 22 | dp = [False] * n # DP table where dp[i] represents if s[:i+1] is segmentable 23 | 24 | for i in range(n): 25 | for word in word_set: 26 | word_length = len(word) 27 | 28 | if i < word_length - 1: 29 | continue 30 | 31 | start_idx = i - word_length + 1 32 | if s[start_idx:i + 1] == word and (start_idx == 0 or dp[start_idx - 1]): 33 | dp[i] = True 34 | break 35 | 36 | return dp[n - 1] 37 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 29/Find_First_and_Last_Position.py: -------------------------------------------------------------------------------- 1 | def searchRange(nums, target): 2 | """ 3 | Find the first and last position of the target in a sorted array. 4 | 5 | Args: 6 | nums (list): A sorted list of integers. 7 | target (int): The target value to search for. 8 | 9 | Returns: 10 | list: A list with the first and last positions of the target value. 11 | """ 12 | 13 | def findLeft(): 14 | left, right = 0, len(nums) - 1 15 | while left <= right: 16 | mid = left + (right - left) // 2 17 | if nums[mid] >= target: 18 | right = mid - 1 19 | else: 20 | left = mid + 1 21 | return left 22 | 23 | def findRight(): 24 | left, right = 0, len(nums) - 1 25 | while left <= right: 26 | mid = left + (right - left) // 2 27 | if nums[mid] <= target: 28 | left = mid + 1 29 | else: 30 | right = mid - 1 31 | return right 32 | 33 | left, right = findLeft(), findRight() 34 | 35 | if left <= right: 36 | return [left, right] 37 | return [-1, -1] 38 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 02/Josephus_Problem_Method_3.py: -------------------------------------------------------------------------------- 1 | def find_the_winner(n, k): 2 | """ 3 | Finds the winner of the Josephus problem using an iterative approach. 4 | 5 | The problem is solved by simulating the elimination process iteratively, 6 | ensuring (O(1)) space complexity. 7 | 8 | Args: 9 | n (int): The total number of people in the circle. 10 | k (int): The step count for elimination. 11 | 12 | Returns: 13 | int: The position of the survivor (1-indexed). 14 | 15 | Time Complexity: 16 | O(n): The loop iterates (n - 1) times. 17 | Space Complexity: 18 | O(1): No additional space is used beyond variables. 19 | """ 20 | # Initialize the survivor's position (0-indexed). 21 | survivor = 0 22 | 23 | # Iteratively calculate the survivor's position. 24 | for i in range(2, n + 1): 25 | survivor = (survivor + k) % i 26 | 27 | # Convert the survivor's position to 1-indexed. 28 | return survivor + 1 29 | 30 | 31 | if __name__ == "__main__": 32 | # Example usage 33 | total_people = 5 34 | step_count = 3 35 | print(f"The winner is at position: {find_the_winner(total_people, step_count)}") 36 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 44/Zigzag_(Spiral)_Traversal.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from logging import root 3 | 4 | def zigzag_traversal(root): 5 | """ 6 | Perform a Zigzag (Spiral) level order traversal of a binary tree. 7 | """ 8 | if not root: 9 | return [] 10 | 11 | result = [] 12 | queue = deque([root]) 13 | left_to_right = True 14 | 15 | while queue: 16 | level = [] 17 | level_length = len(queue) 18 | 19 | for _ in range(level_length): 20 | node = queue.popleft() 21 | level.append(node.value) 22 | 23 | if node.left: 24 | queue.append(node.left) 25 | if node.right: 26 | queue.append(node.right) 27 | 28 | # If the current level's traversal is from right to left, reverse the level 29 | if not left_to_right: 30 | level.reverse() 31 | 32 | result.append(level) 33 | 34 | # Toggle the direction for the next level 35 | left_to_right = not left_to_right 36 | 37 | return result 38 | 39 | # Example: 40 | print("Zigzag Traversal:", zigzag_traversal(root)) 41 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 12/Partition_Equal_Subset_Sum.py: -------------------------------------------------------------------------------- 1 | """Module to determine if a list can be partitioned into two subsets with equal sum.""" 2 | from typing import List 3 | 4 | def can_partition(nums: List[int]) -> bool: 5 | """ 6 | Determine if a list can be partitioned into two subsets with equal sum. 7 | 8 | Args: 9 | nums (List[int]): List of numbers. 10 | 11 | Returns: 12 | bool: True if the list can be partitioned, otherwise False. 13 | """ 14 | n = len(nums) 15 | total_sum = sum(nums) 16 | 17 | if total_sum % 2 != 0: 18 | return False 19 | 20 | target = total_sum // 2 21 | prev = [False] * (target + 1) 22 | curr = [False] * (target + 1) 23 | prev[0] = True 24 | curr[0] = True 25 | 26 | for i in range(1, n + 1): 27 | for j in range(1, target + 1): 28 | # Pick the element if it does not exceed the target sum 29 | if nums[i - 1] <= j: 30 | curr[j] = prev[j - nums[i - 1]] 31 | else: 32 | curr[j] = False 33 | # Don't pick the element 34 | curr[j] = curr[j] or prev[j] 35 | 36 | prev = curr[:] 37 | 38 | return curr[target] 39 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 43/Level_Order_Traversal_(Breadth-First Search).py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | class TreeNode: 4 | def __init__(self, value=0, left=None, right=None): 5 | self.value = value 6 | self.left = left 7 | self.right = right 8 | 9 | def level_order_traversal(root): 10 | """ 11 | Perform level order traversal (Breadth-First Search) of a binary tree. 12 | """ 13 | if not root: 14 | return [] 15 | 16 | result = [] 17 | queue = deque([root]) 18 | 19 | while queue: 20 | level = [] 21 | level_length = len(queue) 22 | 23 | for _ in range(level_length): 24 | node = queue.popleft() 25 | level.append(node.value) 26 | 27 | if node.left: 28 | queue.append(node.left) 29 | if node.right: 30 | queue.append(node.right) 31 | 32 | result.append(level) 33 | 34 | return result 35 | 36 | # Example: 37 | root = TreeNode(1) 38 | root.left = TreeNode(2) 39 | root.right = TreeNode(3) 40 | root.left.left = TreeNode(4) 41 | root.left.right = TreeNode(5) 42 | 43 | print("Level Order Traversal:", level_order_traversal(root)) 44 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 42/Construct_Binary_Tree_from_Pre-order_and_In-order_Traversal.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | def build_tree(preorder, inorder): 8 | """ 9 | Constructs a binary tree from pre-order and in-order traversal lists. 10 | """ 11 | def helper(preorder, inorder): 12 | if not preorder or not inorder: 13 | return None 14 | 15 | # The first element of preorder is the root 16 | root_value = preorder[0] 17 | root = TreeNode(root_value) 18 | 19 | # Find the root in inorder to split left and right subtrees 20 | root_index = inorder.index(root_value) 21 | 22 | # Recursively build the left and right subtrees 23 | root.left = helper(preorder[1:1 + root_index], inorder[:root_index]) 24 | root.right = helper(preorder[1 + root_index:], inorder[root_index + 1:]) 25 | 26 | return root 27 | 28 | return helper(preorder, inorder) 29 | 30 | # Example: 31 | preorder = [3, 9, 20, 15, 7] 32 | inorder = [9, 3, 15, 20, 7] 33 | 34 | root = build_tree(preorder, inorder) -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 22/Task_Scheduler.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | 3 | def leastInterval(tasks, n): 4 | """ 5 | Calculate the least number of intervals needed to schedule all tasks. 6 | 7 | Args: 8 | tasks (list): A list of task characters. 9 | n (int): The cooldown period between the same task. 10 | 11 | Returns: 12 | int: The least number of intervals needed to schedule all tasks. 13 | """ 14 | if not tasks: 15 | return 0 16 | 17 | # Count the frequency of each task 18 | task_count = {} 19 | for task in tasks: 20 | task_count[task] = task_count.get(task, 0) + 1 21 | 22 | # Max-heap to store the frequency of tasks 23 | max_heap = [-count for count in task_count.values()] 24 | heapq.heapify(max_heap) 25 | 26 | time = 0 27 | while max_heap: 28 | temp = [] 29 | for _ in range(n + 1): 30 | if max_heap: 31 | count = heapq.heappop(max_heap) + 1 # Decrease the count 32 | if count != 0: 33 | temp.append(count) 34 | 35 | for item in temp: 36 | heapq.heappush(max_heap, item) 37 | 38 | time += n + 1 if max_heap else len(temp) 39 | 40 | return time 41 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 16/Palindrome_Partitioning_2_Tabulation_Approach_B.py: -------------------------------------------------------------------------------- 1 | def min_cut(s): 2 | """ 3 | Find the minimum cuts needed to partition a string into palindromic substrings 4 | using tabulation (Bottom-Up DP with a 1D DP array). 5 | 6 | Args: 7 | s (str): The input string. 8 | 9 | Returns: 10 | int: The minimum number of cuts required. 11 | """ 12 | n = len(s) 13 | is_palindrome = [[False] * n for _ in range(n)] 14 | dp = [0] * n 15 | 16 | for length in range(1, n + 1): 17 | for i in range(n - length + 1): 18 | j = i + length - 1 19 | if i == j: 20 | is_palindrome[i][j] = True 21 | elif s[i] == s[j] and (j == i + 1 or is_palindrome[i + 1][j - 1]): 22 | is_palindrome[i][j] = True 23 | else: 24 | is_palindrome[i][j] = False 25 | 26 | for end in range(n): 27 | min_cuts = end 28 | for start in range(end + 1): 29 | if is_palindrome[start][end]: 30 | if start == 0: 31 | min_cuts = 0 32 | else: 33 | min_cuts = min(min_cuts, 1 + dp[start - 1]) 34 | dp[end] = min_cuts 35 | 36 | return dp[n - 1] 37 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 48/Lowest_Common_Ancestor_of_BST.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | def lowest_common_ancestor(root, p, q): 8 | """ 9 | Find the lowest common ancestor (LCA) of two nodes in a BST. 10 | """ 11 | if not root: 12 | return None 13 | 14 | # If both nodes are smaller, go left 15 | if p.value < root.value and q.value < root.value: 16 | return lowest_common_ancestor(root.left, p, q) 17 | 18 | # If both nodes are larger, go right 19 | if p.value > root.value and q.value > root.value: 20 | return lowest_common_ancestor(root.right, p, q) 21 | 22 | # Otherwise, root is the LCA 23 | return root 24 | 25 | # Example: 26 | root = TreeNode(6) 27 | root.left = TreeNode(2) 28 | root.right = TreeNode(8) 29 | root.left.left = TreeNode(0) 30 | root.left.right = TreeNode(4) 31 | root.right.left = TreeNode(7) 32 | root.right.right = TreeNode(9) 33 | root.left.right.left = TreeNode(3) 34 | root.left.right.right = TreeNode(5) 35 | 36 | p = root.left # Node 2 37 | q = root.right # Node 8 38 | 39 | print("LCA:", lowest_common_ancestor(root, p, q).value) # Output: 6 40 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 14/LIS_Memorization_Approach.py: -------------------------------------------------------------------------------- 1 | def lengthOfLIS(nums): 2 | """ 3 | Calculate the length of the longest increasing subsequence using memoization. 4 | 5 | Args: 6 | nums (list[int]): A list of integers representing the sequence. 7 | 8 | Returns: 9 | int: The length of the longest increasing subsequence. 10 | """ 11 | n = len(nums) 12 | dp = [[-1] * n for _ in range(n)] 13 | 14 | def helper(curr, prev): 15 | # Base case: if the current index exceeds the array length 16 | if curr > n - 1: 17 | return 0 18 | 19 | # If the result has already been computed, return the cached value 20 | if dp[curr][prev + 1] != -1: 21 | return dp[curr][prev + 1] 22 | 23 | # Exclude the current element from the subsequence 24 | exclude = helper(curr + 1, prev) 25 | 26 | # Include the current element if it forms an increasing subsequence 27 | include = 0 28 | if prev == -1 or nums[curr] > nums[prev]: 29 | include = 1 + helper(curr + 1, curr) 30 | 31 | # Store the result in the dp table and return it 32 | dp[curr][prev + 1] = max(include, exclude) 33 | return dp[curr][prev + 1] 34 | 35 | return helper(0, -1) 36 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 10/Tribonacci.py: -------------------------------------------------------------------------------- 1 | def tribonacci(n: int) -> int: 2 | """ 3 | Calculate the n-th Tribonacci number using space optimization. 4 | 5 | Tribonacci sequence: 6 | - T(0) = 0, T(1) = 1, T(2) = 1 7 | - T(n) = T(n-1) + T(n-2) + T(n-3) for n >= 3 8 | 9 | Args: 10 | n (int): The index of the Tribonacci number to compute. 11 | 12 | Returns: 13 | int: The n-th Tribonacci number. 14 | """ 15 | # Base cases 16 | if n == 0: 17 | return 0 18 | if n == 1 or n == 2: 19 | return 1 20 | 21 | # Initialize variables for T(n-3), T(n-2), and T(n-1) 22 | trib_n_minus_3: int = 0 # T(0) 23 | trib_n_minus_2: int = 1 # T(1) 24 | trib_n_minus_1: int = 1 # T(2) 25 | 26 | # Compute T(n) iteratively 27 | for i in range(3, n + 1): 28 | # Calculate the current Tribonacci number 29 | trib_n: int = trib_n_minus_3 + trib_n_minus_2 + trib_n_minus_1 30 | 31 | # Update the previous values 32 | trib_n_minus_3, trib_n_minus_2, trib_n_minus_1 = trib_n_minus_2, trib_n_minus_1, trib_n 33 | 34 | return trib_n 35 | 36 | 37 | # Space Complexity: O(1) - Only a fixed number of variables are used regardless of `n`. 38 | # Time Complexity: O(n) - A single loop from 3 to `n` (inclusive). 39 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 44/Level_Order_Traversal_2.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | class TreeNode: 4 | def __init__(self, value=0, left=None, right=None): 5 | self.value = value 6 | self.left = left 7 | self.right = right 8 | 9 | def level_order_traversal_2(root): 10 | """ 11 | Perform level order traversal where each level's nodes are in a separate list. 12 | """ 13 | if not root: 14 | return [] 15 | 16 | result = [] 17 | queue = deque([root]) 18 | 19 | while queue: 20 | level = [] 21 | level_length = len(queue) 22 | 23 | for _ in range(level_length): 24 | node = queue.popleft() 25 | level.append(node.value) 26 | 27 | if node.left: 28 | queue.append(node.left) 29 | if node.right: 30 | queue.append(node.right) 31 | 32 | result.append(level) 33 | 34 | return result 35 | 36 | # Example: 37 | root = TreeNode(1) 38 | root.left = TreeNode(2) 39 | root.right = TreeNode(3) 40 | root.left.left = TreeNode(4) 41 | root.left.right = TreeNode(5) 42 | root.right.left = TreeNode(6) 43 | root.right.right = TreeNode(7) 44 | 45 | print("Level Order Traversal 2:", level_order_traversal_2(root)) 46 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 46/Diameter_of_Tree.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, val=0, left=None, right=None): 3 | self.val = val 4 | self.left = left 5 | self.right = right 6 | 7 | def diameter_of_tree(root): 8 | """ 9 | Calculate the diameter of a binary tree. 10 | The diameter is the longest path between any two nodes in the tree. 11 | """ 12 | def helper(node): 13 | """ 14 | Returns the height of the tree rooted at the given node. 15 | """ 16 | if not node: 17 | return 0 18 | 19 | # Recursively find the height of left and right subtrees 20 | left_height = helper(node.left) 21 | right_height = helper(node.right) 22 | 23 | # Update the diameter (longest path seen so far) 24 | diameter[0] = max(diameter[0], left_height + right_height) 25 | 26 | # Return the height of the tree rooted at this node 27 | return 1 + max(left_height, right_height) 28 | 29 | diameter = [0] 30 | helper(root) 31 | return diameter[0] 32 | 33 | # Example: 34 | root = TreeNode(1) 35 | root.left = TreeNode(2) 36 | root.right = TreeNode(3) 37 | root.left.left = TreeNode(4) 38 | root.left.right = TreeNode(5) 39 | 40 | print("Diameter of Tree:", diameter_of_tree(root)) 41 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 03/Power_Sum.py: -------------------------------------------------------------------------------- 1 | def power_sum(array, power=1): 2 | """ 3 | Calculate the power sum of a nested list. 4 | 5 | The function recursively processes nested lists, summing elements 6 | at increasing power levels for each level of nesting. 7 | 8 | Parameters: 9 | array (list): A list of integers or nested lists. 10 | power (int, optional): The power level for the current recursion. Defaults to 1. 11 | 12 | Returns: 13 | int: The power sum of the input list. 14 | """ 15 | # Initialize the running total to zero. 16 | total = 0 17 | 18 | # Iterate through each item in the input array. 19 | for item in array: 20 | # If the item is a list, recursively calculate its power sum. 21 | if isinstance(item, list): 22 | total += power_sum(item, power + 1) 23 | else: 24 | # Add the item's value directly to the total if it's not a list. 25 | total += item 26 | 27 | # Apply the power to the cumulative sum and return the result. 28 | return total ** power 29 | 30 | 31 | if __name__ == "__main__": 32 | # Example usage 33 | nested_list = [1, [2, [3, 4]], 5] 34 | initial_power = 1 35 | print(f"The power sum of {nested_list} with initial power {initial_power} is: {power_sum(nested_list, initial_power)}") 36 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 17/Matrix_Chain_Multiplication_Recursive_Approach.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Matrix Chain Multiplication - Recursive Approach 4 | """ 5 | 6 | from typing import List 7 | 8 | 9 | def matrix_multiplication(n: int, arr: List[int]) -> int: 10 | """ 11 | Computes the minimum cost of multiplying matrices using recursion. 12 | 13 | Args: 14 | n (int): Number of matrices. 15 | arr (List[int]): Dimensions of matrices. 16 | 17 | Returns: 18 | int: Minimum number of scalar multiplications required. 19 | """ 20 | 21 | def find_cost(i: int, j: int) -> int: 22 | """ 23 | Helper function to find the minimum multiplication cost recursively. 24 | 25 | Args: 26 | i (int): Start index. 27 | j (int): End index. 28 | 29 | Returns: 30 | int: Minimum cost of multiplying matrices from i to j. 31 | """ 32 | if i == j: 33 | return 0 # Base case: A single matrix has no multiplication cost 34 | 35 | cost = float('inf') 36 | for k in range(i, j): 37 | curr_cost = ( 38 | find_cost(i, k) + 39 | find_cost(k + 1, j) + 40 | arr[i - 1] * arr[k] * arr[j] 41 | ) 42 | cost = min(cost, curr_cost) 43 | 44 | return cost 45 | 46 | return find_cost(1, n - 1) 47 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 35/Add_2_Numbers.py: -------------------------------------------------------------------------------- 1 | class ListNode: 2 | def __init__(self, val=0, next=None): 3 | self.val = val 4 | self.next = next 5 | 6 | def add_two_numbers(l1, l2): 7 | """ 8 | Add two numbers represented by singly linked lists. 9 | 10 | Args: 11 | l1 (ListNode): The first input linked list representing a number. 12 | l2 (ListNode): The second input linked list representing a number. 13 | 14 | Returns: 15 | ListNode: A new linked list representing the sum of the two numbers. 16 | """ 17 | carry = 0 18 | dummy_head = ListNode(0) # Dummy node to simplify the logic 19 | current = dummy_head 20 | 21 | while l1 or l2 or carry: 22 | # Extract values from the nodes, or 0 if the node is None 23 | val1 = l1.val if l1 else 0 24 | val2 = l2.val if l2 else 0 25 | 26 | # Add the values and the carry 27 | total = val1 + val2 + carry 28 | 29 | carry = total // 10 # Calculate the new carry 30 | current.next = ListNode(total % 10) # Add the digit to the result 31 | current = current.next 32 | 33 | # Move to the next nodes 34 | if l1: 35 | l1 = l1.next 36 | if l2: 37 | l2 = l2.next 38 | 39 | return dummy_head.next # Return the result without the dummy head 40 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 13/LCS_Memorization_Approach.py: -------------------------------------------------------------------------------- 1 | def longestCommonSubsequence(text1, text2): 2 | """ 3 | Calculate the length of the Longest Common Subsequence (LCS) using memoization. 4 | 5 | Args: 6 | text1 (str): The first input string. 7 | text2 (str): The second input string. 8 | 9 | Returns: 10 | int: The length of the Longest Common Subsequence (LCS). 11 | """ 12 | n = len(text1) 13 | m = len(text2) 14 | 15 | # Initialize a memoization table with -1 16 | dp = [[-1] * m for _ in range(n)] 17 | 18 | def helper(index1, index2): 19 | # Base case: If either string has been fully processed 20 | if index1 >= n or index2 >= m: 21 | return 0 22 | 23 | # If this subproblem has already been solved, return the result 24 | if dp[index1][index2] != -1: 25 | return dp[index1][index2] 26 | 27 | # If characters match, include them in the LCS 28 | if text1[index1] == text2[index2]: 29 | dp[index1][index2] = 1 + helper(index1 + 1, index2 + 1) 30 | return dp[index1][index2] 31 | 32 | # Otherwise, explore both possibilities (skip character from either text1 or text2) 33 | dp[index1][index2] = max(helper(index1 + 1, index2), helper(index1, index2 + 1)) 34 | return dp[index1][index2] 35 | 36 | return helper(0, 0) 37 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 39/Traversa_Techniques.py: -------------------------------------------------------------------------------- 1 | class BinarySearchTree: 2 | # Previous methods omitted for brevity... 3 | 4 | def inorder(self, root): 5 | """ 6 | In-order Traversal: Left → Node → Right 7 | """ 8 | return self._inorder(root) 9 | 10 | def _inorder(self, root): 11 | result = [] 12 | if root: 13 | result += self._inorder(root.left) 14 | result.append(root.value) 15 | result += self._inorder(root.right) 16 | return result 17 | 18 | def preorder(self, root): 19 | """ 20 | Pre-order Traversal: Node → Left → Right 21 | """ 22 | return self._preorder(root) 23 | 24 | def _preorder(self, root): 25 | result = [] 26 | if root: 27 | result.append(root.value) 28 | result += self._preorder(root.left) 29 | result += self._preorder(root.right) 30 | return result 31 | 32 | def postorder(self, root): 33 | """ 34 | Post-order Traversal: Left → Right → Node 35 | """ 36 | return self._postorder(root) 37 | 38 | def _postorder(self, root): 39 | result = [] 40 | if root: 41 | result += self._postorder(root.left) 42 | result += self._postorder(root.right) 43 | result.append(root.value) 44 | return result 45 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 31/Merge_Sort.py: -------------------------------------------------------------------------------- 1 | def mergeSort(nums): 2 | """ 3 | Sort the list using Merge Sort. 4 | 5 | Args: 6 | nums (list): The list of integers to be sorted. 7 | 8 | Returns: 9 | list: The sorted list. 10 | """ 11 | if len(nums) <= 1: 12 | return nums 13 | 14 | # Divide the array into two halves 15 | mid = len(nums) // 2 16 | left = mergeSort(nums[:mid]) 17 | right = mergeSort(nums[mid:]) 18 | 19 | # Merge the sorted halves 20 | return merge(left, right) 21 | 22 | def merge(left, right): 23 | """ 24 | Merge two sorted lists into a single sorted list. 25 | 26 | Args: 27 | left (list): The left sorted list. 28 | right (list): The right sorted list. 29 | 30 | Returns: 31 | list: The merged sorted list. 32 | """ 33 | sorted_list = [] 34 | i = j = 0 35 | 36 | # Compare elements from both lists and add the smaller one to sorted_list 37 | while i < len(left) and j < len(right): 38 | if left[i] < right[j]: 39 | sorted_list.append(left[i]) 40 | i += 1 41 | else: 42 | sorted_list.append(right[j]) 43 | j += 1 44 | 45 | # Append remaining elements from both lists (if any) 46 | sorted_list.extend(left[i:]) 47 | sorted_list.extend(right[j:]) 48 | 49 | return sorted_list 50 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 33/Construct_SSL.py: -------------------------------------------------------------------------------- 1 | class ListNode: 2 | def __init__(self, value=0, next=None): 3 | """ 4 | Initialize a ListNode with a value and a reference to the next node. 5 | 6 | Args: 7 | value (int): The value of the node. 8 | next (ListNode): The reference to the next node in the list. 9 | """ 10 | self.value = value 11 | self.next = next 12 | 13 | class SinglyLinkedList: 14 | def __init__(self): 15 | """ 16 | Initialize an empty Singly Linked List. 17 | """ 18 | self.head = None 19 | 20 | def append(self, value): 21 | """ 22 | Append a new node with a given value to the end of the list. 23 | 24 | Args: 25 | value (int): The value to be added to the list. 26 | """ 27 | new_node = ListNode(value) 28 | if not self.head: 29 | self.head = new_node 30 | return 31 | current = self.head 32 | while current.next: 33 | current = current.next 34 | current.next = new_node 35 | 36 | def print_list(self): 37 | """ 38 | Print all the elements of the list. 39 | """ 40 | current = self.head 41 | while current: 42 | print(current.value, end=" -> ") 43 | current = current.next 44 | print("None") 45 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 17/Word_Break_Recursive_Approach.py: -------------------------------------------------------------------------------- 1 | """ 2 | Word Break Problem - Recursive Approach 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | def word_break(s: str, word_dict: List[str]) -> bool: 9 | """ 10 | Determines if the string `s` can be segmented into space-separated 11 | words from the given `word_dict` using recursion. 12 | 13 | Args: 14 | s (str): Input string. 15 | word_dict (List[str]): List of valid words. 16 | 17 | Returns: 18 | bool: True if `s` can be segmented, otherwise False. 19 | """ 20 | word_set = set(word_dict) # Convert list to set for optimized lookup 21 | n = len(s) 22 | 23 | def check_ending_at(index: int) -> bool: 24 | """ 25 | Recursively checks if `s[:index+1]` can be segmented using words in `word_set`. 26 | 27 | Args: 28 | index (int): The current ending index to check. 29 | 30 | Returns: 31 | bool: True if `s[:index+1]` can be segmented, otherwise False. 32 | """ 33 | if index < 0: 34 | return True 35 | 36 | for word in word_set: 37 | word_length = len(word) 38 | start_idx = index - word_length + 1 39 | 40 | if start_idx >= 0 and s[start_idx:index + 1] == word and check_ending_at(start_idx - 1): 41 | return True 42 | 43 | return False 44 | 45 | return check_ending_at(n - 1) 46 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 45/Vertical_Order_Traversal.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, deque 2 | 3 | class TreeNode: 4 | def __init__(self, value=0, left=None, right=None): 5 | self.value = value 6 | self.left = left 7 | self.right = right 8 | 9 | def vertical_order_traversal(root): 10 | """ 11 | Perform vertical order traversal of a binary tree. 12 | """ 13 | if not root: 14 | return [] 15 | 16 | # A dictionary to hold nodes at each horizontal distance 17 | column_map = defaultdict(list) 18 | 19 | # Queue for level order traversal with horizontal distance 20 | queue = deque([(root, 0)]) # (node, horizontal_distance) 21 | 22 | while queue: 23 | node, hd = queue.popleft() 24 | column_map[hd].append(node.value) 25 | 26 | if node.left: 27 | queue.append((node.left, hd - 1)) 28 | if node.right: 29 | queue.append((node.right, hd + 1)) 30 | 31 | # Sort the keys (horizontal distances) and prepare the result 32 | sorted_columns = sorted(column_map.keys()) 33 | result = [column_map[hd] for hd in sorted_columns] 34 | 35 | return result 36 | 37 | # Example: 38 | root = TreeNode(3) 39 | root.left = TreeNode(9) 40 | root.right = TreeNode(20) 41 | root.right.left = TreeNode(15) 42 | root.right.right = TreeNode(7) 43 | 44 | print("Vertical Order Traversal:", vertical_order_traversal(root)) 45 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 50/Serialize_and_Deserialize_Binary_Tree.py: -------------------------------------------------------------------------------- 1 | class TreeNode: 2 | def __init__(self, value=0, left=None, right=None): 3 | self.value = value 4 | self.left = left 5 | self.right = right 6 | 7 | class Codec: 8 | def serialize(self, root): 9 | """Encodes a tree to a single string.""" 10 | def helper(node): 11 | if not node: 12 | return "#" 13 | return str(node.value) + "," + helper(node.left) + "," + helper(node.right) 14 | 15 | return helper(root) 16 | 17 | def deserialize(self, data): 18 | """Decodes your encoded data to tree.""" 19 | values = data.split(",") 20 | def helper(): 21 | val = values.pop(0) 22 | if val == "#": 23 | return None 24 | node = TreeNode(int(val)) 25 | node.left = helper() 26 | node.right = helper() 27 | return node 28 | 29 | return helper() 30 | 31 | # Example: 32 | codec = Codec() 33 | tree = TreeNode(1) 34 | tree.left = TreeNode(2) 35 | tree.right = TreeNode(3) 36 | tree.right.left = TreeNode(4) 37 | tree.right.right = TreeNode(5) 38 | 39 | serialized = codec.serialize(tree) 40 | print("Serialized:", serialized) # Output: "1,2,#,#,3,4,#,#,5,#,#" 41 | 42 | deserialized = codec.deserialize(serialized) 43 | print("Deserialized root value:", deserialized.value) # Output: 1 44 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 13/Edit_Distance_Tabulation_Approach.py: -------------------------------------------------------------------------------- 1 | def minDistance(word1, word2): 2 | """ 3 | Calculate the minimum edit distance between two words using tabulation. 4 | 5 | Args: 6 | word1 (str): The first input word. 7 | word2 (str): The second input word. 8 | 9 | Returns: 10 | int: The minimum number of operations required to convert word1 to word2. 11 | """ 12 | n = len(word1) 13 | m = len(word2) 14 | 15 | # Initialize a 2D DP array with dimensions (n+1) x (m+1) 16 | dp = [[0] * (m + 1) for _ in range(n + 1)] 17 | 18 | # Base case: if one string is empty, the number of operations is the length of the other string 19 | for i in range(n + 1): 20 | dp[i][0] = i 21 | for j in range(m + 1): 22 | dp[0][j] = j 23 | 24 | # Fill the DP table iteratively 25 | for i in range(1, n + 1): 26 | for j in range(1, m + 1): 27 | if word1[i - 1] == word2[j - 1]: 28 | dp[i][j] = dp[i - 1][j - 1] # No operation needed if characters match 29 | else: 30 | # Calculate the minimum number of operations 31 | replace = 1 + dp[i - 1][j - 1] # Replace character 32 | delete = 1 + dp[i - 1][j] # Delete character from word1 33 | insert = 1 + dp[i][j - 1] # Insert character into word1 34 | dp[i][j] = min(replace, delete, insert) # Choose the minimum operation 35 | 36 | return dp[n][m] 37 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 02/K-th_Symbol_Grammar.py: -------------------------------------------------------------------------------- 1 | def kth_grammar(n, k): 2 | """ 3 | Finds the k-th symbol in the n-th row of a specific grammar. 4 | 5 | Grammar Rules: 6 | - Row 1 (n = 1): "0" 7 | - Subsequent rows are formed as: 8 | - Replace each "0" with "01". 9 | - Replace each "1" with "10". 10 | - Example: 11 | - Row 1: "0" 12 | - Row 2: "01" 13 | - Row 3: "0110" 14 | - Row 4: "01101001" 15 | 16 | Args: 17 | n (int): The row number (1-indexed). 18 | k (int): The position of the symbol in the row (1-indexed). 19 | 20 | Returns: 21 | int: The k-th symbol in the nth row (0 or 1). 22 | 23 | Time Complexity: 24 | O(n): Each recursive call reduces the problem size by one. 25 | 26 | Space Complexity: 27 | O(n): Stack space due to recursion. 28 | """ 29 | # Base case: the first row has only "0". 30 | if n == 1: 31 | return 0 32 | 33 | # Calculate the midpoint of the current row. 34 | mid = 2**(n - 2) 35 | 36 | # If k is in the first half, recurse into the previous row. 37 | if k <= mid: 38 | return kth_grammar(n - 1, k) 39 | 40 | # If k is in the second half, recurse with adjusted position and invert the result. 41 | return 1 - kth_grammar(n - 1, k - mid) 42 | 43 | 44 | if __name__ == "__main__": 45 | # Example usage 46 | row_number = 4 47 | position = 5 48 | print(f"The {position}-th symbol in row {row_number} is: {kth_grammar(row_number, position)}") 49 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 11/1_Knapsack_Tabulation_Approach.py: -------------------------------------------------------------------------------- 1 | def knap_sack(max_weight, weights, values, n): 2 | """ 3 | Solve the 0/1 Knapsack problem using tabulation. 4 | 5 | This function calculates the maximum value that can be obtained by including 6 | or excluding items in the knapsack, such that the total weight does not exceed 7 | the given maximum weight. The solution is built iteratively using a DP table. 8 | 9 | Args: 10 | max_weight (int): The maximum weight capacity of the knapsack. 11 | weights (list[int]): A list of integers representing the weight of each item. 12 | values (list[int]): A list of integers representing the value of each item. 13 | n (int): The total number of items. 14 | 15 | Returns: 16 | int: The maximum value that can be obtained within the weight limit. 17 | """ 18 | 19 | # Create a DP table where dp[i][j] represents the maximum value for the first i items 20 | # with a weight limit of j 21 | dp = [[0] * (max_weight + 1) for _ in range(n + 1)] 22 | 23 | for i in range(1, n + 1): 24 | for j in range(1, max_weight + 1): 25 | # Exclude the current item 26 | exclude = dp[i - 1][j] 27 | include = 0 28 | # Include the current item if it fits in the remaining weight capacity 29 | if weights[i - 1] <= j: 30 | include = values[i - 1] + dp[i - 1][j - weights[i - 1]] 31 | 32 | dp[i][j] = max(exclude, include) 33 | 34 | return dp[n][max_weight] 35 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 13/Edit_Distance_Recursive_Approach.py: -------------------------------------------------------------------------------- 1 | def minDistance(word1, word2): 2 | """ 3 | Calculate the minimum edit distance between two words using recursion. 4 | 5 | Args: 6 | word1 (str): The first input word. 7 | word2 (str): The second input word. 8 | 9 | Returns: 10 | int: The minimum number of operations required to convert word1 to word2. 11 | """ 12 | n = len(word1) 13 | m = len(word2) 14 | 15 | def number_of_operations(index1, index2): 16 | # Base case: if both words are fully processed 17 | if index1 > n - 1 and index2 > m - 1: 18 | return 0 19 | 20 | # If one word is exhausted, count remaining operations for the other word 21 | if index1 > n - 1: 22 | return m - index2 23 | if index2 > m - 1: 24 | return n - index1 25 | 26 | # If characters are the same, no operation is needed 27 | if word1[index1] == word2[index2]: 28 | return number_of_operations(index1 + 1, index2 + 1) 29 | 30 | # Try all three operations: insert, delete, and replace 31 | insert = 1 + number_of_operations(index1, index2 + 1) # Insert character into word1 32 | delete = 1 + number_of_operations(index1 + 1, index2) # Delete character from word1 33 | replace = 1 + number_of_operations(index1 + 1, index2 + 1) # Replace character in word1 34 | 35 | return min(insert, delete, replace) # Return the minimum of all operations 36 | 37 | return number_of_operations(0, 0) 38 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 17/Matrix_Chain_Multiplication_Memorization_Approach.py: -------------------------------------------------------------------------------- 1 | """ 2 | Matrix Chain Multiplication - Memoization Approach 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | def matrix_multiplication(n: int, arr: List[int]) -> int: 9 | """ 10 | Computes the minimum cost of multiplying matrices using memoization. 11 | 12 | Args: 13 | n (int): Number of matrices. 14 | arr (List[int]): Dimensions of matrices. 15 | 16 | Returns: 17 | int: Minimum number of scalar multiplications required. 18 | """ 19 | dp = [[-1] * n for _ in range(n)] 20 | 21 | def find_cost(i: int, j: int) -> int: 22 | """ 23 | Helper function to find the minimum multiplication cost using memoization. 24 | 25 | Args: 26 | i (int): Start index. 27 | j (int): End index. 28 | 29 | Returns: 30 | int: Minimum cost of multiplying matrices from i to j. 31 | """ 32 | if i == j: 33 | return 0 # Base case: A single matrix has no multiplication cost 34 | 35 | if dp[i][j] != -1: 36 | return dp[i][j] # Return precomputed value 37 | 38 | cost = float('inf') 39 | for k in range(i, j): 40 | curr_cost = ( 41 | find_cost(i, k) + 42 | find_cost(k + 1, j) + 43 | arr[i - 1] * arr[k] * arr[j] 44 | ) 45 | cost = min(cost, curr_cost) 46 | 47 | dp[i][j] = cost 48 | return dp[i][j] 49 | 50 | return find_cost(1, n - 1) 51 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 38/Implement_Queue_with_Stack.py: -------------------------------------------------------------------------------- 1 | class QueueWithTwoStacks: 2 | def __init__(self): 3 | self.stack1 = [] 4 | self.stack2 = [] 5 | 6 | def enqueue(self, item): 7 | """ 8 | Enqueue an item to the queue by pushing it onto stack1. 9 | """ 10 | self.stack1.append(item) 11 | 12 | def dequeue(self): 13 | """ 14 | Dequeue an item from the queue by transferring elements from stack1 to stack2, if necessary. 15 | """ 16 | if not self.stack2: # If stack2 is empty, transfer elements from stack1 to stack2 17 | while self.stack1: 18 | self.stack2.append(self.stack1.pop()) 19 | 20 | if not self.stack2: # If stack2 is still empty, the queue is empty 21 | raise IndexError("dequeue from empty queue") 22 | 23 | return self.stack2.pop() 24 | 25 | def front(self): 26 | """ 27 | Return the front item without removing it. 28 | """ 29 | if not self.stack2: # If stack2 is empty, transfer elements from stack1 to stack2 30 | while self.stack1: 31 | self.stack2.append(self.stack1.pop()) 32 | 33 | if not self.stack2: # If stack2 is still empty, the queue is empty 34 | raise IndexError("front from empty queue") 35 | 36 | return self.stack2[-1] 37 | 38 | def is_empty(self): 39 | """ 40 | Check if the queue is empty. 41 | """ 42 | return not self.stack1 and not self.stack2 43 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 10/Minimun_Cost_Climbing_Stairs_Tabulation_Approach.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def minCostClimbingStairs(cost: List[int]) -> int: 4 | """ 5 | Calculate the minimum cost to climb to the top of the stairs using a tabulation approach. 6 | 7 | Args: 8 | cost (List[int]): List where each element represents the cost of stepping on the corresponding stair. 9 | 10 | Returns: 11 | int: Minimum cost to reach the top of the stairs. 12 | """ 13 | n = len(cost) 14 | 15 | # Array to store the minimum cost to reach each stair index. 16 | dp: List[int] = [-1] * (n + 1) 17 | 18 | # Initialize base cases. 19 | dp[0] = 0 # Starting from the ground (before the first step). 20 | dp[1] = 0 # Starting from the first step. 21 | 22 | # Iterate from the 2nd step to the top, calculating the cost to reach each step. 23 | for i in range(2, n + 1): 24 | # Option 1: Cost to reach the current step from the previous step. 25 | one_step_cost = cost[i - 1] + dp[i - 1] 26 | 27 | # Option 2: Cost to reach the current step from two steps back. 28 | two_step_cost = cost[i - 2] + dp[i - 2] 29 | 30 | # Store the minimum cost to reach this step. 31 | dp[i] = min(one_step_cost, two_step_cost) 32 | 33 | # The last element in the array represents the minimum cost to reach the top. 34 | return dp[n] 35 | 36 | 37 | # Space Complexity: O(n) - For the `dp` array storing costs for each stair index. 38 | # Time Complexity: O(n) - Linear iteration through the `cost` array. 39 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 39/Construct_BST.py: -------------------------------------------------------------------------------- 1 | class Node: 2 | def __init__(self, key): 3 | self.left = None 4 | self.right = None 5 | self.value = key 6 | 7 | 8 | class BinarySearchTree: 9 | def __init__(self): 10 | self.root = None 11 | 12 | def insert(self, key): 13 | """ 14 | Insert a new key into the Binary Search Tree. 15 | """ 16 | if self.root is None: 17 | self.root = Node(key) 18 | else: 19 | self._insert(self.root, key) 20 | 21 | def _insert(self, root, key): 22 | """ 23 | Helper method for recursive insertion. 24 | """ 25 | if key < root.value: 26 | if root.left is None: 27 | root.left = Node(key) 28 | else: 29 | self._insert(root.left, key) 30 | elif key > root.value: 31 | if root.right is None: 32 | root.right = Node(key) 33 | else: 34 | self._insert(root.right, key) 35 | 36 | def search(self, key): 37 | """ 38 | Search for a key in the BST. Returns True if found, otherwise False. 39 | """ 40 | return self._search(self.root, key) 41 | 42 | def _search(self, root, key): 43 | """ 44 | Helper method for recursive search. 45 | """ 46 | if root is None or root.value == key: 47 | return root is not None 48 | if key < root.value: 49 | return self._search(root.left, key) 50 | return self._search(root.right, key) 51 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 13/Edit_Distance_Space_Optimised_Tabulation_Approach.py: -------------------------------------------------------------------------------- 1 | def minDistance(word1, word2): 2 | """ 3 | Calculate the minimum edit distance between two words using space optimized tabulation. 4 | 5 | Args: 6 | word1 (str): The first input word. 7 | word2 (str): The second input word. 8 | 9 | Returns: 10 | int: The minimum number of operations required to convert word1 to word2. 11 | """ 12 | n = len(word1) 13 | m = len(word2) 14 | 15 | # Initialize two 1D arrays to store the previous and current rows of the DP table 16 | prev = [0] * (m + 1) # Previous row 17 | curr = [0] * (m + 1) # Current row 18 | 19 | # Base case: Fill the first row 20 | for j in range(m + 1): 21 | prev[j] = j 22 | 23 | # Fill the DP table row by row 24 | for i in range(1, n + 1): 25 | curr[0] = i # The first element of the current row 26 | for j in range(1, m + 1): 27 | if word1[i - 1] == word2[j - 1]: 28 | curr[j] = prev[j - 1] # No operation needed if characters match 29 | else: 30 | # Calculate the minimum number of operations 31 | replace = 1 + prev[j - 1] # Replace character 32 | delete = 1 + prev[j] # Delete character from word1 33 | insert = 1 + curr[j - 1] # Insert character into word1 34 | curr[j] = min(delete, replace, insert) # Choose the minimum operation 35 | 36 | # Update the previous row for the next iteration 37 | prev = curr[:] 38 | 39 | return prev[m] 40 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 16/Palindrome_Partitioning_2_Recursive_Approach.py: -------------------------------------------------------------------------------- 1 | def min_cut(s): 2 | """ 3 | Find the minimum cuts needed to partition a string into palindromic substrings. 4 | 5 | Args: 6 | s (str): The input string. 7 | 8 | Returns: 9 | int: The minimum number of cuts required. 10 | """ 11 | 12 | def is_palindrome(start, end): 13 | """ 14 | Check if a substring of s is a palindrome. 15 | 16 | Args: 17 | start (int): Start index of the substring. 18 | end (int): End index of the substring. 19 | 20 | Returns: 21 | bool: True if the substring is a palindrome, False otherwise. 22 | """ 23 | while start < end: 24 | if s[start] != s[end]: 25 | return False 26 | start += 1 27 | end -= 1 28 | return True 29 | 30 | def partitions(start, end): 31 | """ 32 | Recursively find the minimum cuts for palindromic partitioning. 33 | 34 | Args: 35 | start (int): The starting index. 36 | end (int): The ending index. 37 | 38 | Returns: 39 | int: The minimum cuts required for partitioning. 40 | """ 41 | if start == end or is_palindrome(start, end): 42 | return 0 43 | 44 | min_cuts = end - start 45 | for end_index in range(start, end): 46 | if is_palindrome(start, end_index): 47 | min_cuts = min(min_cuts, 1 + partitions(end_index + 1, end)) 48 | return min_cuts 49 | 50 | return partitions(0, len(s) - 1) 51 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 32/Radix_Sort.py: -------------------------------------------------------------------------------- 1 | def radixSort(nums): 2 | """ 3 | Sort the list using Radix Sort. 4 | 5 | Args: 6 | nums (list): The list of integers to be sorted. 7 | 8 | Returns: 9 | list: The sorted list. 10 | """ 11 | if len(nums) == 0: 12 | return nums 13 | 14 | # Find the maximum number to know the number of digits 15 | max_num = max(nums) 16 | place = 1 # Start with the least significant digit 17 | 18 | while max_num // place > 0: 19 | nums = countingSortByDigit(nums, place) 20 | place *= 10 21 | 22 | return nums 23 | 24 | def countingSortByDigit(nums, place): 25 | """ 26 | Perform Counting Sort based on the digit represented by 'place'. 27 | 28 | Args: 29 | nums (list): The list of integers to be sorted. 30 | place (int): The digit place (1 for ones, 10 for tens, etc.). 31 | 32 | Returns: 33 | list: The list sorted by the digit at 'place'. 34 | """ 35 | output = [0] * len(nums) 36 | count = [0] * 10 37 | 38 | # Store count of occurrences in count[] 39 | for num in nums: 40 | index = num // place % 10 41 | count[index] += 1 42 | 43 | # Change count[i] so that count[i] now contains actual position of this digit in output[] 44 | for i in range(1, 10): 45 | count[i] += count[i - 1] 46 | 47 | # Build the output list 48 | for i in range(len(nums) - 1, -1, -1): 49 | num = nums[i] 50 | index = num // place % 10 51 | output[count[index] - 1] = num 52 | count[index] -= 1 53 | 54 | return output 55 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 51/Max_Heap.py: -------------------------------------------------------------------------------- 1 | class MaxHeap: 2 | def __init__(self): 3 | self.heap = [] 4 | 5 | def insert(self, val): 6 | self.heap.append(val) 7 | self._heapify_up(len(self.heap) - 1) 8 | 9 | def _heapify_up(self, index): 10 | while index > 0 and self.heap[index] > self.heap[(index - 1) // 2]: 11 | self.heap[index], self.heap[(index - 1) // 2] = self.heap[(index - 1) // 2], self.heap[index] 12 | index = (index - 1) // 2 13 | 14 | def extract_max(self): 15 | if len(self.heap) == 0: 16 | return None 17 | max_val = self.heap[0] 18 | self.heap[0] = self.heap[-1] 19 | self.heap.pop() 20 | self._heapify_down(0) 21 | return max_val 22 | 23 | def _heapify_down(self, index): 24 | while 2 * index + 1 < len(self.heap): 25 | largest = index 26 | left = 2 * index + 1 27 | right = 2 * index + 2 28 | if left < len(self.heap) and self.heap[left] > self.heap[largest]: 29 | largest = left 30 | if right < len(self.heap) and self.heap[right] > self.heap[largest]: 31 | largest = right 32 | if largest == index: 33 | break 34 | self.heap[index], self.heap[largest] = self.heap[largest], self.heap[index] 35 | index = largest 36 | 37 | # Example: 38 | heap = MaxHeap() 39 | heap.insert(10) 40 | heap.insert(20) 41 | heap.insert(5) 42 | heap.insert(15) 43 | print("Max value:", heap.extract_max()) # Output: 20 44 | print("Max value:", heap.extract_max()) # Output: 15 45 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 10/Minimun_Cost_Climbing_Stairs_Recursive_Approach.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def minCostClimbingStairs(cost: List[int]) -> int: 4 | """ 5 | Calculate the minimum cost to climb to the top of the stairs. 6 | 7 | Args: 8 | cost (List[int]): List where each element represents the cost of stepping on the corresponding stair. 9 | 10 | Returns: 11 | int: Minimum cost to reach the top of the stairs. 12 | """ 13 | n = len(cost) 14 | 15 | def helper(index: int) -> int: 16 | """ 17 | Recursive helper function to compute the minimum cost starting at a given stair index. 18 | 19 | Args: 20 | index (int): Current stair index. 21 | 22 | Returns: 23 | int: Minimum cost from the current stair to the top. 24 | """ 25 | # Base case: If the index is beyond the last stair, the cost is 0. 26 | if index >= n: 27 | return 0 28 | 29 | # Option 1: Climb one step to the next stair. 30 | one_step_cost = cost[index] + helper(index + 1) 31 | 32 | # Option 2: Climb two steps to the stair after the next. 33 | two_step_cost = cost[index] + helper(index + 2) 34 | 35 | # Return the minimum cost of the two options. 36 | return min(one_step_cost, two_step_cost) 37 | 38 | # Start from either the first or the second stair and take the minimum cost. 39 | return min(helper(0), helper(1)) 40 | 41 | 42 | # Space Complexity: O(n) - Due to the recursion stack for calls up to `n`. 43 | # Time Complexity: O(2^n) - Recursive solution with overlapping subproblems. 44 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 16/Palindrome_Partitioning.py: -------------------------------------------------------------------------------- 1 | def partition(s): 2 | """ 3 | Partition a string into all possible palindromic substrings. 4 | 5 | Args: 6 | s (str): The input string to partition. 7 | 8 | Returns: 9 | List[List[str]]: A list of lists, where each list contains a valid partition of palindromic substrings. 10 | """ 11 | n = len(s) 12 | dp = [[False] * n for _ in range(n)] 13 | 14 | # Fill the DP table to check if substrings are palindromes 15 | for length in range(1, n + 1): 16 | for i in range(n - length + 1): 17 | j = i + length - 1 18 | if i == j: 19 | dp[i][j] = True 20 | elif s[i] == s[j] and (j == i + 1 or dp[i + 1][j - 1]): 21 | dp[i][j] = True 22 | 23 | def backtrack(index, current_partition): 24 | """ 25 | Helper function to perform backtracking for palindrome partitioning. 26 | 27 | Args: 28 | index (int): The starting index for the current partition. 29 | current_partition (List[str]): The current list of palindromic substrings. 30 | """ 31 | # Base case: If index exceeds the string length, add partition to result 32 | if index == n: 33 | result.append(current_partition[:]) 34 | return 35 | 36 | # Explore all partitions starting from current index 37 | for i in range(index, n): 38 | if dp[index][i]: 39 | current_partition.append(s[index:i + 1]) 40 | backtrack(i + 1, current_partition) 41 | current_partition.pop() 42 | 43 | result = [] 44 | backtrack(0, []) 45 | return result 46 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 11/1_Knapsack_Space_Optimised_Tabulation_Approach.py: -------------------------------------------------------------------------------- 1 | def knap_sack(max_weight, weights, values, n): 2 | """ 3 | Solve the 0/1 Knapsack problem using space-optimized tabulation. 4 | 5 | This function calculates the maximum value that can be obtained by including 6 | or excluding items in the knapsack, such that the total weight does not exceed 7 | the given maximum weight. The space complexity is optimized by using only two arrays 8 | to store the results of the previous and current rows. 9 | 10 | Args: 11 | max_weight (int): The maximum weight capacity of the knapsack. 12 | weights (list[int]): A list of integers representing the weight of each item. 13 | values (list[int]): A list of integers representing the value of each item. 14 | n (int): The total number of items. 15 | 16 | Returns: 17 | int: The maximum value that can be obtained within the weight limit. 18 | """ 19 | 20 | # Initialize two arrays to store results for the previous and current items 21 | prev = [0] * (max_weight + 1) 22 | curr = [0] * (max_weight + 1) 23 | 24 | for i in range(1, n + 1): 25 | for j in range(1, max_weight + 1): 26 | # Exclude the current item 27 | exclude = prev[j] 28 | include = 0 29 | # Include the current item if it fits in the remaining weight capacity 30 | if weights[i - 1] <= j: 31 | include = values[i - 1] + prev[j - weights[i - 1]] 32 | 33 | curr[j] = max(exclude, include) 34 | 35 | # Update prev to be the current row after each iteration 36 | prev = curr[:] 37 | 38 | return curr[max_weight] 39 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 13/Edit_Distance_Memorization_Approach.py: -------------------------------------------------------------------------------- 1 | def minDistance(word1, word2): 2 | """ 3 | Calculate the minimum edit distance between two words using memoization. 4 | 5 | Args: 6 | word1 (str): The first input word. 7 | word2 (str): The second input word. 8 | 9 | Returns: 10 | int: The minimum number of operations required to convert word1 to word2. 11 | """ 12 | n = len(word1) 13 | m = len(word2) 14 | 15 | # Initialize memoization array with -1 (indicating uncomputed values) 16 | arr = [[-1] * m for _ in range(n)] 17 | 18 | def helper(index1, index2): 19 | # Base case: if one string is empty, return the length of the other string 20 | if index1 < 0: 21 | return index2 + 1 22 | if index2 < 0: 23 | return index1 + 1 24 | 25 | # If the value is already computed, return the stored result 26 | if arr[index1][index2] != -1: 27 | return arr[index1][index2] 28 | 29 | # If characters match, no operation is needed, move to the next pair 30 | if word1[index1] == word2[index2]: 31 | arr[index1][index2] = helper(index1 - 1, index2 - 1) 32 | return arr[index1][index2] 33 | 34 | # Try all three operations: replace, delete, and insert 35 | replace = 1 + helper(index1 - 1, index2 - 1) # Replace 36 | delete = 1 + helper(index1 - 1, index2) # Delete 37 | insert = 1 + helper(index1, index2 - 1) # Insert 38 | 39 | # Store the result of the minimum operation 40 | arr[index1][index2] = min(replace, delete, insert) 41 | return arr[index1][index2] 42 | 43 | return helper(n - 1, m - 1) 44 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 17/Word_Break_Memorization_Approach.py: -------------------------------------------------------------------------------- 1 | """ 2 | Word Break Problem - Memoization (Top-Down DP) Approach 3 | """ 4 | 5 | from typing import List 6 | 7 | 8 | def word_break(s: str, word_dict: List[str]) -> bool: 9 | """ 10 | Determines if the string `s` can be segmented into space-separated 11 | words from the given `word_dict` using memoization. 12 | 13 | Args: 14 | s (str): Input string. 15 | word_dict (List[str]): List of valid words. 16 | 17 | Returns: 18 | bool: True if `s` can be segmented, otherwise False. 19 | """ 20 | word_set = set(word_dict) # Convert list to set for optimized lookup 21 | n = len(s) 22 | dp = [-1] * n # Memoization table (-1: Uncomputed, 0: False, 1: True) 23 | 24 | def check_build(index: int) -> bool: 25 | """ 26 | Recursively checks if `s[:index+1]` can be segmented using words in `word_set`. 27 | Utilizes memoization to store computed results. 28 | 29 | Args: 30 | index (int): The current ending index to check. 31 | 32 | Returns: 33 | bool: True if `s[:index+1]` can be segmented, otherwise False. 34 | """ 35 | if index < 0: 36 | return True 37 | 38 | if dp[index] != -1: 39 | return dp[index] 40 | 41 | for word in word_set: 42 | word_length = len(word) 43 | start_idx = index - word_length + 1 44 | 45 | if start_idx >= 0 and s[start_idx:index + 1] == word and check_build(start_idx - 1): 46 | dp[index] = 1 # Mark as True 47 | return True 48 | 49 | dp[index] = 0 # Mark as False 50 | return False 51 | 52 | return check_build(n - 1) 53 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 09/Claimbing_Stairs_with_Recursion.py: -------------------------------------------------------------------------------- 1 | # Solution: Climbing Stairs using Recursion Approach 2 | 3 | def climbStairs(n): 4 | """ 5 | Calculate the number of distinct ways to climb to the top of a staircase with `n` steps, 6 | where each time you can climb 1 or 2 steps. 7 | 8 | Parameters: 9 | n (int): The total number of steps in the staircase. 10 | 11 | Returns: 12 | int: The number of distinct ways to climb to the top. 13 | 14 | Time Complexity: O(2^n) 15 | - Recursive tree has overlapping subproblems leading to exponential complexity. 16 | Space Complexity: O(n) 17 | - Stack space used for recursive calls. 18 | """ 19 | # Handle small inputs directly 20 | if n <= 2: 21 | return n 22 | 23 | # Helper function to solve the subproblem recursively 24 | def helper(first, second, n, curr): 25 | """ 26 | Recursive helper function to compute the number of ways. 27 | 28 | Parameters: 29 | first (int): Number of ways to reach the step before the current step. 30 | second (int): Number of ways to reach the current step. 31 | n (int): Total number of steps. 32 | curr (int): Current step being evaluated. 33 | 34 | Returns: 35 | int: Number of ways to reach the nth step. 36 | """ 37 | # Subproblem computation 38 | next_num = first + second # Total ways to reach the next step 39 | 40 | # Base condition: If we've reached the nth step 41 | if curr == n: 42 | return next_num 43 | 44 | # Recursive call for the next step 45 | return helper(second, next_num, n, curr + 1) 46 | 47 | # Start the recursion with the first two steps 48 | return helper(1, 2, n, 3) 49 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 02/Josephus_Problem_Method_2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to solve the Josephus problem using an optimized recursive method. 3 | This approach improves the time complexity to O(n). 4 | """ 5 | 6 | def find_the_winner(n, k): 7 | """ 8 | Determines the winner of the Josephus problem using an optimized approach. 9 | 10 | Args: 11 | n (int): Total number of participants. 12 | k (int): Step count for elimination. 13 | 14 | Returns: 15 | int: The position of the winner. 16 | 17 | Time Complexity: 18 | - O(n): Each recursive call performs a simple arithmetic operation, 19 | and the recursion occurs n times. 20 | 21 | Space Complexity: 22 | - O(n): For the recursion stack in the worst case. 23 | """ 24 | def josephus(n): 25 | """ 26 | Recursive function to solve the Josephus problem in O(n) time. 27 | 28 | Args: 29 | n (int): Number of participants remaining. 30 | 31 | Returns: 32 | int: The position of the last remaining participant (0-indexed). 33 | 34 | Time Complexity: 35 | - O(n): One recursive call per participant. 36 | 37 | Space Complexity: 38 | - O(n): For the recursion stack in the worst case. 39 | """ 40 | # Base case: When there is only one participant left 41 | if n == 1: 42 | return 0 # The 0-indexed position of the winner 43 | 44 | # Recursive case: Calculate the winner's position 45 | return (josephus(n - 1) + k) % n 46 | 47 | # Return the 1-indexed winner's position 48 | return josephus(n) + 1 49 | 50 | 51 | if __name__ == "__main__": 52 | # Test the function with an example 53 | print(find_the_winner(4, 2)) # Output: 1 54 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 04/Permutations_Question_1.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def permute(nums: List[int]) -> List[List[int]]: 4 | """ 5 | Generate all permutations of a list of numbers. 6 | 7 | Args: 8 | nums (List[int]): A list of integers to permute. 9 | 10 | Returns: 11 | List[List[int]]: A list containing all permutations of the input list. 12 | 13 | Time Complexity: 14 | The time complexity is O(n * n!), where n is the length of the input list. 15 | - There are n! permutations in total. 16 | - For each permutation, a copy of the list (O(n)) is appended to the result. 17 | """ 18 | res = [] # List to store all the permutations 19 | n = len(nums) # Length of the input list 20 | 21 | def helper(i: int): 22 | """ 23 | Recursive helper function to generate permutations. 24 | 25 | Args: 26 | i (int): Current index for permutation generation. 27 | """ 28 | # Base case: if the current index is the last one, add the current permutation 29 | if i == n - 1: 30 | res.append(nums[:]) # Append a copy of nums to results 31 | return 32 | 33 | # Recursive case: swap and generate permutations 34 | for j in range(i, n): 35 | nums[i], nums[j] = nums[j], nums[i] # Swap elements at indices i and j 36 | helper(i + 1) # Recurse for the next index 37 | nums[i], nums[j] = nums[j], nums[i] # Backtrack (undo the swap) 38 | 39 | helper(0) # Start the recursion from the first index 40 | return res 41 | 42 | if __name__ == "__main__": 43 | # Example usage of the permute function 44 | numbers = [1, 2, 3] 45 | permutations = permute(numbers) 46 | print("Permutations of", numbers, "are:") 47 | for perm in permutations: 48 | print(perm) 49 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 11/1_Knapsack_Recursive_Approach.py: -------------------------------------------------------------------------------- 1 | def knap_sack(max_weight, weights, values, n): 2 | """ 3 | Solve the 0/1 Knapsack problem using recursion. 4 | 5 | This function calculates the maximum value that can be obtained by including 6 | or excluding items in the knapsack, such that the total weight does not exceed 7 | the given maximum weight. It uses recursion to explore all possible combinations. 8 | 9 | Args: 10 | max_weight (int): The maximum weight capacity of the knapsack. 11 | weights (list[int]): A list of integers representing the weight of each item. 12 | values (list[int]): A list of integers representing the value of each item. 13 | n (int): The total number of items. 14 | 15 | Returns: 16 | int: The maximum value that can be obtained within the weight limit. 17 | """ 18 | 19 | def helper(index, remaining_weight): 20 | """ 21 | Helper function to solve the problem recursively. 22 | 23 | Args: 24 | index (int): The current item being considered. 25 | remaining_weight (int): The remaining weight capacity of the knapsack. 26 | 27 | Returns: 28 | int: The maximum value that can be obtained from the current item onward. 29 | """ 30 | # Base case: if all items have been considered or remaining weight is 0 31 | if index >= n or remaining_weight == 0: 32 | return 0 33 | 34 | # Recursive case: either exclude the current item or include it 35 | exclude = helper(index + 1, remaining_weight) 36 | include = 0 37 | if weights[index] <= remaining_weight: 38 | include = values[index] + helper(index + 1, remaining_weight - weights[index]) 39 | 40 | return max(exclude, include) 41 | 42 | return helper(0, max_weight) 43 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 36/DLL_Remove_Insert.py: -------------------------------------------------------------------------------- 1 | class Node: 2 | def __init__(self, value): 3 | self.value = value 4 | self.prev = None 5 | self.next = None 6 | 7 | class DoublyLinkedList: 8 | def __init__(self): 9 | self.head = None 10 | 11 | def insert(self, value, position): 12 | """ 13 | Insert a node with 'value' at the given 'position' in the DLL. 14 | """ 15 | new_node = Node(value) 16 | if position == 0: 17 | # Insert at the head 18 | new_node.next = self.head 19 | if self.head: 20 | self.head.prev = new_node 21 | self.head = new_node 22 | return 23 | 24 | current = self.head 25 | count = 0 26 | while current and count < position - 1: 27 | current = current.next 28 | count += 1 29 | 30 | if current is None: 31 | print("Position out of range") 32 | return 33 | 34 | # Insert the new node 35 | new_node.next = current.next 36 | if current.next: 37 | current.next.prev = new_node 38 | current.next = new_node 39 | new_node.prev = current 40 | 41 | def remove(self, value): 42 | """ 43 | Remove the first occurrence of a node with 'value'. 44 | """ 45 | current = self.head 46 | while current: 47 | if current.value == value: 48 | if current.prev: 49 | current.prev.next = current.next 50 | if current.next: 51 | current.next.prev = current.prev 52 | if current == self.head: 53 | self.head = current.next 54 | return 55 | current = current.next 56 | print("Value not found in the list") 57 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 15/Palindromic_Substring_Memorization_Approach.py: -------------------------------------------------------------------------------- 1 | def count_substrings(s): 2 | """ 3 | This function counts the number of palindromic substrings in the given string. 4 | 5 | Args: 6 | s (str): The input string to check for palindromic substrings. 7 | 8 | Returns: 9 | int: The number of palindromic substrings in the input string. 10 | """ 11 | n = len(s) 12 | dp = [[-1] * n for _ in range(n)] 13 | 14 | def helper(i, j): 15 | """ 16 | A helper function that recursively checks whether the substring s[i:j+1] is a palindrome. 17 | 18 | Args: 19 | i (int): The starting index of the substring. 20 | j (int): The ending index of the substring. 21 | 22 | Returns: 23 | bool: True if s[i:j+1] is a palindrome, False otherwise. 24 | """ 25 | # Base case: single character is always a palindrome 26 | if i == j: 27 | dp[i][j] = True 28 | return dp[i][j] 29 | 30 | # If we've already computed the value for this substring, return it 31 | if dp[i][j] != -1: 32 | return dp[i][j] 33 | 34 | # Recursively check for smaller substrings 35 | helper(i + 1, j) 36 | helper(i, j - 1) 37 | 38 | # Check if current substring is a palindrome 39 | if s[i] == s[j] and (j == i + 1 or helper(i + 1, j - 1)): 40 | dp[i][j] = True 41 | else: 42 | dp[i][j] = False 43 | 44 | return dp[i][j] 45 | 46 | # Start by processing the entire string 47 | helper(0, n - 1) 48 | 49 | # Count how many substrings are palindromes 50 | result = 0 51 | for length in range(1, n + 1): 52 | for i in range(n - length + 1): 53 | j = i + length - 1 54 | if dp[i][j] is True: 55 | result += 1 56 | return result 57 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 16/Palindrome_Partitioning_2_Memorization_Approach.py: -------------------------------------------------------------------------------- 1 | def min_cut(s): 2 | """ 3 | Find the minimum cuts needed to partition a string into palindromic substrings 4 | using memoization (Top-Down Dynamic Programming). 5 | 6 | Args: 7 | s (str): The input string. 8 | 9 | Returns: 10 | int: The minimum number of cuts required. 11 | """ 12 | n = len(s) 13 | is_palindrome = [[None] * n for _ in range(n)] 14 | min_cuts = [[None] * n for _ in range(n)] 15 | 16 | # Precompute palindrome information 17 | for length in range(1, n + 1): 18 | for i in range(n - length + 1): 19 | j = i + length - 1 20 | if i == j: 21 | is_palindrome[i][j] = True 22 | elif s[i] == s[j] and (j == i + 1 or is_palindrome[i + 1][j - 1]): 23 | is_palindrome[i][j] = True 24 | else: 25 | is_palindrome[i][j] = False 26 | 27 | def partitions(start, end): 28 | """ 29 | Recursively find the minimum cuts for palindromic partitioning with memoization. 30 | 31 | Args: 32 | start (int): The starting index. 33 | end (int): The ending index. 34 | 35 | Returns: 36 | int: The minimum cuts required for partitioning. 37 | """ 38 | if start == end or is_palindrome[start][end]: 39 | return 0 40 | 41 | if min_cuts[start][end] is not None: 42 | return min_cuts[start][end] 43 | 44 | min_cut_count = end - start 45 | for end_index in range(start, end): 46 | if is_palindrome[start][end_index]: 47 | min_cut_count = min(min_cut_count, 1 + partitions(end_index + 1, end)) 48 | 49 | min_cuts[start][end] = min_cut_count 50 | return min_cut_count 51 | 52 | return partitions(0, len(s) - 1) 53 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 15/Palindromic_Substring_Recursive_Approach.py: -------------------------------------------------------------------------------- 1 | def count_substrings(s): 2 | """ 3 | This function counts the number of palindromic substrings in the given string using recursion. 4 | 5 | Args: 6 | s (str): The input string to check for palindromic substrings. 7 | 8 | Returns: 9 | int: The number of palindromic substrings in the input string. 10 | """ 11 | n = len(s) 12 | dp = [[-1] * n for _ in range(n)] 13 | 14 | def helper(i, j): 15 | """ 16 | A helper function that recursively checks whether the substring s[i:j+1] is a palindrome. 17 | 18 | Args: 19 | i (int): The starting index of the substring. 20 | j (int): The ending index of the substring. 21 | 22 | Returns: 23 | bool: True if s[i:j+1] is a palindrome, False otherwise. 24 | """ 25 | # Base case: single character is always a palindrome 26 | if i == j: 27 | dp[i][j] = True 28 | return dp[i][j] 29 | 30 | # If we've already computed the value for this substring, return it 31 | if dp[i][j] != -1: 32 | return dp[i][j] 33 | 34 | # Recursively check for smaller substrings 35 | helper(i + 1, j) 36 | helper(i, j - 1) 37 | 38 | # Check if current substring is a palindrome 39 | if s[i] == s[j] and (j == i + 1 or helper(i + 1, j - 1)): 40 | dp[i][j] = True 41 | else: 42 | dp[i][j] = False 43 | 44 | return dp[i][j] 45 | 46 | # Start by processing the entire string 47 | helper(0, n - 1) 48 | 49 | # Count how many substrings are palindromes 50 | result = 0 51 | for length in range(1, n + 1): 52 | for i in range(n - length + 1): 53 | j = i + length - 1 54 | if dp[i][j] is True: 55 | result += 1 56 | return result 57 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 14/LIS_Tabulation_1D_Array_Approach.py: -------------------------------------------------------------------------------- 1 | def lengthOfLIS(nums): 2 | """ 3 | Calculate the length of the longest increasing subsequence using a dynamic programming approach with a 1D array. 4 | 5 | Args: 6 | nums (list[int]): A list of integers representing the sequence. 7 | 8 | Returns: 9 | int: The length of the longest increasing subsequence. 10 | 11 | The function uses a 1D dynamic programming array `dp` where: 12 | - `dp[i]` represents the length of the longest increasing subsequence that ends at index `i`. 13 | - The array is initialized to `1` for each element, since the smallest subsequence ending at any index is the element itself. 14 | - The function iterates over all pairs of elements to check if the current element can form an increasing subsequence with any previous elements. 15 | - If `nums[i] > nums[j]` (where `i > j`), then we update `dp[i]` to be the maximum of its current value and `dp[j] + 1` (i.e., extending the subsequence ending at index `j`). 16 | 17 | Example: 18 | For `nums = [10, 9, 2, 5, 3, 7, 101, 18]`, the function calculates the longest increasing subsequence length using a 1D DP array. 19 | """ 20 | n = len(nums) 21 | dp = [1] * n # Initialize DP array where each element starts with a subsequence length of 1 22 | max_len = 1 # To keep track of the longest subsequence found so far 23 | 24 | # Iterate through each element and calculate the longest subsequence ending at that element 25 | for i in range(1, n): 26 | for j in range(i): 27 | if nums[i] > nums[j] and dp[j] + 1 > dp[i]: 28 | dp[i] = dp[j] + 1 29 | # Update the maximum length found so far 30 | if dp[i] > max_len: 31 | max_len = dp[i] 32 | 33 | # Return the maximum length of the increasing subsequence 34 | return max_len 35 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 11/1_Unbounded_Knapsack.py: -------------------------------------------------------------------------------- 1 | def knap_sack(N, max_weight, values, weights): 2 | """ 3 | Solve the Unbounded Knapsack problem using dynamic programming. 4 | 5 | In the Unbounded Knapsack problem, each item can be chosen multiple times 6 | to maximize the total value of items that can be placed in the knapsack, 7 | such that the total weight does not exceed the given maximum weight. 8 | 9 | Args: 10 | N (int): The number of items. 11 | max_weight (int): The maximum weight capacity of the knapsack. 12 | values (list[int]): A list of integers representing the value of each item. 13 | weights (list[int]): A list of integers representing the weight of each item. 14 | 15 | Returns: 16 | int: The maximum value that can be obtained within the weight limit. 17 | """ 18 | 19 | # Create a DP table where dp[i][j] represents the maximum value that can be obtained 20 | # with the first i items and a knapsack capacity of j 21 | dp = [[-1] * (max_weight + 1) for _ in range(N + 1)] 22 | 23 | # Initialize base cases: If there is no weight capacity or no items, max value is 0 24 | for j in range(max_weight + 1): 25 | dp[0][j] = 0 # No items, so no value can be obtained 26 | 27 | for i in range(N + 1): 28 | dp[i][0] = 0 # No capacity, so no value can be obtained 29 | 30 | # Fill the DP table 31 | for i in range(1, N + 1): 32 | for j in range(1, max_weight + 1): 33 | # Exclude the current item 34 | exclude = dp[i - 1][j] 35 | include = 0 36 | # Include the current item if it fits within the remaining weight capacity 37 | if weights[i - 1] <= j: 38 | include = values[i - 1] + dp[i][j - weights[i - 1]] 39 | 40 | dp[i][j] = max(exclude, include) 41 | 42 | return dp[N][max_weight] 43 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 10/Minimun_Cost_Climbing_Stairs_Memorization_Approach.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def minCostClimbingStairs(cost: List[int]) -> int: 4 | """ 5 | Calculate the minimum cost to climb to the top of the stairs using a memorization approach. 6 | 7 | Args: 8 | cost (List[int]): List where each element represents the cost of stepping on the corresponding stair. 9 | 10 | Returns: 11 | int: Minimum cost to reach the top of the stairs. 12 | """ 13 | n = len(cost) 14 | 15 | # Memoization array to store the minimum cost for each stair. 16 | memo: List[int] = [-1] * n 17 | 18 | def helper(index: int) -> int: 19 | """ 20 | Recursive helper function to compute the minimum cost starting at a given stair index. 21 | 22 | Args: 23 | index (int): Current stair index. 24 | 25 | Returns: 26 | int: Minimum cost from the current stair to the top. 27 | """ 28 | # Base case: If the index is beyond the last stair, the cost is 0. 29 | if index >= n: 30 | return 0 31 | 32 | # If the result for the current index is already computed, return it. 33 | if memo[index] != -1: 34 | return memo[index] 35 | 36 | # Option 1: Climb one step to the next stair. 37 | one_step_cost = cost[index] + helper(index + 1) 38 | 39 | # Option 2: Climb two steps to the stair after the next. 40 | two_step_cost = cost[index] + helper(index + 2) 41 | 42 | # Store the minimum cost in the memo array and return it. 43 | memo[index] = min(one_step_cost, two_step_cost) 44 | return memo[index] 45 | 46 | # Start from either the first or the second stair and take the minimum cost. 47 | return min(helper(0), helper(1)) 48 | 49 | 50 | # Space Complexity: O(n) - For the memoization array and recursion stack. 51 | # Time Complexity: O(n) - Each stair index is computed only once. 52 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 03/Tower_Of_Hanoi.py: -------------------------------------------------------------------------------- 1 | def tower_of_hanoi(n_disks, source, target, auxiliary): 2 | """ 3 | Solves the Tower of Hanoi problem and prints the moves. 4 | 5 | Args: 6 | n_disks (int): The number of disks to move. 7 | source (int): The source rod. 8 | target (int): The target rod. 9 | auxiliary (int): The auxiliary rod. 10 | 11 | Returns: 12 | int: The total number of moves performed. 13 | """ 14 | move_count = 0 15 | 16 | def solve_hanoi(n, source_rod, target_rod, aux_rod): 17 | """ 18 | Recursive helper function to solve the Tower of Hanoi problem. 19 | 20 | Args: 21 | n (int): Number of disks to move. 22 | source_rod (int): The source rod. 23 | target_rod (int): The target rod. 24 | aux_rod (int): The auxiliary rod. 25 | """ 26 | nonlocal move_count 27 | if n == 1: 28 | move_count += 1 29 | print(f"Move disk {n} from rod {source_rod} to rod {target_rod}") 30 | return 31 | 32 | # Move n-1 disks from source to auxiliary using target as auxiliary. 33 | solve_hanoi(n - 1, source_rod, aux_rod, target_rod) 34 | 35 | # Move the nth disk from source to target. 36 | move_count += 1 37 | print(f"Move disk {n} from rod {source_rod} to rod {target_rod}") 38 | 39 | # Move n-1 disks from auxiliary to target using source as auxiliary. 40 | solve_hanoi(n - 1, aux_rod, target_rod, source_rod) 41 | 42 | solve_hanoi(n_disks, source, target, auxiliary) 43 | return move_count 44 | 45 | 46 | if __name__ == "__main__": 47 | # Example usage 48 | number_of_disks = 3 49 | source_rod = 1 50 | target_rod = 3 51 | auxiliary_rod = 2 52 | 53 | print(f"Solving Tower of Hanoi with {number_of_disks} disks:") 54 | total_moves = tower_of_hanoi(number_of_disks, source_rod, target_rod, auxiliary_rod) 55 | print(f"\nTotal moves required: {total_moves}") 56 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 01/Array_Question_2.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides a solution to check whether a given array is monotonic. 3 | A monotonic array is one that is either entirely non-increasing or non-decreasing. 4 | """ 5 | 6 | from typing import List 7 | 8 | 9 | def is_monotonic(array: List[int]) -> bool: 10 | """ 11 | Determines if the given array is monotonic. 12 | 13 | An array is monotonic if: 14 | - It is monotone increasing (every element is greater than or equal to the previous one). 15 | - It is monotone decreasing (every element is less than or equal to the previous one). 16 | 17 | Steps: 18 | 1. Check if the array is non-decreasing. 19 | 2. Check if the array is non-increasing. 20 | 3. If either condition is true, return True; otherwise, return False. 21 | 22 | Args: 23 | array (List[int]): The input array to check. 24 | 25 | Returns: 26 | bool: True if the array is monotonic, False otherwise. 27 | """ 28 | n = len(array) 29 | 30 | if n == 0: 31 | return True # An empty array is monotonic by definition 32 | 33 | # Flags for monotonic properties 34 | is_increasing = True 35 | is_decreasing = True 36 | 37 | for i in range(1, n): 38 | if array[i] > array[i - 1]: 39 | is_decreasing = False 40 | if array[i] < array[i - 1]: 41 | is_increasing = False 42 | 43 | return is_increasing or is_decreasing 44 | 45 | 46 | if __name__ == "__main__": 47 | # Example arrays 48 | test_array_1 = [1, 2, 2, 3] # Monotone increasing 49 | test_array_2 = [6, 5, 4, 4] # Monotone decreasing 50 | test_array_3 = [1, 3, 2] # Not monotonic 51 | test_array_4 = [] # Edge case: Empty array 52 | 53 | # Check monotonicity 54 | print(f"Array: {test_array_1} -> Monotonic: {is_monotonic(test_array_1)}") 55 | print(f"Array: {test_array_2} -> Monotonic: {is_monotonic(test_array_2)}") 56 | print(f"Array: {test_array_3} -> Monotonic: {is_monotonic(test_array_3)}") 57 | print(f"Array: {test_array_4} -> Monotonic: {is_monotonic(test_array_4)}") 58 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 14/LIS_Tabulation_2D_Array_Approach.py: -------------------------------------------------------------------------------- 1 | def lengthOfLIS(nums): 2 | """ 3 | Calculate the length of the longest increasing subsequence using tabulation (bottom-up dynamic programming). 4 | 5 | Args: 6 | nums (list[int]): A list of integers representing the sequence. 7 | 8 | Returns: 9 | int: The length of the longest increasing subsequence. 10 | 11 | The function uses a 2D dynamic programming table `dp` where: 12 | - `dp[i][j]` represents the length of the longest increasing subsequence in the subarray from index `i` to `j`. 13 | - The table is initialized as a 2D array with dimensions `(n+1) x (n+1)` where `n` is the length of the input list `nums`. 14 | - The table is filled by iterating backward over the array: 15 | - `exclude` refers to not including the current element (`nums[i]`), i.e., considering the subarray starting from the next index (`i + 1`). 16 | - `include` refers to including the current element, which is valid if `nums[i] > nums[j-1]`. 17 | - The final value at `dp[0][0]` gives the length of the longest increasing subsequence in the entire list `nums`. 18 | 19 | Example: 20 | For `nums = [10, 9, 2, 5, 3, 7, 101, 18]`, the function calculates the longest increasing subsequence length using a 2D DP table. 21 | """ 22 | n = len(nums) 23 | dp = [[0] * (n + 1) for _ in range(n + 1)] # Initialize 2D dp table 24 | 25 | # Fill the dp table by iterating from the end of the array to the beginning 26 | for i in range(n - 1, -1, -1): 27 | for j in range(i, -1, -1): 28 | # Exclude the current element 29 | exclude = dp[i + 1][j] 30 | 31 | # Include the current element if it forms an increasing subsequence 32 | include = 0 33 | if j - 1 == -1 or nums[i] > nums[j - 1]: 34 | include = 1 + dp[i + 1][i + 1] 35 | 36 | # Store the maximum of including or excluding the current element 37 | dp[i][j] = max(exclude, include) 38 | 39 | # Return the length of the longest increasing subsequence 40 | return dp[0][0] 41 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 11/1_Knapsack_Memorization_Approach .py: -------------------------------------------------------------------------------- 1 | def knap_sack(max_weight, weights, values, n): 2 | """ 3 | Solve the 0/1 Knapsack problem using memoization. 4 | 5 | This function calculates the maximum value that can be obtained by including 6 | or excluding items in the knapsack, such that the total weight does not exceed 7 | the given maximum weight. Memoization is used to store the results of subproblems 8 | to avoid redundant calculations. 9 | 10 | Args: 11 | max_weight (int): The maximum weight capacity of the knapsack. 12 | weights (list[int]): A list of integers representing the weight of each item. 13 | values (list[int]): A list of integers representing the value of each item. 14 | n (int): The total number of items. 15 | 16 | Returns: 17 | int: The maximum value that can be obtained within the weight limit. 18 | """ 19 | 20 | dp = [[-1] * (max_weight + 1) for _ in range(n)] 21 | 22 | def helper(index, remaining_weight): 23 | """ 24 | Helper function to solve the problem using memoization. 25 | 26 | Args: 27 | index (int): The current item being considered. 28 | remaining_weight (int): The remaining weight capacity of the knapsack. 29 | 30 | Returns: 31 | int: The maximum value that can be obtained from the current item onward. 32 | """ 33 | # Base case: if all items have been considered or remaining weight is 0 34 | if index >= n or remaining_weight == 0: 35 | return 0 36 | 37 | # Check if the result has already been computed 38 | if dp[index][remaining_weight] != -1: 39 | return dp[index][remaining_weight] 40 | 41 | # Recursive case: either exclude the current item or include it 42 | exclude = helper(index + 1, remaining_weight) 43 | include = 0 44 | if weights[index] <= remaining_weight: 45 | include = values[index] + helper(index + 1, remaining_weight - weights[index]) 46 | 47 | dp[index][remaining_weight] = max(exclude, include) 48 | return dp[index][remaining_weight] 49 | 50 | return helper(0, max_weight) 51 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 06/Combination_Question_1.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def generate_combinations(n: int, k: int) -> List[List[int]]: 4 | """ 5 | Generate all possible combinations of k numbers chosen from the range [1, n]. 6 | 7 | Args: 8 | n (int): The upper limit of the range (inclusive). 9 | k (int): The size of each combination. 10 | 11 | Returns: 12 | List[List[int]]: A list containing all possible combinations. 13 | 14 | Time Complexity: 15 | O(C(n, k)): The number of combinations is n! / (k! * (n - k)!). 16 | Each combination requires O(k) time to copy to the result. 17 | 18 | Space Complexity: 19 | O(k): The recursion stack can go as deep as the size of each combination. 20 | 21 | Example: 22 | Input: n = 4, k = 2 23 | Output: [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] 24 | """ 25 | combinations = [] 26 | 27 | def backtrack(start: int, current_combination: List[int]) -> None: 28 | """ 29 | Helper function to generate combinations using backtracking. 30 | 31 | Args: 32 | start (int): The starting number for the current recursion. 33 | current_combination (List[int]): The combination being built. 34 | """ 35 | # Base Case: If the current combination has k numbers, add it to the result 36 | if len(current_combination) == k: 37 | combinations.append(current_combination[:]) # Add a copy of the current combination 38 | return 39 | 40 | # Explore further by adding numbers to the combination 41 | for number in range(start, n + 1): 42 | # Include the current number 43 | current_combination.append(number) 44 | backtrack(number + 1, current_combination) 45 | 46 | # Exclude the current number (backtrack) 47 | current_combination.pop() 48 | 49 | # Start backtracking with an empty combination 50 | backtrack(1, []) 51 | return combinations 52 | 53 | if __name__ == "__main__": 54 | n, k = 4, 2 55 | result = generate_combinations(n, k) 56 | print(f"Combinations of {k} numbers from 1 to {n} are:") 57 | for combination in result: 58 | print(combination) 59 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 04/Permutations_Question_2.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def permute_unique(nums: List[int]) -> List[List[int]]: 4 | """ 5 | Generate all unique permutations of a list of numbers, including duplicates. 6 | 7 | Args: 8 | nums (List[int]): A list of integers that might contain duplicates. 9 | 10 | Returns: 11 | List[List[int]]: A list containing all unique permutations of the input list. 12 | 13 | Time Complexity: 14 | The time complexity is O(n * n!), where n is the length of the input list. 15 | - There are at most n! permutations. 16 | - However, duplicate elements reduce the number of unique permutations. 17 | - The function also uses a hash table for deduplication, which adds negligible overhead. 18 | """ 19 | res = [] # List to store all unique permutations 20 | 21 | def permutations(index: int): 22 | """ 23 | Recursive helper function to generate unique permutations. 24 | 25 | Args: 26 | index (int): Current index for permutation generation. 27 | """ 28 | # Base case: if the current index is the last one, add the current permutation 29 | if index == len(nums) - 1: 30 | res.append(nums[:]) # Append a copy of nums to results 31 | return 32 | 33 | hash_set = {} # Hash table to track duplicates in the current recursion 34 | for j in range(index, len(nums)): 35 | # Skip processing duplicate elements 36 | if nums[j] not in hash_set: 37 | hash_set[nums[j]] = True # Mark the element as processed 38 | nums[index], nums[j] = nums[j], nums[index] # Swap elements 39 | permutations(index + 1) # Recurse for the next index 40 | nums[index], nums[j] = nums[j], nums[index] # Backtrack (undo the swap) 41 | 42 | nums.sort() # Sort the input to ensure duplicates are grouped 43 | permutations(0) # Start the recursion from the first index 44 | return res 45 | 46 | if __name__ == "__main__": 47 | # Example usage of the permute_unique function 48 | numbers = [1, 1, 2] 49 | unique_permutations = permute_unique(numbers) 50 | print("Unique permutations of", numbers, "are:") 51 | for perm in unique_permutations: 52 | print(perm) 53 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 05/Subsets_Question_1.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def power_set(nums: List[int]) -> List[List[int]]: 4 | """ 5 | Generate all subsets (the power set) of a given list using backtracking. 6 | 7 | Args: 8 | nums (List[int]): A list of integers for which to generate subsets. 9 | 10 | Returns: 11 | List[List[int]]: A list containing all subsets of the input list. 12 | 13 | Time Complexity: 14 | O(2^n): Each element in the list can either be included or excluded, 15 | resulting in 2^n subsets for a list of size n. 16 | 17 | Space Complexity: 18 | O(n): The recursion stack can go as deep as the number of elements in the list. 19 | """ 20 | # List to store all subsets 21 | output = [] 22 | 23 | def helper(nums: List[int], i: int, subset: List[int]): 24 | """ 25 | Helper function to recursively generate subsets. 26 | 27 | Args: 28 | nums (List[int]): The input list of integers. 29 | i (int): The current index in the list being processed. 30 | subset (List[int]): The current subset being constructed. 31 | 32 | Time Complexity: 33 | Each recursive call either includes or excludes an element, 34 | resulting in a total of O(2^n) calls. 35 | 36 | Space Complexity: 37 | O(n): Due to the recursion stack and the subset list. 38 | """ 39 | # Base Case: If we have processed all elements 40 | if i == len(nums): 41 | output.append(subset.copy()) # Append a copy of the current subset 42 | return 43 | 44 | # Recursive Case 1: Exclude the current element 45 | helper(nums, i + 1, subset) 46 | 47 | # Recursive Case 2: Include the current element 48 | subset.append(nums[i]) 49 | helper(nums, i + 1, subset) 50 | 51 | # Backtrack to remove the element added in this recursion 52 | subset.pop() 53 | 54 | # Initialize the recursion with an empty subset 55 | helper(nums, 0, []) 56 | return output 57 | 58 | if __name__ == "__main__": 59 | # Example usage of the power_set function 60 | numbers = [1, 2, 3] 61 | subsets = power_set(numbers) 62 | print("Power set of", numbers, "is:") 63 | for subset in subsets: 64 | print(subset) 65 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 01/Array_Question_1.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides two methods to solve the problem of returning an array of squared values 3 | from a sorted array, with the squares sorted in ascending order. 4 | """ 5 | 6 | from typing import List 7 | 8 | 9 | def sorted_squares_brute_force(array: List[int]) -> List[int]: 10 | """ 11 | Computes the squares of each element in the input array and returns a sorted array 12 | using the brute force method. 13 | 14 | Steps: 15 | 1. Compute the square of each element. 16 | 2. Use Python's built-in sorting to sort the squared values. 17 | 18 | Args: 19 | array (List[int]): A sorted array of integers. 20 | 21 | Returns: 22 | List[int]: A sorted array of squared integers. 23 | """ 24 | n = len(array) 25 | res = [0] * n # Initialize result array 26 | for i in range(n): 27 | res[i] = array[i] ** 2 # Square each element 28 | res.sort() # Sort the squared elements 29 | return res 30 | 31 | 32 | def sorted_squares_two_pointer(array: List[int]) -> List[int]: 33 | """ 34 | Computes the squares of each element in the input array and returns a sorted array 35 | using the optimized two-pointer method. 36 | 37 | Steps: 38 | 1. Use two pointers to traverse the array from both ends. 39 | 2. Compare absolute values, assign the larger square to the output array from the end. 40 | 41 | Args: 42 | array (List[int]): A sorted array of integers. 43 | 44 | Returns: 45 | List[int]: A sorted array of squared integers. 46 | """ 47 | n = len(array) 48 | res = [0] * n # Initialize result array 49 | left, right = 0, n - 1 # Set two pointers 50 | for k in range(n - 1, -1, -1): 51 | if abs(array[left]) > abs(array[right]): 52 | res[k] = array[left] ** 2 53 | left += 1 54 | else: 55 | res[k] = array[right] ** 2 56 | right -= 1 57 | return res 58 | 59 | 60 | if __name__ == "__main__": 61 | # Example array 62 | input_array = [-7, -3, 2, 3, 11] 63 | 64 | # Brute force solution 65 | brute_force_result = sorted_squares_brute_force(input_array) 66 | print("Brute Force Result:", brute_force_result) 67 | 68 | # Optimized two-pointer solution 69 | two_pointer_result = sorted_squares_two_pointer(input_array) 70 | print("Two Pointer Result:", two_pointer_result) 71 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 06/Combination_Question_1_with_Optimization.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def find_combinations_optimized(n: int, k: int) -> List[List[int]]: 4 | """ 5 | Find all unique combinations of k numbers from the range [1, n]. 6 | 7 | Optimized by reducing the range of iteration based on the remaining elements needed. 8 | 9 | Args: 10 | n (int): The upper limit of the range (inclusive). 11 | k (int): The size of each combination. 12 | 13 | Returns: 14 | List[List[int]]: A list of all unique combinations. 15 | 16 | Time Complexity: 17 | O(C(n, k)): The time complexity corresponds to the number of combinations, which is n! / (k! * (n-k)!) 18 | 19 | Space Complexity: 20 | O(k): The maximum depth of the recursion stack. 21 | 22 | Example: 23 | Input: n = 4, k = 2 24 | Output: [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] 25 | """ 26 | combinations = [] 27 | 28 | def backtrack(start: int, current_combination: List[int]) -> None: 29 | """ 30 | Helper function to generate combinations using backtracking. 31 | 32 | Args: 33 | start (int): The starting index for the current recursion. 34 | current_combination (List[int]): The current combination being built. 35 | """ 36 | # If the current combination has the required size, save it 37 | if len(current_combination) == k: 38 | combinations.append(current_combination[:]) # Add a copy of the current combination 39 | return 40 | 41 | # Calculate the number of elements still needed 42 | need = k - len(current_combination) 43 | 44 | # Iterate over the range with an optimized end to reduce unnecessary recursion 45 | for i in range(start, n - (need - 1) + 1): 46 | # Include the current number 47 | current_combination.append(i) 48 | backtrack(i + 1, current_combination) 49 | 50 | # Exclude the current number (backtrack) 51 | current_combination.pop() 52 | 53 | # Start backtracking with an empty combination 54 | backtrack(1, []) 55 | return combinations 56 | 57 | if __name__ == "__main__": 58 | n_value = 4 59 | k_value = 2 60 | result = find_combinations_optimized(n_value, k_value) 61 | print(f"All {k_value}-combinations of numbers from 1 to {n_value} are:") 62 | for combination in result: 63 | print(combination) 64 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 02/Josephus_Problem_Method_1.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to solve the Josephus problem. 3 | The problem is to find the winner in a circular game where every k-th person is eliminated. 4 | """ 5 | 6 | def find_the_winner(n, k): 7 | """ 8 | Determines the winner of the Josephus problem. 9 | 10 | Args: 11 | n (int): Total number of participants. 12 | k (int): Step count for elimination. 13 | 14 | Returns: 15 | int: The position of the winner. 16 | 17 | Time Complexity: 18 | - O(n^2) in the worst case, due to repeated list modifications during recursion. 19 | Each `del` operation in a list takes O(n) time, and it is called recursively n times. 20 | 21 | Space Complexity: 22 | - O(n) for storing the participants list. 23 | - O(n) for the recursion stack in the worst case. 24 | 25 | Overall space complexity: O(n). 26 | """ 27 | # Create a list of participants numbered from 1 to n 28 | participants = [i + 1 for i in range(n)] 29 | 30 | def helper(participants_list, start_index): 31 | """ 32 | Recursive helper function to eliminate participants. 33 | 34 | Args: 35 | participants_list (list): Current list of participants. 36 | start_index (int): Index to start the elimination. 37 | 38 | Returns: 39 | int: The position of the last remaining participant. 40 | 41 | Time Complexity: 42 | - Each call removes one participant, taking O(n) for deletion. 43 | Recursion occurs n times, making the total O(n^2). 44 | 45 | Space Complexity: 46 | - O(n) for the recursion stack and participant list in memory. 47 | """ 48 | # Base case: When only one participant remains 49 | if len(participants_list) == 1: 50 | return participants_list[0] 51 | 52 | # Calculate the index of the participant to remove 53 | index_to_remove = (start_index + k - 1) % len(participants_list) 54 | 55 | # Remove the participant from the list 56 | del participants_list[index_to_remove] 57 | 58 | # Recursively call the helper with the updated list and start index 59 | return helper(participants_list, index_to_remove) 60 | 61 | # Start the recursive elimination process 62 | return helper(participants, 0) 63 | 64 | 65 | if __name__ == "__main__": 66 | # Test the function with an example 67 | print(find_the_winner(4, 2)) # Output: 1 68 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 14/LIS_Binary_Search_Approach.py: -------------------------------------------------------------------------------- 1 | def lengthOfLIS(nums): 2 | """ 3 | Calculate the length of the longest increasing subsequence using a binary search approach. 4 | 5 | Args: 6 | nums (list[int]): A list of integers representing the sequence. 7 | 8 | Returns: 9 | int: The length of the longest increasing subsequence. 10 | 11 | The function uses a binary search to find the position of the current number in the `sub` array, which keeps track 12 | of the smallest possible tail element for all increasing subsequences of different lengths. 13 | 14 | Steps: 15 | 1. Start with the first element of the list in `sub`, which represents the first subsequence. 16 | 2. For each subsequent element in `nums`: 17 | - If the element is greater than the last element of `sub`, append it to `sub`. 18 | - If the element is smaller or equal to the last element of `sub`, find the position in `sub` where this 19 | element should be placed using binary search. Replace the element at that position. 20 | 3. The length of the `sub` array at the end of the process will represent the length of the longest increasing subsequence. 21 | 22 | Example: 23 | For `nums = [10, 9, 2, 5, 3, 7, 101, 18]`, the binary search approach efficiently calculates the longest increasing subsequence length. 24 | """ 25 | def binarySearch(sub, num): 26 | """ 27 | Perform binary search to find the correct position of `num` in `sub` array. 28 | 29 | Args: 30 | sub (list[int]): A list representing the subsequence so far. 31 | num (int): The number to be inserted or replaced in `sub`. 32 | 33 | Returns: 34 | int: The index at which `num` should be placed in `sub`. 35 | """ 36 | left = 0 37 | right = len(sub) - 1 38 | while left < right: 39 | mid = (left + right) // 2 40 | if sub[mid] < num: 41 | left = mid + 1 42 | else: 43 | right = mid 44 | return left 45 | 46 | if not nums: 47 | return 0 48 | 49 | # `sub` will store the smallest possible tail element for increasing subsequences 50 | sub = [nums[0]] 51 | 52 | for num in nums[1:]: 53 | if num > sub[-1]: 54 | sub.append(num) 55 | else: 56 | index = binarySearch(sub, num) 57 | sub[index] = num 58 | 59 | return len(sub) 60 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 05/Subsets_Question_2.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def subsets_with_duplicates(nums: List[int]) -> List[List[int]]: 4 | """ 5 | Generate all subsets (the power set) of a given list with handling for duplicates using backtracking. 6 | 7 | Args: 8 | nums (List[int]): A list of integers, which may include duplicates. 9 | 10 | Returns: 11 | List[List[int]]: A list containing all unique subsets of the input list. 12 | 13 | Time Complexity: 14 | O(2^n): Each element can either be included or excluded, resulting in 2^n subsets. 15 | Sorting the input adds an additional O(n log n) complexity. 16 | 17 | Space Complexity: 18 | O(n): The recursion stack can go as deep as the number of elements in the list. 19 | """ 20 | # List to store all unique subsets 21 | result = [] 22 | 23 | # Sort the input list to handle duplicates 24 | nums.sort() 25 | 26 | def backtrack(index: int, current_subset: List[int]): 27 | """ 28 | Backtracking helper function to generate subsets. 29 | 30 | Args: 31 | index (int): The current index in the list being processed. 32 | current_subset (List[int]): The current subset being constructed. 33 | 34 | Time Complexity: 35 | O(2^n): Each recursive call represents either including or excluding an element. 36 | 37 | Space Complexity: 38 | O(n): Due to the recursion stack and the subset list. 39 | """ 40 | # Base Case: If all elements have been considered 41 | if index == len(nums): 42 | result.append(current_subset[:]) # Append a copy of the current subset 43 | return 44 | 45 | # Include the current element 46 | current_subset.append(nums[index]) 47 | backtrack(index + 1, current_subset) 48 | current_subset.pop() # Backtrack to exclude the last element 49 | 50 | # Exclude the current element and skip duplicates 51 | while index < len(nums) - 1 and nums[index] == nums[index + 1]: 52 | index += 1 53 | backtrack(index + 1, current_subset) 54 | 55 | # Initialize the recursion with an empty subset 56 | backtrack(0, []) 57 | return result 58 | 59 | if __name__ == "__main__": 60 | # Example usage of the subsets_with_duplicates function 61 | numbers = [1, 2, 2] 62 | unique_subsets = subsets_with_duplicates(numbers) 63 | print("Unique subsets of", numbers, "are:") 64 | for subset in unique_subsets: 65 | print(subset) 66 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 14/Max_Length_of_Pair_Chain.py: -------------------------------------------------------------------------------- 1 | def findLongestChain(pairs): 2 | """ 3 | Find the maximum length of a pair chain. 4 | 5 | Args: 6 | pairs (list of tuple): A list of pairs where each pair is represented as a tuple `(a, b)`. 7 | The goal is to find the maximum number of pairs (a, b) such that for any two pairs 8 | `(a1, b1)` and `(a2, b2)` in the chain, the condition `b1 < a2` holds true. 9 | 10 | Returns: 11 | int: The maximum length of the pair chain that can be formed. 12 | 13 | The approach is similar to the Longest Increasing Subsequence (LIS) problem, but instead of comparing individual 14 | elements, we compare pairs. The steps are as follows: 15 | 1. Sort the pairs by their first element. This allows us to try and form chains by looking at the 16 | second element of the pairs. 17 | 2. Use dynamic programming to store the length of the longest chain ending with each pair. 18 | 3. For each pair `i`, check all previous pairs `j` (where `j < i`) and see if pair `i` can follow pair `j` 19 | (i.e., if `pairs[j][1] < pairs[i][0]`), and update the maximum chain length accordingly. 20 | 4. The final result will be the maximum value found in the dynamic programming array. 21 | 22 | Example: 23 | For `pairs = [[1, 2], [2, 3], [3, 4]]`, the function returns `2` as the longest chain is `[1, 2] -> [3, 4]`. 24 | 25 | Time Complexity: 26 | The time complexity is `O(n^2)`, where `n` is the number of pairs. The pairs are sorted in `O(n log n)`, 27 | and then we check each pair against all previous pairs in the list in the nested loop. 28 | """ 29 | n = len(pairs) 30 | 31 | # Sort the pairs based on their first element 32 | pairs.sort() 33 | 34 | # dp[i] will hold the length of the longest chain ending with the i-th pair 35 | dp = [1] * n 36 | res = 1 # Initialize the result as 1 (at least one pair can be selected) 37 | 38 | # Iterate over the pairs and compute the maximum chain length 39 | for i in range(1, n): 40 | for j in range(i): 41 | # If the second element of pair j is less than the first element of pair i, 42 | # they can form a valid chain 43 | if pairs[j][1] < pairs[i][0] and dp[j] + 1 > dp[i]: 44 | dp[i] = dp[j] + 1 45 | 46 | # Update the result with the maximum chain length so far 47 | if dp[i] > res: 48 | res = dp[i] 49 | 50 | return res 51 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 06/Combination_Sum_Question_1.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def find_combination_sum(candidates: List[int], target: int) -> List[List[int]]: 4 | """ 5 | Find all unique combinations of candidates where the candidate numbers sum to the target. 6 | 7 | Each number in candidates may be used an unlimited number of times in a combination. 8 | 9 | Args: 10 | candidates (List[int]): The list of candidate numbers. 11 | target (int): The target sum. 12 | 13 | Returns: 14 | List[List[int]]: A list of all unique combinations where the sum equals the target. 15 | 16 | Time Complexity: 17 | O(2^n): Each candidate can be included or excluded, leading to exponential growth. 18 | 19 | Space Complexity: 20 | O(target / min(candidates)): The recursion stack depth depends on the smallest candidate. 21 | 22 | Example: 23 | Input: candidates = [2, 3, 6, 7], target = 7 24 | Output: [[2, 2, 3], [7]] 25 | """ 26 | combinations = [] 27 | 28 | def backtrack(start: int, current_combination: List[int], current_sum: int) -> None: 29 | """ 30 | Helper function to generate combinations using backtracking. 31 | 32 | Args: 33 | start (int): The starting index for the current recursion. 34 | current_combination (List[int]): The current combination being built. 35 | current_sum (int): The current sum of the numbers in the combination. 36 | """ 37 | # If the current sum exceeds the target, backtrack 38 | if current_sum > target: 39 | return 40 | 41 | # If the current sum equals the target, save the combination 42 | if current_sum == target: 43 | combinations.append(current_combination[:]) # Add a copy of the current combination 44 | return 45 | 46 | # Explore further by adding numbers to the combination 47 | for i in range(start, len(candidates)): 48 | # Include the current candidate 49 | current_combination.append(candidates[i]) 50 | backtrack(i, current_combination, current_sum + candidates[i]) 51 | 52 | # Exclude the current candidate (backtrack) 53 | current_combination.pop() 54 | 55 | # Start backtracking with an empty combination 56 | backtrack(0, [], 0) 57 | return combinations 58 | 59 | if __name__ == "__main__": 60 | candidates_list = [2, 3, 6, 7] 61 | target_value = 7 62 | result = find_combination_sum(candidates_list, target_value) 63 | print(f"Combinations that sum to {target_value} are:") 64 | for combination in result: 65 | print(combination) 66 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 07/Combination_Sum_Question_3.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def combination_sum_3(k: int, n: int) -> List[List[int]]: 4 | """ 5 | Find all valid combinations of `k` numbers that sum up to `n`. 6 | Only numbers from 1 to 9 can be used, and each number may only appear once in the combination. 7 | 8 | Args: 9 | k (int): The number of numbers in each combination. 10 | n (int): The target sum for the combinations. 11 | 12 | Returns: 13 | List[List[int]]: A list of all valid combinations. 14 | 15 | Time Complexity: 16 | O(2^9): At most, we generate all subsets of the numbers from 1 to 9. 17 | 18 | Space Complexity: 19 | O(k): Space used for recursion stack and current combination. 20 | 21 | Example: 22 | Input: k = 3, n = 7 23 | Output: [[1, 2, 4]] 24 | """ 25 | # To store the resulting combinations 26 | result = [] 27 | 28 | def backtrack(start: int, current_combination: List[int], current_sum: int) -> None: 29 | """ 30 | Helper function to explore combinations using backtracking. 31 | 32 | Args: 33 | start (int): The starting number for the current recursion. 34 | current_combination (List[int]): The current combination being built. 35 | current_sum (int): The sum of the current combination. 36 | """ 37 | # If the current combination meets the conditions, add it to the result 38 | if current_sum == n and len(current_combination) == k: 39 | result.append(current_combination[:]) # Add a copy of the current combination 40 | return 41 | 42 | # If the sum exceeds n or the combination size exceeds k, terminate 43 | if current_sum > n or len(current_combination) == k: 44 | return 45 | 46 | # Iterate through the possible numbers, ensuring no repeats 47 | for number in range(start, 10): 48 | # Include the current number in the combination 49 | current_combination.append(number) 50 | # Recur with updated parameters 51 | backtrack(number + 1, current_combination, current_sum + number) 52 | # Backtrack: remove the number from the combination 53 | current_combination.pop() 54 | 55 | # Start the backtracking process with an empty combination 56 | backtrack(1, [], 0) 57 | return result 58 | 59 | if __name__ == "__main__": 60 | # Example usage 61 | k_value = 3 62 | target_sum = 7 63 | output = combination_sum_3(k_value, target_sum) 64 | print(f"Combinations of {k_value} numbers summing to {target_sum}:") 65 | for combination in output: 66 | print(combination) 67 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 14/Russian_Doll_Envelopes.py: -------------------------------------------------------------------------------- 1 | def maxEnvelopes(envelopes): 2 | """ 3 | Find the maximum number of envelopes that can be Russian-dolled (nested). 4 | 5 | Args: 6 | envelopes (list of tuple): A list of envelopes represented as pairs of integers (width, height). 7 | The goal is to find the maximum number of envelopes that can be nested such 8 | that each envelope is strictly larger than the previous one in both width and height. 9 | 10 | Returns: 11 | int: The maximum number of envelopes that can be Russian-dolled (nested). 12 | 13 | The idea is to first sort the envelopes by width and then, for envelopes with the same width, 14 | sort them by height in descending order. This way, we ensure that no two envelopes with the same 15 | width can be part of the same chain. 16 | 17 | Once sorted, the problem reduces to finding the longest increasing subsequence (LIS) based on the height 18 | of the envelopes. 19 | 20 | Example: 21 | For `envelopes = [[5,4], [6,4], [6,7], [2,3]]`, the function returns `3` because the envelopes 22 | that can be nested are `[2,3] -> [5,4] -> [6,7]`. 23 | 24 | Time Complexity: 25 | The time complexity is `O(n log n)`, where `n` is the number of envelopes. The sorting step takes `O(n log n)`, 26 | and finding the longest increasing subsequence using binary search takes `O(n log n)` as well. 27 | """ 28 | # Step 1: Sort envelopes by width in ascending order, 29 | # and by height in descending order if widths are the same. 30 | envelopes.sort(key=lambda x: (x[0], -x[1])) 31 | 32 | n = len(envelopes) 33 | 34 | # Step 2: Find the longest increasing subsequence of heights using binary search 35 | sub = [envelopes[0][1]] # Start with the first envelope's height 36 | 37 | def binary_search(sub, num): 38 | left, right = 0, len(sub) 39 | while left < right: 40 | mid = (left + right) // 2 41 | if num > sub[mid]: 42 | left = mid + 1 43 | else: 44 | right = mid 45 | return left 46 | 47 | # Iterate through the envelopes and update the subsequence 48 | for i in range(1, n): 49 | num = envelopes[i][1] # Get the height of the current envelope 50 | if num > sub[-1]: 51 | sub.append(num) # Add to subsequence if it's larger than the last element 52 | else: 53 | x = binary_search(sub, num) # Find the correct position to replace 54 | sub[x] = num # Update the subsequence with the smaller number 55 | 56 | return len(sub) # The length of the subsequence gives the number of envelopes that can be nested 57 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 07/Combination_Sum_Question_2.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def combination_sum_2(candidates: List[int], target: int) -> List[List[int]]: 4 | """ 5 | Find all unique combinations in `candidates` where the candidate numbers sum to `target`. 6 | Each number in `candidates` may only be used once in the combination. 7 | 8 | Args: 9 | candidates (List[int]): The list of candidate numbers. 10 | target (int): The target sum for the combinations. 11 | 12 | Returns: 13 | List[List[int]]: A list of unique combinations where the sum of numbers equals `target`. 14 | 15 | Time Complexity: 16 | O(2^n): In the worst case, we generate all subsets of the candidates list. 17 | 18 | Space Complexity: 19 | O(n): Space used for recursion stack and current combination. 20 | 21 | Example: 22 | Input: candidates = [10, 1, 2, 7, 6, 1, 5], target = 8 23 | Output: [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]] 24 | """ 25 | # To store the resulting combinations 26 | result = [] 27 | # Sort candidates to handle duplicates and enable early termination 28 | candidates.sort() 29 | 30 | def backtrack(index: int, current_sum: int, current_combination: List[int]) -> None: 31 | """ 32 | Helper function to explore combinations using backtracking. 33 | 34 | Args: 35 | index (int): Current index in the candidates list. 36 | current_sum (int): The sum of the current combination. 37 | current_combination (List[int]): The current combination being built. 38 | """ 39 | # If the current sum matches the target, add the combination to the result 40 | if current_sum == target: 41 | result.append(current_combination[:]) # Add a copy of the current combination 42 | return 43 | 44 | # If the current sum exceeds the target or we reach the end, terminate 45 | if current_sum > target or index >= len(candidates): 46 | return 47 | 48 | # Use a hash to track duplicates at the current level 49 | seen = {} 50 | for j in range(index, len(candidates)): 51 | # Skip duplicates at the same recursion level 52 | if candidates[j] in seen: 53 | continue 54 | 55 | # Mark the candidate as used in the current level 56 | seen[candidates[j]] = True 57 | 58 | # Include the candidate in the combination 59 | current_combination.append(candidates[j]) 60 | # Recur with updated parameters: move to the next index and update the sum 61 | backtrack(j + 1, current_sum + candidates[j], current_combination) 62 | # Backtrack: remove the candidate from the combination 63 | current_combination.pop() 64 | 65 | # Start the backtracking process with an empty combination 66 | backtrack(0, 0, []) 67 | return result 68 | 69 | if __name__ == "__main__": 70 | # Example usage 71 | input_candidates = [10, 1, 2, 7, 6, 1, 5] 72 | input_target = 8 73 | output = combination_sum_2(input_candidates, input_target) 74 | print(f"Unique combinations summing to {input_target}:") 75 | for combination in output: 76 | print(combination) 77 | -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 27/Readme.md: -------------------------------------------------------------------------------- 1 | # **Day 27: Strings** 2 | 3 | Welcome to Day 27 of **50 Days of DSA in Python**! Today, we dive deeper into **Strings**, focusing on two interesting problems: **Longest Unique Substring** and **Group Anagrams**. 4 | 5 | --- 6 | 7 | ### **Topics Covered:** 8 | - Strings 9 | - Longest Unique Substring 10 | - Group Anagrams 11 | 12 | --- 13 | 14 | ## **1. Longest Unique Substring** 15 | 16 | ### **Problem Statement** 17 | Given a string `s`, find the length of the longest substring that contains **no repeating characters**. 18 | 19 | ### **Approaches** 20 | 21 | #### **1. Using Sliding Window (Optimal Approach)** 22 | 23 | We can use the sliding window technique to maintain a window of characters that doesn't contain any duplicates. We expand the window by moving the right pointer, and if a duplicate character is found, we move the left pointer to remove the duplicate. 24 | 25 | #### **Code:** 26 | ```python 27 | def lengthOfLongestSubstring(s): 28 | """ 29 | Find the length of the longest substring without repeating characters using sliding window. 30 | 31 | Args: 32 | s (str): The input string. 33 | 34 | Returns: 35 | int: The length of the longest substring without repeating characters. 36 | """ 37 | char_set = set() 38 | left = 0 39 | max_length = 0 40 | 41 | for right in range(len(s)): 42 | while s[right] in char_set: 43 | char_set.remove(s[left]) 44 | left += 1 45 | char_set.add(s[right]) 46 | max_length = max(max_length, right - left + 1) 47 | 48 | return max_length 49 | ``` 50 | 51 | --- 52 | 53 | ## **2. Group Anagrams** 54 | 55 | ### **Problem Statement** 56 | Given a list of strings, group the strings that are anagrams of each other. An anagram is a word or phrase formed by rearranging the letters of a different word. 57 | 58 | ### **Approaches** 59 | 60 | #### **1. Using Hash Map (Optimal Approach)** 61 | 62 | We can use a hash map (dictionary) to group anagrams by sorting the characters in each string and using the sorted string as the key. Strings that are anagrams will have the same sorted key. 63 | 64 | #### **Code:** 65 | ```python 66 | def groupAnagrams(strs): 67 | """ 68 | Group the anagrams in the given list of strings. 69 | 70 | Args: 71 | strs (list): A list of strings. 72 | 73 | Returns: 74 | list: A list of lists containing grouped anagrams. 75 | """ 76 | anagrams = {} 77 | 78 | for s in strs: 79 | sorted_str = ''.join(sorted(s)) 80 | if sorted_str not in anagrams: 81 | anagrams[sorted_str] = [] 82 | anagrams[sorted_str].append(s) 83 | 84 | return list(anagrams.values()) 85 | ``` 86 | 87 | --- 88 | 89 | ### **Conclusion** 90 | 91 | Today, we worked on two important string manipulation problems: 92 | 93 | 1. **Longest Unique Substring**: We used the sliding window technique to find the longest substring without repeating characters. 94 | 2. **Group Anagrams**: We grouped strings that are anagrams using a hash map, with the sorted string as the key. 95 | 96 | Both problems are common in string processing and provide a solid foundation for handling various real-world applications like text processing and pattern matching. Keep practicing, and see you tomorrow! -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 26/Readme.md: -------------------------------------------------------------------------------- 1 | # **Day 26: Strings** 2 | 3 | Welcome to Day 26 of **50 Days of DSA in Python**! Today, we focus on **Strings**, one of the most fundamental data structures. We will cover two interesting problems: **Non-Repeating Character** and **Palindrome**. 4 | 5 | --- 6 | 7 | ### **Topics Covered:** 8 | - Strings 9 | - Non-Repeating Character 10 | - Palindrome 11 | 12 | --- 13 | 14 | ## **1. Non-Repeating Character** 15 | 16 | ### **Problem Statement** 17 | Given a string `s`, find the first non-repeating character in it. If there is no non-repeating character, return `None`. 18 | 19 | ### **Approaches** 20 | 21 | #### **1. Using Hash Table (Optimal Approach)** 22 | 23 | We can use a hash table (dictionary in Python) to store the frequency of each character in the string. Once we have the frequencies, we can iterate through the string again to find the first character with a frequency of 1. 24 | 25 | #### **Code:** 26 | ```python 27 | def firstUniqChar(s): 28 | """ 29 | Find the first non-repeating character in the string using a hash table. 30 | 31 | Args: 32 | s (str): The input string. 33 | 34 | Returns: 35 | int: The index of the first non-repeating character, or -1 if none exists. 36 | """ 37 | freq = {} 38 | 39 | # Count the frequency of each character 40 | for char in s: 41 | freq[char] = freq.get(char, 0) + 1 42 | 43 | # Find the first character with frequency 1 44 | for i, char in enumerate(s): 45 | if freq[char] == 1: 46 | return i 47 | 48 | return -1 49 | ``` 50 | 51 | --- 52 | 53 | ## **2. Palindrome** 54 | 55 | ### **Problem Statement** 56 | Given a string `s`, determine if it is a **palindrome**. A palindrome is a word, phrase, or sequence that reads the same backward as forward, ignoring spaces, punctuation, and capitalization. 57 | 58 | ### **Approaches** 59 | 60 | #### **1. Using Two Pointers (Optimal Approach)** 61 | 62 | We can use two pointers, one starting at the beginning of the string and the other at the end. We compare the characters at these two positions and move the pointers towards each other. If the characters don't match, we return `False`. If we reach the middle without finding any mismatches, we return `True`. 63 | 64 | #### **Code:** 65 | ```python 66 | def isPalindrome(s): 67 | """ 68 | Check if the string is a palindrome using two pointers. 69 | 70 | Args: 71 | s (str): The input string. 72 | 73 | Returns: 74 | bool: True if the string is a palindrome, otherwise False. 75 | """ 76 | s = ''.join(char.lower() for char in s if char.isalnum()) # Remove non-alphanumeric characters and lowercase 77 | left, right = 0, len(s) - 1 78 | 79 | while left < right: 80 | if s[left] != s[right]: 81 | return False 82 | left += 1 83 | right -= 1 84 | 85 | return True 86 | ``` 87 | 88 | --- 89 | 90 | ### **Conclusion** 91 | 92 | Today, we focused on **Strings** and covered two classic problems: 93 | 94 | 1. **Non-Repeating Character**: We used a hash table to efficiently find the first non-repeating character in the string. 95 | 2. **Palindrome**: We solved the palindrome check problem using two pointers, ignoring non-alphanumeric characters and case. 96 | 97 | Strings are integral to solving many algorithmic challenges, and these problems showcase how to handle common string manipulations. Keep practicing, and see you tomorrow! -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 24/Readme.md: -------------------------------------------------------------------------------- 1 | # **Day 24: Arrays** 2 | 3 | Welcome to Day 24 of **50 Days of DSA in Python**! Today, we will focus on two important problems involving **Arrays**: **Rotate Array** and **Container with Most Water**. Both problems are excellent exercises in array manipulation and optimization. 4 | 5 | --- 6 | 7 | ### **Topics Covered:** 8 | - Arrays 9 | - Rotate Array 10 | - Container with Most Water 11 | 12 | --- 13 | 14 | ## **1. Rotate Array** 15 | 16 | ### **Problem Statement** 17 | Given an array `nums`, rotate the array to the right by `k` steps, where `k` is a non-negative integer. 18 | 19 | ### **Approaches** 20 | 21 | #### **1. Brute Force Approach** 22 | 23 | The brute force approach involves shifting each element of the array `k` positions to the right. We can do this by using a temporary array or directly manipulating the array. 24 | 25 | #### **Code:** 26 | ```python 27 | def rotate(nums, k): 28 | """ 29 | Rotate the array to the right by k steps. 30 | 31 | Args: 32 | nums (list): The list of integers to rotate. 33 | k (int): The number of positions to rotate. 34 | 35 | Returns: 36 | None: The function modifies the list in-place. 37 | """ 38 | n = len(nums) 39 | k = k % n # Handle cases where k is greater than the length of the array 40 | nums[:] = nums[-k:] + nums[:-k] 41 | ``` 42 | 43 | --- 44 | 45 | ## **2. Container with Most Water** 46 | 47 | ### **Problem Statement** 48 | Given an array of integers `height` representing the height of walls, where each element represents the height of a vertical line, find two lines that together with the x-axis form a container that holds the most water. Return the maximum amount of water a container can store. 49 | 50 | ### **Approaches** 51 | 52 | #### **1. Two Pointer Approach** 53 | 54 | The optimal solution uses the two-pointer technique. We initialize two pointers, one at the start and one at the end of the array, and calculate the area between them. We then move the pointer that points to the shorter line towards the center, aiming to maximize the area at each step. 55 | 56 | #### **Code:** 57 | ```python 58 | def maxArea(height): 59 | """ 60 | Calculate the maximum area that can be formed between two lines. 61 | 62 | Args: 63 | height (list): A list of integers representing the heights of lines. 64 | 65 | Returns: 66 | int: The maximum area formed by any two lines. 67 | """ 68 | left = 0 69 | right = len(height) - 1 70 | max_area = 0 71 | 72 | while left < right: 73 | # Calculate the area between the current pair of lines 74 | width = right - left 75 | current_area = min(height[left], height[right]) * width 76 | max_area = max(max_area, current_area) 77 | 78 | # Move the pointer pointing to the shorter line 79 | if height[left] < height[right]: 80 | left += 1 81 | else: 82 | right -= 1 83 | 84 | return max_area 85 | ``` 86 | 87 | --- 88 | 89 | ### **Conclusion** 90 | 91 | Today, we tackled two interesting **Array** problems: 92 | 93 | 1. **Rotate Array**: Rotating an array by `k` steps using efficient methods. 94 | 2. **Container with Most Water**: Maximizing the area between two lines using the two-pointer technique. 95 | 96 | Arrays are a fundamental data structure, and these problems are great exercises to improve your understanding of array manipulation and optimization. Keep practicing, and see you tomorrow! -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 08/N_Queen.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | def solve_n_queens(n: int) -> List[List[str]]: 4 | """ 5 | Solve the N-Queens problem and return all distinct solutions. 6 | 7 | Args: 8 | n (int): The size of the chessboard and the number of queens. 9 | 10 | Returns: 11 | List[List[str]]: A list of solutions, where each solution is represented as a list of strings. 12 | 13 | Example: 14 | Input: n = 4 15 | Output: [ 16 | [".Q..", "...Q", "Q...", "..Q."], 17 | ["..Q.", "Q...", "...Q", ".Q.."] 18 | ] 19 | 20 | Time Complexity: 21 | O(n!): The number of permutations of placing queens on the board decreases with pruning. 22 | 23 | Space Complexity: 24 | O(n^2): The space required to store the board and recursion stack. 25 | """ 26 | solutions = [] 27 | 28 | def create_board(board: List[List[str]]) -> List[str]: 29 | """ 30 | Convert the board from a list of lists to a list of strings. 31 | 32 | Args: 33 | board (List[List[str]]): The board as a 2D list. 34 | 35 | Returns: 36 | List[str]: The board as a list of strings. 37 | """ 38 | return ["".join(row) for row in board] 39 | 40 | def is_valid(row: int, col: int, board: List[List[str]]) -> bool: 41 | """ 42 | Check if placing a queen at board[row][col] is valid. 43 | 44 | Args: 45 | row (int): The row index. 46 | col (int): The column index. 47 | board (List[List[str]]): The current state of the board. 48 | 49 | Returns: 50 | bool: True if valid, False otherwise. 51 | """ 52 | # Check column 53 | for r in range(row): 54 | if board[r][col] == 'Q': 55 | return False 56 | 57 | # Check top-left diagonal 58 | for r, c in zip(range(row, -1, -1), range(col, -1, -1)): 59 | if board[r][c] == 'Q': 60 | return False 61 | 62 | # Check top-right diagonal 63 | for r, c in zip(range(row, -1, -1), range(col, n)): 64 | if board[r][c] == 'Q': 65 | return False 66 | 67 | return True 68 | 69 | def place_queens(board: List[List[str]], row: int) -> None: 70 | """ 71 | Recursively attempt to place queens on the board. 72 | 73 | Args: 74 | board (List[List[str]]): The current state of the board. 75 | row (int): The current row to place a queen. 76 | """ 77 | # Base case: All queens are placed 78 | if row == n: 79 | solutions.append(create_board(board)) 80 | return 81 | 82 | # Try placing a queen in each column of the current row 83 | for col in range(n): 84 | if is_valid(row, col, board): 85 | # Place the queen 86 | board[row][col] = 'Q' 87 | 88 | # Recurse to the next row 89 | place_queens(board, row + 1) 90 | 91 | # Backtrack by removing the queen 92 | board[row][col] = '.' 93 | 94 | # Initialize the board 95 | initial_board = [['.'] * n for _ in range(n)] 96 | 97 | # Start placing queens from the first row 98 | place_queens(initial_board, 0) 99 | 100 | return solutions 101 | 102 | # Example usage 103 | if __name__ == "__main__": 104 | n_value = 4 105 | result = solve_n_queens(n_value) 106 | print(f"Solutions for {n_value}-Queens problem:") 107 | for solution in result: 108 | for row in solution: 109 | print(row) 110 | print() -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 34/Readme.md: -------------------------------------------------------------------------------- 1 | # **Day 34: Singly Linked Lists** 2 | 3 | Welcome to Day 34 of **50 Days of DSA in Python**! Today, we continue our journey with **Singly Linked Lists (SLL)**. We will cover two important operations: **reversing a singly linked list** and **detecting cycles** in the list. These operations are essential for manipulating linked lists and detecting potential issues like infinite loops. 4 | 5 | --- 6 | 7 | ### **Topics Covered:** 8 | - Singly Linked Lists 9 | - Reverse SLL 10 | - Cycle Detection 11 | 12 | --- 13 | 14 | ## **1. Reverse Singly Linked List (SLL)** 15 | 16 | ### **Problem Statement** 17 | Given a singly linked list, reverse the list. After reversing, the head of the list should point to the last element, and the previous elements should point to the next in reverse order. 18 | 19 | ### **Approach** 20 | 21 | #### **1. Iterative Approach to Reverse the List** 22 | 23 | To reverse a singly linked list, we need to iterate through the list and reverse the pointers of each node one by one. 24 | 25 | #### **Code:** 26 | ```python 27 | def reverse_linked_list(head): 28 | """ 29 | Reverse the given singly linked list. 30 | 31 | Args: 32 | head (ListNode): The head node of the singly linked list. 33 | 34 | Returns: 35 | ListNode: The new head node after the list has been reversed. 36 | """ 37 | prev = None 38 | current = head 39 | 40 | while current: 41 | next_node = current.next # Store the next node 42 | current.next = prev # Reverse the current node's pointer 43 | prev = current # Move prev and current one step forward 44 | current = next_node 45 | 46 | return prev # New head of the reversed list 47 | ``` 48 | 49 | --- 50 | 51 | ## **2. Cycle Detection in Singly Linked List** 52 | 53 | ### **Problem Statement** 54 | Given a singly linked list, detect if it has a cycle. A cycle occurs when a node’s next pointer points back to a previously visited node, creating a loop in the list. 55 | 56 | ### **Approach** 57 | 58 | #### **1. Floyd’s Tortoise and Hare Algorithm (Cycle Detection)** 59 | 60 | Floyd’s Tortoise and Hare algorithm is an efficient way to detect cycles in a linked list. It uses two pointers: one slow (tortoise) and one fast (hare). If there’s a cycle, the fast pointer will eventually meet the slow pointer. 61 | 62 | #### **Code:** 63 | ```python 64 | def has_cycle(head): 65 | """ 66 | Detect if there is a cycle in the given singly linked list. 67 | 68 | Args: 69 | head (ListNode): The head node of the singly linked list. 70 | 71 | Returns: 72 | bool: True if there is a cycle, False otherwise. 73 | """ 74 | slow = head 75 | fast = head 76 | 77 | while fast and fast.next: 78 | slow = slow.next # Move slow pointer by 1 step 79 | fast = fast.next.next # Move fast pointer by 2 steps 80 | 81 | if slow == fast: # Cycle detected 82 | return True 83 | 84 | return False # No cycle 85 | ``` 86 | 87 | --- 88 | 89 | ### **Conclusion** 90 | 91 | Today, we learned how to manipulate **Singly Linked Lists (SLL)** in two important ways: 92 | 93 | 1. **Reverse Singly Linked List**: We implemented a method to reverse the entire linked list by iteratively adjusting the pointers of each node. 94 | 95 | 2. **Cycle Detection**: We implemented **Floyd’s Tortoise and Hare algorithm** to detect if a linked list contains a cycle, which is crucial in identifying infinite loops in the list. 96 | 97 | Both of these operations are commonly used in various algorithmic problems involving linked lists. By mastering these techniques, you'll be well-prepared for problems requiring linked list manipulations. 98 | 99 | Keep practicing, and see you tomorrow for more exciting topics in data structures! -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 23/Readme.md: -------------------------------------------------------------------------------- 1 | # **Day 23: Greedy Algorithms** 2 | 3 | Welcome to Day 23 of **50 Days of DSA in Python**! Today, we continue our exploration of **Greedy Algorithms**, focusing on two classic problems: **Gas Stations** and **Jump Game 2**. Greedy algorithms are efficient solutions to optimization problems, where the goal is to make locally optimal choices at each step. 4 | 5 | --- 6 | 7 | ### **Topics Covered:** 8 | - Greedy Algorithms 9 | - Gas Stations 10 | - Jump Game 2 11 | 12 | --- 13 | 14 | ## **1. Gas Stations** 15 | 16 | ### **Problem Statement** 17 | Given an array of gas stations, each with a certain amount of gas, and an array of costs to travel to the next station, determine if there is a starting gas station such that you can travel around the circuit once without running out of gas. If such a station exists, return the starting index, otherwise, return -1. 18 | 19 | ### **Approaches** 20 | 21 | #### **1. Greedy Approach** 22 | 23 | The greedy approach involves checking whether we can complete a round starting from each station, ensuring that we accumulate enough gas to continue the journey. 24 | 25 | #### **Code:** 26 | ```python 27 | def canCompleteCircuit(gas, cost): 28 | """ 29 | Determine if a trip around the circuit is possible starting from a gas station. 30 | 31 | Args: 32 | gas (list): A list of gas available at each station. 33 | cost (list): A list of gas required to reach the next station. 34 | 35 | Returns: 36 | int: The index of the starting station, or -1 if not possible. 37 | """ 38 | total_gas = 0 39 | current_gas = 0 40 | start_index = 0 41 | 42 | for i in range(len(gas)): 43 | total_gas += gas[i] - cost[i] 44 | current_gas += gas[i] - cost[i] 45 | 46 | # If the current gas is negative, reset the start index 47 | if current_gas < 0: 48 | start_index = i + 1 49 | current_gas = 0 50 | 51 | return start_index if total_gas >= 0 else -1 52 | ``` 53 | 54 | --- 55 | 56 | ## **2. Jump Game 2** 57 | 58 | ### **Problem Statement** 59 | Given an array of non-negative integers `nums`, where each element represents your maximum jump length from that position, determine the minimum number of jumps to reach the last index. 60 | 61 | ### **Approaches** 62 | 63 | #### **1. Greedy Approach** 64 | 65 | The greedy approach involves calculating the farthest reachable index at each position and determining the minimum jumps required to reach the last index. 66 | 67 | #### **Code:** 68 | ```python 69 | def jump(nums): 70 | """ 71 | Calculate the minimum number of jumps to reach the last index. 72 | 73 | Args: 74 | nums (list): A list of non-negative integers representing maximum jumps. 75 | 76 | Returns: 77 | int: The minimum number of jumps required to reach the last index. 78 | """ 79 | n = len(nums) 80 | if n <= 1: 81 | return 0 82 | 83 | jumps = 0 84 | current_end = 0 85 | farthest = 0 86 | 87 | for i in range(n): 88 | farthest = max(farthest, i + nums[i]) 89 | 90 | if i == current_end: 91 | jumps += 1 92 | current_end = farthest 93 | if current_end >= n - 1: 94 | break 95 | 96 | return jumps 97 | ``` 98 | 99 | --- 100 | 101 | ### **Conclusion** 102 | 103 | Today, we explored two more problems that can be solved using **Greedy Algorithms**: 104 | 105 | 1. **Gas Stations**: Finding the starting station for a successful circuit journey using a greedy approach. 106 | 2. **Jump Game 2**: Determining the minimum number of jumps required to reach the last index using a greedy approach. 107 | 108 | Greedy algorithms often provide optimal solutions for problems where local decisions lead to a globally optimal solution. Keep practicing, and see you tomorrow! -------------------------------------------------------------------------------- /50 Days of DSA in Python/Day 25/Readme.md: -------------------------------------------------------------------------------- 1 | # **Day 25: Hash Tables** 2 | 3 | Welcome to Day 25 of **50 Days of DSA in Python**! Today, we dive into **Hash Tables**, one of the most efficient data structures for solving various algorithmic challenges. We will cover two classic problems: **Two Sum** and **Isomorphic Strings**. 4 | 5 | --- 6 | 7 | ### **Topics Covered:** 8 | - Hash Tables 9 | - Two Sum 10 | - Isomorphic Strings 11 | 12 | --- 13 | 14 | ## **1. Two Sum** 15 | 16 | ### **Problem Statement** 17 | Given an array of integers `nums` and an integer `target`, return the indices of the two numbers such that they add up to the `target`. You may assume that each input would have exactly one solution, and you may not use the same element twice. 18 | 19 | ### **Approaches** 20 | 21 | #### **1. Using Hash Table (Optimal Approach)** 22 | 23 | We can use a hash table (dictionary in Python) to store the elements we've seen so far and their indices. For each element in the array, we check if the complement (i.e., `target - num`) exists in the hash table. This provides an efficient solution in `O(n)` time. 24 | 25 | #### **Code:** 26 | ```python 27 | def twoSum(nums, target): 28 | """ 29 | Find two numbers in the list that add up to the target using a hash table. 30 | 31 | Args: 32 | nums (list): List of integers. 33 | target (int): The target sum. 34 | 35 | Returns: 36 | list: Indices of the two numbers. 37 | """ 38 | num_dict = {} 39 | for i, num in enumerate(nums): 40 | complement = target - num 41 | if complement in num_dict: 42 | return [num_dict[complement], i] 43 | num_dict[num] = i 44 | ``` 45 | 46 | --- 47 | 48 | ## **2. Isomorphic Strings** 49 | 50 | ### **Problem Statement** 51 | Given two strings `s` and `t`, determine if they are **isomorphic**. Two strings are isomorphic if the characters in `s` can be replaced to get `t`, with the condition that each character in `s` maps to exactly one character in `t`, and vice versa. 52 | 53 | ### **Approaches** 54 | 55 | #### **1. Using Hash Tables (Optimal Approach)** 56 | 57 | We can use two hash tables (or dictionaries) to track the character mappings between the two strings. For each character in the strings, we check if it already has a corresponding mapping. If not, we create a new mapping. If there's a conflict in mappings, we return `False`. 58 | 59 | #### **Code:** 60 | ```python 61 | def isIsomorphic(s, t): 62 | """ 63 | Check if two strings are isomorphic using hash tables. 64 | 65 | Args: 66 | s (str): The first input string. 67 | t (str): The second input string. 68 | 69 | Returns: 70 | bool: True if the strings are isomorphic, otherwise False. 71 | """ 72 | if len(s) != len(t): 73 | return False 74 | 75 | s_map = {} 76 | t_map = {} 77 | 78 | for char_s, char_t in zip(s, t): 79 | if char_s in s_map: 80 | if s_map[char_s] != char_t: 81 | return False 82 | else: 83 | s_map[char_s] = char_t 84 | 85 | if char_t in t_map: 86 | if t_map[char_t] != char_s: 87 | return False 88 | else: 89 | t_map[char_t] = char_s 90 | 91 | return True 92 | ``` 93 | 94 | --- 95 | 96 | ### **Conclusion** 97 | 98 | Today, we explored **Hash Tables** and how they can be used to efficiently solve problems: 99 | 100 | 1. **Two Sum**: We used a hash table to find two numbers that sum up to the target in `O(n)` time. 101 | 2. **Isomorphic Strings**: We used hash tables to check if two strings can be mapped onto each other, respecting character mappings. 102 | 103 | Hash tables are powerful tools for many algorithmic problems, and these problems highlight how to leverage them for fast lookups and efficient solutions. Keep practicing, and see you tomorrow! --------------------------------------------------------------------------------