-
Notifications
You must be signed in to change notification settings - Fork 18
Did issue 493 #851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Jmendoza194
wants to merge
19
commits into
master
Choose a base branch
from
Jose_4.3.1_Searching_and_sorting
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Did issue 493 #851
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
fc1f39b
add untracked files
mxthu313 227c0a5
Merge branch 'michelle' of https://github.com/bitprj/curriculum into …
mxthu313 7664056
Module 4 Concept Updates
alliebailey 6d1affb
Merge branch 'Allie' of https://github.com/bitprj/curriculum into Allie
alliebailey 4ef06b3
Did issue 493
Jmendoza194 7bcaec5
Did issue 494
Jmendoza194 98cf873
typo fix
mxthu313 2a4623f
fix big O
mxthu313 fdf1fcf
Merge pull request #849 from bitprj/Allie
mxthu313 a52b3bc
Update 10.md
Jmendoza194 cdf5c1a
Update 6.md
Jmendoza194 afb7ecc
Update 7.md
Jmendoza194 51a323a
Update 10.md
Jmendoza194 82269b8
Update 9.md
Jmendoza194 7dbaead
new hint format
mxthu313 cde8efd
Update 431.md
mxthu313 940a7f1
Update 6.md
mxthu313 4ca795c
Merge pull request #868 from bitprj/Jose_4.3.2_sorting
mxthu313 9ca8292
Merge branch 'michelle' into Jose_4.3.1_Searching_and_sorting
mxthu313 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Binary file not shown.
5 changes: 5 additions & 0 deletions
5
...rch_and_Sorting_Algorithms/activities/Act1_SearchingAndSorting/10-checkpoint.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| **Name:** Binary Search Time Complexity | ||
|
|
||
| **Instruction:** Why is the worst case time complexity of Binary search O(logn) instead of O(n)? | ||
|
|
||
| **Type:** Short Answer |
71 changes: 71 additions & 0 deletions
71
Module4.3_Search_and_Sorting_Algorithms/activities/Act1_SearchingAndSorting/10.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| <!--title={Time Complexity of Binary Search}--> | ||
|
|
||
| <!--concepts{Depth First Search}--> | ||
|
|
||
| <!--badges={Algorithmns:15, Python: 5}--> | ||
|
|
||
| Unlike in BFS and DFS, there are three time complexities for Binary search. That is because in BFS and DFS, you have to traverse the whole graph. In Binary search, however, you stop when you find what you are looking for. | ||
|
|
||
| Now, let us delve into the code: | ||
|
|
||
| ```python | ||
| def binarySearch (arr, l, r, x): | ||
|
|
||
| if r >= l: | ||
|
|
||
| mid = l + (r - l)/2 | ||
|
|
||
| if arr[mid] == x: | ||
| return mid | ||
|
|
||
| elif arr[mid] > x: | ||
| return binarySearch(arr, l, mid-1, x) | ||
|
|
||
| else: | ||
| return binarySearch(arr, mid + 1, r, x) | ||
|
|
||
| else: | ||
| return -1 | ||
| ``` | ||
|
|
||
| ### Best Case | ||
|
|
||
| The first time complexity is the best case time complexity. This means that this is the best case scenario, or the fastest that the algorithm will run. | ||
|
|
||
|  | ||
|
|
||
| In Binary search, the best case scenario is when the first value you look at after cutting the list in half is the value you are looking for. If this were to indeed happen, then the best case time complexity would be O(1) time. This can be seen in the second if statement of the code: | ||
|
|
||
| ```python | ||
| if arr[mid] == x: | ||
| return mid | ||
| ``` | ||
|
|
||
| As you can see, when the middle of the list arr is equal to what you are searching for, it immediately stops recursing. Therefore, if the value you are looking for is exactly in the middle in the first recursion, then you will immediately stop the algorithm. | ||
|
|
||
| ### Average Case and Worst Case | ||
|
|
||
| The next two time complexities are called Average case and Worst case. Average case is the average of all run times. The worst case is the worst case scenario, or the slowest the algorithm will run in. | ||
|
|
||
|  | ||
|
|
||
| In binary search, the worst case is when the algorithm cannot find what it is looking for. In that case, the algorithm will keep on halving the list until it can no longer do so (meaning there is only one element left in the list). In that scenario, the worst case time complexity would be O(logn) time, becuase you will NEVER actually iterate through the whole list.This can be seen in the elif and else statements of the code: | ||
|
|
||
| ```python | ||
| elif arr[mid] > x: | ||
| return binarySearch(arr, l, mid-1, x) | ||
|
|
||
| else: | ||
| return binarySearch(arr, mid + 1, r, x) | ||
| ``` | ||
|
|
||
| As you may notice, you will iterate only half of the list in each iteration, thus making it O(logn). And since the best case scenario is very unlikely to happen, then the average case time complexity shall be the same as the worst case time complexity: O(logn). | ||
|
|
||
| ### Why is binary search so good? | ||
|
|
||
| Now that we know the time complexities, the next question is what makes binary search so good? Well, the answer to that question is actually very simple: It is the fastest known searching algorithm. | ||
|
|
||
| <img src="https://askdentalgroup.com/wp-content/uploads/2015/08/best-of-the-best.jpg" alt="Best" style="zoom:33%;" /> | ||
|
|
||
| It has the best Average time complexity amongst all searching algorithms available out there. In fact, it would not be a lie to say that if you were to create a better algorithm than binary search, then you would win an award. | ||
|
|
||
5 changes: 5 additions & 0 deletions
5
...arch_and_Sorting_Algorithms/activities/Act1_SearchingAndSorting/4-checkpoint.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| **Name**:BFS Time Complexity | ||
|
|
||
| **Instruction**: Why is the Time complexity of BFS O(V + E)? Answer must include where we got V, and were we got E in O(V + E). | ||
|
|
||
| **Type**: Short Answer |
63 changes: 52 additions & 11 deletions
63
Module4.3_Search_and_Sorting_Algorithms/activities/Act1_SearchingAndSorting/4.md
100755 → 100644
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,60 @@ | ||
| <!--title={Depth First Search}--> | ||
| <!--title={Big O of BFS}--> | ||
|
|
||
| <!--concepts{Depth First Search}--> | ||
| <!--concepts{Breadth First Search}--> | ||
|
|
||
| <!--badges={Algorithmns:10}--> | ||
| <!--badges={Algorithmns: 20}--> | ||
|
|
||
| **Depth First Search** (**DFS**), similar to the **Breadth-First Search** (**BFS**) we just explored, is an algorithm for traversing tree or graph data structures. The algorithm starts at the root node (selecting some arbitrary node as the root node in the case of a graph) and explores as far as possible along each branch before backtracking. This is different from BFS, where we went across layers of nodes. With DFS, we traverse 'downwards' as far as possible before going back up to the rest of the vertices. | ||
| In case you have forgotten, the Big O notation is used to describe the execution time required or the space used of an algorithm or code in computer science. Therefore, since BFS is an algorithm, it naturally has a Big O that can be used to describe its run time speed. We will start with the code from the previous card : | ||
|
|
||
|  | ||
| ```python | ||
| def BFS(self, s): | ||
| visited = [False] * (len(self.graph)) | ||
| queue = [] | ||
| queue.append(s) | ||
| visited[s] = True | ||
| while queue: | ||
| s = queue.pop(0) | ||
| print (s, end = " ") | ||
| for i in self.graph[s]: | ||
| if visited[i] == False: | ||
| queue.append(i) | ||
| visited[i] = True | ||
| ``` | ||
|
|
||
| As in the example given above, DFS algorithm traverses from S to A to D to G to E to B first, then to F and lastly to C. It employs the following rules. | ||
| As you might have noticed, there is a while loop in the BFS algorithm. | ||
|
|
||
| - **Rule 1** − Visit the adjacent unvisited vertex. Mark it as visited. Display it. Push it in a stack. | ||
| - **Rule 2** − If no adjacent vertex is found, pop up a vertex from the stack. (It will pop up all the vertices from the stack, which do not have adjacent vertices.) | ||
| - **Rule 3** − Repeat Rule 1 and Rule 2 until the stack is empty. | ||
| ```python | ||
| while queue: | ||
| ``` | ||
|
|
||
| With DFS we will also employ **recursion**, the act of calling a function within itself. It seems counterintuitive at first, but the function can be called within itself, which is useful to continue using the function until some condition is reached. | ||
| What this while loop is doing is going through each and every vertex inside the graph. Therefore, the Big O for this part of the algorithm is O(n). That is because the while loop will loop n times, where n is the size of the queue list that is being iterated over. | ||
|
|
||
|  | ||
| The next significant part of the BFS algorithm that affects its time complexity is the for loop after the print statement. | ||
|
|
||
| ```python | ||
| for i in self.graph[s]: | ||
| ``` | ||
|
|
||
| What this for loop is doing is that it is visiting the adjacent vertices of the current vertex as stated previously. Therefore it is going through each and every edge present in the graph. Just like the while loop before, the time complexity of this part of the algorithm will be O(n), where n is the size of the graph list. | ||
|
|
||
| ###Combining Time Complexities | ||
|
|
||
| Now, when you combine the two, you would get O(n^2), as the for loop is inside the while loop. The algorithm loops n times because of the while loop, and inside each while loop, you must again iterate another n times dues to the for loop, thus the reasult being O(n^2). However, that is not an accurate time complexity for BFS! | ||
|
|
||
| <img src="https://qmaxima.com/uploads/3/4/7/1/34719252/2487755_orig.png" alt="Not Accurate" style="zoom:50%;" /> | ||
|
|
||
| We got O(n) for the while loop because it is going through each and every vertex inside the graph, and an O(n) for the for loop because it is going through each and every edge inside the graph. However, an important thing to take note of is that the number of vertices and the number of edges are NOT the same. Therefore, a more accurate Big O for the algorithm is O(V +E), where V is the number of vertices being iterated over in the while loop and E being the number of edges being iterated over by the for loop. This again is unfortunately not completely accurate, as it will depend on the data structure that is being used in the BFS implementation | ||
|
|
||
| ### Adjacency List vs Adjacency Matrix | ||
|
|
||
| Depending on if you use an adjacency list or an adjaceny matrix, the time complexity of the BFS will be change. Before we get into that, however, it is best to describe what the two data structures are. | ||
|
|
||
| An adjacency list is a collection of unordered lists, where each list are neighbors to each other. This data strucutre is used for sparse graphs. A sparse graph is a graph where the number of edges is small, making the graph "sparse" | ||
|
|
||
|  | ||
|
|
||
| An adjacency matrix is a matrix that specifies which nodes in a graph are connected to each other. This data structure is used for dense graphs. A dense graph is a graph where the number of edges is big, making the graph "dense" | ||
|
|
||
|  | ||
|
|
||
| Thus, if you use an adjacency list to implement BFS, then you will get a time complexity of O(V + E) and O(V^2) if you were to use an adjacency matrix. That is because, for an adjacency matrix, you will have to iterate over a matrix V^2 times, but in an adjacency list, it is just V+E times due to it just being a list of nodes. |
106 changes: 10 additions & 96 deletions
106
Module4.3_Search_and_Sorting_Algorithms/activities/Act1_SearchingAndSorting/5.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,105 +1,19 @@ | ||
| <!--title={DFS in Python}--> | ||
| <!--title={Depth First Search}--> | ||
|
|
||
| <!--concepts{Depth First Search}--> | ||
|
|
||
| <!--badges={Algorithmns:15, Python:5}--> | ||
| <!--badges={Algorithmns:10}--> | ||
|
|
||
| This is the code used to test our DFS algorithm. We can use this block to check if our function works. | ||
| **Depth First Search** (**DFS**), similar to the **Breadth-First Search** (**BFS**) we just explored, is an algorithm for traversing tree or graph data structures. The algorithm starts at the root node (selecting some arbitrary node as the root node in the case of a graph) and explores as far as possible along each branch before backtracking. This is different from BFS, where we went across layers of nodes. With DFS, we traverse 'downwards' as far as possible before going back up to the rest of the vertices. | ||
|
|
||
| ```python | ||
| g = Graph() | ||
| g.addEdge(0, 1) | ||
| g.addEdge(0, 2) | ||
| g.addEdge(1, 2) | ||
| g.addEdge(2, 0) | ||
| g.addEdge(2, 3) | ||
| g.addEdge(3, 3) | ||
|  | ||
|
|
||
| print("Following is DFS from (starting from vertex 2)") | ||
| g.DFS(2) | ||
| ``` | ||
| As in the example given above, DFS algorithm traverses from S to A to D to G to E to B first, then to F and lastly to C. It employs the following rules. | ||
|
|
||
| The DFS testing is essentially identical to the BFS testing as it serves the same purpose: to construct a graph with vertices that we can traverse. The only difference is we call `g.DFS(2)` to start the DFS algorithm from vertex 2, as opposed to calling `g.BFS(2)`, which would start the BFS algorithm. | ||
| - **Rule 1** − Visit the adjacent unvisited vertex. Mark it as visited. Display it. Push it in a stack. | ||
| - **Rule 2** − If no adjacent vertex is found, pop up a vertex from the stack. (It will pop up all the vertices from the stack, which do not have adjacent vertices.) | ||
| - **Rule 3** − Repeat Rule 1 and Rule 2 until the stack is empty. | ||
|
|
||
| Identical to BFS, the first step in implementing a DFS algorithm is to construct a `Graph` class with an `addEdge` function. | ||
|
|
||
| ```python | ||
| from collections import defaultdict | ||
|
|
||
| class Graph: | ||
|
|
||
| def __init__(self): | ||
|
|
||
| self.graph = defaultdict(list) | ||
|
|
||
| def addEdge(self, u, v): | ||
| self.graph[u].append(v) | ||
| ``` | ||
|
|
||
| The `DFSUtil` function will be the powerhouse of implementing the DFS algorithm. As arguments, the function takes the vertex we are currently observing (`v`) and the `visited` array. Similar to that of BFS, the `visited` array is used to determine if a certain vertex has already been visited. If `visited[v] = True`, then the vertex `v` has been visited. Likewise, if `visited[v] = False`, `v` has not been visited. | ||
|
|
||
| Here is the code for `DFSUtil` in full. We will break it down step-by-step, but first, I wish to bring your attention to something important in these lines. What do you notice about the last line of the code? It seems like `DFSUtil` is calling the function `DFSUtil`. That is, `DFSUtil` is calling itself! When a function calls itself in its own body, it is known as a recursive function. Recognizing this is essential to understanding how the DFS algorithm is implemented. | ||
|
|
||
| ```python | ||
| def DFSUtil(self, v, visited): | ||
| visited[v] = True | ||
| print(v, end = ' ') | ||
| for i in self.graph[v]: | ||
| if visited[i] == False: | ||
| self.DFSUtil(i, visited) | ||
|
|
||
| def DFS(self, v): | ||
| visited = [False] * (len(self.graph)) | ||
| self.DFSUtil(v, visited) | ||
| ``` | ||
|
|
||
| Back to the line-by-line breakdown of the code. | ||
|
|
||
| `DFSUtil` begins by marking the vertex `v` as visited by changing `visited[v] = True`. It then prints the vertex to the console to let the user know that `v` has just been visited. | ||
|
|
||
| ```python | ||
| visited[v] = True | ||
| print(v, end = ' ') | ||
| ``` | ||
|
|
||
| The function then enters the `for` loop. Just as in BFS, this `for` loop runs for each of the adjacent vertices of `v`. That is, `self.graph[v]` contains a list of all of the vertices from which there is an edge from `v` to it (see chart labeled *Graph In Terms of Adjacent Vertices* in the diagram below). The variable `i` stores this adjacent vertex throughout the `for` loop. The `for` loop contains an `if` statement that checks if said adjacent vertex (`i`) is unvisited (`visited[i] == False`). If this is the case, we recurse and call `DFSUtil`again, passing as arguments the adjacent vertex (`i`) and our updated `visited` array. | ||
|
|
||
| ```python | ||
| for i in self.graph[v]: | ||
| if visited[i] == False: | ||
| self.DFSUtil(i, visited) | ||
| ``` | ||
|
|
||
| Now, you might be wondering, why does this function call itself? DFS is supposed to have a stack, but I don't see any such data structure? To answer these concerns, we must diverge from the code and discuss how arguments are passed to a function. | ||
|
|
||
| Arguments to a function are passed through a stack, which refers to a data structure with the **L**ast-**I**n **F**irst-**O**ut (**LIFO**) functionality. When a function is called, each argument is pushed on to a stack. When a function terminates, all of the arguments are popped from the stack. | ||
|
|
||
| While this does indeed occur for all arguments passed to a function, for simplicity's sake, we are going to focus our attention on how the `v` argument is pushed and popped from the stack everytime `DFSUtil` is called and terminates. | ||
|
|
||
| The diagram below explains DFS step-by-step with special attention given to how recursion, passing arguments, and the stack play a role: | ||
|
|
||
|  | ||
|
|
||
| Often with recursive functions, we require a driver function to set-up certain values or variables before calling the recursive function. For the DFS algorithm, we have the `DFS` function serving as a driver function for the `DFSUtil` function. It first marks all vertices as not visited (the `visited` array only has `False` values) and then calls `DFSUtil` to visit the nodes. | ||
|
|
||
| ```python | ||
| def DFS(self, v): | ||
| visited = [False] * (len(self.graph)) | ||
| self.DFSUtil(v, visited) | ||
| ``` | ||
|
|
||
| Here is the completed code: | ||
|
|
||
| ```python | ||
| def DFSUtil(self, v, visited): | ||
| visited[v] = True | ||
| print(v, end = ' ') | ||
| for i in self.graph[v]: | ||
| if visited[i] == False: | ||
| self.DFSUtil(i, visited) | ||
|
|
||
| def DFS(self, v): | ||
| visited = [False] * (len(self.graph)) | ||
| self.DFSUtil(v, visited) | ||
| ``` | ||
| With DFS we will also employ **recursion**, the act of calling a function within itself. It seems counterintuitive at first, but the function can be called within itself, which is useful to continue using the function until some condition is reached. | ||
|
|
||
|  |
File renamed without changes.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code formatting