Understanding and Implementing Tree Data Structures in C++

Understanding and Implementing Tree Data Structures in C++

·

8 min read

Prerequisites:

Before diving into the concepts and implementations of tree data structures, make sure you have the following:

  • Familiarity with basic data structures such as arrays and linked lists.

  • Understanding of recursion.

  • Proficiency in C++ programming.


Checklist for Today's Session

  1. Introduction to Tree Data Structure.

  2. Coding Implementation:

    • Displaying a Tree.

    • Finding the sum of tree nodes.

    • Finding the size of the tree.

    • Finding the node with the maximum value.

    • Calculating the number of levels in a binary tree.

  3. Understanding types of Binary Trees.

  4. Solving real-world problems:

    • Diameter of a Binary Tree (LeetCode-543).

    • Same Tree (LeetCode-100).

    • Invert Binary Tree (LeetCode-226).

    • Binary Tree Paths (LeetCode-257).


Introduction to Tree Data Structure

A tree is a hierarchical data structure that organizes data in a way that allows for efficient navigation and searching. A tree consists of nodes connected by edges with a hierarchical parent-child relationship.

Key Characteristics:

  • The root is the topmost node.

  • Nodes can have children and may also have a parent, except the root.

  • A tree exhibits a recursive structure, as every subtree of a tree is itself a tree.


Coding Implementation

1. Displaying a Tree

This program demonstrates how to traverse and display a tree using a preorder traversal (root-left-right).

Code:

#include <iostream>
using namespace std;

class Node {
public:
    int val;
    Node* left;
    Node* right;

    Node(int val) {
        this->val = val;
        this->left = NULL;
        this->right = NULL;
    }
};

void displayNode(Node* root) {
    if (root == NULL) return;
    cout << root->val << " ";
    displayNode(root->left);
    displayNode(root->right);
}

int main() {
    Node* a = new Node(1);
    Node* b = new Node(2);
    Node* c = new Node(3);
    Node* d = new Node(4);
    Node* e = new Node(5);
    Node* f = new Node(6);
    Node* g = new Node(7);

    a->left = b;
    a->right = c;
    b->left = d;
    b->right = e;
    c->left = f;
    c->right = g;

    displayNode(a);
    return 0;
}

Explanation:

  1. Tree Structure Creation:

    • Nodes are created using the Node class.

    • Child nodes are assigned to their respective parent nodes.

  2. Tree Traversal:

    • The displayNode function uses recursion for a preorder traversal.

    • The base case handles NULL nodes.

Output:

1 2 4 5 3 6 7

Time Complexity:

  • O(N), where N is the number of nodes in the tree (each node is visited once).

Space Complexity:

  • O(H), where H is the height of the tree (due to the recursive call stack).

2. Sum of Tree Nodes

Problem:

Calculate the sum of all node values in a binary tree.

Code:

int sumNode(Node* root) {
    if (root == NULL) return 0;
    return root->val + sumNode(root->left) + sumNode(root->right);
}

Explanation:

  • The base case returns 0 for a NULL node.

  • For non-NULL nodes, the value of the current node is added to the sum of its left and right subtrees.

Time Complexity:

  • O(N).

Space Complexity:

  • O(H).

3. Size of the Tree

Problem:

Count the number of nodes in a binary tree.

Code:

int sizeOfTree(Node* root) {
    if (root == NULL) return 0;
    return 1 + sizeOfTree(root->left) + sizeOfTree(root->right);
}

Explanation:

  • For each node, count 1 and add the size of its left and right subtrees recursively.

Time Complexity:

  • O(N).

Space Complexity:

  • O(H).

4. Node with Maximum Value

Problem:

Find the node with the maximum value in the binary tree.

Code:

int maxOfTree(Node* root) {
    if (root == NULL) return INT_MIN;
    return max(root->val, max(maxOfTree(root->left), maxOfTree(root->right)));
}

Explanation:

  • Compare the current node value with the maximum values of its left and right subtrees.

Time Complexity:

  • O(N).

Space Complexity:

  • O(H).

5. Number of Levels (Height of Binary Tree)

Problem:

Calculate the height of the binary tree (number of levels).

Code:

int levels(Node* root) {
    if (root == NULL) return 0;
    return 1 + max(levels(root->left), levels(root->right));
}

Explanation:

  • The height is calculated as 1 + the maximum height of the left or right subtree.

Time Complexity:

  • O(N).

Space Complexity:

  • O(H).

Types of Binary Trees

1. Based on the Number of Children:

  • Full Binary Tree: Every node has 0 or 2 children.

  • Degenerate Tree: Each parent node has only one child.

  • Skewed Binary Tree: Nodes are all to one side (left or right).

2. Based on Level Completion:

  • Complete Binary Tree: All levels except the last are completely filled.

  • Perfect Binary Tree: All internal nodes have 2 children, and all leaf nodes are at the same level.

  • Balanced Binary Tree: Ensures O(log⁡N)O(\log N)O(logN) height for efficient operations.


Problem Discussions:

  1. Diameter of Binary Tree.

  2. Same Tree.

  3. Invert Binary Tree.

  4. Binary Tree Paths.


  1. Diameter of Binary Tree: Longest path between two nodes (LeetCode-543).

    Description: Given the root of a binary tree, return the length of the diameter of the tree. The diameter of a binary tree is the length of the longest path between any two nodes in a tree. This path may or may not pass through the root. The number of edges between them represents the length of a path between two nodes.

https://leetcode.com/problems/diameter-of-binary-tree/submissions/1469321378/

