├── .gitattributes ├── .github └── docs │ └── images │ └── logo.png ├── .gitignore ├── LICENSE ├── README.md ├── big_o_notation ├── big_o_cheat_sheet.pdf ├── exercises │ ├── big_o_notation_1.py │ └── big_o_notation_2.py ├── rules │ ├── different_terms_for_inputs.py │ ├── drop_non_dominants.py │ ├── remove_constants.py │ └── worst_case.py ├── space_complexity │ ├── constant_space.py │ └── linear_space.py └── time_complexity │ ├── constant_time.py │ ├── linear_time.py │ └── quadratic_time.py └── data_structures └── arrays ├── arrays_operations.py ├── exercises ├── duplicate_zeros.py ├── find_common_item.py ├── find_numbers_with_even_number_of_digits.py ├── has_pair_with_sum.py ├── is_valid_subsequence.py ├── max_consecutive_ones.py ├── merge_sorted_arrays.py ├── remove_duplicates_from_sorted_array.py ├── reverse_string.py ├── squares_of_a_sorted_array.py └── two_sum.py └── implement_array.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LauraBeatris/algorithms-and-data-structures/fe8c2f096bf409a6ac7bfbc55c63a023a3f3ce60/.github/docs/images/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __pycache__/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Laura Beatris 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Big O Notation 6 | > Big O describes how the time is taken, or memory is used, by a program scales with the amount of data it has to work on 7 | 8 | > Big O helps us to measure the scalability of our code 9 | 10 | > Big O is a way to indicate complexities (Space Complexity & Time Complexity) 11 | 12 | - [Exercises](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/exercises) 13 | - [Cheat Sheet](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/big_o_cheat_sheet.pdf) 14 | 15 | ## Time Complexity 16 | - [Linear Time - O(n)](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/time_complexity/linear_time.py) 17 | - [Constant Time - O(1)](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/time_complexity/constant_time.py) 18 | - [Quadratic Time - O(n^2)](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/time_complexity/quadratic_time.py) 19 | 20 | ## Space Complexity 21 | - [Linear Space - O(n)](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/space_complexity/linear_space.py) 22 | - [Constant Space - O(1)](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/space_complexity/constant_space.py) 23 | 24 | ## Rules 25 | - [Worst Case](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/rules/worst_case.py) 26 | - [Remove Constants](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/rules/remove_constants.py) 27 | - [Different Terms for Inputs](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/rules/different_terms_for_inputs.py) 28 | - [Drop Non Dominants](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/big_o_notation/rules/drop_non_dominants.py) 29 | 30 | 31 | # Data Structures 32 | > A data structure is a specialized format for organizing, processing, retrieving and storing data. 33 | 34 | > A data structure is a way of organizing the data so that it can be used efficiently. 35 | 36 | ## Arrays 37 | 38 | - [Exercises](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/data_structures/arrays/exercises) 39 | - [Operations](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/data_structures/arrays/arrays_operations.py) 40 | - [Implementing an array from scratch](https://github.com/LauraBeatris/algorithms-and-data-structures/tree/main/data_structures/arrays/implement_array.py) 41 | -------------------------------------------------------------------------------- /big_o_notation/big_o_cheat_sheet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LauraBeatris/algorithms-and-data-structures/fe8c2f096bf409a6ac7bfbc55c63a023a3f3ce60/big_o_notation/big_o_cheat_sheet.pdf -------------------------------------------------------------------------------- /big_o_notation/exercises/big_o_notation_1.py: -------------------------------------------------------------------------------- 1 | """" 2 | What's the Big O Notation of the following algorithm? 3 | """ 4 | 5 | def another_function(): 6 | return 7 | 8 | def fun_challenge(input): 9 | a = 10 # O(1) 10 | a = 50 + 3 # O(1) 11 | 12 | for i in range(len(input)): # O(n) 13 | another_function() # O(n) 14 | stranger = True # O(n) 15 | a += 1 # O(n) 16 | 17 | return a # O(1) 18 | 19 | best_case_scenario_input = [1] 20 | average_case_scenario_input = [1] * 10 21 | worst_case_scenario_input = [1] * 1000 22 | 23 | fun_challenge(worst_case_scenario_input) 24 | 25 | # Big O Notation: O(3 + 4n) = O(n) -------------------------------------------------------------------------------- /big_o_notation/exercises/big_o_notation_2.py: -------------------------------------------------------------------------------- 1 | """" 2 | What's the Big O Notation of the following algorithm? 3 | """ 4 | 5 | def anotherFunChallenge(input): 6 | a = 5 # O(1) 7 | b = 10 # O(1) 8 | c = 50 # O(1) 9 | 10 | i = 0 # O(1) 11 | while i < input: # O(n) 12 | x = i + 1 # O(n) 13 | y = i + 2 # O(n) 14 | z = i + 3 # O(n) 15 | 16 | j = 0 # O(1) 17 | while j < input: # O(n) 18 | p = j * 2 # O(n) 19 | q = j * 2 # O(n) 20 | 21 | whoAmI = "I don't know" # O(1) 22 | 23 | bestCaseScenarioInput = 1 24 | averageCaseScenarioInput = 10 25 | worstCaseScenarioInput = 1000 26 | 27 | anotherFunChallenge(worstCaseScenarioInput) 28 | 29 | # Big O Notation: O(6 + 7n) = O(n) -------------------------------------------------------------------------------- /big_o_notation/rules/different_terms_for_inputs.py: -------------------------------------------------------------------------------- 1 | """ 2 | === Big O Notations Rules - Different Terms for Inputs === 3 | 4 | What if an algorithm receives two different inputs and it depends on the size of both? 5 | In that case, the Big O Notation should have different variables for each input, like the following: 6 | - O(a + b) 7 | - O(n*m) 8 | """ 9 | 10 | def compressBoxesTwice(boxes1, boxes2): # O(2a + 2b) => O(a + b) 11 | for box in boxes1: # O(a) 12 | print(f'Compress {box}') # O(a) 13 | 14 | for box in boxes2: # O(b) 15 | print(f'Compress {box}') # O(b) 16 | -------------------------------------------------------------------------------- /big_o_notation/rules/drop_non_dominants.py: -------------------------------------------------------------------------------- 1 | """ 2 | === Big O Notations Rules - Drop Non Dominants === 3 | 4 | Drop non dominants terms since these aren't going to change significantly 5 | the amount of operations according to the input size 6 | 7 | In Big O, our main concern is scalability and as the input size gets larger, 8 | then we should analyze the operations that are going to significantly increase more 9 | 10 | As you can see in the example below, the dominant operation is a nested for loop, which 11 | has a quadratic complexity and can significantly increase more according to the input size 12 | """ 13 | 14 | def printAllNumbersThenAllPairSums(numbers): # Big O = O(n + n^2) => O(n^2) 15 | for number in numbers: # O(n) 16 | print(number) 17 | 18 | for number1 in numbers: # O(n^2) 19 | for number2 in numbers: 20 | print(f'Pair sum: {number1 + number2}') 21 | 22 | printAllNumbersThenAllPairSums([1, 2, 3, 4]) -------------------------------------------------------------------------------- /big_o_notation/rules/remove_constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | === Big O Notations Rules - Remove Constants === 3 | 4 | The Big O Notation of a certain algorithm is always going to be one of the following: 5 | - O(1) 6 | - O(log n) 7 | - O(n) 8 | - O(n log n) 9 | - O(n^2) 10 | - O(2^n) 11 | - O(n!) 12 | 13 | That's why we remove constants, refer to the following examples: 14 | - O(4n) = O(n) 15 | - O(4n + 100) = O(n) 16 | 17 | When referring to the Bio O Chart, what matters is how the line of operations moves 18 | according to the input size and constants always remain the same when the input size gets bigger 19 | """ 20 | 21 | def compressBoxesTwice(boxes): # Big O = O(4n) => O(n) 22 | for box in boxes: # O(n) 23 | print(f'Compress {box}') # O(n) 24 | 25 | for box in boxes: # O(n) 26 | print(f'Compress {box}') # O(n) 27 | 28 | def printFirstItemThenFirstHalfThenSayHiTen100Times(items): # Big O = O(7 + 3O(n/2)) => O(n) 29 | print(items[0]) # O(1) 30 | 31 | middleIndex = int(len(items) / 2) # O(1) 32 | i = 0 # O(1) 33 | 34 | while (i < middleIndex): # O(n/2) 35 | print(items[i]) # O(n/2) 36 | i += 1 # O(n/2) 37 | 38 | j = 0 # O(1) 39 | while j < 100: # O(1) 40 | print('hi') # O(1) 41 | j += 1 # O(1) -------------------------------------------------------------------------------- /big_o_notation/rules/worst_case.py: -------------------------------------------------------------------------------- 1 | """ 2 | === Big O Notations Rules - Worst Case === 3 | 4 | Big O is care more about the worst case scenario, but we can also think about best & average cases. 5 | 6 | Programmers typically solve for the worst-case scenario, Big O (Because we are not expecting our algorithm to run in the best or even average cases.) 7 | """ 8 | 9 | companiesList = ['twitter', 'spotify', 'tiktok', 'tinder', 'google'] 10 | companiesLargeList = ['facebook'] * 100000 11 | companiesLargeList.append('twitter') 12 | 13 | def findTwitter(companies): # Big O = O(n) 14 | for company in companies: 15 | if company != 'twitter': continue 16 | 17 | print('Found twitter') 18 | break # Breaking the loop doesn't matter for the worst case scenario because it's already the last loop 19 | 20 | findTwitter(companiesLargeList) 21 | 22 | """ 23 | === Why Linear time? === 24 | 25 | Because the worst-case scenario would be, for instance, an array of one million items with twitter as the last item 26 | and the number of operations executed would increase linearly according to the input size 27 | 28 | - Best case: Constant time (Twitter is the first item of the companies list) 29 | - Average case: Linear time (Twitter is in the middle of the companies list) 30 | - Worst case: Linear time (Twitter is the last item of the companies list) 31 | """ -------------------------------------------------------------------------------- /big_o_notation/space_complexity/constant_space.py: -------------------------------------------------------------------------------- 1 | def constant_space(numbers_list): # Big O => O(1) => The space allocated is always constant doesn't matter the input size 2 | unused_variable = 1 # O(1) 3 | for number in numbers_list: # O(1) 4 | print(number) -------------------------------------------------------------------------------- /big_o_notation/space_complexity/linear_space.py: -------------------------------------------------------------------------------- 1 | def return_squares(numbers_list): # Big O => O(n) => The algorithm has to allocate memory for the same number of items as in the input list 2 | square_list = [] 3 | 4 | for num in numbers_list: 5 | square_list.append(num * num) 6 | 7 | return square_list 8 | 9 | return_squares([1, 2, 3, 4, 5]) -------------------------------------------------------------------------------- /big_o_notation/time_complexity/constant_time.py: -------------------------------------------------------------------------------- 1 | candies = ['chocolate', 'marshmallow', 'strawberry with chocolate'] * 1000000 2 | 3 | def logFirstCandyFromList(candies): 4 | print(f'First candy: {candies[0]}') # O(1) 5 | 6 | def logFirstTwoCandiesFromList(candies): 7 | print(f'First candy: {candies[0]}') # O(1) 8 | print(f'Second candy: {candies[1]}') # O(1) 9 | 10 | logFirstCandyFromList(candies) 11 | 12 | """ 13 | logFirstCandyFromList Worst Case Scenario (1000000 candies): O(1) Constant Time 14 | 15 | logFirstTwoCandiesFromList Worst Case Scenario (1000000 candies): O(1) Constant Time 16 | """ 17 | 18 | """ 19 | === What Big O Notation is used to represent Constant Time? === 20 | 21 | O(1) 22 | 23 | === When an operation has Constant Time? === 24 | 25 | Constant Time happens when the amount of operations remains the same (constant) even when the input size gets bigger 26 | 27 | === What happens when an algorithm has more than one operation with Constant Time? === 28 | 29 | In the end, O(3) which can be the sum of 3 Constant Time operations, 30 | it's going to be summarize to O(1) because even if the input gets bigger, 31 | 3 operations are going to be executed 32 | """ -------------------------------------------------------------------------------- /big_o_notation/time_complexity/linear_time.py: -------------------------------------------------------------------------------- 1 | companiesList = ['twitter', 'spotify', 'tiktok', 'tinder', 'google'] 2 | companiesLargeList = ['facebook'] * 100000 3 | companiesLargeList.append('twitter') 4 | 5 | def findTwitter(companies): 6 | for company in companies: 7 | if company != 'twitter': continue 8 | 9 | print('Found twitter') 10 | break # Breaking the loop doesn't matter for the worst case scenario because it's already the last loop 11 | 12 | findTwitter(companiesLargeList) 13 | 14 | """ 15 | Big O Notation of findTwitter: O(n) - Linear time 16 | 17 | === What's the Bio O Notation for Linear time? === 18 | 19 | O(n) 20 | 21 | === What n represents in O(n) Notation? === 22 | 23 | It presents the input size - So the amount of operations 24 | increase linearly with the input size 25 | 26 | === Why the efficacy of an algorithm can't be measure by how long it takes to run? === 27 | 28 | The runtime velocity measure in milliseconds can be really different depending on the computer CPU and also internet connectivity, 29 | and that's why we don't measure code runtime efficiency based on milliseconds but instead, on the number of operations that the algorithm 30 | will need to execute according to a certain input size 31 | """ -------------------------------------------------------------------------------- /big_o_notation/time_complexity/quadratic_time.py: -------------------------------------------------------------------------------- 1 | """ 2 | An algorithm has a complexity of O(n^2) if the runtime is quadratic according 3 | to the input size. Refer to the following examples: 4 | """ 5 | 6 | arrayInput = ['a', 'b', 'c', 'd', 'e'] 7 | 8 | def logAllArrayPairs(array): # O(n^2) 9 | firstPairElementIndex = 0 10 | secondPairElementIndex = 0 11 | 12 | while firstPairElementIndex < len(array): 13 | firstPairElement = array[firstPairElementIndex] 14 | 15 | while secondPairElementIndex < len(array): 16 | secondPairElement = array[secondPairElementIndex] 17 | print(f'{firstPairElement},{secondPairElement}') 18 | secondPairElementIndex += 1 19 | 20 | firstPairElementIndex += 1 21 | 22 | def compressBoxesTwice(boxes1, boxes2): # O(a*b) 23 | for box in boxes1: 24 | print(f'Compress {box}') 25 | 26 | for box in boxes2: 27 | print(f'Compress {box}') 28 | 29 | -------------------------------------------------------------------------------- /data_structures/arrays/arrays_operations.py: -------------------------------------------------------------------------------- 1 | """ 2 | === Speed Complexity by Operation === 3 | 4 | - Lookup -> O(1) - Unshift -> O(n) 5 | - Push -> O(1) - Insert -> O(n) 6 | - Pop -> O(1) - Delete -> O(n) 7 | """ 8 | 9 | import collections 10 | 11 | list1 = [1, 2, 3] 12 | 13 | """ 14 | Deque is preferred over list in the cases where we need quicker append and pop operations from both the ends of container, 15 | as deque provides an O(1) time complexity for append and pop operations as compared to list which provides O(n) time complexity. 16 | """ 17 | deque1 = collections.deque(list1) 18 | 19 | ## Lookup 20 | print("Lookup", list1[0]) 21 | 22 | ## Push 23 | list1.append(1) 24 | print("Push", list1) 25 | 26 | ## Pop 27 | deque1.pop() 28 | print("Pop", deque1) 29 | 30 | ## Unshift 31 | deque1.appendleft(4) 32 | print("Unshift", deque1) 33 | 34 | ## Shift 35 | deque1.popleft() 36 | print("Shift", deque1) -------------------------------------------------------------------------------- /data_structures/arrays/exercises/duplicate_zeros.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==== KEY POINTS ==== 3 | - Given a fixed length array arr of integers, duplicate each occurrence of zero, shifting the remaining elements to the right. 4 | - Do the above modifications to the input array in place, do not return anything from your function. 5 | - arr.length <= 10000 6 | - arr[i] <= 9 7 | """ 8 | 9 | """ 10 | ==== BRUTE FORCE SOLUTION (IN-PLACE) ==== 11 | Time Complexity: O(n^2) 12 | Space Complexity: O(1) 13 | """ 14 | def duplicate_zeros1(arr): 15 | length = len(arr) 16 | highest_length = length - 1 17 | 18 | for i in range(highest_length, -1, -1): 19 | num = arr[i] 20 | should_duplicate_zero = num == 0 and i < highest_length 21 | 22 | if should_duplicate_zero: 23 | for j in range(highest_length, i, -1): 24 | is_out_of_range = j >= highest_length 25 | 26 | if not is_out_of_range: 27 | arr[j + 1] = arr[j] 28 | 29 | j -= 1 30 | 31 | arr[i + 1] = 0 32 | 33 | print(arr) 34 | 35 | # duplicate_zeros1([1, 0, 2, 3, 0, 4, 5, 0]) 36 | # duplicate_zeros1([1, 2, 3]) 37 | 38 | """ 39 | ==== SCALABLE SOLUTION (IN-PLACE) ==== 40 | Time Complexity: O(n) 41 | Space Complexity: O(1) 42 | """ 43 | def duplicate_zeros2(arr): 44 | length = len(arr) 45 | with_zeros_length = length + arr.count(0) 46 | 47 | i = length - 1 48 | j = with_zeros_length - 1 49 | while j >= 0: 50 | if j < length: arr[j] = arr[i] 51 | j -= 1 52 | 53 | if arr[i] == 0: 54 | if j < length: arr[j] = arr[i] 55 | j -= 1 56 | 57 | i -= 1 58 | 59 | print(arr) 60 | 61 | 62 | # duplicate_zeros2([1, 0, 2, 3, 0, 4, 5, 0]) 63 | # duplicate_zeros2([1, 2, 3]) 64 | 65 | """ 66 | ==== SCALABLE SOLUTION (EXTRA SPACE) ==== 67 | Time Complexity: O(n) 68 | Space Complexity: O(n) 69 | """ 70 | def duplicate_zeros3(arr): 71 | source = arr[:] 72 | 73 | i = j = 0 74 | n = len(arr) 75 | 76 | while j < n: 77 | arr[j] = source[i] 78 | j += 1 79 | 80 | if source[i] == 0: 81 | if j < n: arr[j] = source[i] 82 | j += 1 83 | 84 | i += 1 85 | 86 | print(arr) 87 | 88 | duplicate_zeros3([1, 0, 2, 3, 0, 4, 5, 0]) 89 | duplicate_zeros3([1, 2, 3]) -------------------------------------------------------------------------------- /data_structures/arrays/exercises/find_common_item.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==== KEY POINTS ==== 3 | - Return boolean if it finds common items 4 | - Input is a list of strings 5 | - It's possible to have duplicated items 6 | - Is there a change of the lists having more than 10 items? If so, is that a problem if we use a solution with quadratic complexity? 7 | - Which one is more important: Space Complexity or Time Complexity? 8 | """ 9 | 10 | list_input_1 = ['a', 'b', 'c'] 11 | list_input_2 = ['d', 'e', 'f', 'c'] 12 | list_input_3 = ['g', 'y'] 13 | 14 | """ 15 | ==== BRUTE FORCE SOLUTION ==== 16 | - Time Complexity: O(a*b) 17 | - Space Complexity: O(1) 18 | """ 19 | def find_common_item_1(list1, list2): 20 | for item in list1: 21 | if item in list2: return True 22 | 23 | return False 24 | 25 | """ 26 | ==== SCALABLE SOLUTION ==== 27 | - Time Complexity: O(a + b) 28 | - Space Complexity: O(a) 29 | """ 30 | def find_common_2(list1, list2): 31 | list1_set = set(list1) 32 | 33 | for item in list2: 34 | if item in list1_set: return True 35 | 36 | return False 37 | 38 | print(find_common_2(list_input_1, list_input_2)) 39 | print(find_common_2(list_input_1, list_input_3)) -------------------------------------------------------------------------------- /data_structures/arrays/exercises/find_numbers_with_even_number_of_digits.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | """ 4 | ==== KEY POINTS ==== 5 | - Given an array nums of integers, return how many of them contain an even number of digits. 6 | - Input is a list of positive numbers 7 | - nums.length <= 500 8 | - nums[i] <= 10^5 9 | """ 10 | 11 | def is_even(num): 12 | return num % 2 == 0 13 | 14 | def get_digits_quantity(num): 15 | return math.floor(math.log10(num)) + 1 16 | 17 | """ 18 | - Time Complexity: O(n) 19 | - Space Complexity: O(1) 20 | """ 21 | def find_numbers_with_even_number_of_digits1(nums): 22 | result = 0 23 | 24 | for num in nums: 25 | if not is_even(get_digits_quantity(num)): continue 26 | 27 | result += 1 28 | 29 | return result 30 | 31 | # print(find_numbers_with_even_number_of_digits1([12,345,2,6,7896])) 32 | # print(find_numbers_with_even_number_of_digits1([555,901,482,1771])) 33 | 34 | """ 35 | - Time Complexity: O(n) 36 | - Space Complexity: O(1) 37 | """ 38 | def find_numbers_with_even_number_of_digits2(nums): 39 | return sum(is_even(get_digits_quantity(n)) for n in nums) 40 | 41 | print(find_numbers_with_even_number_of_digits2([12,345,2,6,7896])) 42 | print(find_numbers_with_even_number_of_digits2([555,901,482,1771])) -------------------------------------------------------------------------------- /data_structures/arrays/exercises/has_pair_with_sum.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==== KEY POINTS ==== 3 | - Input: Collection of positive/negative/float numbers 4 | - Output: boolean indicating if the pair was found 5 | - The memory isn't limited, so we can favor Time Complexity instead of Space Complexity 6 | """ 7 | 8 | list_without_pair_sum = [1, 2, 3, 9] 9 | list_with_pair_sum = [1, 2, 4, 4] 10 | sum_input = 8 11 | 12 | """ 13 | ==== BRUTE FORCE SOLUTION ==== 14 | - Time Complexity: O(n^2) 15 | - Space Complexity: O(1) 16 | """ 17 | def has_pair_with_sum_1(numbersList, targetSum): 18 | for number1 in numbersList: 19 | for number2 in numbersList[1:]: 20 | if (number1 + number2) == targetSum: 21 | return True 22 | 23 | return False 24 | 25 | """ 26 | ==== SCALABLE SOLUTION ==== 27 | - Time Complexity: O(n) 28 | - Space Complexity: O(n) 29 | """ 30 | def has_pair_with_sum_2(numbersList, targetSum): 31 | sum_complement_set = set([]) 32 | 33 | for number in numbersList: 34 | if number in sum_complement_set: return True 35 | 36 | sum_complement_set.add(targetSum - number) 37 | 38 | return False 39 | 40 | print(has_pair_with_sum_2(list_with_pair_sum, sum_input)) 41 | print(has_pair_with_sum_2(list_without_pair_sum, sum_input)) 42 | -------------------------------------------------------------------------------- /data_structures/arrays/exercises/is_valid_subsequence.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==== KEY POINTS ==== 3 | - Given two non-empty arrays of integers, write a function that determines 4 | whether the second array is a subsequence of the first one. 5 | - [1, 3, 4] form a subsequence of the array [1, 2, 3, 4] 6 | """ 7 | 8 | """ 9 | - Time Complexity: O(n) 10 | - Space Complexity: O(1) 11 | """ 12 | def is_valid_subsequence1(array, sequence): 13 | sequence_i = 0 14 | 15 | for num in array: 16 | if sequence_i == len(sequence): 17 | break 18 | 19 | has_sequence_num = num == sequence[sequence_i] 20 | 21 | if has_sequence_num: 22 | sequence_i += 1 23 | 24 | return sequence_i == len(sequence) 25 | 26 | 27 | # print(is_valid_subsequence1([5, 1, 22, 25, 6, -1, 8, 10], [1, 6, -1, 10])) 28 | # print(is_valid_subsequence1([5, 1, 22, 25, 6, -1, 8, 10], [5, 1, 22, 25, 6, -1, 8, 10, 12])) 29 | 30 | """ 31 | - Time Complexity: O(n) 32 | - Space Complexity: O(1) 33 | """ 34 | def is_valid_subsequence2(array, sequence): 35 | sequence_i = 0 36 | array_i = 0 37 | 38 | while array_i < len(array) and sequence_i < len(sequence): 39 | array_num = array[array_i] 40 | sequence_num = sequence[sequence_i] 41 | 42 | has_sequence_num = array_num == sequence_num 43 | 44 | if has_sequence_num: 45 | sequence_i += 1 46 | 47 | array_i += 1 48 | 49 | return sequence_i == len(sequence) 50 | 51 | 52 | print(is_valid_subsequence2([5, 1, 22, 25, 6, -1, 8, 10], [1, 6, -1, 10])) 53 | print(is_valid_subsequence2( 54 | [5, 1, 22, 25, 6, -1, 8, 10], [5, 1, 22, 25, 6, -1, 8, 10, 12])) 55 | -------------------------------------------------------------------------------- /data_structures/arrays/exercises/max_consecutive_ones.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==== KEY POINTS ==== 3 | - Given a binary array of numbers, return the maximum number of consecutive 1's in the array. 4 | - Output: The maximum number of consecutive 1's in the array. 5 | - nums[i] is either 0 or 1. 6 | """ 7 | 8 | """ 9 | - Time Complexity: O(n) 10 | - Space Complexity: O(1) 11 | """ 12 | def max_consecutive_ones1(binaryNums): 13 | result = 0 14 | count = 0 15 | 16 | for num in binaryNums: 17 | # If num is zero, then it's going to reset the count 18 | count = (count + num) * num 19 | result = max(count, result) 20 | 21 | return result 22 | 23 | # print(max_consecutive_ones1([1, 1, 0, 1, 1, 1])) 24 | # print(max_consecutive_ones1([1 ,0, 1, 1, 0, 1])) 25 | 26 | """ 27 | - Time Complexity: O(n) 28 | - Space Complexity: O(1) 29 | """ 30 | def max_consecutive_ones2(binaryNums): 31 | result = 0 32 | count = 0 33 | 34 | for num in binaryNums: 35 | if num == 1: 36 | count += 1 37 | continue 38 | 39 | """ 40 | Handle consecutive 1's in the end of the list since it won't stop on a zero number 41 | """ 42 | result = max(count, result) 43 | count = 0 44 | 45 | result = max(count, result) 46 | 47 | return result 48 | 49 | print(max_consecutive_ones2([1, 1, 0, 1, 1, 1])) 50 | print(max_consecutive_ones2([1 ,0, 1, 1, 0, 1])) -------------------------------------------------------------------------------- /data_structures/arrays/exercises/merge_sorted_arrays.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==== KEY POINTS ==== 3 | - Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 as one sorted array. 4 | - Input: The number of elements initialized in nums1 and nums2 are m and n respectively. 5 | - nums1 has a size equal to m + n such that it has enough space to hold additional elements from nums2 6 | 7 | - It's not needed to return the value but instead, reassign nums1 argument 8 | """ 9 | 10 | def should_merge_directly(n): 11 | if (n == 0): return True 12 | 13 | """ 14 | - Time Complexity: O(n + m) 15 | - Space Complexity: O(1) - nums1 is already stored with additional space for later be merged with nums2 16 | """ 17 | def merge_sorted_arrays1(nums1, m, nums2, n): 18 | if should_merge_directly(n): 19 | nums1 = nums1 + nums2 20 | print(nums1) 21 | 22 | return 23 | 24 | nums1[:] = sorted(nums1[:m] + nums2) 25 | print(nums1) 26 | 27 | # merge_sorted_arrays1([1,2,3,0,0,0], 3, [4, 3.5, 9], 3) 28 | # merge_sorted_arrays1([1,2,3,4,0,0], 4, [5, 0], 2) 29 | # merge_sorted_arrays1([1,2,3], 3, [], 0) 30 | 31 | """ 32 | - Time Complexity: O(n + m) 33 | - Space Complexity: O(1) 34 | """ 35 | def merge_sorted_arrays2(nums1, m, nums2, n): 36 | if should_merge_directly(n): 37 | nums1 = nums1 + nums2 38 | print(nums1) 39 | 40 | return 41 | 42 | nums1_size_with_merged_items = m + n 43 | for index in range(nums1_size_with_merged_items): 44 | if not index >= m: continue 45 | 46 | nums1[index] = nums2.pop() 47 | 48 | nums1.sort() 49 | print(nums1) 50 | 51 | merge_sorted_arrays2([1,2,3,0,0,0], 3, [4, 3.5, 9], 3) 52 | merge_sorted_arrays2([1,2,3,4,0,0], 4, [5, 0], 2) 53 | merge_sorted_arrays2([1,2,3], 3, [], 0) -------------------------------------------------------------------------------- /data_structures/arrays/exercises/remove_duplicates_from_sorted_array.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==== KEY POINTS ==== 3 | - Given a sorted array nums, remove the duplicates in-place such that 4 | each element appears only once and returns the new length. 5 | - Output: The length of the nums array without duplicates 6 | - nums is sorted in ascending order. 7 | """ 8 | 9 | """ 10 | === SCALABLE SOLUTION (IN-PLACE) === 11 | - Time Complexity: O(n) 12 | - Space Complexity: O(1) 13 | """ 14 | def remove_duplicates1(nums): 15 | # Start index at second element because the first one is already unique 16 | i = 1 17 | 18 | while i < len(nums): 19 | is_unique = nums[i] != nums[i - 1] 20 | 21 | # Doesn't need to increase index if element is going to be deleted 22 | if is_unique: 23 | i += 1 24 | else: 25 | nums.pop(i) 26 | 27 | print(nums) 28 | 29 | return len(nums) 30 | 31 | remove_duplicates1([1,1,2]) 32 | remove_duplicates1([0,0,1,1,1,2,2,3,3,4]) 33 | 34 | """ 35 | === ALLOCATING MEMORY === 36 | - Time Complexity: O(n) 37 | - Space Complexity: O(n) 38 | """ 39 | def remove_duplicates2(nums): 40 | nums[:] = sorted(set(nums)) 41 | 42 | print(nums) 43 | 44 | return len(nums) 45 | 46 | remove_duplicates2([1,1,2]) 47 | remove_duplicates2([0,0,1,1,1,2,2,3,3,4]) -------------------------------------------------------------------------------- /data_structures/arrays/exercises/reverse_string.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==== KEY POINTS ==== 3 | - Given a string, return its reversed version 4 | - Input: A string. It can't not be empty such as "" or null 5 | - Is it possible to receive a string with less than 2 characters? Of so, validate this case 6 | - The input can have multiple spaces such as "Hello, my name is Laura" 7 | """ 8 | 9 | """ 10 | === SOLUTIONS COMPLEXITIES === 11 | - Time Complexity: O(n) 12 | - Space Complexity: O(n) 13 | """ 14 | 15 | def validate_string(string): 16 | is_invalid_string = len(string) < 2 17 | 18 | if (is_invalid_string): raise ValueError('Invalid String - The string needs to have at least 2 characters') 19 | 20 | # Using join + reversed built-in function 21 | def reverse_string_1(string): 22 | validate_string(string) 23 | 24 | reversed_string = ''.join(reversed(string)) 25 | 26 | return reversed_string 27 | 28 | # reverse_string_1("Hello Laura") # "aruaL olleH" 29 | # reverse_string_1("Hello Laura, what's up?") # "?pu s'tahw ,aruaL olleH" 30 | # reverse_string1("H") - Should raise error 31 | 32 | # Using loop 33 | def reverse_string_2(string): 34 | validate_string(string) 35 | 36 | reversed_string = '' 37 | 38 | index = len(string) - 1 39 | 40 | while index >= 0: 41 | reversed_string += string[index] 42 | index -= 1 43 | 44 | return reversed_string 45 | 46 | # reverse_string_2("Hello Laura") # "aruaL olleH" 47 | # reverse_string_2("Hello Laura, what's up?") # "?pu s'tahw ,aruaL olleH" 48 | 49 | # Using slicing 50 | def reverse_string_3(string): 51 | validate_string(string) 52 | 53 | """ 54 | The slice statement means start at string length, end at position 0, move with the step -1 55 | """ 56 | reversed_string = string[len(string):0:-1] 57 | 58 | return reversed_string 59 | 60 | # reverse_string_3("Hello Laura") # "aruaL olleH" 61 | # reverse_string_3("Hello Laura, what's up?") # "?pu s'tahw ,aruaL olleH" -------------------------------------------------------------------------------- /data_structures/arrays/exercises/squares_of_a_sorted_array.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==== KEY POINTS ==== 3 | - Given an integer array nums sorted in non-decreasing order, return an array of the squares of each number sorted in non-decreasing order. 4 | - Input is a list of positive/negative numbers 5 | - nums.length <= 10^4 6 | - nums[i] <= 10^4 7 | - nums is sorted in non-decreasing order. 8 | """ 9 | 10 | """ 11 | - Time Complexity: O(n) 12 | - Space Complexity: O(n) 13 | """ 14 | def squares_of_a_sorted_array(nums): 15 | return sorted(map(lambda num: num * num, nums)) 16 | 17 | # print(squares_of_a_sorted_array([-4,-1,0,3,10])) 18 | # print(squares_of_a_sorted_array([-7,-3,2,3,11])) 19 | -------------------------------------------------------------------------------- /data_structures/arrays/exercises/two_sum.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==== KEY POINTS ==== 3 | - Given an array of integers nums and an integer target, 4 | return indices of the two numbers such that they add up to target 5 | - Output: The array with the pair indexes & they don't need to be in order 6 | - Each input would have exactly one solution, 7 | """ 8 | 9 | """ 10 | ==== SCALABLE SOLUTION ==== 11 | - Time Complexity: O(n) 12 | - Space Complexity: O(n) 13 | """ 14 | def two_sum1(nums, target): 15 | complement_hash_map = {} 16 | 17 | for i, num in enumerate(nums): 18 | is_complement = num in complement_hash_map 19 | 20 | if is_complement: 21 | pair_i = complement_hash_map[num] 22 | 23 | return [pair_i, i] 24 | 25 | complement_hash_map[target - num] = i 26 | 27 | # print(two_sum1([2,7,11,15], 9)) 28 | # print(two_sum1([3,2,4], 6)) 29 | # print(two_sum1([3,3], 6)) 30 | 31 | """ 32 | ==== BRUTE FORCE SOLUTION ==== 33 | - Time Complexity: O(n^2) 34 | - Space Complexity: O(1) 35 | """ 36 | def two_sum2(nums, target): 37 | length = len(nums) 38 | 39 | for i in range(length): 40 | num1 = nums[i] 41 | 42 | for j in range(i + 1, length): 43 | num2 = nums[j] 44 | 45 | if num1 + num2 == target: 46 | return [i, j] 47 | 48 | print(two_sum2([2,7,11,15], 9)) 49 | print(two_sum2([3,2,4], 6)) 50 | print(two_sum2([3,3], 6)) -------------------------------------------------------------------------------- /data_structures/arrays/implement_array.py: -------------------------------------------------------------------------------- 1 | class Array: 2 | def __init__(self): 3 | self.length = 0 4 | self.data = dict() 5 | 6 | def push(self, item): 7 | self.data[self.length] = item 8 | self.length += 1 9 | return self.data 10 | 11 | def get(self, index): 12 | return self.data.get(index) 13 | 14 | def pop(self): 15 | lastItem = self.data.popitem()[1] 16 | self.length -= 1 17 | 18 | return lastItem 19 | 20 | def unshift(self, item): 21 | i = self.length - 1 22 | 23 | while i >= 0: 24 | self.data[i + 1] = self.data.get(i) 25 | i -= 1 26 | 27 | self.data[0] = item 28 | self.length += 1 29 | 30 | return self.length 31 | 32 | def delete(self, index): 33 | i = index 34 | deletedItem = self.get(index) 35 | 36 | while i < self.length - 1: 37 | self.data[i] = self.get(i + 1) 38 | i += 1 39 | 40 | self.data.pop(index) 41 | self.length -= 1 42 | 43 | return deletedItem 44 | 45 | array1 = Array() 46 | 47 | # ["Hello", "What's up", "What's your name?"] 48 | array1.push("Hello") 49 | array1.push("What's up") 50 | array1.push("What's your name?") 51 | print(f"Push: {array1.data}, Array length: {array1.length}") 52 | 53 | # ["Hello", "What's up"] 54 | array1.pop() 55 | print(f"Pop: {array1.data}, Array length: {array1.length}") 56 | 57 | # ["Hello", "What's up", "Nice!"] 58 | array1.push("Nice!") 59 | print(f"Push: {array1.data}, Array length: {array1.length}") 60 | 61 | # [1, "Hello", "What's up", "Nice!"] 62 | array1.unshift(1) 63 | print(f"Unshift: {array1.data}, Array length: {array1.length}") 64 | 65 | # [1, "Hello", "Nice!"] 66 | array1.delete(2) 67 | print(f"Delete: {array1.data}, Array length: {array1.length}") --------------------------------------------------------------------------------