Day 3: Binary Tree-I

Day 3: Binary Tree-I

They are everywhere.

What is a Tree?

Trees are non-linear data structures that are quite often used to represent hierarchical data with a set of connected nodes.

Example:

image.png

The above example student has a set of hierarchical data such as Name, Address, Course and another set of hierarchical data followed on.

What is a Binary Tree?

A binary tree is a tree data structure made up of nodes, each of which has at most two children, known as the left and right nodes. The tree begins with a single node called the root.

Each node in a binary tree contains

  • Data
  • Pointer to the left node.
  • Pointer to the right node.

image.png

Applications of Binary Tree

image.png

  • Binary Search Tree - Used in many search applications where data is constantly entering/leaving, such as the map and set objects in many languages' libraries.
  • Binary Space Partition - Used in almost every 3D video game to determine what objects need to be rendered.
  • Binary Tries - Used in almost every high-bandwidth router for storing router-tables.
  • Hash Trees - Used in torrents and specialized image signatures in which a hash needs to be verified, but the whole file is not available. Also used in blockchains for eg. Bitcoin.
  • Heaps - Used in implementing efficient priority queues, which in turn are used for scheduling processes in many operating systems, Quality-of-Service in routers, and A* (path-finding algorithm used in AI applications, including robotics and video games). Also used in heap-sort.
  • Huffman Coding Tree (Chip Uni) - Used in compression algorithms, such as those used by the .jpeg and .mp3 file-formats.
  • GGM Trees - Used in cryptographic applications to generate a tree of pseudo-random numbers.
  • Syntax Tree - Constructed by compilers and (implicitly) calculators to parse expressions.
  • Treap - Randomized data structure used in wireless networking and memory allocation.
  • T-tree - Though most databases use some form of B-tree to store data on the drive, databases that keep all (most) their data in memory often use T-trees to do so.

Binary Tree Implementation

We will have a Node class, which has two attributes data, left and right.

Node.java

public class Node {
    int data;
    Node left;
    Node right;

    public Node(int data){
        this.data = data;
        this.left = null;
        this.right = null;
    }
}

Insertion into Binary Tree.

  • If the root is null, create a new Node and make it the root.
  • If the root already exists, then recursively search for empty locations(left, or right).
  • Upon finding an empty left or right child, the new element is inserted. By convention, the insertion always begins from the left child node.

      public void insert(int key)
      {
    
          if (root == null) {
              root = new Node(key);
              return;
          }
          Queue<Node> q = new LinkedList<Node>();
          q.add(root);
    
          while (!q.isEmpty()) {
              root = q.peek();
              q.remove();
    
              if (root.left == null) {
                  root.left = new Node(key);
                  break;
              }
              else
                  q.add(root.left);
    
              if (root.right == null) {
                  root.right = new Node(key);
                  break;
              }
              else
                  q.add(root.right);
          }
      }
    

Searching in Binary Tree.

  • If the root is null, then there is nothing to search for, so return False.
  • As a binary tree doesn't follow any insertion principles like in Binary Search Trees, we have to do a complete traversal to search a node in Binary Tree.
    public boolean Search(Node root, int value){
        if(root==null){
            return false;
        }

        if(root.data == value){
            return true;
        }

        //Left Sub Tree
        boolean LST = Search(root.left, value);

        //Right Sub Tree
        boolean RST = Search(root.right, value);

        return LST || RST;
    }

Deletion in Binary Tree.

  • Starting at the root, find the deepest and rightmost node in the binary tree and the node which we want to delete.
  • Replace the deepest rightmost node’s data with the node to be deleted.
  • Then delete the deepest rightmost node.

image.png

