From e4d556dbe08d837eb061149606284b9c6d2fe967 Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 15 Oct 2022 13:27:28 -0700 Subject: [PATCH 01/17] time_complexity practice problems --- dsa/chapter1/practice/time_complexity.py | 74 ++++++++++++++++++++ dsa/chapter1/solutions/time_complexity.py | 84 +++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 dsa/chapter1/practice/time_complexity.py create mode 100644 dsa/chapter1/solutions/time_complexity.py diff --git a/dsa/chapter1/practice/time_complexity.py b/dsa/chapter1/practice/time_complexity.py new file mode 100644 index 00000000..070a4ba1 --- /dev/null +++ b/dsa/chapter1/practice/time_complexity.py @@ -0,0 +1,74 @@ +""" +Classify the following code examples with their +runtimes. Write your responses as comments. +""" + + +def do_something(): + # runtime for do_something() is O(1) + pass + + +# what is the runtime for example 1? +def example_one(n): + for i in range(n): + do_something() + + +# what is the runtime for example 2? +def example_two(n): + do_something() + + +# what is the runtime for example 3? +def example_three(n): + for i in range(n): + for x in range(i): + do_something() + + +# what is the runtime for example 4? +def example_four(n): + for i in range(n // 2): + do_something() + + +# what is the runtime for example 5? +def example_five(n): + i = 0 + while i < n: + do_something() + i *= 2 + + +# what is the runtime for example 6? +def example_six(n): + for i in range(10): + do_something() + + +# what is the runtime for example 7? +def example_seven(n): + for i in range(2 ** n): + do_something() + + +# what is the runtime for example 8? +def example_eight(n): + for i in range(n): + for x in range(7): + do_something() + + +# what is the runtime for example 9? +def example_nine(n): + for i in range(n): + example_one(n) + + +# what is the runtime for example 10? +def example_ten(n): + i = 0 + while i < n: + do_something() + i += 2 diff --git a/dsa/chapter1/solutions/time_complexity.py b/dsa/chapter1/solutions/time_complexity.py new file mode 100644 index 00000000..79d477b9 --- /dev/null +++ b/dsa/chapter1/solutions/time_complexity.py @@ -0,0 +1,84 @@ +""" +Classify the following code examples with their +runtimes. Write your responses as comments. +""" + + +def do_something(): + # runtime for do_something() is O(1) + pass + + +# what is the runtime for example 1? +# runtime is O(n) +def example_one(n): + for i in range(n): + do_something() + + +# what is the runtime for example 2? +# runtime is O(1) +def example_two(n): + do_something() + + +# what is the runtime for example 3? +# runtime is O(n^2) +def example_three(n): + for i in range(n): + for x in range(i): + do_something() + + +# what is the runtime for example 4? +# runtime is O(n) +def example_four(n): + for i in range(n // 2): + do_something() + + +# what is the runtime for example 5? +# runtime is O(log(n)) +def example_five(n): + i = 0 + while i < n: + do_something() + i *= 2 + + +# what is the runtime for example 6? +# runtime is O(1) +def example_six(n): + for i in range(10): + do_something() + + +# what is the runtime for example 7? +# runtime is O(2**n) +def example_seven(n): + for i in range(2 ** n): + do_something() + + +# what is the runtime for example 8? +# runtime is O(n) +def example_eight(n): + for i in range(n): + for x in range(7): + do_something() + + +# what is the runtime for example 9? +# runtime is O(n^2) +def example_nine(n): + for i in range(n): + example_one(n) + + +# what is the runtime for example 10? +# runtime is O(n) +def example_ten(n): + i = 0 + while i < n: + do_something() + i += 2 From deac5ee52fc98fbe5b9af4564b95c2390a0f08f9 Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 15 Oct 2022 14:02:34 -0700 Subject: [PATCH 02/17] examples/practice/solutions directory layout --- dsa/chapter1/{ => examples}/recursion.py | 0 dsa/chapter2/{ => examples}/bst.py | 0 dsa/chapter2/{ => examples}/graph.py | 0 dsa/chapter2/{ => examples}/linked_list.py | 0 dsa/chapter2/{ => examples}/queue.py | 0 dsa/chapter2/{ => examples}/stack.py | 0 dsa/chapter2/practice/nodes_to_10.py | 35 +++++++++++++++++ dsa/chapter2/solutions/nodes_to_10.py | 39 +++++++++++++++++++ dsa/chapter3/{ => examples}/a_star.py | 0 dsa/chapter3/{ => examples}/bfs.py | 0 dsa/chapter3/{ => examples}/binary_search.py | 0 dsa/chapter3/{ => examples}/dfs.py | 0 dsa/chapter3/{ => examples}/mergesort.py | 0 dsa/chapter3/{ => examples}/quicksort.py | 0 dsa/chapter3/{ => examples}/selection_sort.py | 0 15 files changed, 74 insertions(+) rename dsa/chapter1/{ => examples}/recursion.py (100%) rename dsa/chapter2/{ => examples}/bst.py (100%) rename dsa/chapter2/{ => examples}/graph.py (100%) rename dsa/chapter2/{ => examples}/linked_list.py (100%) rename dsa/chapter2/{ => examples}/queue.py (100%) rename dsa/chapter2/{ => examples}/stack.py (100%) create mode 100644 dsa/chapter2/practice/nodes_to_10.py create mode 100644 dsa/chapter2/solutions/nodes_to_10.py rename dsa/chapter3/{ => examples}/a_star.py (100%) rename dsa/chapter3/{ => examples}/bfs.py (100%) rename dsa/chapter3/{ => examples}/binary_search.py (100%) rename dsa/chapter3/{ => examples}/dfs.py (100%) rename dsa/chapter3/{ => examples}/mergesort.py (100%) rename dsa/chapter3/{ => examples}/quicksort.py (100%) rename dsa/chapter3/{ => examples}/selection_sort.py (100%) diff --git a/dsa/chapter1/recursion.py b/dsa/chapter1/examples/recursion.py similarity index 100% rename from dsa/chapter1/recursion.py rename to dsa/chapter1/examples/recursion.py diff --git a/dsa/chapter2/bst.py b/dsa/chapter2/examples/bst.py similarity index 100% rename from dsa/chapter2/bst.py rename to dsa/chapter2/examples/bst.py diff --git a/dsa/chapter2/graph.py b/dsa/chapter2/examples/graph.py similarity index 100% rename from dsa/chapter2/graph.py rename to dsa/chapter2/examples/graph.py diff --git a/dsa/chapter2/linked_list.py b/dsa/chapter2/examples/linked_list.py similarity index 100% rename from dsa/chapter2/linked_list.py rename to dsa/chapter2/examples/linked_list.py diff --git a/dsa/chapter2/queue.py b/dsa/chapter2/examples/queue.py similarity index 100% rename from dsa/chapter2/queue.py rename to dsa/chapter2/examples/queue.py diff --git a/dsa/chapter2/stack.py b/dsa/chapter2/examples/stack.py similarity index 100% rename from dsa/chapter2/stack.py rename to dsa/chapter2/examples/stack.py diff --git a/dsa/chapter2/practice/nodes_to_10.py b/dsa/chapter2/practice/nodes_to_10.py new file mode 100644 index 00000000..e8af4d1f --- /dev/null +++ b/dsa/chapter2/practice/nodes_to_10.py @@ -0,0 +1,35 @@ +""" +Nodes to 10 + +Fill in the node class, then create nodes so that +printing start_node will print the numbers from 0 to 10. +""" + + +class Node: + def __init__(self, value): + self.value = value + + # create a neighbors list + # your code here + + def __repr__(self) -> str: + # start with just the node's value + ret = f"{self.value}, " + + # add all of the neighbors' values (recursively) to the string + for node in self.neighbors: + ret += f"{node}" + + return ret + + +start_node = Node(0) + +# add code that creates nodes and adds them as neighbors so that +# start_node is connected to 1, 1 is connected to 2, 2 is connected to 3, +# 3 is connected to 4, etc. If done correctly, printing start_node will +# print 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, +# your code here + +print(start_node) diff --git a/dsa/chapter2/solutions/nodes_to_10.py b/dsa/chapter2/solutions/nodes_to_10.py new file mode 100644 index 00000000..aef5a221 --- /dev/null +++ b/dsa/chapter2/solutions/nodes_to_10.py @@ -0,0 +1,39 @@ +""" +Nodes to 10 + +Fill in the node class, then create nodes so that +printing start_node will print the numbers from 0 to 10. +""" + + +class Node: + def __init__(self, value): + self.value = value + + # create a neighbors list + self.neighbors = [] + + def __repr__(self) -> str: + # start with just the node's value + ret = f"{self.value}, " + + # add all of the neighbors' values (recursively) to the string + for node in self.neighbors: + ret += f"{node}" + + return ret + + +start_node = Node(0) + +# add code that creates nodes and adds them as neighbors so that +# start_node is connected to 1, 1 is connected to 2, 2 is connected to 3, +# 3 is connected to 4, etc. If done correctly, printing start_node will +# print 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, +previous_node = start_node +for value in range(1, 11): + cur_node = Node(value) + previous_node.neighbors.append(cur_node) + previous_node = cur_node + +print(start_node) diff --git a/dsa/chapter3/a_star.py b/dsa/chapter3/examples/a_star.py similarity index 100% rename from dsa/chapter3/a_star.py rename to dsa/chapter3/examples/a_star.py diff --git a/dsa/chapter3/bfs.py b/dsa/chapter3/examples/bfs.py similarity index 100% rename from dsa/chapter3/bfs.py rename to dsa/chapter3/examples/bfs.py diff --git a/dsa/chapter3/binary_search.py b/dsa/chapter3/examples/binary_search.py similarity index 100% rename from dsa/chapter3/binary_search.py rename to dsa/chapter3/examples/binary_search.py diff --git a/dsa/chapter3/dfs.py b/dsa/chapter3/examples/dfs.py similarity index 100% rename from dsa/chapter3/dfs.py rename to dsa/chapter3/examples/dfs.py diff --git a/dsa/chapter3/mergesort.py b/dsa/chapter3/examples/mergesort.py similarity index 100% rename from dsa/chapter3/mergesort.py rename to dsa/chapter3/examples/mergesort.py diff --git a/dsa/chapter3/quicksort.py b/dsa/chapter3/examples/quicksort.py similarity index 100% rename from dsa/chapter3/quicksort.py rename to dsa/chapter3/examples/quicksort.py diff --git a/dsa/chapter3/selection_sort.py b/dsa/chapter3/examples/selection_sort.py similarity index 100% rename from dsa/chapter3/selection_sort.py rename to dsa/chapter3/examples/selection_sort.py From ada6782429cb49a09a4d8d63583f62a70bf531dc Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 15 Oct 2022 14:36:13 -0700 Subject: [PATCH 03/17] more realistic time_complexity examples --- dsa/chapter1/practice/time_complexity.py | 77 ++-------- .../practice/time_complexity_questions.py | 74 ++++++++++ dsa/chapter1/solutions/time_complexity.py | 134 ++++++++++-------- .../solutions/time_complexity_questions.py | 84 +++++++++++ 4 files changed, 246 insertions(+), 123 deletions(-) create mode 100644 dsa/chapter1/practice/time_complexity_questions.py create mode 100644 dsa/chapter1/solutions/time_complexity_questions.py diff --git a/dsa/chapter1/practice/time_complexity.py b/dsa/chapter1/practice/time_complexity.py index 070a4ba1..e3acacf8 100644 --- a/dsa/chapter1/practice/time_complexity.py +++ b/dsa/chapter1/practice/time_complexity.py @@ -1,74 +1,25 @@ """ -Classify the following code examples with their -runtimes. Write your responses as comments. +For each of the following time complexities, create +a function that has that time complexity. """ +# time complexity: O(1) +# your code here -def do_something(): - # runtime for do_something() is O(1) - pass +# time complexity: O(n) +# your code here -# what is the runtime for example 1? -def example_one(n): - for i in range(n): - do_something() +# time complexity: O(n^2) +# your code here -# what is the runtime for example 2? -def example_two(n): - do_something() +# time complexity: O(log(n)) +# your code here -# what is the runtime for example 3? -def example_three(n): - for i in range(n): - for x in range(i): - do_something() +# time complexity: O(n * log(n)) +# your code here - -# what is the runtime for example 4? -def example_four(n): - for i in range(n // 2): - do_something() - - -# what is the runtime for example 5? -def example_five(n): - i = 0 - while i < n: - do_something() - i *= 2 - - -# what is the runtime for example 6? -def example_six(n): - for i in range(10): - do_something() - - -# what is the runtime for example 7? -def example_seven(n): - for i in range(2 ** n): - do_something() - - -# what is the runtime for example 8? -def example_eight(n): - for i in range(n): - for x in range(7): - do_something() - - -# what is the runtime for example 9? -def example_nine(n): - for i in range(n): - example_one(n) - - -# what is the runtime for example 10? -def example_ten(n): - i = 0 - while i < n: - do_something() - i += 2 +# time complexity: O(2**n) +# your code here diff --git a/dsa/chapter1/practice/time_complexity_questions.py b/dsa/chapter1/practice/time_complexity_questions.py new file mode 100644 index 00000000..070a4ba1 --- /dev/null +++ b/dsa/chapter1/practice/time_complexity_questions.py @@ -0,0 +1,74 @@ +""" +Classify the following code examples with their +runtimes. Write your responses as comments. +""" + + +def do_something(): + # runtime for do_something() is O(1) + pass + + +# what is the runtime for example 1? +def example_one(n): + for i in range(n): + do_something() + + +# what is the runtime for example 2? +def example_two(n): + do_something() + + +# what is the runtime for example 3? +def example_three(n): + for i in range(n): + for x in range(i): + do_something() + + +# what is the runtime for example 4? +def example_four(n): + for i in range(n // 2): + do_something() + + +# what is the runtime for example 5? +def example_five(n): + i = 0 + while i < n: + do_something() + i *= 2 + + +# what is the runtime for example 6? +def example_six(n): + for i in range(10): + do_something() + + +# what is the runtime for example 7? +def example_seven(n): + for i in range(2 ** n): + do_something() + + +# what is the runtime for example 8? +def example_eight(n): + for i in range(n): + for x in range(7): + do_something() + + +# what is the runtime for example 9? +def example_nine(n): + for i in range(n): + example_one(n) + + +# what is the runtime for example 10? +def example_ten(n): + i = 0 + while i < n: + do_something() + i += 2 diff --git a/dsa/chapter1/solutions/time_complexity.py b/dsa/chapter1/solutions/time_complexity.py index 79d477b9..da8099a1 100644 --- a/dsa/chapter1/solutions/time_complexity.py +++ b/dsa/chapter1/solutions/time_complexity.py @@ -1,84 +1,98 @@ """ -Classify the following code examples with their -runtimes. Write your responses as comments. +For each of the following time complexities, create +a function that has that time complexity. The following solutions +are examples and not the only ways to have done this problem. """ +# time complexity: O(1) +def double_my_number(number): + x = number + x *= 2 + return x -def do_something(): - # runtime for do_something() is O(1) - pass - -# what is the runtime for example 1? -# runtime is O(n) -def example_one(n): +# time complexity: O(n) +def sum_till_n(n): + total = 0 for i in range(n): - do_something() + total += i + return total -# what is the runtime for example 2? -# runtime is O(1) -def example_two(n): - do_something() +# time complexity: O(n^2) +def print_triangle(n): + for row in range(n): + for column in range(row): + print("* ", end="") + print() -# what is the runtime for example 3? -# runtime is O(n^2) -def example_three(n): - for i in range(n): - for x in range(i): - do_something() +# time complexity: O(log(n)) +def sum_powers_of_two(max_number): + power_of_two = 1 + total = 0 -# what is the runtime for example 4? -# runtime is O(n) -def example_four(n): - for i in range(n // 2): - do_something() + while power_of_two < max_number: + total += power_of_two + power_of_two *= 2 + return total -# what is the runtime for example 5? -# runtime is O(log(n)) -def example_five(n): - i = 0 - while i < n: - do_something() - i *= 2 +# time complexity: O(n * log(n)) +def sum_many_powers_of_two(number_of_times): + total = 0 + for i in range(number_of_times): + # since sum_powers_of_two is O(log(n))and this for loop is O(n), + # the resulting time complexity is O(n * log(n)) + total += sum_powers_of_two(i) -# what is the runtime for example 6? -# runtime is O(1) -def example_six(n): - for i in range(10): - do_something() + return total -# what is the runtime for example 7? -# runtime is O(2**n) -def example_seven(n): - for i in range(2 ** n): - do_something() +# time complexity: O(2**n) +def get_binary_combinations(number_of_digits): + """ + Gets the combinations of binary numbers with number_of_digits digits + For example, get_binary_combinations(2) should give + ["00", "01", "10", "11"]. + """ + cur_options = ["0", "1"] + next_options = [] -# what is the runtime for example 8? -# runtime is O(n) -def example_eight(n): - for i in range(n): - for x in range(7): - do_something() + operations = 0 + # In total, this is O(2**n). It may be a bit confusing, but + # this is O(2**n) because of the fact that the current options + # doubles each time we go through the for loop, so it has to + # spend twice as long each time. + for i in range(number_of_digits - 1): + for option in cur_options: + next_options.append(option + "0") + next_options.append(option + "1") + operations += 1 + cur_options = next_options + next_options = [] -# what is the runtime for example 9? -# runtime is O(n^2) -def example_nine(n): - for i in range(n): - example_one(n) + print(f"took {operations} operations") + return cur_options + + +# for comparison, here's a very easy to understand +# function with O(2**n) runtime. +def regular_o_2_to_the_n(n): + operations = 0 + for i in range(2 ** n): + operations += 1 + print(f"took {operations} operations") -# what is the runtime for example 10? -# runtime is O(n) -def example_ten(n): - i = 0 - while i < n: - do_something() - i += 2 +# if you actually don't believe that get_binary_combinations is O(2**n), +# try running the below. +# as you can see, they have similar operational cost, +# meaning that get_binary_combinations really is O(2**n) +# times = 20 +# get_binary_combinations(times) +# regular_o_2_to_the_n(times) diff --git a/dsa/chapter1/solutions/time_complexity_questions.py b/dsa/chapter1/solutions/time_complexity_questions.py new file mode 100644 index 00000000..79d477b9 --- /dev/null +++ b/dsa/chapter1/solutions/time_complexity_questions.py @@ -0,0 +1,84 @@ +""" +Classify the following code examples with their +runtimes. Write your responses as comments. +""" + + +def do_something(): + # runtime for do_something() is O(1) + pass + + +# what is the runtime for example 1? +# runtime is O(n) +def example_one(n): + for i in range(n): + do_something() + + +# what is the runtime for example 2? +# runtime is O(1) +def example_two(n): + do_something() + + +# what is the runtime for example 3? +# runtime is O(n^2) +def example_three(n): + for i in range(n): + for x in range(i): + do_something() + + +# what is the runtime for example 4? +# runtime is O(n) +def example_four(n): + for i in range(n // 2): + do_something() + + +# what is the runtime for example 5? +# runtime is O(log(n)) +def example_five(n): + i = 0 + while i < n: + do_something() + i *= 2 + + +# what is the runtime for example 6? +# runtime is O(1) +def example_six(n): + for i in range(10): + do_something() + + +# what is the runtime for example 7? +# runtime is O(2**n) +def example_seven(n): + for i in range(2 ** n): + do_something() + + +# what is the runtime for example 8? +# runtime is O(n) +def example_eight(n): + for i in range(n): + for x in range(7): + do_something() + + +# what is the runtime for example 9? +# runtime is O(n^2) +def example_nine(n): + for i in range(n): + example_one(n) + + +# what is the runtime for example 10? +# runtime is O(n) +def example_ten(n): + i = 0 + while i < n: + do_something() + i += 2 From a4ce90d786dc222c93ea94c8ef04f2ad7b30e8b8 Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 15 Oct 2022 15:27:41 -0700 Subject: [PATCH 04/17] basic bst practice problem --- dsa/chapter2/practice/basic_bst.py | 109 ++++++++++++++++++++ dsa/chapter2/solutions/basic_bst.py | 149 ++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 dsa/chapter2/practice/basic_bst.py create mode 100644 dsa/chapter2/solutions/basic_bst.py diff --git a/dsa/chapter2/practice/basic_bst.py b/dsa/chapter2/practice/basic_bst.py new file mode 100644 index 00000000..70b14b79 --- /dev/null +++ b/dsa/chapter2/practice/basic_bst.py @@ -0,0 +1,109 @@ +""" +Let's create a very basic version of a BST that only +has addition capabilities. Your task will be to fill +in the node class and the BST class +""" + + +class BSTNode: + def __init__(self, key, value): + """ + set self.key to key + set self.value to the value + create a left neighbor/child and a right neighbor/child, each of which + start as None + """ + + # your code here + pass + + def add_recursively(self, other_node): + """ + Adds the other node to this node or this node's children. + If other_node's key is equal to this node's key, do nothing. + If other_node's key is less than this node's key, then: + - if this node's left child is None, set this node's left child + to that other node + - if this node's left child is not None, then add this node + recursively to the left child + If other_node's key is greater than this node's key, then: + - if this node's right child is None, set this node's right child + to that other node + - if this node's right child is not None, then add this node + recursively to the right child + """ + + # your code here + pass + + def get_value(self, key): + """ + Tries to return the value of the node whose key matches `key`. + If this node's key matches `key`, return this node's value. + If the key is less than this node's key: + - if left child is None, return 0 + - else, get the value recursively + If the key is greater than this node's key: + - if right child is None, return 0 + - else, get the value recursively + """ + + # your code here + pass + + def __str__(self): + """ + Creates and returns a string that looks like: + left_child, self value, right_child + However, if left child or right_child is None, don't add them + to the string. + """ + + # your code here + pass + + +class BST: + def __init__(self): + # set a root node to None + + # your code here + pass + + def __setitem__(self, item, value): + """ + create a new node whose key is item and whose value is value + then, if root is None, set root to that node. + else, add that node recursively. + """ + + # your code here + pass + + def __getitem__(self, item): + """ + Try to find the node with key that matches item. + If no match is found, return 0 + """ + + # your code here + pass + + def __repr__(self): + """ + Returns a string representation of the root + """ + pass + + +my_bst = BST() +my_bst[50] = 30 +my_bst[40] = 31 +my_bst[60] = 32 +my_bst[30] = 33 +my_bst[70] = 34 +my_bst[20] = 35 +my_bst[80] = 36 +my_bst[10] = 37 +my_bst[90] = 38 +print(my_bst) # 37, 35, 33, 31, 10, 32, 34, 36, 38 diff --git a/dsa/chapter2/solutions/basic_bst.py b/dsa/chapter2/solutions/basic_bst.py new file mode 100644 index 00000000..aea6922a --- /dev/null +++ b/dsa/chapter2/solutions/basic_bst.py @@ -0,0 +1,149 @@ +""" +Let's create a very basic version of a BST that only +has addition capabilities. Your task will be to fill +in the node class and the BST class +""" + + +class BSTNode: + def __init__(self, key, value): + """ + set self.key to key + set self.value to the value + create a left neighbor/child and a right neighbor/child, each of which + start as None + """ + + # set key, value + self.key, self.value = key, value + + # set left child, right child + self.left_child: BSTNode = None + self.right_child: BSTNode = None + + def add_recursively(self, other_node): + """ + Adds the other node to this node or this node's children. + If other_node's key is equal to this node's key, do nothing. + If other_node's key is less than this node's key, then: + - if this node's left child is None, set this node's left child + to that other node + - if this node's left child is not None, then add this node + recursively to the left child + If other_node's key is greater than this node's key, then: + - if this node's right child is None, set this node's right child + to that other node + - if this node's right child is not None, then add this node + recursively to the right child + """ + if other_node.key == self.key: + return # do nothing + + if other_node.key < self.key: + if self.left_child is None: + self.left_child = other_node + else: + self.left_child.add_recursively(other_node) + + if other_node.key > self.key: + if self.right_child is None: + self.right_child = other_node + else: + self.right_child.add_recursively(other_node) + + def get_value(self, key): + """ + Tries to return the value of the node whose key matches `key`. + If this node's key matches `key`, return this node's value. + If the key is less than this node's key: + - if left child is None, return 0 + - else, get the value recursively + If the key is greater than this node's key: + - if right child is None, return 0 + - else, get the value recursively + """ + if self.key == key: + return self.value + + if key < self.key: + if self.left_child is None: + return 0 + else: + return self.left_child.get_value(key) + + if key > self.key: + if self.right_child is None: + return 0 + else: + return self.right_child.get_value(key) + + def __str__(self): + """ + Creates and returns a string that looks like: + left_child, self value, right_child + However, if left child or right_child is None, don't add them + to the string. + """ + ret = "" + + # add left child to the string if it isn't None + if self.left_child is not None: + ret += str(self.left_child) + ", " + + # add self.value to the string + ret += str(self.value) + + # add right child to the string if it isn't None + if self.right_child is not None: + ret += ", " + str(self.right_child) + + return ret + + +class BST: + def __init__(self): + # set a root node to None + self.root = None + + def __setitem__(self, item, value): + """ + create a new node whose key is item and whose value is value + then, if root is None, set root to that node. + else, add that node recursively. + """ + # create the new node + new_node = BSTNode(item, value) + + # either add it recursively or set it as the root + if self.root is None: + self.root = new_node + else: + self.root.add_recursively(new_node) + + def __getitem__(self, item): + """ + Try to find the node with key that matches item. + If root is None or no match is found return 0 + """ + if self.root is None: + return 0 + return self.root.get_value(item) + + def __repr__(self): + """ + Returns a string representation of the root + """ + return str(self.root) + + +my_bst = BST() +my_bst[50] = 30 +my_bst[40] = 31 +my_bst[60] = 32 +my_bst[30] = 33 +my_bst[70] = 34 +my_bst[20] = 35 +my_bst[80] = 36 +my_bst[10] = 37 +my_bst[90] = 38 +print(my_bst) From 02bb8bd8e20fefced3dc38424f32a77890718f11 Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 22 Oct 2022 15:08:03 -0700 Subject: [PATCH 05/17] basic linked list practice problem --- dsa/chapter2/practice/basic_linked_list.py | 136 ++++++++++++++++++ dsa/chapter2/solutions/basic_linked_list.py | 147 ++++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 dsa/chapter2/practice/basic_linked_list.py create mode 100644 dsa/chapter2/solutions/basic_linked_list.py diff --git a/dsa/chapter2/practice/basic_linked_list.py b/dsa/chapter2/practice/basic_linked_list.py new file mode 100644 index 00000000..08ffd30d --- /dev/null +++ b/dsa/chapter2/practice/basic_linked_list.py @@ -0,0 +1,136 @@ +""" +In this problem, you will create a basic Doubly Linked List. +The goal is to be able to have nodes connected all the way +to 100. All you have to do is fill in the add_front and +add_back methods. +""" + + +class DoublyLinkedListNode: + def __init__(self, value) -> None: + self.value = value + + self.next = None + self.prev = None + + def __repr__(self): + return f"{self.value}" + + +class DoublyLinkedList: + def __init__(self) -> None: + """ + Creates a head and tail node. For convenience, + set the head to a node with the value `None` + and the tail to a node with the value `None`. + Sets head's next node as tail, and tail's previous + node as head. + Sets the size of the list to 0 as well. + + This way, the list will be "empty" when + the head's next node is the tail (and the tail's + previous node is the head). The purpose of these + nodes is to make insertion and deletion much faster. + They will not store any value besides `None` and can + be thought of as placeholders for the beginning and + end of the list + """ + # create the nodes + self.head = DoublyLinkedListNode(None) + self.tail = DoublyLinkedListNode(None) + + # set head's next to tail, and tail's prev to head + self.head.next, self.tail.prev = self.tail, self.head + + # set the size of the list to 0 + self.size = 0 + + def add_front(self, value): + """ + Adds a node with the provided value to the front + of the Doubly Linked List. Increases size by 1 as well. + + By "front of the Doubly Linked List," we mean that it + should be the node right after the placeholder head node. + + Ex: if you had nodes A, B, C, D, and you inserted node E, + then you would have A, E, B, C, D. A's next node would be + E, E's next node would be B, B's prev node would be E, + and E's prev node would be A. + """ + # create a node with the provided value + # add it to the front of the Doubly Linked List + # make sure to correctly set the prev/next nodes + # your code here + + # increase size by 1 + self.size += 1 + + def add_back(self, value): + """ + Adds a node with the provided value to the back + of the Doubly Linked List. Increases size by 1 as well. + + By "back of the Doubly Linked List," we mean that it should + be the node right before the placeholder tail node. + + Ex: if you had nodes A, B, C, D, and you inserted node E, + then you would have A, B, C, E, D. C's next node would be + E, E's next node would be D, D's prev node would be E, + and E's prev node would be C. + """ + # create a node with the provided value + # add it to the back of the Doubly Linked List + # make sure to correctly set the prev/next nodes + + # increase size by 1 + self.size += 1 + + def print_forward(self): + """ + Iterates through and prints all the nodes. + This should start at the head and end at the tail. + """ + # ignore self.head since self.head is a "placeholder" + cur_node = self.head.next + + while cur_node.next is not None: + print(cur_node, end=", ") + cur_node = cur_node.next + print() + + def print_backward(self): + """ + Iterates through and prints all the nodes. + This should start at the tail and end at the tail + """ + # ignore self.tail since self.tail is a "placeholder" + cur_node = self.tail.prev + + while cur_node.prev is not None: + print(cur_node, end=", ") + cur_node = cur_node.prev + print() + + def __len__(self): + """ + Returns the length of the list + """ + return self.size + + +our_doubly_linked_list = DoublyLinkedList() + +# add the numbers 50-99 +for i in range(50, 100): + our_doubly_linked_list.add_back(i) + +# add numbers 49 - 0 +for i in range(49, -1, -1): + our_doubly_linked_list.add_front(i) + +# print our list forward (0 -> 99) +our_doubly_linked_list.print_forward() + +# print our list backward (99 -> 9) +our_doubly_linked_list.print_backward() diff --git a/dsa/chapter2/solutions/basic_linked_list.py b/dsa/chapter2/solutions/basic_linked_list.py new file mode 100644 index 00000000..0719dea7 --- /dev/null +++ b/dsa/chapter2/solutions/basic_linked_list.py @@ -0,0 +1,147 @@ +""" +In this problem, you will create a basic Doubly Linked List. +The goal is to be able to have nodes connected all the way +to 100. All you have to do is fill in the add_front and +add_back methods. +""" + + +class DoublyLinkedListNode: + def __init__(self, value) -> None: + self.value = value + + self.next = None + self.prev = None + + def __repr__(self): + return f"{self.value}" + + +class DoublyLinkedList: + def __init__(self) -> None: + """ + Creates a head and tail node. For convenience, + set the head to a node with the value `None` + and the tail to a node with the value `None`. + Sets head's next node as tail, and tail's previous + node as head. + Sets the size of the list to 0 as well. + + This way, the list will be "empty" when + the head's next node is the tail (and the tail's + previous node is the head). The purpose of these + nodes is to make insertion and deletion much faster. + They will not store any value besides `None` and can + be thought of as placeholders for the beginning and + end of the list + """ + # create the nodes + self.head = DoublyLinkedListNode(None) + self.tail = DoublyLinkedListNode(None) + + # set head's next to tail, and tail's prev to head + self.head.next, self.tail.prev = self.tail, self.head + + # set the size of the list to 0 + self.size = 0 + + def add_front(self, value): + """ + Adds a node with the provided value to the front + of the Doubly Linked List. Increases size by 1 as well. + + By "front of the Doubly Linked List," we mean that it + should be the node right after the placeholder head node. + + Ex: if you had nodes A, B, C, D, and you inserted node E, + then you would have A, E, B, C, D. A's next node would be + E, E's next node would be B, B's prev node would be E, + and E's prev node would be A. + """ + # create a node with the provided value + new_node = DoublyLinkedListNode(value) + + # get the old second-to-front node + old_second_to_front_node = self.head.next + + # change the orders + old_second_to_front_node.prev, new_node.prev = new_node, self.head + self.head.next, new_node.next = new_node, old_second_to_front_node + + # increase size by 1 + self.size += 1 + + def add_back(self, value): + """ + Adds a node with the provided value to the back + of the Doubly Linked List. Increases size by 1 as well. + + By "back of the Doubly Linked List," we mean that it should + be the node right before the placeholder tail node. + + Ex: if you had nodes A, B, C, D, and you inserted node E, + then you would have A, B, C, E, D. C's next node would be + E, E's next node would be D, D's prev node would be E, + and E's prev node would be C. + """ + # create a node with the provided value + new_node = DoublyLinkedListNode(value) + + # get the old second-to-last node + old_second_to_last_node = self.tail.prev + + # change the orders + old_second_to_last_node.next, new_node.next = new_node, self.tail + self.tail.prev, new_node.prev = new_node, old_second_to_last_node + + # increase size by 1 + self.size += 1 + + def print_forward(self): + """ + Iterates through and prints all the nodes. + This should start at the head and end at the tail. + """ + # ignore self.head since self.head is a "placeholder" + cur_node = self.head.next + + while cur_node.next is not None: + print(cur_node, end=", ") + cur_node = cur_node.next + print() + + def print_backward(self): + """ + Iterates through and prints all the nodes. + This should start at the tail and end at the tail + """ + # ignore self.tail since self.tail is a "placeholder" + cur_node = self.tail.prev + + while cur_node.prev is not None: + print(cur_node, end=", ") + cur_node = cur_node.prev + print() + + def __len__(self): + """ + Returns the length of the list + """ + return self.size + + +our_doubly_linked_list = DoublyLinkedList() + +# add the numbers 50-99 +for i in range(50, 100): + our_doubly_linked_list.add_back(i) + +# add numbers 49 - 0 +for i in range(49, -1, -1): + our_doubly_linked_list.add_front(i) + +# print our list forward (0 -> 99) +our_doubly_linked_list.print_forward() + +# print our list backward (99 -> 9) +our_doubly_linked_list.print_backward() From 072ae874250400f6c5227ff10a1519b16acf4587 Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 12 Nov 2022 14:27:33 -0800 Subject: [PATCH 06/17] queue practice problem --- dsa/chapter2/practice/restaurant_queue.py | 56 ++++++++++++++++++++++ dsa/chapter2/solutions/restaurant_queue.py | 54 +++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 dsa/chapter2/practice/restaurant_queue.py create mode 100644 dsa/chapter2/solutions/restaurant_queue.py diff --git a/dsa/chapter2/practice/restaurant_queue.py b/dsa/chapter2/practice/restaurant_queue.py new file mode 100644 index 00000000..3ca77d68 --- /dev/null +++ b/dsa/chapter2/practice/restaurant_queue.py @@ -0,0 +1,56 @@ +""" +Cook cook cook, orders all day + +As a chef in a restaurant, you cook a bunch of dishes +You can only take one order at a time, and you're tired of having +people complain at you when you don't do their order first. So you decide to +set up a system where you accumulate a "list" of orders and cook one order +-- the first order that was put into the "list" -- at a time. + +Your job is to implement this "list" as an OrderQueue. +You should be able to add new orders into your OrderQueue +and remove finished orders from your OrderQueue. +Starter code is provided. +""" + + +class OrderQueue: + def __init__(self) -> None: + self.orders = [] + + def dequeue(self) -> str: + """ + Removes the first order in the OrderQueue + and returns it + """ + # your code here + pass + + def enqueue(self, order: str) -> None: + """ + Inserts the order into the OrderQueue + + Args: + order: str - the order to be inserted into the OrderQueue + """ + # your code here + pass # your job is to implement this + + def __len__(self): + return len(self.orders) + + +# test code +uncooked_orders = OrderQueue() + +# 3 customers ordered at the same time +uncooked_orders.enqueue("a medium rare steak") +uncooked_orders.enqueue("six gyoza") +uncooked_orders.enqueue("two enchiladas") + +# now you cook them one by one +for order in range(len(uncooked_orders)): + # this should print + # the medium rare steak first, then the gyoza, and finally the enchiladas + print(f"finished cooking {uncooked_orders.dequeue()}") +print(f"done! (exactly {len(uncooked_orders)} orders left)") diff --git a/dsa/chapter2/solutions/restaurant_queue.py b/dsa/chapter2/solutions/restaurant_queue.py new file mode 100644 index 00000000..2eb55e07 --- /dev/null +++ b/dsa/chapter2/solutions/restaurant_queue.py @@ -0,0 +1,54 @@ +""" +Cook cook cook, orders all day + +As a chef in a restaurant, you cook a bunch of dishes +You can only take one order at a time, and you're tired of having +people complain at you when you don't do their order first. So you decide to +set up a system where you accumulate a "list" of orders and cook one order +-- the first order that was put into the "list" -- at a time. + +Your job is to implement this "list" as an OrderQueue. +You should be able to add new orders into your OrderQueue +and remove finished orders from your OrderQueue. +Starter code is provided. +""" + + +class OrderQueue: + def __init__(self) -> None: + self.orders = [] + + def dequeue(self) -> str: + """ + Removes the first order in the OrderQueue + and returns it + """ + return self.orders.pop(0) + + def enqueue(self, order: str) -> None: + """ + Inserts the order into the OrderQueue + + Args: + order: str - the order to be inserted into the OrderQueue + """ + self.orders.append(order) + + def __len__(self): + return len(self.orders) + + +# test code +uncooked_orders = OrderQueue() + +# 3 customers ordered at the same time +uncooked_orders.enqueue("a medium rare steak") +uncooked_orders.enqueue("six gyoza") +uncooked_orders.enqueue("two enchiladas") + +# now you cook them one by one +for order in range(len(uncooked_orders)): + # this should print + # the medium rare steak first, then the gyoza, and finally the enchiladas + print(f"finished cooking {uncooked_orders.dequeue()}") +print(f"done! (exactly {len(uncooked_orders)} orders left)") From b24ba4e07699ca5f970ea22826557e9106015e6c Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 12 Nov 2022 15:02:24 -0800 Subject: [PATCH 07/17] stack practice problem --- dsa/chapter2/practice/restaurant_queue.py | 2 +- dsa/chapter2/practice/word_reversal.py | 66 +++++++++++++++++++++++ dsa/chapter2/solutions/word_reversal.py | 62 +++++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 dsa/chapter2/practice/word_reversal.py create mode 100644 dsa/chapter2/solutions/word_reversal.py diff --git a/dsa/chapter2/practice/restaurant_queue.py b/dsa/chapter2/practice/restaurant_queue.py index 3ca77d68..1aea151a 100644 --- a/dsa/chapter2/practice/restaurant_queue.py +++ b/dsa/chapter2/practice/restaurant_queue.py @@ -34,7 +34,7 @@ def enqueue(self, order: str) -> None: order: str - the order to be inserted into the OrderQueue """ # your code here - pass # your job is to implement this + pass def __len__(self): return len(self.orders) diff --git a/dsa/chapter2/practice/word_reversal.py b/dsa/chapter2/practice/word_reversal.py new file mode 100644 index 00000000..dfd89765 --- /dev/null +++ b/dsa/chapter2/practice/word_reversal.py @@ -0,0 +1,66 @@ +""" +Ever wanted to reverse a word a harder way? +Well, look no further than this problem that puts your +knowledge of Stacks to the test in order to solve a problem +that is already solveable by python builtins! + +Your job is to reverse a string by using a stack, +adding every letter in the string (starting from the beginning of the string) +into the stack, and then popping every letter from the stack. +If done correctly, this will result in a reversed version of the string. + +Starter code is given. +""" + + +class Stack: + def __init__(self) -> None: + """ + Initializes the stack. The back of the list will be + the top of the stack (so self.items[-1] is the first item + in the stack) + """ + self.items = [] + + def push(self, letter: str) -> None: + """ + Adds the letter to the stack. The letter should end up + on the *top* of the stack (the back of the list) + """ + # your code here + pass + + def pop(self) -> str: + """ + Removes the top letter from the stack. Returns this letter. + """ + # your code here + pass + + def __len__(self) -> int: + return len(self.items) + + +def reverse_word(word: str) -> str: + letter_stack = Stack() + + # push every letter in the word (starting from the beginning of the word) + # into the stack + for letter in word: + # your code here + pass + + reversed_word = "" + # pop every letter from the stack and add it to our reversed word + for i in range(len(letter_stack)): + # your code here + pass + + return reversed_word + + +# test code +print(reverse_word("boj doog")) +print(reverse_word("racecar")) +print(reverse_word("a man a plan a canal panama")) +print(reverse_word("read kool")) diff --git a/dsa/chapter2/solutions/word_reversal.py b/dsa/chapter2/solutions/word_reversal.py new file mode 100644 index 00000000..e8372fcb --- /dev/null +++ b/dsa/chapter2/solutions/word_reversal.py @@ -0,0 +1,62 @@ +""" +Ever wanted to reverse a word a harder way? +Well, look no further than this problem that puts your +knowledge of Stacks to the test in order to solve a problem +that is already solveable by python builtins! + +Your job is to reverse a string by using a stack, +adding every letter in the string (starting from the beginning of the string) +into the stack, and then popping every letter from the stack. +If done correctly, this will result in a reversed version of the string. + +Starter code is given. +""" + + +class Stack: + def __init__(self) -> None: + """ + Initializes the stack. The back of the list will be + the top of the stack (so self.items[-1] is the first item + in the stack) + """ + self.items = [] + + def push(self, letter: str) -> None: + """ + Adds the letter to the stack. The letter should end up + on the *top* of the stack (the back of the list) + """ + self.items.append(letter) + + def pop(self) -> str: + """ + Removes the top letter from the stack. Returns this letter. + """ + return self.items.pop() + + def __len__(self) -> int: + return len(self.items) + + +def reverse_word(word: str) -> str: + letter_stack = Stack() + + # push every letter in the word (starting from the beginning of the word) + # into the stack + for letter in word: + letter_stack.push(letter) + + reversed_word = "" + # pop every letter from the stack and add it to our reversed word + for i in range(len(letter_stack)): + reversed_word += letter_stack.pop() + + return reversed_word + + +# test code +print(reverse_word("boj doog")) +print(reverse_word("racecar")) +print(reverse_word("a man a plan a canal panama")) +print(reverse_word("read kool")) From c2d45e04c112d99b6c37379fbdf8d415e17ef9fa Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 10 Dec 2022 08:10:06 -0800 Subject: [PATCH 08/17] linear search to contrast w/ binary search --- dsa/chapter3/examples/linear_search.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 dsa/chapter3/examples/linear_search.py diff --git a/dsa/chapter3/examples/linear_search.py b/dsa/chapter3/examples/linear_search.py new file mode 100644 index 00000000..e8821336 --- /dev/null +++ b/dsa/chapter3/examples/linear_search.py @@ -0,0 +1,14 @@ +def linear_search(arr, val) -> int: + """ + Search the provided array for the provided value + and get the index, if found + Arguments: + arr - the array to search in + val - the value to search for + Returns: + int - the index of the value if it was found, else -1 + """ + for i in range(len(arr)): + if arr[i] == val: + return i + return -1 From 6b144f595e3b1db83c369e6ad86cb662fab3ed85 Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 10 Dec 2022 14:10:09 -0800 Subject: [PATCH 09/17] make the BT example have deletion --- dsa/chapter2/examples/bst.py | 240 ++++++++++++++++++---------- dsa/chapter2/practice/basic_bst.py | 7 +- dsa/chapter2/solutions/basic_bst.py | 6 +- 3 files changed, 165 insertions(+), 88 deletions(-) diff --git a/dsa/chapter2/examples/bst.py b/dsa/chapter2/examples/bst.py index a257a547..f3d8c4d5 100644 --- a/dsa/chapter2/examples/bst.py +++ b/dsa/chapter2/examples/bst.py @@ -1,107 +1,179 @@ class Node: - # this class is meant to be used with BinaryTree def __init__(self, key, value) -> None: + # set key/value self.key = key self.value = value - self.left = None - self.right = None + + # set children to None + self.left: Node = None + self.right: Node = None def __str__(self) -> str: - ret = "" - if self.left is not None: - ret += str(self.left) - ret += self.plain_str() + "\n" - if self.right is not None: - ret += str(self.right) - return ret - - def height(self) -> int: - if self.left is None and self.right is None: - return 1 - if self.left is not None and self.right is None: - return 1 + self.left.height() - if self.right is not None and self.left is None: - return 1 + self.right.height() - return 1 + max(self.left.height(), self.right.height()) - - def plain_str(self) -> str: return str(self.key) + ": " + str(self.value) + def __repr__(self) -> str: + return str(self) + class BinaryTree: - def __init__(self, default_val=None) -> None: + def __init__(self) -> None: self.root = None - self.default_val = default_val - - def recursive_contains_key(self, key, current) -> bool: - if current is None: - return False - - if current.key == key: - return True - - if key < current.key: - return self.recursive_contains_key(key, current.left) - return self.recursive_contains_key(key, current.right) - - def contains_key(self, key) -> bool: - return self.recursive_contains_key(key, self.root) - - def recursive_add(self, key, value, current): - if current.key == key: - current.value = value - return True - if key < current.key: - if current.left is not None: - return self.recursive_add(key, value, current.left) - current.left = BinaryTree.Node(key, value) - return True - - if current.right is not None: - return self.recursive_add(key, value, current.right) - current.right = BinaryTree.Node(key, value) - return True - - def add(self, key, value) -> bool: + + def search(self, key): + """ + Searches the binary tree for a node with the given key. + Takes advantage of the fact that, in a binary tree, + keys with lesser values go on the left and keys with greater + values go on the right + """ + current = self.root + while current is not None: + if key == current.key: + return current.value + elif key < current.key: + current = current.left + else: + current = current.right + raise Exception("KEY NOT FOUND") + + def get_height(self): + """ + Gets the height of the binary tree. + """ + if self.root is None: + return 0 + + height = 0 + next = [self.root] + while len(next) != 0: + temp_next = [] + height += 1 + for node in next: + if node.left is not None: + temp_next.append(node.left) + if node.right is not None: + temp_next.append(node.right) + next = temp_next + return height + + def insert_node(self, node: Node) -> None: + """ + Tries to insert a node into the tree. + If a node with the same key is already found, + sets the value of that node to the value of + the provided node + """ + # case where root is None if self.root is None: - self.root = BinaryTree.Node(key, value) - return True - return self.recursive_add(key, value, self.root) - - def recursive_get(self, key, current): - if current is None: - raise Exception("KEY NOT FOUND") - if current.key == key: - return current.value - if key < current.key: - return self.recursive_get(key, current.left) - return self.recursive_get(key, current.right) - - def get(self, key): - return self.recursive_get(key, self.root) + self.root = node + return - def __setitem__(self, key, value): - self.add(key, value) + # go through the nodes + current = self.root + while current is not None: + if node.key == current.key: + current.value = node.value + return + elif node.key < current.key: + if current.left is None: + current.left = node + return + else: + current = current.left + else: + if current.right is None: + current.right = node + return + else: + current = current.right + + def remove_node(self, parent, right_or_left="right"): + """ + Helper method to remove a node. + Notice how we have to set `parent.xxx` to something. + This is because, in order to remove a node from a binary + tree, what you are really doing is getting rid of all + references to that node. So, we make sure to change + the value stored in `parent.xxx` to a different node + (or `None`) so that we remove the node we're trying to get rid of + """ + if right_or_left == "right": + node = parent.right + else: + node = parent.left + + if node.right is not None: + temp = node.left + if right_or_left == "right": + parent.right = node.right + else: + parent.left = node.right + + if temp is not None: + self.insert_node(temp) + elif node.left is not None: + temp = node.right + if right_or_left == "right": + parent.right = node.left + else: + parent.left = node.left + if temp is not None: + self.insert_node(temp) + else: + if right_or_left == "right": + parent.right = None + else: + parent.left = None def __getitem__(self, key): - return self.get(key) - - def __str__(self) -> str: - ret = "{\n" - if self.root is not None: - ret += str(self.root) - ret += "}" - return ret + return self.search(key) - def __repr__(self) -> str: - return str(self) + def __setitem__(self, key, value): + self.insert_node(Node(key, value)) + + def __delitem__(self, key): + """ + Deletes an entry from the binary tree + """ + # case where key to delete is the root + if self.root is not None and self.root.key == key: + if self.root.right is not None: + temp = self.root.left + self.root = self.root.right + if temp is not None: + self.insert_node(temp) + elif self.root.left is not None: + temp = self.root.right + self.root = self.root.left + if temp is not None: + self.insert_node(temp) + else: + self.root = None + + # regular cases + current = self.root + while current is not None: + if current.left is not None and current.left.key == key: + self.remove_node(current, "left") + break + if current.right is not None and current.right.key == key: + self.remove_node(current, "right") + break + + if key < current.key: + current = current.left + if key > current.key: + current = current.right def print_structure(self) -> None: + """ + Prints out what the binary tree looks like + """ if self.root is None: print("{}") return - height = self.root.height() + height = self.get_height() spacing = 6 total_width = spacing * (2**height) @@ -120,7 +192,7 @@ def print_structure(self) -> None: print(" " * spacing, end="") next_generation.extend([None] * 2) else: - print(node.plain_str(), end="") + print(node, end="") next_generation.extend([node.left, node.right]) # print a newline print() @@ -137,7 +209,7 @@ def print_structure(self) -> None: myBinaryTree[22] = 11 myBinaryTree[44] = 33 myBinaryTree[55] = 22 -print(myBinaryTree) +del myBinaryTree[33] # to see how it internally arranges data myBinaryTree.print_structure() diff --git a/dsa/chapter2/practice/basic_bst.py b/dsa/chapter2/practice/basic_bst.py index 70b14b79..5f6469fa 100644 --- a/dsa/chapter2/practice/basic_bst.py +++ b/dsa/chapter2/practice/basic_bst.py @@ -1,7 +1,9 @@ """ Let's create a very basic version of a BST that only -has addition capabilities. Your task will be to fill -in the node class and the BST class +has insertion capabilities. Your task will be to fill +in the node class and the BST class. The structure is somewhat +different from the BST that was given as an example, but +the logic is the same. """ @@ -93,6 +95,7 @@ def __repr__(self): """ Returns a string representation of the root """ + # your code here pass diff --git a/dsa/chapter2/solutions/basic_bst.py b/dsa/chapter2/solutions/basic_bst.py index aea6922a..5186e4e3 100644 --- a/dsa/chapter2/solutions/basic_bst.py +++ b/dsa/chapter2/solutions/basic_bst.py @@ -1,7 +1,9 @@ """ Let's create a very basic version of a BST that only -has addition capabilities. Your task will be to fill -in the node class and the BST class +has insertion capabilities. Your task will be to fill +in the node class and the BST class. The structure is somewhat +different from the BST that was given as an example, but +the logic is the same. """ From 1bc55ead09d156e00197b4c37718587e70a79e74 Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 10 Dec 2022 14:50:42 -0800 Subject: [PATCH 10/17] searching practice --- dsa/chapter3/practice/searching.py | 124 ++++++++++++++++++++++++++++ dsa/chapter3/solutions/searching.py | 121 +++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 dsa/chapter3/practice/searching.py create mode 100644 dsa/chapter3/solutions/searching.py diff --git a/dsa/chapter3/practice/searching.py b/dsa/chapter3/practice/searching.py new file mode 100644 index 00000000..fb3bb591 --- /dev/null +++ b/dsa/chapter3/practice/searching.py @@ -0,0 +1,124 @@ +""" +Let's see the difference between linear and binary searches! +Some of the algorithm is already done for you, but you +will have to fill in some areas. + +Then, run the code and you can see the results +""" + +import random +from datetime import datetime as d + + +def linear_search(arr, val) -> int: + """ + Linear Search - iterates through all the items in the array and checks + equality with the provided value. If the value matches, returns + the index of that value. Else, returns -1 + Arguments: + arr - the array to search + val - the value to search for + Returns: + int - index of the value on success, -1 on failure + """ + for i in range(len(arr)): + # your code here + pass + return -1 + + +def binary_search(arr, val) -> int: + """ + Binary Search - checks the list for a value using a binary search. + Only works on sorted lists since it assumes that all the values + in indexes greater than i are greater and all the values in + indexes less than i are less. + Arguments: + arr - the array to search + val - the value to search for + Returns: + int - index of the value on success, -1 on failure + """ + low = 0 + high = len(arr) - 1 + while low <= high: + current = (low + high) // 2 + if val == arr[current]: + # your code here + pass + elif val < arr[current]: + # your code here + pass + else: # val > arr[current] + # your code here + pass + return -1 + + +# example 1 - sorted list +# the below demonstrates the binary search is faster than +# linear search on sorted lists +size = 100000 +lst_1 = [i for i in range(size)] + +tests = 3 +for i in range(tests): + print(f"sorted test #{i+1}:") + print("searching linearly") + target = random.randint(0, size) + linear_start = d.now() + linear_result = linear_search(lst_1, target) + linear_end = d.now() + print( + "finished searching linearly in " + + f"{(linear_end - linear_start).total_seconds()} seconds " + + f"and got the {'right' if linear_result == target else 'wrong'} result" + + f" ({linear_result})" + ) + + print("searching binarily") + binary_start = d.now() + binary_result = binary_search(lst_1, target) + binary_end = d.now() + print( + "finished searching binarily in " + + f"{(binary_end - binary_start).total_seconds()} seconds " + + f"and got the {'right' if binary_result == target else 'wrong'} result" + + f" ({binary_result})" + ) + print() + +# example 2 - unsorted list +# the below demonstrates that binary search doesn't work on unsorted +# lists, but linear search does +size = 100000 +lst_2 = [i for i in range(size)] +random.shuffle(lst_2) + +tests = 3 +for i in range(tests): + print(f"unsorted test #{i+1}:") + print("searching linearly") + idx = random.randint(0, size) + target = lst_2[idx] + linear_start = d.now() + linear_result = linear_search(lst_2, target) + linear_end = d.now() + print( + "finished searching linearly in " + + f"{(linear_end - linear_start).total_seconds()} seconds " + + f"and got the {'right' if linear_result == idx else 'wrong'} result" + + f" ({linear_result})" + ) + + print("searching binarily") + binary_start = d.now() + binary_result = binary_search(lst_2, target) + binary_end = d.now() + print( + "finished searching binarily in " + + f"{(binary_end - binary_start).total_seconds()} seconds " + + f"and got the {'right' if binary_result == idx else 'wrong'} result" + + f" ({binary_result})" + ) + print() diff --git a/dsa/chapter3/solutions/searching.py b/dsa/chapter3/solutions/searching.py new file mode 100644 index 00000000..8b6d332f --- /dev/null +++ b/dsa/chapter3/solutions/searching.py @@ -0,0 +1,121 @@ +""" +Let's see the difference between linear and binary searches! +Some of the algorithm is already done for you, but you +will have to fill in some areas. + +Then, run the code and you can see the results +""" + +import random +from datetime import datetime as d + + +def linear_search(arr, val) -> int: + """ + Linear Search - iterates through all the items in the array and checks + equality with the provided value. If the value matches, returns + the index of that value. Else, returns -1 + Arguments: + arr - the array to search + val - the value to search for + Returns: + int - index of the value on success, -1 on failure + """ + for i in range(len(arr)): + if arr[i] == val: + return i + return -1 + + +def binary_search(arr, val) -> int: + """ + Binary Search - checks the list for a value using a binary search. + Only works on sorted lists since it assumes that all the values + in indexes greater than i are greater and all the values in + indexes less than i are less. + Arguments: + arr - the array to search + val - the value to search for + Returns: + int - index of the value on success, -1 on failure + """ + low = 0 + high = len(arr) - 1 + while low <= high: + current = (low + high) // 2 + if val == arr[current]: + return current + elif val < arr[current]: + high = current - 1 + else: # val > arr[current] + low = current + 1 + return -1 + + +# example 1 - sorted list +# the below demonstrates the binary search is faster than +# linear search on sorted lists +size = 100000 +lst_1 = [i for i in range(size)] + +tests = 3 +for i in range(tests): + print(f"sorted test #{i+1}:") + print("searching linearly") + target = random.randint(0, size) + linear_start = d.now() + linear_result = linear_search(lst_1, target) + linear_end = d.now() + print( + "finished searching linearly in " + + f"{(linear_end - linear_start).total_seconds()} seconds " + + f"and got the {'right' if linear_result == target else 'wrong'} result" + + f" ({linear_result})" + ) + + print("searching binarily") + binary_start = d.now() + binary_result = binary_search(lst_1, target) + binary_end = d.now() + print( + "finished searching binarily in " + + f"{(binary_end - binary_start).total_seconds()} seconds " + + f"and got the {'right' if binary_result == target else 'wrong'} result" + + f" ({binary_result})" + ) + print() + +# example 2 - unsorted list +# the below demonstrates that binary search doesn't work on unsorted +# lists, but linear search does +size = 100000 +lst_2 = [i for i in range(size)] +random.shuffle(lst_2) + +tests = 3 +for i in range(tests): + print(f"unsorted test #{i+1}:") + print("searching linearly") + idx = random.randint(0, size) + target = lst_2[idx] + linear_start = d.now() + linear_result = linear_search(lst_2, target) + linear_end = d.now() + print( + "finished searching linearly in " + + f"{(linear_end - linear_start).total_seconds()} seconds " + + f"and got the {'right' if linear_result == idx else 'wrong'} result" + + f" ({linear_result})" + ) + + print("searching binarily") + binary_start = d.now() + binary_result = binary_search(lst_2, target) + binary_end = d.now() + print( + "finished searching binarily in " + + f"{(binary_end - binary_start).total_seconds()} seconds " + + f"and got the {'right' if binary_result == idx else 'wrong'} result" + + f" ({binary_result})" + ) + print() From 8c4f09ecfdd8b3ebef94248811005ed1fa571682 Mon Sep 17 00:00:00 2001 From: Harsh Panchal Date: Sat, 10 Dec 2022 15:41:04 -0800 Subject: [PATCH 11/17] quicksort practice --- dsa/chapter3/practice/quicksort.py | 89 ++++++++++++++++++++++++++++ dsa/chapter3/solutions/quicksort.py | 91 +++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 dsa/chapter3/practice/quicksort.py create mode 100644 dsa/chapter3/solutions/quicksort.py diff --git a/dsa/chapter3/practice/quicksort.py b/dsa/chapter3/practice/quicksort.py new file mode 100644 index 00000000..a5b76662 --- /dev/null +++ b/dsa/chapter3/practice/quicksort.py @@ -0,0 +1,89 @@ +lst = [-3, 5, -10, 18, 74, 22, 1, -40] + + +def quicksort(arr: list): + """ + quicksort_recursive takes in the list you are sorting, the first index of + the sublist you want to sort, and the last index of the sublist you want + to sort, in that order + + For the first call to quicksort_recursive, the first index and last index + should be 0 and the index to the last item of the list respectively + + Make a call to quicksort_recursive with the appropriate arguments below + """ + # your code here + + +def quicksort_recursive(arr, low, high): + """ + Arguments: + arr: list, the entire list we are sorting, + low: int, the first index of the sublist we are sorting + high: int, the last index of the sublist we are sorting + """ + # base case + if low >= high: + return + + # partition the sublist and return the pivot_index + pivot_index = partition(arr, low, high) # NOQA + + # recursive calls + """ + After the list has been partitioned around the pivot_index, we need to + call quicksort_recursive on the two sublists: the one to the left of the + pivot_index, and the one to the right + + We do this on the right side by setting the high index to one less than + pivot_index, and on the left side by setting the low index to one higher + than pivot_index + + Make calls to quicksort_recursive with the appropriate arguments below + """ + # your code here + + +def partition(arr, low, high): + """ + Partition takes a pivot (in our case, arr[high]), and accomplishes the + following: + All of the elements between low and high that are SMALLER than the + pivot are placed to the LEFT of the pivot. + Conversely, all elements between low and high that are LARGER than + the pivot are placed to the RIGHT of the pivot + This has the side effect that the location of pivot after the partition has + taken place is the same as if the list was sorted. Of course, the areas to + the left and right of the pivot are not yet sorted. + """ + i = low # initialize i to the left side of what we are sorting + + """ + Create a for loop that creates an index j, and loops through indexes low + (inclusive) to high (exclusive) + """ + # your code here + """ + Inside our loop, we are trying to find items (arr[j]) that are less than + our pivot (arr[high]). If we find one, we want to swap our item + (arr[j]) with arr[i], an item thats to the left side of our + sublist. Then, we will increment i by one, so we don't continuously + swap with same arr[i] over and over again. + + Create an if statement to do this below + """ + # your code here + + """ + Our pivot (arr[high]) is still on the right side of our sublist. Let's swap + it with arr[i] so it moves to the right spot. + """ + # your code here + + # return the pivot_index + return i + + +if __name__ == "__main__": + quicksort(lst, 0, len(lst) - 1) + print(lst) diff --git a/dsa/chapter3/solutions/quicksort.py b/dsa/chapter3/solutions/quicksort.py new file mode 100644 index 00000000..ba717384 --- /dev/null +++ b/dsa/chapter3/solutions/quicksort.py @@ -0,0 +1,91 @@ +lst = [-3, 5, -10, 18, 74, 22, 1, -40] + + +def quicksort(arr: list): + """ + quicksort_recursive takes in the list you are sorting, the first index of + the sublist you want to sort, and the last index of the sublist you want + to sort, in that order + + For the first call to quicksort_recursive, the first index and last index + should be 0 and the index to the last item of the list respectively + + Make a call to quicksort_recursive with the appropriate arguments below + """ + quicksort_recursive(arr, 0, len(arr) - 1) + + +def quicksort_recursive(arr, low, high): + """ + Arguments: + arr: list, the entire list we are sorting, + low: int, the first index of the sublist we are sorting + high: int, the last index of the sublist we are sorting + """ + # base case + if low >= high: + return + + pivot_index = partition(arr, low, high) + + # recursive calls + """ + After the list has been partitioned around the pivot_index, we need to + call quicksort_recursive on the two sublists: the one to the left of the + pivot_index, and the one to the right + + We do this on the right side by setting the high index to one less than + pivot_index, and on the left side by setting the low index to one higher + than pivot_index + + Make calls to quicksort_recursive with the appropriate arguments below + """ + quicksort_recursive(arr, low, pivot_index - 1) # right side + quicksort_recursive(arr, pivot_index + 1, high) # left side + + +def partition(arr, low, high): + """ + Partition takes a pivot (in our case, arr[high]), and accomplishes the + following: + All of the elements between low and high that are SMALLER than the + pivot are placed to the LEFT of the pivot. + Conversely, all elements between low and high that are LARGER than + the pivot are placed to the RIGHT of the pivot + This has the side effect that the location of pivot after the partition has + taken place is the same as if the list was sorted. Of course, the areas to + the left and right of the pivot are not yet sorted. + """ + i = low # initialize i to the left side of what we are sorting + + """ + Create a for loop that creates an index j, and loops through indexes low + (inclusive) to high (exclusive) + """ + for j in range(low, high): # iterate through the list with arr[j] + """ + In our loop, we are trying to find items (arr[j]) that are less than + our pivot (arr[high]). If we find one, we want to swap our item + (arr[j]) with arr[i], an item thats to the left side of our + sublist. Then, we will increment i by one, so we don't continuously + swap with same arr[i] over and over again. + + Create an if statement to do this below + """ + if arr[j] < arr[high]: + # swap arr[j] with arr[i] so arr[j] is at the left side + arr[i], arr[j] = arr[j], arr[i] + i += 1 + """ + Our pivot (arr[high]) is still on the right side of our sublist. Let's swap + it with arr[i] so it moves to the right spot. + """ + arr[i], arr[high] = arr[high], arr[i] + + # return the pivot_index + return i + + +if __name__ == "__main__": + quicksort(lst, 0, len(lst) - 1) + print(lst) From 8fb857c090982b0c5140a1fe15eb84ff30772f97 Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 21 Jan 2023 15:48:16 -0800 Subject: [PATCH 12/17] bfs dfs practice problem --- dsa/chapter3/practice/bfs_dfs.py | 101 ++++++++++++++++++++++++++++++ dsa/chapter3/solutions/bfs_dfs.py | 87 +++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 dsa/chapter3/practice/bfs_dfs.py create mode 100644 dsa/chapter3/solutions/bfs_dfs.py diff --git a/dsa/chapter3/practice/bfs_dfs.py b/dsa/chapter3/practice/bfs_dfs.py new file mode 100644 index 00000000..a15901f8 --- /dev/null +++ b/dsa/chapter3/practice/bfs_dfs.py @@ -0,0 +1,101 @@ +""" +In this practice problem, we practice implementing +breadth first search and depth first search and see them +in action +""" + + +class Node: + def __init__(self, value: int) -> None: + self.value: int = value + self.children = [] + + def __str__(self) -> str: + return str(self.value) + + def __repr__(self) -> str: + return str(self) + + +def BFS(start_node: Node): + """ + Implement a breadth-first search algorithm, + that prints the nodes that you visit as you go. + + Remember, a breadth-first search algorithm works by + visiting all the children in a certain depth before + advancing to the next depth + + Note that your function will be slightly different from + the one in the example since the nodes in this file + have children and not neighbors + """ + # first, initialize an empty list of all visited nodes + # next, initialize a list of all the nodes at the current depth + # and have it contain start_node + # lastly, create an empty list of all the nodes at the next depth. + # your code here + + # iterate until the the list of nodes at the current depth is empty + # in each iteration, go through all the nodes at the current depth + # and, if it isn't in the list of visited nodes: + # print it + # add it to visited nodes + # add its children to the list of nodes at the next depth + # once done iterating through all the nodes at the current depth, + # set the list of nodes at the current depth equal to the + # list of nodes at the next depth + # and set the next depth to an empty list + # your code here + + +def DFS(start_node: Node, visited: list = []): + """ + Implement a recursive depth-first search algorithm, + that prints the nodes that you visit as you go. + + Remember, a depth-first search algorithm goes all the way + down to the last child before working its way up and visiting + neighbors + + Note that your function will be slightly different from + the one in the example since the nodes in this file + have children and not neighbors + """ + # check if the start node is in visited + # if it isn't, then print the node + # add it to the list of visited, + # and then use DFS on each of its children + # (make sure to pass the list of visited as an argument) + + +if __name__ == "__main__": + # make a graph that looks like the following + # / 5 + # 2 - 6 - 10 + # / + # 1 - 3 - 7 - 11 - 12 + # \ + # 4 - 8 + # \ 9 + start_node = Node(1) + for i in range(3): + start_node.children.append(Node(i + 2)) + start_node.children[0].children.append(Node(5)) + start_node.children[0].children.append(Node(6)) + start_node.children[0].children[1].children.append(Node(10)) + + start_node.children[1].children.append(Node(7)) + start_node.children[1].children[0].children.append(Node(11)) + start_node.children[1].children[0].children[0].children.append(Node(12)) + + start_node.children[2].children.append(Node(8)) + start_node.children[2].children.append(Node(9)) + + print("with BFS") + BFS(start_node) # 1 2 3 4 5 6 7 8 9 10 11 12 + print() + + print("with DFS") + DFS(start_node) # 1 2 5 6 10 3 7 11 12 4 8 9 + print() diff --git a/dsa/chapter3/solutions/bfs_dfs.py b/dsa/chapter3/solutions/bfs_dfs.py new file mode 100644 index 00000000..acd6fe8a --- /dev/null +++ b/dsa/chapter3/solutions/bfs_dfs.py @@ -0,0 +1,87 @@ +class Node: + def __init__(self, value: int) -> None: + self.value: int = value + self.children = [] + + def __str__(self) -> str: + return str(self.value) + + def __repr__(self) -> str: + return str(self) + + +def BFS(start_node: Node): + """ + Implement a breadth-first search algorithm, + but print the nodes that you visit as you go. + + Remember, a breadth-first search algorithm works by + visiting all the children in a certain depth before + advancing to the next depth + + Note that your function will be slightly different from + the one in the example since the nodes in this file + have children and not neighbors + """ + + # initialize our lists + visited_nodes = [] + current_depth_nodes = [start_node] + next_depth_nodes = [] + + # iterate until there are no nodes at the current depth + while len(current_depth_nodes) != 0: + for node in current_depth_nodes: + if node not in visited_nodes: + print(node, end=" ") + + # add the node to visited + # and add its children to the list of nodes at the next depth + visited_nodes.append(node) + next_depth_nodes.extend(node.children) + + # "go to the next depth level" by setting + # current_depth_nodes = next_depth_nodes + current_depth_nodes = next_depth_nodes + next_depth_nodes = [] + + +def DFS(start_node: Node, visited: list = []): + if start_node not in visited: + print(start_node, end=" ") + + visited.append(start_node) + for node in start_node.children: + DFS(node, visited) + + +if __name__ == "__main__": + # make a graph that looks like the following + # / 5 + # 2 - 6 - 10 + # / + # 1 - 3 - 7 - 11 - 12 + # \ + # 4 - 8 + # \ 9 + start_node = Node(1) + for i in range(3): + start_node.children.append(Node(i + 2)) + start_node.children[0].children.append(Node(5)) + start_node.children[0].children.append(Node(6)) + start_node.children[0].children[1].children.append(Node(10)) + + start_node.children[1].children.append(Node(7)) + start_node.children[1].children[0].children.append(Node(11)) + start_node.children[1].children[0].children[0].children.append(Node(12)) + + start_node.children[2].children.append(Node(8)) + start_node.children[2].children.append(Node(9)) + + print("with BFS") + BFS(start_node) # 1 2 3 4 5 6 7 8 9 10 11 12 + print() + + print("with DFS") + DFS(start_node) # 1 2 5 6 10 3 7 11 12 4 8 9 + print() From 15af5b1df8f19ae181fec35f1aca87e2574847e4 Mon Sep 17 00:00:00 2001 From: Harsh Panchal Date: Sat, 28 Jan 2023 12:39:55 -0800 Subject: [PATCH 13/17] Add mergesort practice and solution code, - also formatted some stuff --- dsa/chapter3/practice/bfs_dfs.py | 4 +- dsa/chapter3/practice/mergesort.py | 69 +++++++++++++++++++++++++++ dsa/chapter3/solutions/mergesort.py | 74 +++++++++++++++++++++++++++++ dsa/chapter3/solutions/quicksort.py | 6 +-- 4 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 dsa/chapter3/practice/mergesort.py create mode 100644 dsa/chapter3/solutions/mergesort.py diff --git a/dsa/chapter3/practice/bfs_dfs.py b/dsa/chapter3/practice/bfs_dfs.py index a15901f8..7480d097 100644 --- a/dsa/chapter3/practice/bfs_dfs.py +++ b/dsa/chapter3/practice/bfs_dfs.py @@ -1,7 +1,7 @@ """ -In this practice problem, we practice implementing +In this practice problem, we practice implementing breadth first search and depth first search and see them -in action +in action """ diff --git a/dsa/chapter3/practice/mergesort.py b/dsa/chapter3/practice/mergesort.py new file mode 100644 index 00000000..11d85596 --- /dev/null +++ b/dsa/chapter3/practice/mergesort.py @@ -0,0 +1,69 @@ +def mergesort(lst: list) -> list: + """ + Let's implement mergesort, + First let's create a base case where if the list is 0 or 1 elements long, + return it + """ + # your code here + + """ + Now that we have handled the base case, if the list is any longer, we can + go into typical mergesort logic, + + We need to split the list into 2 halves, so lets first find the middle + index value. Use // instead of / because we want an integer + """ + # your code here + + """ + Now we can run mergesort on the first and second halves of lst. Create a + variable first_half which is the result of calling mergesort on the first + half of lst. Repeat for the second half of the list, creating the variable + second_half. Use list splicing for this. + """ + # your code here + + """ + Now we need to merge the two sorted halves. In order to do this, we will + implement a mergelists helper function. Return the result of mergelists + with first_half and second_half as the two parameters. + """ + return # your code here + + +def mergelists(lst1: list, lst2: list) -> list: + idx1 = 0 + idx2 = 0 + ret = [] + + """ + Let's create a while loop that runs for as long as idx1 or idx2 is less + than the len of lst1 or lst2. + """ + while idx1 < len(lst1) or idx2 < len(lst2): + # If both lists have items, we need to compare the first items of the + # lst1 and lst2, and append whichever item is smaller to ret. Then, we + # increment the idx1 or idx2 variable respectively + if idx1 < len(lst1) and idx2 < len(lst2): + # your code here + pass + + elif idx1 < len(lst1): + # if only lst1 has items left, append the remaining items to the + # end of ret, and set idx1 to len(lst1) + + # your code here + pass + elif idx2 < len(lst2): + # if only lst2 has items left, append the remaining items to the + # end of ret, and set idx2 to len(lst2) + + # your code here + pass + return ret + + +if __name__ == "__main__": + lst = [-3, 5, -10, 18, 74, 22, 1, -40] + mergesort(lst) + print(lst) diff --git a/dsa/chapter3/solutions/mergesort.py b/dsa/chapter3/solutions/mergesort.py new file mode 100644 index 00000000..93deb682 --- /dev/null +++ b/dsa/chapter3/solutions/mergesort.py @@ -0,0 +1,74 @@ +def mergesort(lst: list) -> list: + """ + Let's implement mergesort, + First let's create a base case where if the list is 0 or 1 elements long, + return it + """ + if len(lst) <= 1: + return lst + + """ + Now that we have handled the base case, if the list is any longer, we can + go into typical mergesort logic, + + We need to split the list into 2 halves, so lets first find the middle + index value. Use // instead of / because we want an integer + """ + middle_idx = len(lst) // 2 + + """ + Now we can run mergesort on the first and second halves of lst. Create a + variable first_half which is the result of calling mergesort on the first + half of lst. Repeat for the second half of the list, creating the variable + second_half. Use list splicing for this. + """ + first_half = mergesort(lst[:middle_idx]) # sort the first half + second_half = mergesort(lst[middle_idx:]) # sort the second half + + """ + Now we need to merge the two sorted halves. In order to do this, we will + implement a mergelists helper function. Return the result of mergelists + with first_half and second_half as the two parameters. + """ + return mergelists(first_half, second_half) # merge the two sorted halves + + +def mergelists(lst1: list, lst2: list) -> list: + idx1 = 0 + idx2 = 0 + ret = [] + + """ + Let's create a while loop that runs for as long as idx1 or idx2 is less + than the len of lst1 or lst2. + """ + while idx1 < len(lst1) or idx2 < len(lst2): + # If both lists have items, we need to compare the first item of the + # lst1 and lst2, and append whichever item is smaller to ret. Then, we + # increment the idx1 or idx2 variable respectively + if idx1 < len(lst1) and idx2 < len(lst2): + if lst1[idx1] < lst2[idx2]: + ret.append(lst1[idx1]) # add the item from lst1 + idx1 += 1 # increment our idx in lst1 + else: # lst2[idx2] <= lst1[idx1] + ret.append(lst2[idx2]) # add the item from lst2 + idx2 += 1 # increment our idx in lst2 + + elif idx1 < len(lst1): + # if only lst1 has items left, append the remaining items to the + # end of ret, and set idx1 to len(lst1) + ret.extend(lst1[idx1:]) + idx1 = len(lst1) + elif idx2 < len(lst2): + # if only lst2 has items left, append the remaining items to the + # end of ret, and set idx2 to len(lst2) + ret.extend(lst2[idx2:]) + idx2 = len(lst2) + + return ret + + +if __name__ == "__main__": + lst = [-3, 5, -10, 18, 74, 22, 1, -40] + mergesort(lst) + print(lst) diff --git a/dsa/chapter3/solutions/quicksort.py b/dsa/chapter3/solutions/quicksort.py index ba717384..45d0bd90 100644 --- a/dsa/chapter3/solutions/quicksort.py +++ b/dsa/chapter3/solutions/quicksort.py @@ -1,6 +1,3 @@ -lst = [-3, 5, -10, 18, 74, 22, 1, -40] - - def quicksort(arr: list): """ quicksort_recursive takes in the list you are sorting, the first index of @@ -87,5 +84,6 @@ def partition(arr, low, high): if __name__ == "__main__": - quicksort(lst, 0, len(lst) - 1) + lst = [-3, 5, -10, 18, 74, 22, 1, -40] + quicksort(lst) print(lst) From 1c260d9a766e63a2b72527c14a48c54e0c5a6fc2 Mon Sep 17 00:00:00 2001 From: chrehall68 Date: Sat, 28 Jan 2023 14:19:53 -0800 Subject: [PATCH 14/17] a star practice problem --- dsa/chapter3/practice/a_star.py | 186 +++++++++++++++++++++++++++++ dsa/chapter3/solutions/a_star.py | 195 +++++++++++++++++++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 dsa/chapter3/practice/a_star.py create mode 100644 dsa/chapter3/solutions/a_star.py diff --git a/dsa/chapter3/practice/a_star.py b/dsa/chapter3/practice/a_star.py new file mode 100644 index 00000000..43fa25e9 --- /dev/null +++ b/dsa/chapter3/practice/a_star.py @@ -0,0 +1,186 @@ +""" +A Star Practice + +In this practice problem, you get to fill in some a_star code +as well as see the effects of using different heuristics on +a_star's execution time. + +Heuristics and helper functions are given. Your job is to fill +in sections of the A* algorithm where it says 'your code here' +""" + +from queue import PriorityQueue +import math +import random + + +class Point: + def __init__(self, x: int, y: int) -> None: + self.x: int = x + self.y: int = y + + def get_neighbors(self, start, end): + """ + This function returns a list of neighboring points + using the fact that neighboring points will be the + following (p = neighboring, c = current) + + ``` + p p p + p c p + p p p + ``` + """ + + def between(a, b, c): + return (b <= a and a <= c) or (b >= a and a >= c) + + return [ + Point(x + self.x, y + self.y) + for x in range(-1, 2) + for y in range(-1, 2) + if ( + between(x + self.x, start.x, end.x) + and between(y + self.y, start.y, end.y) + ) + ] + + def __eq__(self, __o: object) -> bool: + return self.x == __o.x and self.y == __o.y + + def __hash__(self) -> int: + return hash((self.x, self.y)) + + def __str__(self): + return f"({self.x}, {self.y})" + + def __repr__(self) -> str: + return str(self) + + +def adding_heuristic(cur: Point, end: Point): + """ + this heuristic returns a value + that looks like the following: + abs(x-x1) + abs(y-y1). + """ + return abs(cur.x - end.x) + abs(cur.y - end.y) + + +def triangle_heuristic(cur: Point, end: Point): + """ + this heuristic will return a value + based off the pythagorean theorem + that looks like the following + sqrt((x-x1)^2 + (y-y1)^2) + """ + return math.sqrt((cur.x - end.x) ** 2 + (cur.y - end.y) ** 2) + + +def bad_heuristic(cur: Point, end: Point): + """ + This heuristic will return a value + that is the opposite of the distance, + meaning that the closer cur is to end, + the worse (higher) score this will give it + """ + return -(abs(cur.x - end.x) + abs(cur.y - end.y)) + + +def random_heuristic(cur: Point, end: Point): + """ + Returns a totally random number. + """ + return random.randint(cur.x, end.x) + random.randint(cur.y, end.y) + + +def reconstruct_path(path: dict, start, end): + backwards_path = [] + curr = end # we know that we start at the end + while curr in path: + # add the current node to the backwards_path + backwards_path.append(curr) + + # since path is a dictionary of node : how to get there, + # we get the previous node in the path by doing path[curr] + curr = path[curr] + + # this will be the first item after we reverse the list + backwards_path.append(start) + + return reversed(backwards_path) + + +def a_star(start: Point, end: Point, heuristic): + min_x, max_x = min(start.x, end.x), max(start.x, end.x) + min_y, max_y = min(start.y, end.y), max(start.y, end.y) + + """ + initialize f_scores (final scores) to infinity for every + point between the [min_x, min_y] and [max_x, max_y] + + initialize g_scores (distance to get there) to infinity for every + point between [min_x, min_y] and [max_x, max_y] + + Since it takes 0 steps to get to the start, initialize that g score to 0 + """ + # your code here + + # initialize our unexplored queue and add + # insert the start node. + # format for inserting nodes: (f_score, count, node) + count = 0 + unexplored = PriorityQueue() + unexplored.put((0, count, start)) + + # this is a dictionary that stores entries in the format + # node: how to get there + # this means that path[(1, 1)] might equal (0, 0) + # since maybe the path goes from (0, 0) to (1, 1) + # we use this variable to help us reconstruct the path that + # a star found + path = {} + + # allows us to see how many executions it really took + num_executions = 0 + + while not unexplored.empty(): + current: Point = unexplored.get()[2] # just get the Point + + # it takes 1 more step to get to any neighbor, so their g_scores will be + # one more than the current g score + for node in current.get_neighbors(start, end): + if node == end: + # the way to get to the end is from the current node + path[node] = current + print(f"finished after {num_executions} executions") + return reconstruct_path(path, start, end) + else: + """ + if either we haven't explored this node yet + (meaning g_scores[node] = infinity) or this + is a shorter path to get to this node + (g_score[current] + 1 < g_scores[node]), then: + + * update our path + * update our f and g scores: + * remember, f score = g score + heuristic + * if it wasn't already in unexplored: + * update our count + * add the unexplored node w/ its score and count to unexplored + """ + # your code here + + num_executions += 1 + print(f"no solution found after {num_executions} executions") + return None # no path found + + +# you can try changing the heuristic and seeing how that affects the path taken, +# as well as the number of executions it took +path = a_star(Point(0, 0), Point(10, 15), adding_heuristic) +path_len = 0 +for i in path: + print(i) + path_len += 1 +print(f"path length was {path_len}") diff --git a/dsa/chapter3/solutions/a_star.py b/dsa/chapter3/solutions/a_star.py new file mode 100644 index 00000000..759f28a3 --- /dev/null +++ b/dsa/chapter3/solutions/a_star.py @@ -0,0 +1,195 @@ +""" +A Star Practice + +In this practice problem, you get to fill in some a_star code +as well as see the effects of using different heuristics on +a_star's execution time. + +Heuristics and helper functions are given. Your job is to fill +in sections of the A* algorithm where it says 'your code here' +""" + +from queue import PriorityQueue +import math +import random + + +class Point: + def __init__(self, x: int, y: int) -> None: + self.x: int = x + self.y: int = y + + def get_neighbors(self, start, end): + """ + This function returns a list of neighboring points + using the fact that neighboring points will be the + following (p = neighboring, c = current) + + ``` + p p p + p c p + p p p + ``` + """ + + def between(a, b, c): + return (b <= a and a <= c) or (b >= a and a >= c) + + return [ + Point(x + self.x, y + self.y) + for x in range(-1, 2) + for y in range(-1, 2) + if ( + between(x + self.x, start.x, end.x) + and between(y + self.y, start.y, end.y) + ) + ] + + def __eq__(self, __o: object) -> bool: + return self.x == __o.x and self.y == __o.y + + def __hash__(self) -> int: + return hash((self.x, self.y)) + + def __str__(self): + return f"({self.x}, {self.y})" + + def __repr__(self) -> str: + return str(self) + + +def adding_heuristic(cur: Point, end: Point): + """ + this heuristic returns a value + that looks like the following: + abs(x-x1) + abs(y-y1). + """ + return abs(cur.x - end.x) + abs(cur.y - end.y) + + +def triangle_heuristic(cur: Point, end: Point): + """ + this heuristic will return a value + based off the pythagorean theorem + that looks like the following + sqrt((x-x1)^2 + (y-y1)^2) + """ + return math.sqrt((cur.x - end.x) ** 2 + (cur.y - end.y) ** 2) + + +def bad_heuristic(cur: Point, end: Point): + """ + This heuristic will return a value + that is the opposite of the distance, + meaning that the closer cur is to end, + the worse (higher) score this will give it + """ + return -(abs(cur.x - end.x) + abs(cur.y - end.y)) + + +def random_heuristic(cur: Point, end: Point): + """ + Returns a totally random number. + """ + return random.randint(cur.x, end.x) + random.randint(cur.y, end.y) + + +def reconstruct_path(path: dict, start, end): + backwards_path = [] + curr = end # we know that we start at the end + while curr in path: + # add the current node to the backwards_path + backwards_path.append(curr) + + # since path is a dictionary of node : how to get there, + # we get the previous node in the path by doing path[curr] + curr = path[curr] + + # this will be the first item after we reverse the list + backwards_path.append(start) + + return reversed(backwards_path) + + +def a_star(start: Point, end: Point, heuristic): + min_x, max_x = min(start.x, end.x), max(start.x, end.x) + min_y, max_y = min(start.y, end.y), max(start.y, end.y) + + # initialize f scores (final scores) to infinity for every + # point between the (min_x, min_y) and (max_x, max_y) + f_scores = { + Point(x, y): float("inf") + for x in range(min_x, max_x + 1) + for y in range(min_y, max_y + 1) + } + + # initialize g scores (distance to get there) to infinity for every + # point between (min_x, min_y) and (max_x, max_y) + g_scores = { + Point(x, y): float("inf") + for x in range(min_x, max_x + 1) + for y in range(min_y, max_y + 1) + } + # it takes 0 steps to get to the start, so initialize that g score to 0 + g_scores[start] = 0 + + # this will be how many nodes we have added + # because priorityqueue sorts things, this is added as a backup measure + # when putting items into the queue to say that, if their f scores are + # the same, then just explore the one that we found first + count = 0 + unexplored = PriorityQueue() + unexplored.put((0, count, start)) + + # this is a dictionary that stores node: how to get there + # this means that path[(1, 1)] might equal (0, 0) + # we use this variable to help us reconstruct the path that + # a star found + path = {} + + # allows us to see how many executions it really took + num_executions = 0 + + while not unexplored.empty(): + current: Point = unexplored.get()[2] # just get the Point + + # it takes 1 more step to get to any neighbor, so their g_scores will be + # one more than the current g score + temp_g_score = g_scores[current] + 1 + for node in current.get_neighbors(start, end): + if node == end: + # the way to get to the end is from the current node + path[node] = current + print(f"finished after {num_executions} executions") + return reconstruct_path(path, start, end) + else: + # if either we haven't explored this node yet + # (meaning g_scores[node] = infinity) or this is a shorter path to + # get to this node, then + if temp_g_score < g_scores[node]: + # update our path that way now the shortest way to get to this node + # is through the current node + path[node] = current + + # update our f and g scores + g_scores[node] = temp_g_score + f_scores[node] = temp_g_score + heuristic(node, end) + + # add the node to unexplored if it wasn't already in unexplored + if not any(node == item[2] for item in unexplored.queue): + # update our count and add the unexplored node w/ its score + count += 1 + unexplored.put((f_scores[node], count, node)) + num_executions += 1 + print(f"no solution found after {num_executions} executions") + return None # no path found + + +# you can try changing the heuristic and seeing how that affects the path taken, +# as well as the number of executions it took +path = a_star(Point(0, 0), Point(15, 33), adding_heuristic) +path_len = 0 +for i in path: + print(i) + path_len += 1 +print(f"path length was {path_len}") From 7ab6cb623dd736c8674743ad0c150be4077e8648 Mon Sep 17 00:00:00 2001 From: Harsh Panchal Date: Sat, 4 Feb 2023 14:43:28 -0800 Subject: [PATCH 15/17] add selection sort --- dsa/chapter3/practice/selectionsort.py | 31 +++++++++++++++++++++++++ dsa/chapter3/solutions/selectionsort.py | 31 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 dsa/chapter3/practice/selectionsort.py create mode 100644 dsa/chapter3/solutions/selectionsort.py diff --git a/dsa/chapter3/practice/selectionsort.py b/dsa/chapter3/practice/selectionsort.py new file mode 100644 index 00000000..a1ac9f51 --- /dev/null +++ b/dsa/chapter3/practice/selectionsort.py @@ -0,0 +1,31 @@ +def selectionsort(arr: list): + """ + Let's implement selection sort! There are 4 easy steps to follow in order + to implement it. + 1. Create a loop through iterate through the list. + 2. Create an inner loop that iterates from the outer index + 1 to the + end of the list. + 3. Compare the element at the outer index to the element at the inner + index. + 4. If the element at the outer index is larger than at the inner index, + swap the 2 elements. + """ + + # Step 1, create an outer loop that iterates through the whole list. Let's + # name the outer index "i" + + # Step 2, create an inner loop that iterates from i+1 to the end of the + # list, let's name inner index "j" + + # Step 3, check if the element at index i is larger than the element at + # index j + + # Step 4, swap the element at the outer index with the element at the + # inner index + pass + + +if __name__ == "__main__": + lst = [-3, 5, -10, 18, 74, 22, 1, -40] + selectionsort(lst) + print(lst) diff --git a/dsa/chapter3/solutions/selectionsort.py b/dsa/chapter3/solutions/selectionsort.py new file mode 100644 index 00000000..387bc298 --- /dev/null +++ b/dsa/chapter3/solutions/selectionsort.py @@ -0,0 +1,31 @@ +def selectionsort(arr: list): + """ + Let's implement selection sort! There are 4 easy steps to follow in order + to implement it. + 1. Create a loop through iterate through the list. + 2. Create an inner loop that iterates from the outer index + 1 to the + end of the list. + 3. Compare the element at the outer index to the element at the inner + index. + 4. If the element at the outer index is larger than at the inner index, + swap the 2 elements. + """ + + # Step 1, create an outer loop that iterates through the whole list. Let's + # name the outer index "i" + for i in range(len(arr)): + # Step 2, create an inner loop that iterates from i+1 to the end of the + # list, let's name inner index "j" + for j in range(i+1, len(arr)): + # Step 3, check if the element at index i is larger than the + # element at index j + if arr[i] > arr[j]: + # Step 4, swap the element at the outer index with the element + # at the inner index + arr[i], arr[j] = arr[j], arr[i] + + +if __name__ == "__main__": + lst = [-3, 5, -10, 18, 74, 22, 1, -40] + selectionsort(lst) + print(lst) From 753b9274875b41d786769f22e25dd433160f7947 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Sat, 4 Feb 2023 22:49:14 +0000 Subject: [PATCH 16/17] Fix code style issues with Black --- 3_advanced/chapter17/solutions/min_superset.py | 1 + dsa/chapter1/practice/time_complexity_questions.py | 2 +- dsa/chapter1/solutions/time_complexity.py | 3 ++- dsa/chapter1/solutions/time_complexity_questions.py | 2 +- dsa/chapter3/solutions/selectionsort.py | 2 +- games/chapter1/solution/blackjack.py | 1 - games/chapter3/solutions/SpaceCounter.py | 3 --- 7 files changed, 6 insertions(+), 8 deletions(-) diff --git a/3_advanced/chapter17/solutions/min_superset.py b/3_advanced/chapter17/solutions/min_superset.py index cce07d80..e7b55ff7 100644 --- a/3_advanced/chapter17/solutions/min_superset.py +++ b/3_advanced/chapter17/solutions/min_superset.py @@ -2,6 +2,7 @@ # of minimum size which is the superset of all the given sets. # Implement the following method: + # superset calcuated using Principle of Inclusion and Exclusion # sets: a vector containing 3 sets def findMinSupersetLength(sets): diff --git a/dsa/chapter1/practice/time_complexity_questions.py b/dsa/chapter1/practice/time_complexity_questions.py index 070a4ba1..f272e328 100644 --- a/dsa/chapter1/practice/time_complexity_questions.py +++ b/dsa/chapter1/practice/time_complexity_questions.py @@ -49,7 +49,7 @@ def example_six(n): # what is the runtime for example 7? def example_seven(n): - for i in range(2 ** n): + for i in range(2**n): do_something() diff --git a/dsa/chapter1/solutions/time_complexity.py b/dsa/chapter1/solutions/time_complexity.py index da8099a1..d006e0f8 100644 --- a/dsa/chapter1/solutions/time_complexity.py +++ b/dsa/chapter1/solutions/time_complexity.py @@ -4,6 +4,7 @@ are examples and not the only ways to have done this problem. """ + # time complexity: O(1) def double_my_number(number): x = number @@ -84,7 +85,7 @@ def get_binary_combinations(number_of_digits): # function with O(2**n) runtime. def regular_o_2_to_the_n(n): operations = 0 - for i in range(2 ** n): + for i in range(2**n): operations += 1 print(f"took {operations} operations") diff --git a/dsa/chapter1/solutions/time_complexity_questions.py b/dsa/chapter1/solutions/time_complexity_questions.py index 79d477b9..96945dc9 100644 --- a/dsa/chapter1/solutions/time_complexity_questions.py +++ b/dsa/chapter1/solutions/time_complexity_questions.py @@ -56,7 +56,7 @@ def example_six(n): # what is the runtime for example 7? # runtime is O(2**n) def example_seven(n): - for i in range(2 ** n): + for i in range(2**n): do_something() diff --git a/dsa/chapter3/solutions/selectionsort.py b/dsa/chapter3/solutions/selectionsort.py index 387bc298..22cb686c 100644 --- a/dsa/chapter3/solutions/selectionsort.py +++ b/dsa/chapter3/solutions/selectionsort.py @@ -16,7 +16,7 @@ def selectionsort(arr: list): for i in range(len(arr)): # Step 2, create an inner loop that iterates from i+1 to the end of the # list, let's name inner index "j" - for j in range(i+1, len(arr)): + for j in range(i + 1, len(arr)): # Step 3, check if the element at index i is larger than the # element at index j if arr[i] > arr[j]: diff --git a/games/chapter1/solution/blackjack.py b/games/chapter1/solution/blackjack.py index 0e88a11f..8cf43ec2 100644 --- a/games/chapter1/solution/blackjack.py +++ b/games/chapter1/solution/blackjack.py @@ -74,7 +74,6 @@ def hit(cards): exit() if sum(dealerList) == 21 and sum(userList) == 21: - print("It is a tie") exit() if sum(dealerList) == 21: diff --git a/games/chapter3/solutions/SpaceCounter.py b/games/chapter3/solutions/SpaceCounter.py index c693844e..bb742f0f 100644 --- a/games/chapter3/solutions/SpaceCounter.py +++ b/games/chapter3/solutions/SpaceCounter.py @@ -13,7 +13,6 @@ run = True while run: - # Render the "display_counter" to the screen show_counter = font.render(str(display_counter), True, (255, 192, 203)) @@ -29,10 +28,8 @@ # Checks to see if key is pressed if event.type == pygame.KEYDOWN: - # Checks to see if the space is pressed if event.key == pygame.K_SPACE: - # Adds one to the counter display_counter += 1 From 5499f49910fd869a0bd0a984f3d160e4d5eae3c3 Mon Sep 17 00:00:00 2001 From: Harsh Panchal Date: Sat, 4 Feb 2023 14:56:01 -0800 Subject: [PATCH 17/17] Formatting --- dsa/chapter3/practice/a_star.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dsa/chapter3/practice/a_star.py b/dsa/chapter3/practice/a_star.py index 43fa25e9..eb2b598a 100644 --- a/dsa/chapter3/practice/a_star.py +++ b/dsa/chapter3/practice/a_star.py @@ -112,16 +112,15 @@ def reconstruct_path(path: dict, start, end): def a_star(start: Point, end: Point, heuristic): - min_x, max_x = min(start.x, end.x), max(start.x, end.x) - min_y, max_y = min(start.y, end.y), max(start.y, end.y) - + # min_x, max_x = min(start.x, end.x), max(start.x, end.x) # uncomment this + # min_y, max_y = min(start.y, end.y), max(start.y, end.y) # uncomment this """ initialize f_scores (final scores) to infinity for every point between the [min_x, min_y] and [max_x, max_y] - + initialize g_scores (distance to get there) to infinity for every point between [min_x, min_y] and [max_x, max_y] - + Since it takes 0 steps to get to the start, initialize that g score to 0 """ # your code here @@ -147,8 +146,8 @@ def a_star(start: Point, end: Point, heuristic): while not unexplored.empty(): current: Point = unexplored.get()[2] # just get the Point - # it takes 1 more step to get to any neighbor, so their g_scores will be - # one more than the current g score + # it takes 1 more step to get to any neighbor, so their g_scores will + # be one more than the current g score for node in current.get_neighbors(start, end): if node == end: # the way to get to the end is from the current node @@ -167,7 +166,8 @@ def a_star(start: Point, end: Point, heuristic): * remember, f score = g score + heuristic * if it wasn't already in unexplored: * update our count - * add the unexplored node w/ its score and count to unexplored + * add the unexplored node w/ its score and count to + unexplored """ # your code here @@ -176,7 +176,7 @@ def a_star(start: Point, end: Point, heuristic): return None # no path found -# you can try changing the heuristic and seeing how that affects the path taken, +# you can try changing the heuristic and seeing how that affects the path taken # as well as the number of executions it took path = a_star(Point(0, 0), Point(10, 15), adding_heuristic) path_len = 0