mirror of
https://github.com/autistic-symposium/master-algorithms-py.git
synced 2025-04-29 20:26:07 -04:00
Update README.md
This commit is contained in:
parent
66060486a4
commit
ffe28f3d8e
321
trees/README.md
321
trees/README.md
@ -2,20 +2,213 @@
|
||||
|
||||
<br>
|
||||
|
||||
* a tree is a widely used abstract data type that represents a hierarchical structure with a set of connected nodes.
|
||||
* each node in the tree can be connected to many children, but must be connect to exactly one parent (except for the root node).
|
||||
* a tree is an undirected and connected acyclic graph and there are no cycle or loops.
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
### binary trees
|
||||
|
||||
<br>
|
||||
|
||||
* **binary trees** are trees that have each up to 2 children. a node is called **leaf** if it has no children.
|
||||
* the **depth of node** is the number of edges from the tree's root node until the node.
|
||||
* the **height of node** is the number of edges on the longest path between that node and a leaf. the **height of tree** is the height of its root node.
|
||||
* access, search, remove, insert are all `O(log(N)`. space complexity of traversing balanced trees is `O(h)` where `h` is the height of the tree (while very skewed trees will be `O(N)`.
|
||||
* the **width** is the number of nodes in a level.
|
||||
* the **degree** is the nunber of children of a node.
|
||||
* a **balanced tree** is a binary tree in which the left and right subtrees of every node differ in height by no more than 1.
|
||||
* a **complete tree** is a tree on which every level is fully filled (except perhaps for the last).
|
||||
* a **full binary tree** has each node with either zero or two children (and no node has only one child).
|
||||
* a **perfect tree** is both full and complete (it must have exactly 2**k - 1 nodes, where k is the number of levels).
|
||||
* a **perfect tree** is both full and complete (it must have exactly `2**k - 1` nodes, where `k` is the number of levels).
|
||||
|
||||
<br>
|
||||
|
||||
#### find height
|
||||
----
|
||||
|
||||
### depth of a binary tree
|
||||
|
||||
<br>
|
||||
|
||||
* the **depth** (or level) of node is the number of edges from the tree's root node until the node.
|
||||
|
||||
<br>
|
||||
|
||||
```python
|
||||
def max_depth(root) -> int:
|
||||
|
||||
if root is None:
|
||||
return 0
|
||||
|
||||
return max(max_depth(root.left) + 1, max_depth(root.right) + 1)
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
### height of a tree
|
||||
|
||||
<br>
|
||||
|
||||
* the **height** of a node is the number of edges on the **longest path** between that node and a leaf.
|
||||
* the **height of tree** is the height of its root node, or the depth of its deepest node.
|
||||
|
||||
<br>
|
||||
|
||||
```python
|
||||
def height(root):
|
||||
|
||||
if not root:
|
||||
return 0
|
||||
|
||||
return 1 + max(height(root.left), height(root.right))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### tree traversal: breath-first search (level-order)
|
||||
|
||||
<br>
|
||||
|
||||
* give you all elements **in order** with time `O(log(N)`. used to traverse a tree by level.
|
||||
|
||||
* iterative solutions use a queue for traversal or find the shortest path from the root node to the target node:
|
||||
- in the first round, we process the root node.
|
||||
- in the second round, we process the nodes next to the root node.
|
||||
- in the third round, we process the nodes which are two steps from the root node, etc.
|
||||
- newly-added nodes will not be traversed immediately but will be processed in the next round.
|
||||
- if node X is added to the kth round queue, the shortest path between the root node and X is exactly k.
|
||||
- the processing order of the nodes in the exact same order as how they were added to the queue (which is FIFO).
|
||||
|
||||
<br>
|
||||
|
||||
```python
|
||||
def bfs_iterative(root):
|
||||
|
||||
result = []
|
||||
queue = collections.deque([root])
|
||||
|
||||
if root is None:
|
||||
return result
|
||||
|
||||
while queue:
|
||||
|
||||
node = queue.popleft()
|
||||
|
||||
if node:
|
||||
result.append(node.val)
|
||||
queue.append(node.left)
|
||||
queue.append(node.right)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def bfs_recursive(root) -> list:
|
||||
|
||||
result = []
|
||||
if root is None:
|
||||
return root
|
||||
|
||||
def helper(node):
|
||||
|
||||
if node:
|
||||
result.append(node.val)
|
||||
helper(node.left)
|
||||
helper(node.right)
|
||||
|
||||
helper(root)
|
||||
return result
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
### tree traversal: depth-first search
|
||||
|
||||
<br>
|
||||
|
||||
- deep-first search (DFS) can also be used to find the path from the root node to the target node if you want to visit every node and/or search the deepest paths firsts.
|
||||
- recursion solutions are easier to implement; however, if the recursion depth is too high, stack overflow might occur. in that case, you might use BFS instead or implement DFS using an explicit stack (i.e., use a while loop and a stack structure to simulate the system call stack).
|
||||
- overall, we only trace back and try another path after we reach the deepest node. as a result, the first path you find in DFS is not always the shortest path.
|
||||
- we first push the root node to the stack, then we try the first neighbor and push its node to the stack, etc.
|
||||
- when we reach the deepest node, we need to trace back.
|
||||
- when we track back, we pop the deepest node from the stack, which is actually the last node pushed to the stack.
|
||||
- the processing order of the nodes is exactly the opposite order of how they are added to the stack.
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
#### in-order
|
||||
|
||||
<br>
|
||||
|
||||
- `left -> node -> right`
|
||||
- in a bst, in-order traversal will be sorted in the ascending order (therefore, it's the most frequently used method).
|
||||
- converting a sorted array to a bst with inorder has no unique solution (in another hadnd, both preorder and postorder are unique identifiers of a bst).
|
||||
|
||||
```python
|
||||
def inorder(root):
|
||||
if root is None:
|
||||
return []
|
||||
return inorder(root.left) + [root.val] + inorder(root.right)
|
||||
````
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
#### pre-order
|
||||
|
||||
<br>
|
||||
|
||||
- `node -> left -> right`
|
||||
- top-down (parameters are passed down to children), so deserialize with a queue.
|
||||
|
||||
<br>
|
||||
|
||||
```python
|
||||
def preorder(root):
|
||||
if root is None:
|
||||
return []
|
||||
return [root.val] + preorder(root.left) + preorder(root.right)
|
||||
```
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
#### post-order
|
||||
|
||||
<br>
|
||||
|
||||
- `left -> right -> node`
|
||||
- bottom-up solution.
|
||||
- deletion process is always post-order: when you delete a node, you will delete its left child and its right child before you delete the node itself.
|
||||
- post-order can be used in mathematical expressions as it's easier to write a program to parse a post-order expression. using a stack, each time when you meet an operator, you can just pop 2 elements from the stack, calculate the result and push the result back into the stack.
|
||||
|
||||
<br>
|
||||
|
||||
```python
|
||||
def postorder(root):
|
||||
if root is None:
|
||||
return []
|
||||
return postorder(root.left) + postorder(root.right) + [root.val]
|
||||
```
|
||||
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
### find height
|
||||
|
||||
<br>
|
||||
|
||||
@ -318,124 +511,4 @@ def delete_node(root, key):
|
||||
|
||||
---
|
||||
|
||||
### tree traversal: breath-first search (level-order)
|
||||
|
||||
<br>
|
||||
|
||||
- iterative solutions use a queue for traversal or find the shortest path from the root node to the target node:
|
||||
- in the first round, we process the root node, in the second round, we process the nodes next to the root node, in the third round, we process the nodes which are two steps from the root node, etc. newly-added nodes will not be traversed immediately but will be processed in the next round.
|
||||
- if node X is added to the kth round queue, the shortest path between the root node and X is exactly k.
|
||||
- the processing order of the nodes in the exact same order as how they were added to the queue, which is FIFO.
|
||||
|
||||
<br>
|
||||
|
||||
```python
|
||||
def bfs(root):
|
||||
|
||||
queue = queue()
|
||||
root.marked = True
|
||||
queue.enqueue(root)
|
||||
|
||||
while !queue.is_empty():
|
||||
node = queue.deque()
|
||||
visit(node)
|
||||
for n in node_adj:
|
||||
n.marked = True
|
||||
queue.enque(n)
|
||||
```
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
### tree traversal: depth-first search
|
||||
|
||||
<br>
|
||||
|
||||
- similar to BFS, deep-first search (DFS) can also be used to find the path from the root node to the target node.
|
||||
- it's a good option for finding the first path (instead of the first path), or if you want to visit every node.
|
||||
- overall, we only trace back and try another path after we reach the deepest node. as a result, the first path you find in DFS is not always the shortest path.
|
||||
- we first push the root node to the stack, then we try the first neighbor and push its node to the stack, etc.
|
||||
- when we reach the deepest node, we need to trace back. when we track back, we pop the deepest node from the stack, which is actually the last node pushed to the stack.
|
||||
- the processing order of the nodes is exactly the opposite order of how they are added to the stack.
|
||||
- recursion solutions are easier to implement; however, if the recursion depth is too high, stack overflow might occur. in that case, you might use BFS instead or implement DFS using an explicit stack (i.e., use a while loop and a stack structure to simulate the system call stack).
|
||||
- if the depth of the tree is too large, stack overflow might happen, therefore iterative solutions might be better.
|
||||
- work with stacks.
|
||||
|
||||
<br>
|
||||
|
||||
```python
|
||||
def dfs(root, visited):
|
||||
if root is None:
|
||||
return root
|
||||
while root.next:
|
||||
if root.next not in visited:
|
||||
visited.add(root.next)
|
||||
return dfs(root.next, visited)
|
||||
return False
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
#### in-order
|
||||
|
||||
<br>
|
||||
|
||||
- `left -> node -> right`
|
||||
- in a bst, in-order traversal will be sorted in the ascending order (therefore, it's the most frequently used method).
|
||||
- note, however, that convert a sorted array to a bst with inorder has no unique solution (at the same time, both preorder and postorder are unique identifiers of a bst).
|
||||
|
||||
```python
|
||||
def inorder(self, root):
|
||||
if root is None:
|
||||
return []
|
||||
return inorder(root.left) + [root.val] + inorder(root.right)
|
||||
````
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
#### pre-order
|
||||
|
||||
<br>
|
||||
|
||||
- `node -> left -> right`
|
||||
- top-down (parameters are passed down to children), so deserialize with a queue.
|
||||
|
||||
<br>
|
||||
|
||||
```python
|
||||
def preorder(self, root):
|
||||
if root is None:
|
||||
return []
|
||||
return [root.val] + preorder(root.left) + preorder(root.right)
|
||||
```
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
#### post-order
|
||||
|
||||
<br>
|
||||
|
||||
- `left -> right -> node`
|
||||
- bottom-up solution (if you know the answer of the children, can you concatenate the answer of the nodes?):
|
||||
- deletion process is always post-order: when you delete a node, you will delete its left child and its right child before you delete the node itself.
|
||||
- also, post-order is used in mathematical expressions as it's easier to write a program to parse a post-order expression. using a stack, each time when you meet an operator, you can just pop 2 elements from the stack, calculate the result and push the result back into the stack.
|
||||
|
||||
<br>
|
||||
|
||||
```python
|
||||
def postorder(self, root):
|
||||
if root is None:
|
||||
return []
|
||||
return postorder(root.left) + postorder(root.right) + [root.val]
|
||||
```
|
||||
|
||||
|
||||
|
||||
<br>
|
||||
|
Loading…
x
Reference in New Issue
Block a user