Blog

  • AVL Trees

    The first type of self-balancing binary search tree to be invented is the AVL tree. The name AVL tree is coined after its inventor’s names − Adelson-Velsky and Landis.

    In AVL trees, the difference between the heights of left and right subtrees, known as the Balance Factor, must be at most one. Once the difference exceeds one, the tree automatically executes the balancing algorithm until the difference becomes one again.

    BALANCE FACTOR = 
    HEIGHT(LEFT SUBTREE) − HEIGHT(RIGHT SUBTREE)
    

    There are usually four cases of rotation in the balancing algorithm of AVL trees: LL, RR, LR, RL.

    LL Rotations

    LL rotation is performed when the node is inserted into the right subtree leading to an unbalanced tree. This is a single left rotation to make the tree balanced again −

    LL Rotations

    Fig : LL Rotation

    The node where the unbalance occurs becomes the left child and the newly added node becomes the right child with the middle node as the parent node.

    RR Rotations

    RR rotation is performed when the node is inserted into the left subtree leading to an unbalanced tree. This is a single right rotation to make the tree balanced again −

    RR_Rotations

    Fig : RR Rotation

    The node where the unbalance occurs becomes the right child and the newly added node becomes the left child with the middle node as the parent node.

    LR Rotations

    LR rotation is the extended version of the previous single rotations, also called a double rotation. It is performed when a node is inserted into the right subtree of the left subtree. The LR rotation is a combination of the left rotation followed by the right rotation. There are multiple steps to be followed to carry this out.

    • Consider an example with A as the root node, B as the left child of A and C as the right child of B.
    • Since the unbalance occurs at A, a left rotation is applied on the child nodes of A, i.e. B and C.
    • After the rotation, the C node becomes the left child of A and B becomes the left child of C.
    • The unbalance still persists, therefore a right rotation is applied at the root node A and the left child C.
    • After the final right rotation, C becomes the root node, A becomes the right child and B is the left child.
    LR_Rotation

    Fig : LR Rotation

    RL Rotations

    RL rotation is also the extended version of the previous single rotations, hence it is called a double rotation and it is performed if a node is inserted into the left subtree of the right subtree. The RL rotation is a combination of the right rotation followed by the left rotation. There are multiple steps to be followed to carry this out.

    • Consider an example with A as the root node, B as the right child of A and C as the left child of B.
    • Since the unbalance occurs at A, a right rotation is applied on the child nodes of A, i.e. B and C.
    • After the rotation, the C node becomes the right child of A and B becomes the right child of C.
    • The unbalance still persists, therefore a left rotation is applied at the root node A and the right child C.
    • After the final left rotation, C becomes the root node, A becomes the left child and B is the right child.
    RL Rotations

    Fig : RL Rotation

    Basic Operations of AVL Trees

    The basic operations performed on the AVL Tree structures include all the operations performed on a binary search tree, since the AVL Tree at its core is actually just a binary search tree holding all its properties. Therefore, basic operations performed on an AVL Tree are − Insertion and Deletion.

    Insertion operation

    The data is inserted into the AVL Tree by following the Binary Search Tree property of insertion, i.e. the left subtree must contain elements less than the root value and right subtree must contain all the greater elements.

    However, in AVL Trees, after the insertion of each element, the balance factor of the tree is checked; if it does not exceed 1, the tree is left as it is. But if the balance factor exceeds 1, a balancing algorithm is applied to readjust the tree such that balance factor becomes less than or equal to 1 again.Algorithm

    The following steps are involved in performing the insertion operation of an AVL Tree −

    Step 1 − Create a node
    Step 2 − Check if the tree is empty
    Step 3 − If the tree is empty, the new node created will become the 
             root node of the AVL Tree.
    Step 4 − If the tree is not empty, we perform the Binary Search Tree 
             insertion operation and check the balancing factor of the node 
             in the tree.
    Step 5 − Suppose the balancing factor exceeds 1, we apply suitable 
             rotations on the said node and resume the insertion from Step 4.
    

    Let us understand the insertion operation by constructing an example AVL tree with 1 to 7 integers.

    Starting with the first element 1, we create a node and measure the balance, i.e., 0.

    AVL1

    Since both the binary search property and the balance factor are satisfied, we insert another element into the tree.

    AVL2

    The balance factor for the two nodes are calculated and is found to be -1 (Height of left subtree is 0 and height of the right subtree is 1). Since it does not exceed 1, we add another element to the tree.

    3rd element

    Now, after adding the third element, the balance factor exceeds 1 and becomes 2. Therefore, rotations are applied. In this case, the RR rotation is applied since the imbalance occurs at two right nodes.

    RR rotation applied

    The tree is rearranged as −

    tree rearranged

    Similarly, the next elements are inserted and rearranged using these rotations. After rearrangement, we achieve the tree as −

    balance

    Example

    Following are the implementations of this operation in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structNode{int data;structNode*leftChild;structNode*rightChild;int height;};intmax(int a,int b);intheight(structNode*N){if(N ==NULL)return0;return N->height;}intmax(int a,int b){return(a > b)? a : b;}structNode*newNode(int data){structNode*node =(structNode*)malloc(sizeof(structNode));
       node->data = data;
       node->leftChild =NULL;
       node->rightChild =NULL;
       node->height =1;return(node);}structNode*rightRotate(structNode*y){structNode*x = y->leftChild;structNode*T2 = x->rightChild;
       x->rightChild = y;
       y->leftChild = T2;
       y->height =max(height(y->leftChild),height(y->rightChild))+1;
       x->height =max(height(x->leftChild),height(x->rightChild))+1;return x;}structNode*leftRotate(structNode*x){structNode*y = x->rightChild;structNode*T2 = y->leftChild;
       y->leftChild = x;
       x->rightChild = T2;
       x->height =max(height(x->leftChild),height(x->rightChild))+1;
       y->height =max(height(y->leftChild),height(y->rightChild))+1;return y;}intgetBalance(structNode*N){if(N ==NULL)return0;returnheight(N->leftChild)-height(N->rightChild);}structNode*insertNode(structNode*node,int data){if(node ==NULL)return(newNode(data));if(data < node->data)
          node->leftChild =insertNode(node->leftChild, data);elseif(data > node->data)
          node->rightChild =insertNode(node->rightChild, data);elsereturn node;
       node->height =1+max(height(node->leftChild),height(node->rightChild));int balance =getBalance(node);if(balance >1&& data < node->leftChild->data)returnrightRotate(node);if(balance <-1&& data > node->rightChild->data)returnleftRotate(node);if(balance >1&& data > node->leftChild->data){
          node->leftChild =leftRotate(node->leftChild);returnrightRotate(node);}if(balance <-1&& data < node->rightChild->data){
          node->rightChild =rightRotate(node->rightChild);returnleftRotate(node);}return node;}structNode*minValueNode(structNode*node){structNode*current = node;while(current->leftChild !=NULL)
          current = current->leftChild;return current;}voidprintTree(structNode*root){if(root ==NULL)return;if(root !=NULL){printTree(root->leftChild);printf("%d ", root->data);printTree(root->rightChild);}}intmain(){structNode*root =NULL;
       root =insertNode(root,22);
       root =insertNode(root,14);
       root =insertNode(root,72);
       root =insertNode(root,44);
       root =insertNode(root,25);
       root =insertNode(root,63);
       root =insertNode(root,98);printf("AVL Tree: ");printTree(root);return0;}

    Output

    AVL Tree: 14 22 25 44 63 72 98 
    

    Deletion operation

    Deletion in the AVL Trees take place in three different scenarios −

    • Scenario 1 (Deletion of a leaf node) − If the node to be deleted is a leaf node, then it is deleted without any replacement as it does not disturb the binary search tree property. However, the balance factor may get disturbed, so rotations are applied to restore it.
    • Scenario 2 (Deletion of a node with one child) − If the node to be deleted has one child, replace the value in that node with the value in its child node. Then delete the child node. If the balance factor is disturbed, rotations are applied.
    • Scenario 3 (Deletion of a node with two child nodes) − If the node to be deleted has two child nodes, find the inorder successor of that node and replace its value with the inorder successor value. Then try to delete the inorder successor node. If the balance factor exceeds 1 after deletion, apply balance algorithms.

    Using the same tree given above, let us perform deletion in three scenarios −

    balance
    • Deleting element 7 from the tree above −

    Since the element 7 is a leaf, we normally remove the element without disturbing any other node in the tree

    remove 7th element
    • Deleting element 6 from the output tree achieved −

    However, element 6 is not a leaf node and has one child node attached to it. In this case, we replace node 6 with its child node: node 5.

    replace node

    The balance of the tree becomes 1, and since it does not exceed 1 the tree is left as it is. If we delete the element 5 further, we would have to apply the left rotations; either LL or LR since the imbalance occurs at both 1-2-4 and 3-2-4.

    balance2

    The balance factor is disturbed after deleting the element 5, therefore we apply LL rotation (we can also apply the LR rotation here).

    apply LR rotation

    Once the LL rotation is applied on path 1-2-4, the node 3 remains as it was supposed to be the right child of node 2 (which is now occupied by node 4). Hence, the node is added to the right subtree of the node 2 and as the left child of the node 4.

    balance minus one
    • Deleting element 2 from the remaining tree −

    As mentioned in scenario 3, this node has two children. Therefore, we find its inorder successor that is a leaf node (say, 3) and replace its value with the inorder successor.

    balance zero

    The balance of the tree still remains 1, therefore we leave the tree as it is without performing any rotations.Example

    Following are the implementations of this operation in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structNode{int data;structNode*leftChild;structNode*rightChild;int height;};intmax(int a,int b);intheight(structNode*N){if(N ==NULL)return0;return N->height;}intmax(int a,int b){return(a > b)? a : b;}structNode*newNode(int data){structNode*node =(structNode*)malloc(sizeof(structNode));
       node->data = data;
       node->leftChild =NULL;
       node->rightChild =NULL;
       node->height =1;return(node);}structNode*rightRotate(structNode*y){structNode*x = y->leftChild;structNode*T2 = x->rightChild;
       x->rightChild = y;
       y->leftChild = T2;
       y->height =max(height(y->leftChild),height(y->rightChild))+1;
       x->height =max(height(x->leftChild),height(x->rightChild))+1;return x;}structNode*leftRotate(structNode*x){structNode*y = x->rightChild;structNode*T2 = y->leftChild;
       y->leftChild = x;
       x->rightChild = T2;
       x->height =max(height(x->leftChild),height(x->rightChild))+1;
       y->height =max(height(y->leftChild),height(y->rightChild))+1;return y;}intgetBalance(structNode*N){if(N ==NULL)return0;returnheight(N->leftChild)-height(N->rightChild);}structNode*insertNode(structNode*node,int data){if(node ==NULL)return(newNode(data));if(data < node->data)
          node->leftChild =insertNode(node->leftChild, data);elseif(data > node->data)
          node->rightChild =insertNode(node->rightChild, data);elsereturn node;
       node->height =1+max(height(node->leftChild),height(node->rightChild));int balance =getBalance(node);if(balance >1&& data < node->leftChild->data)returnrightRotate(node);if(balance <-1&& data > node->rightChild->data)returnleftRotate(node);if(balance >1&& data > node->leftChild->data){
          node->leftChild =leftRotate(node->leftChild);returnrightRotate(node);}if(balance <-1&& data < node->rightChild->data){
          node->rightChild =rightRotate(node->rightChild);returnleftRotate(node);}return node;}structNode*minValueNode(structNode*node){structNode*current = node;while(current->leftChild !=NULL)
          current = current->leftChild;return current;}structNode*deleteNode(structNode*root,int data){if(root ==NULL)return root;if(data < root->data)
          root->leftChild =deleteNode(root->leftChild, data);elseif(data > root->data)
          root->rightChild =deleteNode(root->rightChild, data);else{if((root->leftChild ==NULL)||(root->rightChild ==NULL)){structNode*temp = root->leftChild ? root->leftChild : root->rightChild;if(temp ==NULL){
                temp = root;
                root =NULL;}else*root =*temp;free(temp);}else{structNode*temp =minValueNode(root->rightChild);
             root->data = temp->data;
             root->rightChild =deleteNode(root->rightChild, temp->data);}}if(root ==NULL)return root;
       root->height =1+max(height(root->leftChild),height(root->rightChild));int balance =getBalance(root);if(balance >1&&getBalance(root->leftChild)>=0)returnrightRotate(root);if(balance >1&&getBalance(root->leftChild)<0){
          root->leftChild =leftRotate(root->leftChild);returnrightRotate(root);}if(balance <-1&&getBalance(root->rightChild)<=0)returnleftRotate(root);if(balance <-1&&getBalance(root->rightChild)>0){
          root->rightChild =rightRotate(root->rightChild);returnleftRotate(root);}return root;}// Print the treevoidprintTree(structNode*root){if(root !=NULL){printTree(root->leftChild);printf("%d ", root->data);printTree(root->rightChild);}}intmain(){structNode*root =NULL;
       root =insertNode(root,22);
       root =insertNode(root,14);
       root =insertNode(root,72);
       root =insertNode(root,44);
       root =insertNode(root,25);
       root =insertNode(root,63);
       root =insertNode(root,98);printf("AVL Tree: ");printTree(root);
       root =deleteNode(root,25);printf("\nAfter deletion: ");printTree(root);return0;}

    Output

    AVL Tree: 14 22 25 44 63 72 98 
    After deletion: 14 22 44 63 72 98 
  • Binary Search Tree

    A Binary Search Tree (BST) is a tree in which all the nodes follow the below-mentioned properties −

    • The left sub-tree of a node has a key less than or equal to its parent node’s key.
    • The right sub-tree of a node has a key greater than or equal to its parent node’s key.

    Thus, BST divides all its sub-trees into two segments; the left sub-tree and the right sub-tree and can be defined as −

    left_subtree (keys) &leq; node (key) &leq; right_subtree (keys)
    

    Binary Tree Representation

    BST is a collection of nodes arranged in a way where they maintain BST properties. Each node has a key and an associated value. While searching, the desired key is compared to the keys in BST and if found, the associated value is retrieved.

    Following is a pictorial representation of BST −

    Tree Traversal

    We observe that the root node key (27) has all less-valued keys on the left sub-tree and the higher valued keys on the right sub-tree.

    Basic Operations

    Following are the basic operations of a Binary Search Tree −

    • Search − Searches an element in a tree.
    • Insert − Inserts an element in a tree.
    • Pre-order Traversal − Traverses a tree in a pre-order manner.
    • In-order Traversal − Traverses a tree in an in-order manner.
    • Post-order Traversal − Traverses a tree in a post-order manner.

    Defining a Node

    Define a node that stores some data, and references to its left and right child nodes.

    struct node {
       int data;
       struct node *leftChild;
       struct node *rightChild;
    };
    

    Whenever an element is to be searched, start searching from the root node. Then if the data is less than the key value, search for the element in the left subtree. Otherwise, search for the element in the right subtree. Follow the same algorithm for each node.

    Algorithm

    1. START
    2. Check whether the tree is empty or not
    3. If the tree is empty, search is not possible
    4. Otherwise, first search the root of the tree.
    5. If the key does not match with the value in the root, 
       search its subtrees.
    6. If the value of the key is less than the root value, 
       search the left subtree
    7. If the value of the key is greater than the root value, 
       search the right subtree.
    8. If the key is not found in the tree, return unsuccessful search.
    9. END
    

    Example

    Following are the implementations of this operation in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structnode{int data;structnode*leftChild,*rightChild;};structnode*root =NULL;structnode*newNode(int item){structnode*temp =(structnode*)malloc(sizeof(structnode));
       temp->data = item;
       temp->leftChild = temp->rightChild =NULL;return temp;}voidinsert(int data){structnode*tempNode =(structnode*)malloc(sizeof(structnode));structnode*current;structnode*parent;
       tempNode->data = data;
       tempNode->leftChild =NULL;
       tempNode->rightChild =NULL;//if tree is emptyif(root ==NULL){
          root = tempNode;}else{
          current = root;
          parent =NULL;while(1){
             parent = current;//go to left of the treeif(data < parent->data){
                current = current->leftChild;//insert to the leftif(current ==NULL){
                   parent->leftChild = tempNode;return;}}//go to right of the treeelse{
                current = current->rightChild;//insert to the rightif(current ==NULL){
                   parent->rightChild = tempNode;return;}}}}}structnode*search(int data){structnode*current = root;while(current->data != data){//go to left treeif(current->data > data){
                current = current->leftChild;}//else go to right treeelse{
                current = current->rightChild;}//not foundif(current ==NULL){returnNULL;}}return current;}voidprintTree(structnode* Node){if(Node ==NULL)return;printTree(Node->leftChild);printf(" --%d", Node->data);printTree(Node->rightChild);}intmain(){insert(55);insert(20);insert(90);insert(50);insert(35);insert(15);insert(65);printf("Insertion done");printf("\nBST: \n");printTree(root);structnode* k;int ele =35;printf("\nElement to be searched: %d", ele);
       k =search(35);if(k !=NULL)printf("\nElement %d found", k->data);elseprintf("\nElement not found");return0;}

    Output

    Insertion done
    BST: 
     --15 --20 --35 --50 --55 --65 --90
    Element to be searched: 35
    Element 35 found
    

    Insertion Operation

    Whenever an element is to be inserted, first locate its proper location. Start searching from the root node, then if the data is less than the key value, search for the empty location in the left subtree and insert the data. Otherwise, search for the empty location in the right subtree and insert the data.

    Algorithm

    1. START
    2. If the tree is empty, insert the first element as the root node of the 
       tree. The following elements are added as the leaf nodes.
    3. If an element is less than the root value, it is added into the left 
       subtree as a leaf node.
    4. If an element is greater than the root value, it is added into the right 
       subtree as a leaf node.
    5. The final leaf nodes of the tree point to NULL values as their 
       child nodes.
    6. END
    

    Example

    Following are the implementations of this operation in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structnode{int data;structnode*leftChild,*rightChild;};structnode*root =NULL;structnode*newNode(int item){structnode*temp =(structnode*)malloc(sizeof(structnode));
       temp->data = item;
       temp->leftChild = temp->rightChild =NULL;return temp;}voidinsert(int data){structnode*tempNode =(structnode*)malloc(sizeof(structnode));structnode*current;structnode*parent;
       tempNode->data = data;
       tempNode->leftChild =NULL;
       tempNode->rightChild =NULL;//if tree is emptyif(root ==NULL){
          root = tempNode;}else{
          current = root;
          parent =NULL;while(1){
             parent = current;//go to left of the treeif(data < parent->data){
                current = current->leftChild;//insert to the leftif(current ==NULL){
                   parent->leftChild = tempNode;return;}}//go to right of the treeelse{
                current = current->rightChild;//insert to the rightif(current ==NULL){
                   parent->rightChild = tempNode;return;}}}}}voidprintTree(structnode* Node){if(Node ==NULL)return;printTree(Node->leftChild);printf(" --%d", Node->data);printTree(Node->rightChild);}intmain(){insert(55);insert(20);insert(90);insert(50);insert(35);insert(15);insert(65);printf("Insertion done\n");printf("BST: \n");printTree(root);return0;}

    Output

    Insertion done
    BST: 
     --15 --20 --35 --50 --55 --65 --90
    

    Inorder Traversal

    The inorder traversal operation in a Binary Search Tree visits all its nodes in the following order −

    • Firstly, we traverse the left child of the root node/current node, if any.
    • Next, traverse the current node.
    • Lastly, traverse the right child of the current node, if any.

    Algorithm

    1. START
    2. Traverse the left subtree, recursively
    3. Then, traverse the root node
    4. Traverse the right subtree, recursively.
    5. END
    

    Example

    Following are the implementations of this operation in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structnode{int key;structnode*left,*right;};structnode*newNode(int item){structnode*temp =(structnode*)malloc(sizeof(structnode));
       temp->key = item;
       temp->left = temp->right =NULL;return temp;}// Inorder Traversalvoidinorder(structnode*root){if(root !=NULL){inorder(root->left);printf("%d -> ", root->key);inorder(root->right);}}// Insertion operationstructnode*insert(structnode*node,int key){if(node ==NULL)returnnewNode(key);if(key < node->key)
          node->left =insert(node->left, key);else
          node->right =insert(node->right, key);return node;}intmain(){structnode*root =NULL;
       root =insert(root,55);
       root =insert(root,20);
       root =insert(root,90);
       root =insert(root,50);
       root =insert(root,35);
       root =insert(root,15);
       root =insert(root,65);printf("Inorder traversal: ");inorder(root);}

    Output

    Inorder traversal: 15 -> 20 -> 35 -> 50 -> 55 -> 65 -> 90 -> 
    

    Preorder Traversal

    The preorder traversal operation in a Binary Search Tree visits all its nodes. However, the root node in it is first printed, followed by its left subtree and then its right subtree.

    Algorithm

    1. START
    2. Traverse the root node first.
    3. Then traverse the left subtree, recursively
    4. Later, traverse the right subtree, recursively.
    5. END
    

    Example

    Following are the implementations of this operation in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structnode{int key;structnode*left,*right;};structnode*newNode(int item){structnode*temp =(structnode*)malloc(sizeof(structnode));
       temp->key = item;
       temp->left = temp->right =NULL;return temp;}// Preorder Traversalvoidpreorder(structnode*root){if(root !=NULL){printf("%d -> ", root->key);preorder(root->left);preorder(root->right);}}// Insertion operationstructnode*insert(structnode*node,int key){if(node ==NULL)returnnewNode(key);if(key < node->key)
          node->left =insert(node->left, key);else
          node->right =insert(node->right, key);return node;}intmain(){structnode*root =NULL;
       root =insert(root,55);
       root =insert(root,20);
       root =insert(root,90);
       root =insert(root,50);
       root =insert(root,35);
       root =insert(root,15);
       root =insert(root,65);printf("Preorder traversal: ");preorder(root);}

    Output

    Preorder traversal: 55 -> 20 -> 15 -> 50 -> 35 -> 90 -> 65 -> 
    

    Postorder Traversal

    Like the other traversals, postorder traversal also visits all the nodes in a Binary Search Tree and displays them. However, the left subtree is printed first, followed by the right subtree and lastly, the root node.

    Algorithm

    1. START
    2. Traverse the left subtree, recursively
    3. Traverse the right subtree, recursively.
    4. Then, traverse the root node
    5. END
    

    Example

    Following are the implementations of this operation in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structnode{int key;structnode*left,*right;};structnode*newNode(int item){structnode*temp =(structnode*)malloc(sizeof(structnode));
       temp->key = item;
       temp->left = temp->right =NULL;return temp;}// Postorder Traversalvoidpostorder(structnode*root){if(root !=NULL){printf("%d -> ", root->key);postorder(root->left);postorder(root->right);}}// Insertion operationstructnode*insert(structnode*node,int key){if(node ==NULL)returnnewNode(key);if(key < node->key)
          node->left =insert(node->left, key);else
          node->right =insert(node->right, key);return node;}intmain(){structnode*root =NULL;
       root =insert(root,55);
       root =insert(root,20);
       root =insert(root,90);
       root =insert(root,50);
       root =insert(root,35);
       root =insert(root,15);
       root =insert(root,65);printf("Postorder traversal: ");postorder(root);}

    Output

    Postorder traversal: 55 -> 20 -> 15 -> 50 -> 35 -> 90 > 65 -> 
    

    Complete implementation

    Following are the complete implementations of Binary Search Tree in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structnode{int data;structnode*leftChild,*rightChild;};structnode*root =NULL;structnode*newNode(int item){structnode*temp =(structnode*)malloc(sizeof(structnode));
       temp->data = item;
       temp->leftChild = temp->rightChild =NULL;return temp;}voidinsert(int data){structnode*tempNode =(structnode*)malloc(sizeof(structnode));structnode*current;structnode*parent;
       tempNode->data = data;
       tempNode->leftChild =NULL;
       tempNode->rightChild =NULL;//if tree is emptyif(root ==NULL){
          root = tempNode;}else{
          current = root;
          parent =NULL;while(1){
             parent = current;//go to left of the treeif(data < parent->data){
                current = current->leftChild;//insert to the leftif(current ==NULL){
                   parent->leftChild = tempNode;return;}}//go to right of the treeelse{
                current = current->rightChild;//insert to the rightif(current ==NULL){
                   parent->rightChild = tempNode;return;}}}}}structnode*search(int data){structnode*current = root;while(current->data != data){if(current !=NULL){//go to left treeif(current->data > data){
                current = current->leftChild;}//else go to right treeelse{
                current = current->rightChild;}//not foundif(current ==NULL){returnNULL;}}}return current;}// Inorder Traversalvoidinorder(structnode*root){if(root !=NULL){inorder(root->leftChild);printf("%d -> ", root->data);inorder(root->rightChild);}}// Preorder Traversalvoidpreorder(structnode*root){if(root !=NULL){printf("%d -> ", root->data);preorder(root->leftChild);preorder(root->rightChild);}}// Postorder Traversalvoidpostorder(structnode*root){if(root !=NULL){printf("%d -> ", root->data);postorder(root->leftChild);postorder(root->rightChild);}}intmain(){insert(55);insert(20);insert(90);insert(50);insert(35);insert(15);insert(65);printf("Insertion done");printf("\nPreorder Traversal: ");preorder(root);printf("\nInorder Traversal: ");inorder(root);printf("\nPostorder Traversal: ");postorder(root);structnode* k;int ele =35;printf("\nElement to be searched: %d", ele);
       k =search(35);if(k !=NULL)printf("\nElement %d found", k->data);elseprintf("\nElement not found");return0;}

    Output

    Insertion done
    Preorder Traversal: 55 -> 20 -> 15 -> 50 -> 35 -> 90 -> 65 -> 
    Inorder Traversal: 15 -> 20 -> 35 -> 50 -> 55 -> 65 -> 90 -> 
    Postorder Traversal: 55 -> 20 -> 15 -> 50 -> 35 -> 90 -> 65 -> 
    Element to be searched: 35
    Element 35 found
  • Tree Traversal

    Traversal is a process to visit all the nodes of a tree and may print their values too. Because, all nodes are connected via edges (links) we always start from the root (head) node. That is, we cannot randomly access a node in a tree. There are three ways which we use to traverse a tree −

    • In-order Traversal
    • Pre-order Traversal
    • Post-order Traversal

    Generally, we traverse a tree to search or locate a given item or key in the tree or to print all the values it contains.

    In-order Traversal

    In this traversal method, the left subtree is visited first, then the root and later the right sub-tree. We should always remember that every node may represent a subtree itself.

    If a binary tree is traversed in-order, the output will produce sorted key values in an ascending order.

    In-order Traversal

    We start from A, and following in-order traversal, we move to its left subtree B.B is also traversed in-order. The process goes on until all the nodes are visited. The output of in-order traversal of this tree will be −

    D B E A F C G

    Algorithm

    Until all nodes are traversed −

    Step 1 − Recursively traverse left subtree.
    Step 2 − Visit root node.
    Step 3 − Recursively traverse right subtree.
    

    Example

    Following are the implementations of this operation in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structnode{int data;structnode*leftChild;structnode*rightChild;};structnode*root =NULL;voidinsert(int data){structnode*tempNode =(structnode*)malloc(sizeof(structnode));structnode*current;structnode*parent;
       tempNode->data = data;
       tempNode->leftChild =NULL;
       tempNode->rightChild =NULL;//if tree is emptyif(root ==NULL){
          root = tempNode;}else{
          current = root;
          parent =NULL;while(1){
             parent = current;//go to left of the treeif(data < parent->data){
                current = current->leftChild;//insert to the leftif(current ==NULL){
                   parent->leftChild = tempNode;return;}}//go to right of the treeelse{
                current = current->rightChild;//insert to the rightif(current ==NULL){
                   parent->rightChild = tempNode;return;}}}}}voidinorder_traversal(structnode* root){if(root !=NULL){inorder_traversal(root->leftChild);printf("%d ",root->data);inorder_traversal(root->rightChild);}}intmain(){int i;int array[7]={27,14,35,10,19,31,42};for(i =0; i <7; i++)insert(array[i]);printf("Inorder traversal: ");inorder_traversal(root);return0;}

    Output

    Inorder traversal: 10 14 19 27 31 35 42
    

    Pre-order Traversal

    In this traversal method, the root node is visited first, then the left subtree and finally the right subtree.

    Pre-order Traversal

    We start from A, and following pre-order traversal, we first visit A itself and then move to its left subtree BB is also traversed pre-order. The process goes on until all the nodes are visited. The output of pre-order traversal of this tree will be −

    A → B → D → E → C → F → G

    Algorithm

    Until all nodes are traversed −

    Step 1 − Visit root node.
    Step 2 − Recursively traverse left subtree.
    Step 3 − Recursively traverse right subtree.
    

    Example

    Following are the implementations of this operation in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structnode{int data;structnode*leftChild;structnode*rightChild;};structnode*root =NULL;voidinsert(int data){structnode*tempNode =(structnode*)malloc(sizeof(structnode));structnode*current;structnode*parent;
       tempNode->data = data;
       tempNode->leftChild =NULL;
       tempNode->rightChild =NULL;//if tree is emptyif(root ==NULL){
          root = tempNode;}else{
          current = root;
          parent =NULL;while(1){
             parent = current;//go to left of the treeif(data < parent->data){
                current = current->leftChild;//insert to the leftif(current ==NULL){
                   parent->leftChild = tempNode;return;}}//go to right of the treeelse{
                current = current->rightChild;//insert to the rightif(current ==NULL){
                   parent->rightChild = tempNode;return;}}}}}voidpre_order_traversal(structnode* root){if(root !=NULL){printf("%d ",root->data);pre_order_traversal(root->leftChild);pre_order_traversal(root->rightChild);}}intmain(){int i;int array[7]={27,14,35,10,19,31,42};for(i =0; i <7; i++)insert(array[i]);printf("Preorder traversal: ");pre_order_traversal(root);return0;}

    Output

    Preorder traversal: 27 14 10 19 35 31 42 
    

    Post-order Traversal

    In this traversal method, the root node is visited last, hence the name. First we traverse the left subtree, then the right subtree and finally the root node.

    Post-order Traversal

    We start from A, and following pre-order traversal, we first visit the left subtree BB is also traversed post-order. The process goes on until all the nodes are visited. The output of post-order traversal of this tree will be

    D → E → B → F → G → C → A

    Algorithm

    Until all nodes are traversed −

    Step 1 − Recursively traverse left subtree.
    Step 2 − Recursively traverse right subtree.
    Step 3 − Visit root node.
    

    Example

    Following are the implementations of this operation in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structnode{int data;structnode*leftChild;structnode*rightChild;};structnode*root =NULL;voidinsert(int data){structnode*tempNode =(structnode*)malloc(sizeof(structnode));structnode*current;structnode*parent;
       tempNode->data = data;
       tempNode->leftChild =NULL;
       tempNode->rightChild =NULL;//if tree is emptyif(root ==NULL){
          root = tempNode;}else{
          current = root;
          parent =NULL;while(1){
             parent = current;//go to left of the treeif(data < parent->data){
                current = current->leftChild;//insert to the leftif(current ==NULL){
                   parent->leftChild = tempNode;return;}}//go to right of the treeelse{
                current = current->rightChild;//insert to the rightif(current ==NULL){
                   parent->rightChild = tempNode;return;}}}}}voidpost_order_traversal(structnode* root){if(root !=NULL){post_order_traversal(root->leftChild);post_order_traversal(root->rightChild);printf("%d ", root->data);}}intmain(){int i;int array[7]={27,14,35,10,19,31,42};for(i =0; i <7; i++)insert(array[i]);printf("Post order traversal: ");post_order_traversal(root);return0;}

    Output

    Post order traversal: 10 19 14 31 42 35 27
    

    Complete Implementation

    Now let’s see the complete implementation of tree traversal in various programming languages −

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>structnode{int data;structnode*leftChild;structnode*rightChild;};structnode*root =NULL;voidinsert(int data){structnode*tempNode =(structnode*)malloc(sizeof(structnode));structnode*current;structnode*parent;
       tempNode->data = data;
       tempNode->leftChild =NULL;
       tempNode->rightChild =NULL;//if tree is emptyif(root ==NULL){
          root = tempNode;}else{
          current = root;
          parent =NULL;while(1){
             parent = current;//go to left of the treeif(data < parent->data){
                current = current->leftChild;//insert to the leftif(current ==NULL){
                   parent->leftChild = tempNode;return;}}//go to right of the treeelse{
                current = current->rightChild;//insert to the rightif(current ==NULL){
                   parent->rightChild = tempNode;return;}}}}}voidpre_order_traversal(structnode* root){if(root !=NULL){printf("%d ",root->data);pre_order_traversal(root->leftChild);pre_order_traversal(root->rightChild);}}voidinorder_traversal(structnode* root){if(root !=NULL){inorder_traversal(root->leftChild);printf("%d ",root->data);inorder_traversal(root->rightChild);}}voidpost_order_traversal(structnode* root){if(root !=NULL){post_order_traversal(root->leftChild);post_order_traversal(root->rightChild);printf("%d ", root->data);}}intmain(){int i;int array[7]={27,14,35,10,19,31,42};for(i =0; i <7; i++)insert(array[i]);printf("Preorder traversal: ");pre_order_traversal(root);printf("\nInorder traversal: ");inorder_traversal(root);printf("\nPost order traversal: ");post_order_traversal(root);return0;}

    Output

    Preorder traversal: 27 14 10 19 35 31 42 
    Inorder traversal: 10 14 19 27 31 35 42 
    Post order traversal: 10 19 14 31 42 35 27
  • Tree Data Structure

    Tree Data Structrue

    A tree is a non-linear abstract data type with a hierarchy-based structure. It consists of nodes (where the data is stored) that are connected via links. The tree data structure stems from a single node called a root node and has subtrees connected to the root.

    Tree Data Structure

    Important Terms

    Following are the important terms with respect to tree.

    • Path − Path refers to the sequence of nodes along the edges of a tree.
    • Root − The node at the top of the tree is called root. There is only one root per tree and one path from the root node to any node.
    • Parent − Any node except the root node has one edge upward to a node called parent.
    • Child − The node below a given node connected by its edge downward is called its child node.
    • Leaf − The node which does not have any child node is called the leaf node.
    • Subtree − Subtree represents the descendants of a node.
    • Visiting − Visiting refers to checking the value of a node when control is on the node.
    • Traversing − Traversing means passing through nodes in a specific order.
    • Levels − Level of a node represents the generation of a node. If the root node is at level 0, then its next child node is at level 1, its grandchild is at level 2, and so on.
    • Keys − Key represents a value of a node based on which a search operation is to be carried out for a node.

    Types of Trees

    There are three types of trees −

    • General Trees
    • Binary Trees
    • Binary Search Trees

    General Trees

    General trees are unordered tree data structures where the root node has minimum 0 or maximum n subtrees.

    The General trees have no constraint placed on their hierarchy. The root node thus acts like the superset of all the other subtrees.

    General Trees

    Binary Trees

    Binary Trees are general trees in which the root node can only hold up to maximum 2 subtrees: left subtree and right subtree. Based on the number of children, binary trees are divided into three types.

    Full Binary Tree

    • A full binary tree is a binary tree type where every node has either 0 or 2 child nodes.

    Complete Binary Tree

    • A complete binary tree is a binary tree type where all the leaf nodes must be on the same level. However, root and internal nodes in a complete binary tree can either have 0, 1 or 2 child nodes.

    Perfect Binary Tree

    • A perfect binary tree is a binary tree type where all the leaf nodes are on the same level and every node except leaf nodes have 2 children.
    Binary Trees

    Binary Search Trees

    Binary Search Trees possess all the properties of Binary Trees including some extra properties of their own, based on some constraints, making them more efficient than binary trees.

    The data in the Binary Search Trees (BST) is always stored in such a way that the values in the left subtree are always less than the values in the root node and the values in the right subtree are always greater than the values in the root node, i.e. left subtree < root node right subtree.

    binary serach tree

    Advantages of BST

    • Binary Search Trees are more efficient than Binary Trees since time complexity for performing various operations reduces.
    • Since the order of keys is based on just the parent node, searching operation becomes simpler.
    • The alignment of BST also favors Range Queries, which are executed to find values existing between two keys. This helps in the Database Management System.

    Disadvantages of BST

    The main disadvantage of Binary Search Trees is that if all elements in nodes are either greater than or lesser than the root node, the tree becomes skewed. Simply put, the tree becomes slanted to one side completely.

    This skewness will make the tree a linked list rather than a BST, since the worst case time complexity for searching operation becomes O(n).

    To overcome this issue of skewness in the Binary Search Trees, the concept of Balanced Binary Search Trees was introduced.

    Balanced Binary Search Trees

    Consider a Binary Search Tree with m as the height of the left subtree and n as the height of the right subtree. If the value of (m-n) is equal to 0,1 or -1, the tree is said to be a Balanced Binary Search Tree.

    The trees are designed in a way that they self-balance once the height difference exceeds 1. Binary Search Trees use rotations as self-balancing algorithms. There are four different types of rotations: Left Left, Right Right, Left Right, Right Left.

    There are various types of self-balancing binary search trees −

  • Maxflow Mincut Theorem

    Let’s talk about Maxflow and Mincut. These are like two sides of the same coin when dealing with network flow problems. If you ever wondered how stuff moves through a network, whether its water in pipes, cars on roads, or data in a network, then youre already thinking in terms of max flow and min cut.

    What is Maxflow?

    Maxflow (Maximum Flow) is all about figuring out the most stuff (data, water, traffic) that can move from one point (source) to another (sink) in a network without breaking any capacity limits. Imagine a bunch of pipes carrying water, and you want to know the maximum amount of water that can flow through the system without overflowing.

    What is Mincut?

    Mincut (Minimum Cut) is kind of the opposite. Its about finding the weakest link in the network that, if cut, would completely stop the flow from the source to the sink. Think of it like identifying the narrowest point in a road system where a traffic jam would completely block all movement.

    Maxflow and Mincut Theorem

    Heres something cool: The Maxflow-Mincut Theorem says that the maximum flow in a network is equal to the total capacity of the smallest set of edges that, if removed, would stop the flow completely. This means if you

    These concepts arent just theoretical; they are used in real life all the time:

    • Traffic Management: Figuring out the best way to manage congestion in roads.
    • Computer Networks: Making sure data packets travel efficiently.
    • Airline Scheduling: Ensuring flights are assigned to routes in the best way possible.
    • Water Distribution: Managing water flow in pipelines to avoid shortages.
    • Project Management: Finding the critical path in workflows to improve efficiency.

    Example to Understand Maxflow

    Imagine a city with roads connecting different areas. If you want to get the max number of cars from the start point to the endpoint, thats maxflow. Now, let’s write code.

    Maxflow using Ford-Fulkerson Algorithm

    Steps to find the maxflow in a graph using the Ford-Fulkerson Algorithm:

    1. Start with the initial flow as 0.
    2. While there is an augmenting path from source to sink:
        a. Find the path with the minimum capacity.
        b. Increase the flow along the path.
    3. The maximum flow is the sum of all flows along the augmenting paths.
    

    Here’s the code to find the maxflow in a graph using the Ford-Fulkerson Algorithm:

    CC++JavaPython

    #include <stdio.h>#include <string.h>#include <limits.h>#define V 6  // Number of vertices// Function to perform DFS and find an augmenting pathintdfs(int rGraph[V][V],int s,int t,int parent[]){int visited[V];memset(visited,0,sizeof(visited));int stack[V], top =-1;
       stack[++top]= s;
       visited[s]=1;while(top >=0){int u = stack[top--];for(int v =0; v < V; v++){if(!visited[v]&& rGraph[u][v]>0){
                stack[++top]= v;
                visited[v]=1;
                parent[v]= u;if(v == t){return1;}}}}return0;}// Ford-Fulkerson algorithm to find the max-flowintfordFulkerson(int graph[V][V],int source,int sink){int rGraph[V][V];// Residual graphfor(int i =0; i < V; i++){for(int j =0; j < V; j++){
             rGraph[i][j]= graph[i][j];}}int parent[V];int maxFlow =0;// Augment the flow while there is an augmenting pathwhile(dfs(rGraph, source, sink, parent)){int pathFlow = INT_MAX;for(int v = sink; v != source; v = parent[v]){int u = parent[v];
             pathFlow =(pathFlow < rGraph[u][v])? pathFlow : rGraph[u][v];}
          maxFlow += pathFlow;// Update the residual graphfor(int v = sink; v != source; v = parent[v]){int u = parent[v];
             rGraph[u][v]-= pathFlow;
             rGraph[v][u]+= pathFlow;}}return maxFlow;}intmain(){// Example graphint graph[V][V]={{0,16,13,0,0,0},{0,0,10,12,0,0},{0,4,0,0,14,0},{0,0,9,0,0,20},{0,0,0,7,0,4},{0,0,0,0,0,0}};int source =0, sink =5;printf("Maximum flow: %d\n",fordFulkerson(graph, source, sink));return0;}

    Output

    Following is the output of the above code:

    Maximum flow: 23
    

    Example to Understand Mincut

    We already know what mincut is, let’s code to find the mincut in a graph using the Ford-Fulkerson Algorithm:

    Mincut using Ford-Fulkerson Algorithm

    Steps to find the mincut in a graph using the Ford-Fulkerson Algorithm:

    Initialize the graph and residual graph, and set up the parent[] array to track paths.
    Perform BFS to find an augmenting path from source to sink in the residual graph.
    Update residual graph by reducing the flow along the found path and adding reverse flow.
    Repeat BFS until no augmenting path is found.
    Perform DFS to find all reachable vertices from the source in the residual graph.
    Print the edges that go from a reachable vertex to a non-reachable vertex, which form the minimum cut.
    

    Now, let’s see how to find the mincut in a graph using the Ford-Fulkerson Algorithm:

    CC++JavaPython

    #include <stdio.h>#include <string.h>#include <limits.h>#include <stdbool.h>#define V 6// Simple queue structure for BFStypedefstruct{int items[V];int front, rear;} Queue;voidinitQueue(Queue* q){
       q->front =-1;
       q->rear =-1;}intisEmpty(Queue* q){return q->front ==-1;}voidenqueue(Queue* q,int value){if(q->rear == V -1)return;// Queue overflowif(q->front ==-1)
          q->front =0;
       q->items[++(q->rear)]= value;}intdequeue(Queue* q){if(isEmpty(q))return-1;// Queue underflowint item = q->items[q->front];if(q->front == q->rear)
          q->front = q->rear =-1;else
          q->front++;return item;}intbfs(int rGraph[V][V],int s,int t,int parent[]){
       bool visited[V];memset(visited,0,sizeof(visited));
    
       Queue q;initQueue(&q);enqueue(&q, s);
       visited[s]=1;
       parent[s]=-1;while(!isEmpty(&q)){int u =dequeue(&q);for(int v =0; v < V; v++){if(!visited[v]&& rGraph[u][v]>0){enqueue(&q, v);
                parent[v]= u;
                visited[v]=1;}}}return(visited[t]==1);}voiddfs(int rGraph[V][V],int s, bool visited[]){
       visited[s]=1;for(int i =0; i < V; i++){if(rGraph[s][i]&&!visited[i])dfs(rGraph, i, visited);}}voidminCut(int graph[V][V],int s,int t){int u, v;int rGraph[V][V];for(u =0; u < V; u++){for(v =0; v < V; v++){
             rGraph[u][v]= graph[u][v];}}int parent[V];while(bfs(rGraph, s, t, parent)){int path_flow = INT_MAX;for(v = t; v != s; v = parent[v]){
             u = parent[v];
             path_flow =(path_flow < rGraph[u][v])? path_flow : rGraph[u][v];}for(v = t; v != s; v = parent[v]){
             u = parent[v];
             rGraph[u][v]-= path_flow;
             rGraph[v][u]+= path_flow;}}
    
       bool visited[V];memset(visited,0,sizeof(visited));dfs(rGraph, s, visited);for(int i =0; i < V; i++){for(int j =0; j < V; j++){if(visited[i]&&!visited[j]&& graph[i][j]){printf("%d - %d\n", i, j);}}}}intmain(){int graph[V][V]={{0,10,0,10,0,0},{0,0,5,0,0,0},{0,0,0,0,10,0},{0,0,0,0,0,20},{0,0,0,0,0,10},{0,0,0,0,0,0}};printf("Minimum Cut Edges: \n");minCut(graph,0,5);return0;}

    Output

    Following is the output of the above code:

    Minimum Cut Edges:
    0 - 3
    1 - 2
    

    Conclusion

    Maxflow helps us figure out how much we can push through a system, while Mincut tells us where the weakest spots are. Both are super useful in real-life applications, from networks to transportation and beyond. Learning these concepts can help solve big optimization problems in different fields.

  • Edmond’s Blossom Algorithm

    The Edmonds Blossom Algorithm is used to find the maximum matchings in general graphs. It extends the Blossom Algorithm, which is designed for bipartite graphs. The Edmonds Blossom Algorithm works with any graph, making it a versatile tool for solving matching problems.

    Why Edmonds Blossom Algorithm?

    The Edmonds Blossom Algorithm is a smart way to find the largest possible matchings in general graphs. It’s an extension of the Blossom Algorithm, which was originally created to work with bipartite graphs.

    By building on the Blossom Algorithm, the Edmonds Blossom Algorithm can also work with more complicated graphs, making it more versatile and efficient at finding maximum matchings with fewer steps.

    Edmonds Blossom Algorithm Example

    Consider a simple graph with vertices and edges. Our goal is to find a maximum matching using the Edmonds Blossom Algorithm. The graph is shown below:

       A----B----D
       / \ /     |
      C---E      F   
    

    matching is a set of edges where no two edges share a common vertex. A maximum matching contains the maximum number of edges possible.

    Edmonds Blossom Algorithm Steps

    The Edmonds Blossom Algorithm finds augmenting paths in the graph to increase the matching size. Here are the steps:

    1. Find an augmenting path, which is a path from a free vertex to another free vertex that alternates between matched and unmatched edges.
    2. If an augmenting path is found, update the matching by flipping the edges along the path, increasing the matching size by one.
    3. If no augmenting path is found, the current matching is maximum, and the algorithm stops.
    4. Repeat steps 1-3 until no more augmenting paths are found.
    

    Following these steps, the Edmonds Blossom Algorithm efficiently finds a maximum matching in a general graph.

    Code for Edmonds Blossom Algorithm

    Here’s an example code snippet for the Edmonds Blossom Algorithm in C, C++, Java, and Python:

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>#include <string.h>#define MAX_VERTICES 6#define MAX_EDGES 6int adj[MAX_VERTICES][MAX_VERTICES]={{0,1,1,0,0,0},{1,0,0,1,1,0},{1,0,0,0,1,0},{0,1,0,0,0,1},{0,1,1,0,0,0},{0,0,0,1,0,0}};int match[MAX_VERTICES];int visited[MAX_VERTICES];intfind_augmenting_path(int u){for(int v =0; v < MAX_VERTICES; v++){if(adj[u][v]&&!visited[v]){
             visited[v]=1;if(match[v]==-1||find_augmenting_path(match[v])){
                match[v]= u;return1;}}}}intedmonds_blossom_algorithm(){memset(match,-1,sizeof(match));int max_matching =0;for(int u =0; u < MAX_VERTICES; u++){memset(visited,0,sizeof(visited));if(find_augmenting_path(u)){
             max_matching++;}}return max_matching;}intmain(){int max_matching =edmonds_blossom_algorithm();printf("Maximum Matching: %d\n", max_matching);return0;}

    Output

    Following is the output of the above code:

    Maximum Matching: 6
    

    Applications of Edmonds Blossom Algorithm

    The Edmonds Blossom Algorithm is used in various applications, including:

    • Matching problems: Finding maximum matchings in graphs for scheduling, resource allocation, and network design.
    • Graph theory: Studying graph properties and their applications in computer science and other fields.
    • Combinatorial optimization: Solving problems that involve finding the best solution from a finite set of possibilities.

    Conclusion

    In this chapter, we discussed the Edmonds Blossom Algorithm, why it’s useful and steps to implement it. We have also provided example code snippets in CC++Java, and Python to demonstrate how the Edmonds Blossom Algorithm works. And we explored application of the Edmonds Blossom Algorithm.

  • Flow Networks

    Flow networks are a special kind of graph that are used for representing the flow of data or resources from one point to another. They are used in a variety of applications, including transportation networks, communication networks, and computer networks. In this chapter, we will discuss the basic concepts of flow networks and how they are used in data structures.

    What is a Flow Network?

    flow network is a directed graph in which each edge has a capacity and a flow. The capacity of an edge represents the maximum amount of flow that can pass through it, while the flow of an edge represents the actual amount of flow passing through it. The flow of an edge must be less than or equal to its capacity.

    Flow networks are used to model the flow of data or resources from a source to a sink. The source is the starting point of the flow, while the sink is the ending point. The goal of a flow network is to maximize the flow from the source to the sink while respecting the capacities of the edges.

    Flow Network Example

    Let’s consider a simple flow network with a source, a sink, and some intermediate nodes. The edges of the flow network have capacities and flows as shown below:

            10(5)
        A --------> B
        |           |
       5(3)        8(6)
        |           |
        v           v
        C --------> D
            7(4)    | 6(4)
                    v
                    E
                    | 4(4)
                    v
                    F
    
    

    The flow network represents the flow of data from the source A to the sink F. The goal is to maximize the flow from A to F while respecting the capacities of the edges. In this example, the maximum flow from A to F is 10, which is achieved by sending 5 units of flow along edge AB, 3 units along edge AC, 6 units along edge BD, 4 units along edge CE, and 4 units along edge EF.

    • Edge AB: Capacity = 10, Flow = 5
    • Edge AC: Capacity = 5, Flow = 3
    • Edge BD: Capacity = 8, Flow = 6
    • Edge CE: Capacity = 7, Flow = 4
    • Edge DE: Capacity = 6, Flow = 4
    • Edge EF: Capacity = 4, Flow = 4

    Flow Network Algorithms

    There are several algorithms that can be used to find the maximum flow in a flow network. Some of the most common algorithms include:

    1. Ford-Fulkerson Algorithm
    2. Edmonds-Karp Algorithm
    3. Dinic’s Algorithm
    4. Push-Relabel Algorithm

    These algorithms use different techniques to find the maximum flow in a flow network. They are based on the concept of augmenting paths, which are paths from the source to the sink that can carry additional flow. By finding augmenting paths and increasing the flow along them.

    Ford-Fulkerson Algorithm to Find Maximum Flow

    Let’s take a look at the Ford-Fulkerson algorithm, which is one of the most popular algorithms for finding the maximum flow in a flow network. The algorithm works by finding augmenting paths from the source to the sink and increasing the flow along these paths.

    The steps of the Ford-Fulkerson algorithm are as follows:

    1. Initialize the flow of each edge to 0.
    2. While there is an augmenting path from the source to the sink:
        a. Find an augmenting path using a depth-first search or breadth-first search.
        b. Determine the maximum flow that can be sent along the augmenting path.
        c. Increase the flow along the augmenting path by the maximum flow.
    3. Return the maximum flow.
    

    By following these steps, the Ford-Fulkerson algorithm can find the maximum flow in a flow network. The algorithm terminates when there are no more augmenting paths from the source to the sink.

    Code for Ford-Fulkerson Algorithm

    Here’s an example code snippet for the Ford-Fulkerson algorithm in C, C++, Java, and Python:

    CC++JavaPython

    #include <stdio.h>#include <limits.h>#include <stdbool.h>#include <string.h>#define V 6
    
    bool bfs(int rGraph[V][V],int s,int t,int parent[]){
       bool visited[V];memset(visited,0,sizeof(visited));int queue[V];int front =0, rear =0;
       queue[rear++]= s;
       visited[s]= true;
       parent[s]=-1;while(front != rear){int u = queue[front++];for(int v =0; v < V; v++){if(!visited[v]&& rGraph[u][v]>0){
                queue[rear++]= v;
                parent[v]= u;
                visited[v]= true;}}}return visited[t];}intfordFulkerson(int graph[V][V],int s,int t){int u, v;int rGraph[V][V];for(u =0; u < V; u++)for(v =0; v < V; v++)
             rGraph[u][v]= graph[u][v];int parent[V];int max_flow =0;while(bfs(rGraph, s, t, parent)){int path_flow = INT_MAX;for(v = t; v != s; v = parent[v]){
             u = parent[v];
             path_flow = path_flow < rGraph[u][v]? path_flow : rGraph[u][v];}for(v = t; v != s; v = parent[v]){
             u = parent[v];
             rGraph[u][v]-= path_flow;
             rGraph[v][u]+= path_flow;}
    
          max_flow += path_flow;}return max_flow;}intmain(){int graph[V][V]={{0,10,5,0,0,0},{0,0,0,8,0,0},{0,0,0,0,7,0},{0,0,0,0,6,6},{0,0,0,0,0,4},{0,0,0,0,0,0}};printf("The maximum possible flow is %d\n",fordFulkerson(graph,0,5));return0;}

    Output

    Following is the output of the above code:

    The maximum possible flow is 10
    

    Applications of Flow Networks

    Flow networks are used in a variety of applications, including:

    • Transportation networks: Flow networks can be used to model the flow of traffic on roads, railways, and other transportation systems.
    • Communication networks: Flow networks can be used to model the flow of data in computer networks, telecommunication networks, and other communication systems.
    • Supply chain networks: Flow networks can be used to model the flow of goods, materials, and resources in supply chain networks.
    • Fluid networks: Flow networks can be used to model the flow of fluids in pipelines, water distribution systems, and other fluid systems.

    By using flow networks, engineers and researchers can analyze and optimize the flow of data or resources in a wide range of applications. Flow networks are a powerful tool for modeling complex systems and finding efficient solutions to flow-related problems.

    Conclusion

    In this chapter, we discussed the concept of flow networks and how they are used in data structures. We also explored the Ford-Fulkerson algorithm, which is one of the most popular algorithms for finding the maximum flow in a flow network. By understanding flow networks and the algorithms used to analyze them, you can solve a wide range of flow-related problems in various domains.

  • Network Flow Problems

    Network flow problems are all about figuring out how stuff (data, resources, whatever) can move from a start point (source) to an endpoint (sink) in a network. Its like optimizing a traffic system, a data network, or even supply chains. In this article, well talk about what network flow problems are and how we handle them using data structures.

    What are Network Flow Problems?

    So, network flow problems deal with this idea of moving flow from one point to another. We have a network of nodes connected by edges, and each edge has a capacity. The capacity shows how much can actually flow through that edge, and the flow is the amount thats actually moving. The rule is that the flow on an edge can’t be more than its capacity.

    The whole point of these problems is to move as much “stuff” (data, goods, etc.) as we can from the source to the sink. But we have to respect the limits (capacities) of each edge along the way. The goal is to maximize the flow without breaking any edges.

    Network Flow Problem Example

    Imagine we have a simple network with a source node and a sink node. The nodes are connected by edges with certain capacities. We apply the right algorithm to find out how much flow can go from source to sink while respecting the edge capacities. We want to push as much flow through the network as possible.

    Network Flow Problem Algorithms

    To solve these problems, we use a few different algorithms. They help us figure out the max flow in the network. Heres a quick rundown:

    • Ford-Fulkerson Algorithm: This ones pretty basic. It keeps finding augmenting paths and pushing flow through them until it cant anymore.
    • Edmonds-Karp Algorithm: Its a bit faster than Ford-Fulkerson, using BFS to find the shortest augmenting path.
    • Dinic’s Algorithm: A bit more advanced and faster in some cases. It uses a layered approach.
    • Push-Relabel Algorithm: Works by pushing flow around efficiently and adjusting the flow with labels.

    They all work by finding these paths called augmenting pathspaths where we can add more flow. Its like a race to find the best path to push more stuff through until theres no room left to move more.

    Example of Network Flow Problems

    You are given a directed graph representing a network with capacities on edges. The graph has a source node (s) and a sink node (t). The goal is to determine the maximum flow that can be sent from source to sink while respecting the capacity constraints.

        (0)
        /   \
      10     15
      /       \
    (1)---5---(3)  
      \       /
       10    10
        \   /
         (4)
           \
            10
             \
             (5)
    
    

    In the above graph, the numbers on the edges represent the capacities. The maximum flow from source (0) to sink (5) is 20.

    Network Flow Problem Code

    Here’s code for above problem in C, C++, Java, and Python:

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>#include <string.h>#include <limits.h>  #define MAX_VERTICES 6#define min(a, b) ((a) < (b) ? (a) : (b))intbfs(int rGraph[MAX_VERTICES][MAX_VERTICES],int s,int t,int parent[MAX_VERTICES]){int visited[MAX_VERTICES];memset(visited,0,sizeof(visited));
       visited[s]=1;int queue[MAX_VERTICES];int front =0, rear =0;
       queue[rear++]= s;
       parent[s]=-1;while(front != rear){int u = queue[front++];for(int v =0; v < MAX_VERTICES; v++){if(!visited[v]&& rGraph[u][v]>0){
                queue[rear++]= v;
                parent[v]= u;
                visited[v]=1;if(v == t){return1;}}}}return0;// No more augmenting path}intford_fulkerson(int graph[MAX_VERTICES][MAX_VERTICES],int s,int t){int u, v;int rGraph[MAX_VERTICES][MAX_VERTICES];for(u =0; u < MAX_VERTICES; u++)for(v =0; v < MAX_VERTICES; v++)
             rGraph[u][v]= graph[u][v];int parent[MAX_VERTICES];int max_flow =0;while(bfs(rGraph, s, t, parent)){int path_flow = INT_MAX;for(v = t; v != s; v = parent[v]){
             u = parent[v];
             path_flow =min(path_flow, rGraph[u][v]);}for(v = t; v != s; v = parent[v]){
             u = parent[v];
             rGraph[u][v]-= path_flow;
             rGraph[v][u]+= path_flow;}
    
          max_flow += path_flow;}return max_flow;}intmain(){int adj[MAX_VERTICES][MAX_VERTICES]={{0,10,15,0,0,0},{0,0,0,5,10,0},{0,0,0,0,0,10},{0,0,0,0,0,5},{0,0,0,0,0,10},{0,0,0,0,0,0}};int source =0;int sink =5;int max_flow =ford_fulkerson(adj, source, sink);printf("Maximum flow from source to sink is %d\n", max_flow);return0;}

    Output

    Following is the output of the above code:

    Maximum flow from source to sink is 20
    

    Applications of Network Flow Problems

    These problems arent just theoretical. They show up everywhere! Here are some areas where we use them:

    • Transportation Networks: Traffic, buses, trainsanything that moves people or goods. We use network flow problems to manage how stuff moves around in these systems.
    • Communication Networks: Data, video calls, web trafficall of that depends on network flow. By solving these problems, we can figure out how to route the data in the most efficient way.
    • Supply Chains: Getting goods and materials from one place to another is a classic flow problem. We use these algorithms to optimize how resources move through a supply chain.
    • Fluid Systems: Think water pipes or oil pipelines. We use network flow problems to manage the flow of liquids or gases in these systems.

    By solving network flow problems, engineers and researchers can tweak and improve systems where resources flow from one place to another. Whether its traffic, data, or anything in between, these problems help optimize flow in the best way possible.

    Conclusion

    In this tutorial, we talked about network flow problems and how they help us figure out the best way to move stuff from one place to another. We use algorithms to find the maximum flow in a network, respecting the capacities of each edge. These problems show up in all sorts of systems, from transportation to communication to supply chains. By solving network flow problems, we can optimize how resources move around in these systems, making them more efficient and effective.

  • What Is an Augmenting Path?

    An augmenting path in a graph is just a way to get from the source to the sink where we can push more flow. In a flow network, the goal is to find these paths and increase the flow along them, ultimately making the total flow as big as possible.

    These paths are essential in finding the maximum flow from the source to the sink in flow networks.

    Augmenting Path Example

    Lets break it down with an example. Imagine we have a simple flow network with a source, a sink, and a few intermediate nodes. Each edge between nodes has a capacity (how much it can carry) and flow (how much is currently flowing). Here’s how it looks:

    In this network, node A is the source and node F is the sink. The capacities and flows of the edges are as follows:

    • Edge AB: Capacity = 10, Flow = 5
    • Edge AC: Capacity = 15, Flow = 10
    • Edge BD: Capacity = 5, Flow = 3
    • Edge BE: Capacity = 10, Flow = 7
    • Edge CF: Capacity = 10, Flow = 8

    This flow network shows how data flows from A to F. The key here is to push as much flow as possible while respecting the capacities of the edges. In this case, the maximum flow from A to F is 15, which is achieved by sending 5 units of flow through edge AB10 units through edge AC, and 5 units through edge CF. But theres still room for improvement by finding augmenting paths to push more flow through!

    Augmenting Path Algorithms

    Now, lets talk about some algorithms that help us find these augmenting paths. There are a few options, and they work in different ways to maximize the flow:

    • Ford-Fulkerson Algorithm: This is a basic and simple approach, but it might not be the fastest.
    • Edmonds-Karp Algorithm: This one is more efficient because it uses BFS to find augmenting paths.
    • Dinic’s Algorithm: A more advanced method that uses a layered approach for better performance.
    • Push-Relabel Algorithm: Instead of finding paths directly, this method pushes excess flow and adjusts labels to optimize the flow.

    Implemention of Augmenting Paths

    Now that we know what augmenting paths are and how they help us increase the flow in a network, lets see how we can implement them in code. Heres a simple example of the Ford-Fulkerson algorithm –

    Algorithm for Augmenting Paths

    1. Create a residual graph with the same capacities as the original graph.
    2. While theres an augmenting path from the source to the sink:
       a. Find the path using a search algorithm (like BFS).
       b. Determine the maximum flow that can be pushed through the path.
       c. Update the residual graph by reducing the capacities of forward edges and increasing the capacities of backward edges.
    3. Return the maximum flow found.
    

    Code for Augmenting Paths

    Heres a simple code snippet that uses the Ford-Fulkerson algorithm to find the maximum flow in a flow network:

    CC++JavaPython

    #include <stdio.h>#include <limits.h>#include <stdbool.h>#define V 6
    
    bool bfs(int rGraph[V][V],int s,int t,int parent[]){
       bool visited[V]={false};int queue[V];int front =0, rear =0;
       queue[rear++]= s;
       visited[s]= true;
       parent[s]=-1;while(front != rear){int u = queue[front++];for(int v =0; v < V; v++){if(!visited[v]&& rGraph[u][v]>0){
                queue[rear++]= v;
                parent[v]= u;
                visited[v]= true;}}}return visited[t];}intfordFulkerson(int graph[V][V],int s,int t){int u, v;int rGraph[V][V];for(u =0; u < V; u++){for(v =0; v < V; v++){
             rGraph[u][v]= graph[u][v];}}int parent[V];int max_flow =0;while(bfs(rGraph, s, t, parent)){int path_flow = INT_MAX;for(v = t; v != s; v = parent[v]){
             u = parent[v];
             path_flow = path_flow < rGraph[u][v]? path_flow : rGraph[u][v];}for(v = t; v != s; v = parent[v]){
             u = parent[v];
             rGraph[u][v]-= path_flow;
             rGraph[v][u]+= path_flow;}
    
          max_flow += path_flow;}return max_flow;}intmain(){int graph[V][V]={{0,10,15,0,0,0},{0,0,0,5,10,0},{0,0,0,0,0,10},{0,0,0,0,0,5},{0,0,0,0,0,10},{0,0,0,0,0,0}};printf("The maximum possible flow is %d\n",fordFulkerson(graph,0,5));return0;}

    Output

    Following is the output of the above code:

    The maximum possible flow is 20
    

    Each of these algorithms helps us find the best paths to push more flow through the network and, eventually, determine the maximum flow possible.

    Real-Life Uses of Augmenting Paths

    Augmenting paths are super useful in many real-world situations, especially when we need to manage the flow of things like datatraffic, or materials. Here are some areas where these concepts come in handy:

    • Transportation Networks: Augmenting paths can be used to model how traffic moves through roadsrailways, and other transport systems. This helps us optimize traffic flow.
    • Communication Networks: In computer and telecom networks, augmenting paths can help manage data flow and figure out the most efficient way to route information.
    • Supply Chain Networks: In supply chains, augmenting paths help track the flow of goods and materials, ensuring smooth movement from suppliers to customers.
    • Fluid Systems: Augmenting paths can also be applied in systems that move liquids or gases, like pipes or water distribution systems, to make sure everything flows as efficiently as possible.

    By using augmenting paths, engineers can better understand how things flow through these systems and find ways to improve them. Whether its transportationcommunicationsupply chains, or fluid systems, these techniques help us optimize how resources move and find the most efficient solutions!

  • Bi-connected Components

    Imagine you have a big network of cities (nodes) connected by roads (edges). Now, if you remove just one city, and the whole network breaks into separate parts, then that city is called an articulation point (or cut vertex).

    So, Bi-connected Components(BCC) are the largest possible parts of a graph(subgraph) where no single node removal can disconnect them.

    For example, consider the following graph:

    Biconnected Components

    The articulation points in the graph are:

    {2}
    {4}
    {6}
    

    The Bi-connected Components (BCCs) in the graph are:

    {0, 1, 2, 3}
    {2, 4}
    {4, 5}
    {4, 6}
    {6, 7, 8}
    

    Properties of Bi-connected Components

    Here are some key properties of bi-connected components:

    • Edge: Every edge belongs to exactly one biconnected component.
    • Articulation Points: A node can have multiple biconnected components, but it can be an articulation point for only one of them.
    • Bridge: An edge that, when removed, increases the number of biconnected components.
    • If a graph has no articulation points, the whole graph itself is BCC.

    Algorithm for Finding BCC

    We use DFS(depth first search) to find biconnected components.

    Here’s a simple algorithm to find biconnected components in a graph:

    1. Perform a Depth First Search (DFS) traversal of the graph.
    2. During the DFS traversal, maintain a stack to keep track of the nodes visited.
    3. For each node, keep track of the discovery time and low value.
    4. When visiting a node, push it onto the stack and update the discovery time and low value.
    5. If a back edge is found (i.e., an edge to an already visited node), update the low value of the current node.
    6. If the low value of the current node is less than or equal to the discovery time of the parent node, pop nodes from the stack until the current node is reached.
    7. The popped nodes form a biconnected component.
    

    Let’s say we have this graph

         A
        / \
       B   C
       |   |
       D - E
          / \
         F   G 
    

    The edges are:

    AB, AC, BD, CE, EF, EG, DE
    

    If we remove node E, the graph look like this:

         A
        / \
       B   C
       |   
       D        E
               / \
              F   G
    
    • {A, B, C, D, E}
    • {E, F, G}

    Code for Biconnected Components

    Let’s see the code for finding biconnected components in a above graph:

    CC++JavaPython

    #include <stdio.h>#include <stdlib.h>#include <string.h>#include <stdbool.h>#define MAX 100int time =0;int disc[MAX], low[MAX], parent[MAX];
    bool visited[MAX];typedefstruct{int u, v;} Edge;
    
    Edge stack[MAX];int top =-1;// Mapping indexes to letterschar nodes[]={'A','B','C','D','E','F','G'};voidpush(int u,int v){
       stack[++top].u = u;
       stack[top].v = v;}voidpopUntil(int u,int v){printf("\nBiconnected Component: ");while(top >=0){printf("{%c, %c} ", nodes[stack[top].u], nodes[stack[top].v]);if(stack[top].u == u && stack[top].v == v){
             top--;break;}
          top--;}}voidDFS(int graph[MAX][MAX],int u,int n){
       visited[u]= true;
       disc[u]= low[u]=++time;for(int v =0; v < n; v++){if(graph[u][v]){if(!visited[v]){
                parent[v]= u;push(u, v);DFS(graph, v, n);
    
                low[u]=(low[u]< low[v])? low[u]: low[v];if(low[v]>= disc[u]){popUntil(u, v);}}elseif(v != parent[u]&& disc[v]< disc[u]){
                low[u]=(low[u]< disc[v])? low[u]: disc[v];push(u, v);}}}}voidfindBCC(int graph[MAX][MAX],int n){memset(visited, false,sizeof(visited));memset(parent,-1,sizeof(parent));memset(disc,0,sizeof(disc));memset(low,0,sizeof(low));for(int i =0; i < n; i++){if(!visited[i]){DFS(graph, i, n);}}if(top >=0){printf("\nRemaining Biconnected Component: ");while(top >=0){printf("{%c, %c} ", nodes[stack[top].u], nodes[stack[top].v]);
             top--;}}}intmain(){int n =7;int graph[MAX][MAX]={0};
    
       graph[0][1]= graph[1][0]=1;// A-B
       graph[0][2]= graph[2][0]=1;// A-C
       graph[1][3]= graph[3][1]=1;// B-D
       graph[2][4]= graph[4][2]=1;// C-E
       graph[4][5]= graph[5][4]=1;// E-F
       graph[4][6]= graph[6][4]=1;// E-G
       graph[3][4]= graph[4][3]=1;// D-Eprintf("Biconnected Components are:\n");findBCC(graph, n);return0;}

    Output

    Following is the output of the above code:

    Biconnected Components are:
    
    Biconnected Component: {E, F} 
    Biconnected Component: {E, G} 
    Biconnected Component: {C, A} {E, C} {D, E} {B, D} {A, B} 
    

    Time Complexity of BCC

    • The time complexity of finding biconnected components using the above algorithm is O(V + E), where V is the number of vertices and E is the number of edges in the graph.

    Applications of Biconnected Components

    Biconnected components are used in various applications, such as:

    • Network Reliability: If you want to know the weakest link in a network, BCC helps.
    • Bridges & Roads: Cities connected by roads? Find which roads are critical.
    • Computer Networks: In network design, BCC helps in fault tolerance.
    • File Systems: Used in distributed storage systems to avoid single points of failure.

    Conclusion

    We have learned about biconnected components in a graph. We have seen how to find biconnected components using a simple algorithm. We have also seen the code in C, C++, Java, and Python. Biconnected components are essential in network design, fault tolerance, and distributed systems.