├── .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}")
--------------------------------------------------------------------------------