Binary Tree is one of the most fundamental data structures in computer science. In this guide, we’ll explore how to implement a Binary Tree in Go, with detailed step-by-step diagrams that visualize each operation.
A Binary Tree is a hierarchical data structure where each node has at most two children, typically referred to as the left child and right child. Unlike a Binary Search Tree (BST), a Binary Tree doesn’t enforce any ordering rules on the values stored in nodes.
Before we dive into implementation, let’s understand the basic terms:
Here’s a visual representation of a Binary Tree:
graph TD
Root[Root: 10] --> Left1[Left: 5]
Root --> Right1[Right: 15]
Left1 --> Left2[Left: 3]
Left1 --> Right2[Right: 7]
Right1 --> Left3[Left: 12]
Right1 --> Right3[Right: 20]
style Root fill:#e1f5ff
style Left1 fill:#fff4e1
style Right1 fill:#fff4e1
style Left2 fill:#ffe1f5
style Right2 fill:#ffe1f5
style Left3 fill:#ffe1f5
style Right3 fill:#ffe1f5
In this example:
Let’s start by defining the basic structure for our Binary Tree in Go.
Each node in our Binary Tree will store a value and pointers to its left and right children:
// Node represents a node in the Binary Tree
type Node struct {
Value int
Left *Node
Right *Node
}
classDiagram
class Node {
+Value: int
+Left: *Node
+Right: *Node
}
The Binary Tree itself only needs to hold a reference to the root node:
// BinaryTree represents a Binary Tree
type BinaryTree struct {
Root *Node
}
classDiagram
class BinaryTree {
+Root: *Node
}
BinaryTree --> Node
class Node {
+Value: int
+Left: *Node
+Right: *Node
}
We’ll create a constructor function to initialize a new empty Binary Tree:
// NewBinaryTree creates a new empty Binary Tree
func NewBinaryTree() *BinaryTree {
return &BinaryTree{Root: nil}
}
Initially, the tree is empty, so the root is nil. Let’s visualize this:
graph TD
Empty[Empty Tree]
Root[nil]
Empty --> Root
style Empty fill:#ffcccc
style Root fill:#ffcccc
Inserting nodes into a Binary Tree requires a strategy. For simplicity, we’ll use a level-order insertion approach: we insert nodes from left to right at each level, filling each level completely before moving to the next.
Important Note: This insertion strategy always creates a Complete Binary Tree (CBT) - a tree where all levels are completely filled except possibly the last level, which is filled from left to right. This property ensures the tree maintains a balanced structure and allows for efficient array-based representation.
Here’s the implementation using a queue-based approach:
// Insert adds a new node to the tree using level-order insertion
func (bt *BinaryTree) Insert(value int) {
newNode := &Node{Value: value}
// If tree is empty, make this node the root
if bt.Root == nil {
bt.Root = newNode
return
}
// Use a queue to find the first available position
queue := []*Node{bt.Root}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
// If left child is empty, insert here
if node.Left == nil {
node.Left = newNode
return
}
// If right child is empty, insert here
if node.Right == nil {
node.Right = newNode
return
}
// Both children exist, add them to queue for next level
queue = append(queue, node.Left, node.Right)
}
}
Let’s visualize the insertion process step by step. We’ll insert values: 10, 5, 15, 3, 7, 12, 20.
Deleting a node from a Binary Tree that uses level-order insertion requires a specific approach to maintain the Complete Binary Tree (CBT) property. Since we’re maintaining a CBT structure, we need to:
This approach ensures the tree remains a Complete Binary Tree after deletion, maintaining the level-order structure.
// Delete removes a node with the given value from the tree
// It maintains the Complete Binary Tree property by replacing
// the deleted node with the rightmost node in the last level
func (bt *BinaryTree) Delete(value int) bool {
if bt.Root == nil {
return false
}
// Find the node to delete and its parent
var nodeToDelete *Node
var parent *Node
var isLeftChild bool
// Special case: deleting the root
if bt.Root.Value == value {
nodeToDelete = bt.Root
} else {
// Find the node using level-order traversal
queue := []*Node{bt.Root}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
if node.Left != nil {
if node.Left.Value == value {
parent = node
nodeToDelete = node.Left
isLeftChild = true
break
}
queue = append(queue, node.Left)
}
if node.Right != nil {
if node.Right.Value == value {
parent = node
nodeToDelete = node.Right
isLeftChild = false
break
}
queue = append(queue, node.Right)
}
}
}
if nodeToDelete == nil {
return false // Value not found
}
// Find the rightmost node in the last level
rightmostNode, rightmostParent := bt.findRightmostNode()
if rightmostNode == nil {
// Tree has only one node (root)
bt.Root = nil
return true
}
// Replace the value of the node to delete with the rightmost node's value
nodeToDelete.Value = rightmostNode.Value
// Delete the rightmost node
if rightmostParent != nil {
if rightmostParent.Right == rightmostNode {
rightmostParent.Right = nil
} else {
rightmostParent.Left = nil
}
} else {
// Rightmost node is the root (tree has only root)
bt.Root = nil
}
return true
}
// findRightmostNode finds the rightmost node in the last level.
// Returns the node and its parent
func (bt *BinaryTree) findRightmostNode() (*Node, *Node) {
if bt.Root == nil {
return nil, nil
}
if bt.Root.Left == nil && bt.Root.Right == nil {
// Only root exists
return bt.Root, nil
}
queue := []*Node{bt.Root}
var rightmostNode *Node
var rightmostParent *Node
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
// Track the rightmost node at the current level
if node.Left != nil {
rightmostNode = node.Left
rightmostParent = node
queue = append(queue, node.Left)
}
if node.Right != nil {
rightmostNode = node.Right
rightmostParent = node
queue = append(queue, node.Right)
}
}
return rightmostNode, rightmostParent
}
The deletion process for a Complete Binary Tree follows these steps:
Let’s see this in action with examples:
Before deletion:
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
Right --> Right3[20]
style Left2 fill:#ff6b6b
Step 1: Find rightmost node in last level (node 20):
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
Right --> Right3[20]
style Left2 fill:#ff6b6b
style Right3 fill:#90ee90
Step 2: Replace node 3’s value with 20, then delete node 20:
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[20]
Left --> Right2[7]
Right --> Left3[12]
style Root fill:#e1f5ff
style Left fill:#fff4e1
style Right fill:#fff4e1
style Left2 fill:#90ee90
style Right2 fill:#ffe1f5
style Left3 fill:#ffe1f5
Before deletion:
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
Right --> Right3[20]
style Left fill:#ff6b6b
Step 1: Find rightmost node in last level (node 20):
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
Right --> Right3[20]
style Left fill:#ff6b6b
style Right3 fill:#90ee90
Step 2: Replace node 5’s value with 20, then delete node 20:
graph TD
Root[10] --> Left[20]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
style Root fill:#e1f5ff
style Left fill:#90ee90
style Right fill:#fff4e1
style Left2 fill:#ffe1f5
style Right2 fill:#ffe1f5
style Left3 fill:#ffe1f5
Before deletion:
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
Right --> Right3[20]
style Root fill:#ff6b6b
Step 1: Find rightmost node in last level (node 20):
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
Right --> Right3[20]
style Root fill:#ff6b6b
style Right3 fill:#90ee90
Step 2: Replace root’s value with 20, then delete node 20:
graph TD
Root[20] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
style Root fill:#90ee90
style Left fill:#fff4e1
style Right fill:#fff4e1
style Left2 fill:#ffe1f5
style Right2 fill:#ffe1f5
style Left3 fill:#ffe1f5
Key Point: This deletion strategy maintains the Complete Binary Tree property by always removing the rightmost node from the last level, ensuring the tree structure remains balanced and consistent with level-order insertion.
Traversal is the process of visiting all nodes in a tree. There are three main ways to traverse a Binary Tree, each with different visit orders.
In in-order traversal, we visit the left subtree first, then the root, then the right subtree.
// InOrderTraversal performs in-order traversal (Left, Root, Right)
func (bt *BinaryTree) InOrderTraversal() []int {
var result []int
bt.inOrderRecursive(bt.Root, &result)
return result
}
func (bt *BinaryTree) inOrderRecursive(node *Node, result *[]int) {
if node == nil {
return
}
bt.inOrderRecursive(node.Left, result)
*result = append(*result, node.Value)
bt.inOrderRecursive(node.Right, result)
}
Step-by-step visualization:
Result: [3, 5, 7, 10, 12, 15, 20]
In pre-order traversal, we visit the root first, then the left subtree, then the right subtree.
// PreOrderTraversal performs pre-order traversal (Root, Left, Right)
func (bt *BinaryTree) PreOrderTraversal() []int {
var result []int
bt.preOrderRecursive(bt.Root, &result)
return result
}
func (bt *BinaryTree) preOrderRecursive(node *Node, result *[]int) {
if node == nil {
return
}
*result = append(*result, node.Value)
bt.preOrderRecursive(node.Left, result)
bt.preOrderRecursive(node.Right, result)
}
Step-by-step visualization:
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
Right --> Right3[20]
Visit order:
Result: [10, 5, 3, 7, 15, 12, 20]
In post-order traversal, we visit the left subtree first, then the right subtree, then the root.
// PostOrderTraversal performs post-order traversal (Left, Right, Root)
func (bt *BinaryTree) PostOrderTraversal() []int {
var result []int
bt.postOrderRecursive(bt.Root, &result)
return result
}
func (bt *BinaryTree) postOrderRecursive(node *Node, result *[]int) {
if node == nil {
return
}
bt.postOrderRecursive(node.Left, result)
bt.postOrderRecursive(node.Right, result)
*result = append(*result, node.Value)
}
Step-by-step visualization:
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
Right --> Right3[20]
Visit order:
Result: [3, 7, 5, 12, 20, 15, 10]
Searching in a Binary Tree requires checking every node since there’s no ordering guarantee. We’ll use a recursive approach to search through the entire tree.
// Search looks for a value in the tree
func (bt *BinaryTree) Search(value int) bool {
return bt.searchRecursive(bt.Root, value)
}
func (bt *BinaryTree) searchRecursive(node *Node, value int) bool {
if node == nil {
return false
}
if node.Value == value {
return true
}
// Search in left and right subtrees
return bt.searchRecursive(node.Left, value) ||
bt.searchRecursive(node.Right, value)
}
Let’s search for value 7 in our tree:
Initial tree:
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
Right --> Right3[20]
Search path:
graph TD
Root[10] --> Left[5]
Root --> Right[15]
Left --> Left2[3]
Left --> Right2[7]
Right --> Left3[12]
Right --> Right3[20]
Root -.->|"Check: 10 != 7"| Left
Left -.->|"Check: 5 != 7"| Left2
Left2 -.->|"Not found, backtrack"| Left
Left -.->|"Check right"| Right2
Right2 -.->|"Found: 7 == 7"| Right2
style Root fill:#fff4e1
style Left fill:#fff4e1
style Left2 fill:#ffcccc
style Right2 fill:#90ee90
style Right fill:#ffcccc
style Left3 fill:#ffcccc
style Right3 fill:#ffcccc
The search starts at the root (10), checks the left subtree, finds 7 in the right child of node 5, and returns true.
Let’s analyze the time complexity of each operation:
| Operation | Time Complexity | Explanation |
|---|---|---|
| Insert | O(n) | In worst case, we may need to traverse all nodes to find the insertion point |
| Delete | O(n) | We need to find the node first (O(n)), then handle deletion (O(n) in worst case) |
| Search | O(n) | Must check every node in worst case since there’s no ordering |
| Traversal | O(n) | Must visit every node exactly once |
The deletion operation has O(n) time complexity, which can be broken down as follows:
Finding the node to delete: O(n)
Finding the rightmost node in the last level: O(n)
Replacing and removing: O(1)
Total: O(n) + O(n) + O(1) = O(n)
Space Complexity:
Note: Since we’re maintaining a Complete Binary Tree structure, the height h is approximately log₂(n), making the space complexity for recursive operations O(log n) in the typical case.
In this guide, we’ve explored the Binary Tree data structure and its implementation in Go. We covered:
Key Implementation Details:
Binary Trees are fundamental building blocks for more advanced data structures like Binary Search Trees, AVL Trees, and Red-Black Trees. Understanding Binary Trees is essential for solving many tree-related problems in computer science.
The key takeaway is that Binary Trees provide a flexible hierarchical structure, but without ordering rules, operations like search and delete require checking potentially all nodes, resulting in O(n) time complexity. For applications requiring efficient search, consider using a Binary Search Tree instead.
This post is written or assisted by AI. Read more about it here.