//Code taken from GFG
public void deleteDeepest(Node root, Node delNode)
    {
        Queue<Node> q = new LinkedList<Node>();
        q.add(root);

        Node temp = null;

        // Do level order traversal until last node
        while (!q.isEmpty())
        {
            temp = q.peek();
            q.remove();

            if (temp == delNode)
            {
                temp = null;
                return;

            }
            if (temp.right!=null)
            {
                if (temp.right == delNode)
                {
                    temp.right = null;
                    return;
            }
            else
                q.add(temp.right);
        }

        if (temp.left != null)
        {
            if (temp.left == delNode)
            {
                temp.left = null;
                return;
            }
            else
                q.add(temp.left);
        }
    }

//Finding the node to be deleted and the leaf node.
public void delete(Node root, int key)
{
    if (root == null)
        return;

    if (root.left == null &&
       root.right == null)
    {
        if (root.data == key)
        {
              root=null;
              return;
        }
        else
            return;
    }

    Queue<Node> q = new LinkedList<Node>();
    q.add(root);
    Node temp = null, keyNode = null;

    // Do level order traversal until
    // we find key and last node.
    while (!q.isEmpty())
    {
        temp = q.peek();
        q.remove();

        if (temp.data == key)
            keyNode = temp;

        if (temp.left != null)
            q.add(temp.left);

        if (temp.right != null)
            q.add(temp.right);
    }

    if (keyNode != null)
    {
        int x = temp.data;
        deleteDeepest(root, temp);
        keyNode.data = x;
    }
}

Traversals on Binary Tree.

Another frequently used tree operation is traversal. Tree traversal is the process of visiting each node present in a tree. There are three methods of tree traversal:

  • In-order traversal
  • Post-order traversal
  • Pre-order traversal

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.

image.png

  • Recursively traverse Left Subtree.
  • Visit the root node.
  • Recursively traverse Right Subtree.

Inorder Traversal using recursion.

    /**
     * inOrder Traversal Using Recursion
     * @param node
     * @return
     */
    public List<Integer> inOrderTraversalsUsingRecursion(Node node){
        List<Integer> result = new ArrayList<Integer>();

        //Edge case
        if(node==null) return result;

        inOrderHelper(node, result);

        return result;
    }

    private void inOrderHelper(Node node, List<Integer> result) {

        //Edge Case
        if(node==null) return;

        //Left sub tree
        inOrderHelper(node.left, result);

        //Adding into List
        result.add(node.value);

        //Right Sub Tree
        inOrderHelper(node.right, result);
    }

In-Order Traversal using stack

    public static void inorderIterative(Node root)
    {
        // create an empty stack
        Stack<Node> stack = new Stack<>();

        // start from the root node (set current node to the root node)
        Node curr = root;

        // if the current node is null and the stack is also empty, we are done
        while (!stack.empty() || curr != null)
        {
            // if the current node exists, push it into the stack (defer it)
            // and move to its left child
            if (curr != null)
            {
                stack.push(curr);
                curr = curr.left;
            }
            else {
                // otherwise, if the current node is null, pop an element from
                // the stack, print it, and finally set the current node to its
                // right child
                curr = stack.pop();
                System.out.print(curr.data + " ");

                curr = curr.right;
            }
        }

Pre-Order Traversal

In preorder traversal, first, the root node is visited, then the left sub-tree, and after that right sub-tree is visited.

image.png

  • Visit the root node.
  • Recursively traverse Left Subtree.
  • Recursively traverse Right Subtree.

Pre-Order Traversal using recursion.

    /**
     * inOrder Traversal Using Recursion
     * @param node
     * @return
     */
    public List<Integer> preOrderTraversalsUsingRecursion(Node node){
        List<Integer> result = new ArrayList<Integer>();

        //Edge case
        if(node==null) return result;

        preOrderHelper(node, result);

        return result;
    }

    private void preOrderHelper(Node node, List<Integer> result) {

        //Edge Case
        if(node==null) return;

        //Adding into List
        result.add(node.value);

        //Left sub tree
        preOrderHelper(node.left, result);

        //Right Sub Tree
        preOrderHelper(node.right, result);
    }

Pre-Order Traversal using stack.

    public static void preorderIterative(Node root)
    {
        // return if the tree is empty
        if (root == null) {
            return;
        }

        // create an empty stack and push the root node
        Stack<Node> stack = new Stack<>();
        stack.push(root);

        // loop till stack is empty
        while (!stack.empty())
        {
            // pop a node from the stack and print it
            Node curr = stack.pop();

            System.out.print(curr.data + " ");

            // push the right child of the popped node into the stack
            if (curr.right != null) {
                stack.push(curr.right);
            }

            // push the left child of the popped node into the stack
            if (curr.left != null) {
                stack.push(curr.left);
            }

            // the right child must be pushed first so that the left child
            // is processed first (LIFO order)
        }

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.

image.png

  • Recursively traverse Left Subtree.
  • Recursively traverse Right Subtree.
  • Visit the root node.

Post-Order Traversal using recursion.

    public List<Integer> postOrderTraversalUsingRecursionn(Node node){
        //Edge Case
        if(node==null) return new ArrayList<Integer>();

        List<Integer> result = new ArrayList<Integer>();

        postOrderHelper(node, result);

        return result;
    }

    private void postOrderHelper(Node node, List<Integer> result) {
        //Base Case
        if(node==null) return;

        //Left Sub Tree
        postOrderHelper(node.left, result);

        //Right Sub Tree
        postOrderHelper(node.right, result);

        //Adding to  List
        result.add(node.value);
    }

Post-Order Traversal using stack.

public List<Integer> postOrderTraversal(Node node, List<Integer> result)
{
    ArrayList<Integer> postOrderIterative(Node node)
    {
        Stack<Node> S = new Stack<Node>();

        // Check for empty tree
        if (node == null)
            return list;
        S.push(node);
        Node prev = null;
        while (!S.isEmpty()) {
            Node current = S.peek();

            /* go down the tree in search of a leaf an if so
            process it and pop stack otherwise move down */
            if (prev == null || prev.left == current || prev.right == current) {
                if (current.left != null)
                    S.push(current.left);
                else if (current.right != null)
                    S.push(current.right);
                else {
                    S.pop();
                    list.add(current.data);
                }

                /* go up the tree from left node, if the
                child is right push it onto stack otherwise
                process parent and pop stack */
            }
            else if (current.left == prev) {
                if (current.right != null)
                    S.push(current.right);
                else {
                    S.pop();
                    list.add(current.data);
                }

                /* go up the tree from right node and after
                coming back from right node process parent
                and pop stack */
            }
            else if (current.right == prev) {
                S.pop();
                list.add(current.data);
            }

            prev = current;
        }

        return list;
    }
}

Types of Binary Tree

  • Full Binary Tree

    A full Binary tree is a special type of binary tree in which every parent node/internal node has either two or no children.

image.png

  • Perfect Binary Tree

    A perfect binary tree is a type of binary tree in which every internal node has exactly two child nodes and all the leaf nodes are at the same level.

image.png

  • Complete Binary Tree

    A complete binary tree is just like a full binary tree, but with two major differences like Every level must be completely filled. All the leaf elements must lean towards the left. The last leaf element might not have a right sibling i.e. a complete binary tree doesn't have to be a full binary tree.

image.png