How to visit a tree?
Visits allow us to access the information contained in the trees we are using.
::todo ciao ::
We assume the trees we are visiting are binary trees, but they can be generalized for any data structure.
| Visit | Order |
|---|---|
| Pre-Order | Root > L-Child > R-Child |
| Symmetric | L-Child > Root > R-Child |
| Post-Order | L-Child > R-Child > Root |
| BFS | At each level, go from L to R |
Generic Visit
This general type of visit helps understand how we can obtain different types of visits, by changing of data structure.
- Inserts the root inside a set
- Inserts the root inside a set
- While
do: - Extract a node
from S
- Extract a node
- Visit the node
- Add the children of
to
- Add the children of
generic_visit(Node r)
S = {r} # Inserts the root inside a set S
while (S != EMPTY_SET):
# Extract a node u from S
# Visit the node u
S = set_union(S, children(u));generic_visit(Node r)
S = {r} # Inserts the root inside a set S
while (S != EMPTY_SET):
# Extract a node u from S
# Visit the node u
S = set_union(S, children(u));Theorem
The algorithm generic_visit applied to the root of a tree
Demonstration
Hypothesis:
Demonstration:
- A node will be inserted and extracted once from
. - By construction of the tree, we cannot go back to a parent node, starting from its children and iterating backwards.
- The maximum possible count of iterations are limited by the count of nodes, which is the upperbound.
- This means that the count of nodes represents an upperbound for the total space needed too.
Depth First Search - Pre Order
What happens here is that we start from the root and keep going deeper on the left child side until we manage to explore the whole left subtree. Then, we pass to the right subtree.
Iterative Version - Using a Stack
- Create an empty stack S
- Push a node in the stack
- While the stack is not empty
- Pop the first node u from the stack
- Check if u is empty
- Push first right child
- Push left child (as last, so we can go in depth)
DFS_visit_iter(Node r)
Stack s = create_stack();
push(s,r);
while(not stack_empty(s)):
Node u = pop(s);
if(u!= NULL):
# Visit u
push(s, u.right);
push(s, u.left);DFS_visit_iter(Node r)
Stack s = create_stack();
push(s,r);
while(not stack_empty(s)):
Node u = pop(s);
if(u!= NULL):
# Visit u
push(s, u.right);
push(s, u.left);Final Time Complexity: T(n) = O(n)
- Push, Pop can be implemented in costant time
Recursive Version
- Start from the root node
- If tree is not empty
- Visit the root
- Visit the left-child
- Visit the right-child
DFS_visit_rec(Node r)
if(r != NULL): # If not empty
#Visit r
DFS_visit_rec(r.left);
DFS_visit_rec(r.right);DFS_visit_rec(Node r)
if(r != NULL): # If not empty
#Visit r
DFS_visit_rec(r.left);
DFS_visit_rec(r.right);Theorem
If DFS_visit_rec(r) takes
Demonstration
Let DFS_visit_rec visits all the
We can rewrite the formula with the occurrences as
Where
is the time to cover the left subtree's nodes is the time to cover the right subtree' s nodes to exclude the root and the left subtree
Since we do not have a clue whether the tree is balanced or not, we cannot use the master theorem to get the total time complexity. Knowing that the tree is balanced means that
We use the substitution method:
- Hypothesis: The time is a generic linear combination
- Proceed by induction to find
and - Base Case:
by definition of - Inductive Hypothesis: We assume that
so we can rewrite - Inductive Step: Demonstrate it for
- Base Case:
Since
Now, we want to demonstrate that
Final Time Complexity:
Example: implementation C++
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
class Solution {
void preorderUtil(Node* root, vector<int> & v) {
if (!root) return;
v.push_back(root->val);
for (int i = 0; i< root->children.size(); i++) {
preorderUtil(root->children[i], v);
}
}
public:
vector<int> preorder(Node* root) {
vector <int> v;
preorderUtil(root, v);
return v;
}
};class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
class Solution {
void preorderUtil(Node* root, vector<int> & v) {
if (!root) return;
v.push_back(root->val);
for (int i = 0; i< root->children.size(); i++) {
preorderUtil(root->children[i], v);
}
}
public:
vector<int> preorder(Node* root) {
vector <int> v;
preorderUtil(root, v);
return v;
}
};Symmetric Visit - In Order
Now things change, here we find the first recursive call on the left child. Subsequently, we visit the root node, and then we have the recursive call on the right child.
We use a Stack
Example: implementation c++
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector <int> result;
inorderTraversal_ric (root, result);
return result;
}
void inorderTraversal_ric(TreeNode* root, vector<int> & v) {
if(root == NULL){
return;
}else{
inorderTraversal_ric(root->left, v);
v.push_back(root->val);
inorderTraversal_ric(root->right, v);
}
}
};struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector <int> result;
inorderTraversal_ric (root, result);
return result;
}
void inorderTraversal_ric(TreeNode* root, vector<int> & v) {
if(root == NULL){
return;
}else{
inorderTraversal_ric(root->left, v);
v.push_back(root->val);
inorderTraversal_ric(root->right, v);
}
}
};Visit - Post-Order
Firstly we have the first two recursive calls, respectively, on the left child and the right child. Finally, we visit the root.
Example: implementation c++
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
class Solution {
public:
vector<int> postorder(Node* root) {
vector <int> v;
preorderUtil(root, v);
return v;
}
void preorderUtil(Node* root, vector<int> & v) {
if (!root) return;
for (int i = 0; i< root->children.size(); i++) {
preorderUtil(root->children[i], v);
}
v.push_back(root->val);
}
};class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
class Solution {
public:
vector<int> postorder(Node* root) {
vector <int> v;
preorderUtil(root, v);
return v;
}
void preorderUtil(Node* root, vector<int> & v) {
if (!root) return;
for (int i = 0; i< root->children.size(); i++) {
preorderUtil(root->children[i], v);
}
v.push_back(root->val);
}
};Bread First Search (BFS)
The name comes from the fact that our tree will be "cut like a loaf of bread".
We use a Queue as auxiliary data structure since it follows a First In First Out principle. This means that the first node inserted into the data structure, is the same whose children are taken to begin with.
- We need to read for left to right (FIFO)
BFS
- Create a Queue
- Visit Root
- Enqueue Left Child;
- Enqueue Right Child;
- Visit on Dequeue;
- Back to 2. with the next element of the queue which is popped out and deleted
BFS_visit(Node r){
Queue c = create_queue();
enqueue(c,r);
while(not queue_empty(c))
Node u = dequeue(c);
if(u != NULL)
# Visit node u
enqueue(c, u.left); # Which is the first processed
enqueue(c, u.right);BFS_visit(Node r){
Queue c = create_queue();
enqueue(c,r);
while(not queue_empty(c))
Node u = dequeue(c);
if(u != NULL)
# Visit node u
enqueue(c, u.left); # Which is the first processed
enqueue(c, u.right);Final Time Complexity:
- n being the count of the nodes
enqueue,dequeuecan be implemented in constant times