class Solution {
public:
    int maxDia = 0;
    int levels(TreeNode* root){
        if(root==NULL) return 0;
        return 1+ max(levels(root->left), levels(root->right));
    }
    int diameterOfBinaryTree(TreeNode* root) {
        if(root == NULL) return 0;
        int dia = levels(root->left) + levels(root->right);
        maxDia = max(dia, maxDia);
        diameterOfBinaryTree(root->left);
        diameterOfBinaryTree(root->right);
        return maxDia;
    }
};

Explanation:

  1. levels Function:

    • Calculates the height of the binary tree rooted at root.

    • Uses recursion to compute 1 + max(height of left subtree, height of right subtree).

  2. diameterOfBinaryTree Function:

    • Calculates the diameter at each node as the sum of the heights of its left and right subtrees.

    • Updates the global maxDia if the current diameter is larger.

    • Recursively calls itself for left and right children to ensure all nodes are checked.

Time Complexity:

  • O(N²): The levels function is called for each node, and within each call, it traverses the subtree, making it O(N) per node.

Space Complexity:

  • Worst-Case: O(N)

  • Best-Case: O(log N)

class Solution {
public:
    void helper(TreeNode* root, int &maxDia){
        if(root == NULL) return;
        int dia = levels(root->left) + levels(root->right);
        maxDia = max(dia, maxDia);
        helper(root->left,maxDia);
        helper(root->right,maxDia);
    }

    int levels(TreeNode* root){
        if(root==NULL) return 0;
        return 1+ max(levels(root->left), levels(root->right));
    }
    int diameterOfBinaryTree(TreeNode* root) {
        int maxDia = 0;
        helper(root, maxDia);
        return maxDia;
    }
};

Explanation:

  • Instead of using a global variable, maxDia is passed by reference to the helper function.

  • This approach avoids global state and makes the function more modular.

Time Complexity and Space Complexity:

Same as Code 1.


  1. Same Tree: Check if two trees are identical (LeetCode-100).

    Description: Given the roots of two binary trees p and q, write a function to check if they are the same or not. Two binary trees are considered the same if they are structurally identical, and the nodes have the same value.

     https://leetcode.com/problems/same-tree/submissions/1468950957/
    
     class Solution {
     public:
         bool isSameTree(TreeNode* p, TreeNode* q) {
             if(p==NULL && q==NULL) return true;
             if(p!=NULL && q==NULL) return false; 
             if(p==NULL && q!=NULL) return false;
    
             if(p->val != q->val) return false;
    
             bool leftAns = isSameTree(p->left, q->left);
             if(leftAns == false) return false;
    
             bool rightAns = isSameTree(p->right, q->right);
             if(rightAns == false) return false;
    
             return true;
         }
     };
    

Space Complexity:

  • Recursion Stack: The function makes recursive calls for both left and right subtrees. The maximum depth of the recursion corresponds to the height HHH of the tree.
  • Worst-Case(Skewed Tree): O(N)

  • Best-Case(Balance Tree): O(log N)


  1. Invert Binary Tree: Swap left and right children (LeetCode-226).

    Description: Given the root of a binary tree, invert the tree, and return its root.

https://leetcode.com/problems/invert-binary-tree/submissions/1468957636/

class Solution {
public:
    void helper(TreeNode* root){
        if(root == NULL) return;
        swap(root->left, root->right);
        helper(root->left);
        helper(root->right);
    }
    TreeNode* invertTree(TreeNode* root) {
        helper(root);
        return root;
    }
};
  • Recursion Stack: Similar to the "Same Tree" problem, the recursion depth depends on the height H of the tree.

  • Worst-Case: O(N)

  • Best-Case: O(log N)


4. Binary Tree Paths: Find all root-to-leaf paths (LeetCode-257).

Description: Given the root of a binary tree, return all root-to-leaf paths in any order. A leaf is a node with no children.

class Solution {
public:
    void helper(TreeNode* root, string s, vector<string>& ans){
        if(root == NULL) return;
        string a = to_string(root->val);
        if(root->left == NULL and root->right == NULL){
            s +=a;
            ans.push_back(s);
            return;
        }
        helper(root->left, s+a+"->", ans);
        helper(root->right, s+a+"->", ans);
    }

    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string>ans;
        helper(root, "", ans);
        return ans;
    }
};

Explanation:

  1. The helper function:

    • Maintains the current path as a string s.

    • When a leaf node is reached, appends the complete path to the result vector ans.

  2. Recursive calls:

    • Adds the current node's value to the path.

    • Calls itself for left and right subtrees.

Time Complexity:

  • O(N): Each node is visited once.
  • Recursion Stack: The depth of recursion corresponds to the height H of the tree.

  • Auxiliary Space for Path Strings: The function accumulates strings during traversal. In the worst case, the size of strings depends on the number of nodes N, but the stack size remains tied to H.

Space Complexity:

  • Worst-Case: O(N) {skewed tree, H = N}

  • Best-Case: O(log N) {balanced tree, H = log N}


Conclusion:

In this blog, we delved into the fundamental operations of binary trees, including traversal, size computation, maximum value identification, and various problem-solving techniques. Each concept was reinforced with detailed explanations, code implementations, and analysis of time and space complexities to deepen your understanding. Binary trees serve as a cornerstone in the study of data structures, offering a solid foundation for more advanced topics.

This is just the beginning of our journey into binary trees. Stay tuned as we explore more intricate problems and advanced concepts, unlocking the full potential of this versatile data structure in the upcoming blogs!