Join Regular Classroom : Visit ClassroomTech

Data Structure – codewindow.in

Related Topics

Data Structure

Implement a binary tree that supports insert, search and delete operations in logarithmic time?

Yes, it is possible to implement a binary tree that supports insert, search and delete operations in logarithmic time. One way to achieve this is by using a self-balancing binary search tree like AVL tree or Red-Black tree.

Here is an example implementation of a Red-Black tree in Python that supports insert, search and delete operations in O(log n) time:

class Node:
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None
        self.parent = None
        self.color = 1
 
class RedBlackTree:
    def __init__(self):
        self.nil = Node(0)
        self.nil.color = 0
        self.root = self.nil
 
    def left_rotate(self, x):
        y = x.right
        x.right = y.left
        if y.left != self.nil:
            y.left.parent = x
        y.parent = x.parent
        if x.parent == self.nil:
            self.root = y
        elif x == x.parent.left:
            x.parent.left = y
        else:
            x.parent.right = y
        y.left = x
        x.parent = y
 
    def right_rotate(self, x):
        y = x.left
        x.left = y.right
        if y.right != self.nil:
            y.right.parent = x
        y.parent = x.parent
        if x.parent == self.nil:
            self.root = y
        elif x == x.parent.right:
            x.parent.right = y
        else:
            x.parent.left = y
        y.right = x
        x.parent = y
 
    def insert(self, key):
        node = Node(key)
        node.parent = None
        node.key = key
        node.left = self.nil
        node.right = self.nil
        node.color = 1
 
        y = None
        x = self.root
 
        while x != self.nil:
            y = x
            if node.key < x.key:
                x = x.left
            else:
                x = x.right
 
        node.parent = y
        if y == None:
            self.root = node
        elif node.key < y.key:
            y.left = node
        else:
            y.right = node
 
        if node.parent == None:
            node.color = 0
            return
 
        if node.parent.parent == None:
            return
 
        self.fix_insert(node)
 
    def delete(self, key):
        z = self.search(key)
        if z == self.nil:
            return
        self.delete_node(z)
 
    def find_min(self, node):
        while node.left != self.nil:
            node = node.left
        return node
 
    def delete_node(self, node):
        y = node
        y_original_color = y.color
        if node.left == self.nil:
            x = node.right
            self.transplant(node, node.right)
        elif node.right == self.nil:
            x = node.left
            self.transplant(node, node.left)
        else:
            y = self.find_min(node.right)
            y_original_color = y.color
            x = y.right
            if y.parent == node:
                x.parent = y
            else:
                self.transplant(y, y.right)
                y.right = node.right
                y.right.parent = y
            self.transplant(node, y)
            y.left = node.left
            y.left.parent = y
            y.color = node.color
        if y_original_color == 0:
            self.fix_delete(x)
 
    def transplant(self, u, v):
        if u.parent == None:
            self.root = v

Explain the difference between a full binary tree and a complete binary tree?

A full binary tree is a binary tree in which every node has either zero or two children. In other words, every level of the tree is completely filled, except possibly for the last level, which may be partially filled from the left.

A complete binary tree, on the other hand, is a binary tree in which every level is completely filled, except possibly for the last level, which must be filled from left to right. In other words, all nodes are as far left as possible.

To illustrate the difference, consider the following example:

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

The left tree is a full binary tree, as every node has either zero or two children. The right tree is a complete binary tree, as every level is completely filled, except for the last level which is filled from left to right. Note that the right tree is not a full binary tree because node C has only one child.

How to find the lowest common ancestor (LCA) of two nodes in a binary tree?

To find the lowest common ancestor (LCA) of two nodes in a binary tree, we can use a recursive algorithm that traverses the tree from the root node, checking if the two nodes are in the left and right subtrees of the current node.

Here’s how the algorithm works:

  1. If the root is NULL or equal to either of the two nodes, return the root.

  2. Recursively search for the left and right subtrees of the root for the two nodes.

  3. If the two nodes are found in different subtrees of the root, return the root since it is the LCA.

  4. If the two nodes are found in the same subtree of the root, recursively search that subtree for the LCA.

Here’s a Python implementation of the algorithm:

class Node:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

def find_lca(root, p, q):
    # If root is NULL or equal to either of the two nodes, return the root
    if root is None or root.val == p.val or root.val == q.val:
        return root
    
    # Recursively search for the left and right subtrees of the root for the two nodes
    left_lca = find_lca(root.left, p, q)
    right_lca = find_lca(root.right, p, q)

    # If the two nodes are found in different subtrees of the root, return the root since it is the LCA
    if left_lca and right_lca:
        return root

    # If the two nodes are found in the same subtree of the root, recursively search that subtree for the LCA
    return left_lca if left_lca else right_lca

In this implementation, root is the root node of the binary tree, p and q are the nodes for which we want to find the LCA. The function returns the LCA of p and q. If either p or q is not present in the tree, it returns None.

Implement a breadth-first search (BFS) or a depth-first search (DFS) on a binary tree?

Here are examples of how to implement BFS and DFS on a binary tree in Python:

BFS:

def bfs(root):
    queue = [root]
    while queue:
        node = queue.pop(0)
        print(node.val)
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)

DFS (in-order):

def dfs_inorder(node):
    if node:
        dfs_inorder(node.left)
        print(node.val)
        dfs_inorder(node.right)

DFS (pre-order):

def dfs_preorder(node):
    if node:
        print(node.val)
        dfs_preorder(node.left)
        dfs_preorder(node.right)

DFS (post-order):

def dfs_postorder(node):
    if node:
        dfs_postorder(node.left)
        dfs_postorder(node.right)
        print(node.val)

Explain the difference between a breadth-first search and a depth-first search?

The main difference between a breadth-first search (BFS) and a depth-first search (DFS) is the order in which nodes are visited in a graph or tree.

In a BFS, the search begins at the root node and visits all the nodes at the current level before moving on to the next level. That is, it explores all the nodes at a given depth before moving to nodes at the next depth. BFS can be implemented using a queue data structure, where each level of nodes is added to the end of the queue and processed one by one in the order they were added.

In contrast, in a DFS, the search begins at the root node and explores as far as possible along each branch before backtracking. This means that it explores one path as deeply as possible before moving on to another path. DFS can be implemented using a stack data structure (or recursion), where each child node is pushed onto the stack to be processed later.

To summarize, BFS is a level-by-level exploration of the tree, while DFS is a branch-by-branch exploration of the tree.

How to convert a binary tree to its mirror image?

To convert a binary tree to its mirror image, we need to swap the left and right subtrees of every node in the tree. We can do this recursively by traversing the tree and swapping the left and right subtrees of each node until we reach the leaf nodes. Here’s an example implementation in Python:

class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

def mirror_tree(root):
    if root is None:
        return
    else:
        mirror_tree(root.left)
        mirror_tree(root.right)
        temp = root.left
        root.left = root.right
        root.right = temp

In this implementation, we first check if the root is None. If it is, we return. Otherwise, we recursively call mirror_tree on the left and right subtrees of the root, and then swap their positions using a temporary variable. This effectively mirrors the tree. We can call this function on the root node of our binary tree to obtain its mirror image.

Explain the concept of a perfect binary tree and how it is different from a complete binary tree?

A perfect binary tree is a type of binary tree where all the internal nodes have two children and all the leaves have the same depth or level. In other words, a perfect binary tree is a complete binary tree where all the leaf nodes are at the same level.

For example, the following is a perfect binary tree of height 3:

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

In contrast, a complete binary tree is a binary tree where all levels are completely filled, except possibly the last level, which is filled from left to right. So, a complete binary tree may have some internal nodes with only one child.

For example, the following is a complete binary tree, but not a perfect binary tree:

          A
       /     \
      B       C
    /   \     /  
   D     E   F   

What are some common applications of binary trees in computer science and software engineering?

Binary trees have a wide range of applications in computer science and software engineering. Some common applications include:

  1. Binary search: Binary trees are commonly used for implementing efficient searching algorithms. For example, binary search trees provide logarithmic time complexity for search, insert, and delete operations.

  2. Expression trees: Binary trees can be used to represent mathematical expressions. In an expression tree, each operator is represented by a node, and its operands are its children.

  3. Huffman coding: Binary trees are used in data compression algorithms like Huffman coding. In this technique, a binary tree is constructed where each leaf node represents a symbol and the path from the root to the leaf represents the code for that symbol.

  4. Parsing: Binary trees can be used to parse and evaluate expressions in programming languages. In this application, the binary tree represents the structure of the expression, and the nodes represent the operators and operands.

  5. Game theory: Binary trees can be used to represent game trees in game theory. In a game tree, each node represents a game state, and the children of the node represent the possible moves that can be made from that state.

  6. Decision trees: Binary trees can be used to represent decision trees in machine learning. In this application, the binary tree represents a decision-making process where each node represents a decision based on a feature of the input data, and the branches represent the possible outcomes of that decision.

Overall, binary trees are a versatile data structure that can be used in many different applications in computer science and software engineering.

Questions on Chapter 8

Questions on Chapter 9

      

We Love to Support you

Go through our study material. Your Job is awaiting.

Recent Posts
Categories