DEV Community

Cover image for Implementing High-Performance Binary Search Trees in Go: A Complete Guide
Aarav Joshi
Aarav Joshi

Posted on

Implementing High-Performance Binary Search Trees in Go: A Complete Guide

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Binary Search Trees (BST) are fundamental data structures in computer science that enable efficient data organization and retrieval. I'll share my experience implementing BSTs in Go, focusing on practical applications and performance optimization.

A BST maintains elements in a hierarchical order where each node's left subtree contains values smaller than the node, and the right subtree contains larger values. In Go, we can achieve high-performance BST operations through careful implementation and optimization.

Let's start with a basic BST implementation and progress to more sophisticated features:

type Node struct {
    Value int
    Left  *Node
    Right *Node
}

type BST struct {
    root *Node
    size int
}

func NewBST() *BST {
    return &BST{}
}
Enter fullscreen mode Exit fullscreen mode

The insertion operation is critical for maintaining the BST property. Here's an efficient implementation:

func (bst *BST) Insert(value int) {
    bst.root = insert(bst.root, value)
    bst.size++
}

func insert(node *Node, value int) *Node {
    if node == nil {
        return &Node{Value: value}
    }

    if value < node.Value {
        node.Left = insert(node.Left, value)
    } else if value > node.Value {
        node.Right = insert(node.Right, value)
    }

    return node
}
Enter fullscreen mode Exit fullscreen mode

Search operations in BSTs are particularly efficient, with average-case time complexity of O(log n):

func (bst *BST) Search(value int) bool {
    return search(bst.root, value)
}

func search(node *Node, value int) bool {
    if node == nil {
        return false
    }

    if value == node.Value {
        return true
    }

    if value < node.Value {
        return search(node.Left, value)
    }
    return search(node.Right, value)
}
Enter fullscreen mode Exit fullscreen mode

Tree traversal is essential for various applications. Here's an implementation of in-order traversal:

func (bst *BST) InOrderTraversal() []int {
    result := make([]int, 0, bst.size)
    inOrder(bst.root, &result)
    return result
}

func inOrder(node *Node, result *[]int) {
    if node != nil {
        inOrder(node.Left, result)
        *result = append(*result, node.Value)
        inOrder(node.Right, result)
    }
}
Enter fullscreen mode Exit fullscreen mode

For enhanced performance, we can implement a self-balancing BST like AVL tree. Here's the height-balanced node implementation:

type AVLNode struct {
    Value   int
    Left    *AVLNode
    Right   *AVLNode
    Height  int
}

func height(node *AVLNode) int {
    if node == nil {
        return 0
    }
    return node.Height
}

func balanceFactor(node *AVLNode) int {
    if node == nil {
        return 0
    }
    return height(node.Left) - height(node.Right)
}
Enter fullscreen mode Exit fullscreen mode

Rotation operations maintain balance:

func rightRotate(y *AVLNode) *AVLNode {
    x := y.Left
    T2 := x.Right

    x.Right = y
    y.Left = T2

    y.Height = max(height(y.Left), height(y.Right)) + 1
    x.Height = max(height(x.Left), height(x.Right)) + 1

    return x
}

func leftRotate(x *AVLNode) *AVLNode {
    y := x.Right
    T2 := y.Left

    y.Left = x
    x.Right = T2

    x.Height = max(height(x.Left), height(x.Right)) + 1
    y.Height = max(height(y.Left), height(y.Right)) + 1

    return y
}
Enter fullscreen mode Exit fullscreen mode

For concurrent access, we can implement thread-safe operations:

type ThreadSafeBST struct {
    root *Node
    lock sync.RWMutex
}

func (t *ThreadSafeBST) Insert(value int) {
    t.lock.Lock()
    defer t.lock.Unlock()

    t.root = insert(t.root, value)
}

func (t *ThreadSafeBST) Search(value int) bool {
    t.lock.RLock()
    defer t.lock.RUnlock()

    return search(t.root, value)
}
Enter fullscreen mode Exit fullscreen mode

Memory optimization is crucial for large-scale applications. Here's a memory-efficient node implementation:

type CompactNode struct {
    Value  int16
    Left   *CompactNode
    Right  *CompactNode
}
Enter fullscreen mode Exit fullscreen mode

For handling duplicate values, we can modify our node structure:

type NodeWithCount struct {
    Value     int
    Count     int
    Left      *NodeWithCount
    Right     *NodeWithCount
}
Enter fullscreen mode Exit fullscreen mode

Deletion operation requires careful handling of different cases:

func (bst *BST) Delete(value int) {
    bst.root = delete(bst.root, value)
}

func delete(node *Node, value int) *Node {
    if node == nil {
        return nil
    }

    if value < node.Value {
        node.Left = delete(node.Left, value)
    } else if value > node.Value {
        node.Right = delete(node.Right, value)
    } else {
        if node.Left == nil {
            return node.Right
        } else if node.Right == nil {
            return node.Left
        }

        minNode := findMin(node.Right)
        node.Value = minNode.Value
        node.Right = delete(node.Right, minNode.Value)
    }
    return node
}
Enter fullscreen mode Exit fullscreen mode

For range queries, we can implement an efficient iterator:

type Iterator struct {
    stack []*Node
    current *Node
}

func (bst *BST) Iterator() *Iterator {
    iterator := &Iterator{stack: make([]*Node, 0)}
    iterator.pushLeft(bst.root)
    return iterator
}

func (it *Iterator) pushLeft(node *Node) {
    for node != nil {
        it.stack = append(it.stack, node)
        node = node.Left
    }
}

func (it *Iterator) Next() (int, bool) {
    if len(it.stack) == 0 {
        return 0, false
    }

    node := it.stack[len(it.stack)-1]
    it.stack = it.stack[:len(it.stack)-1]

    if node.Right != nil {
        it.pushLeft(node.Right)
    }

    return node.Value, true
}
Enter fullscreen mode Exit fullscreen mode

Performance monitoring can be added through metrics:

type Metrics struct {
    insertions  int64
    searches    int64
    deletions   int64
    rotations   int64
}

func (m *Metrics) recordOperation(operation string) {
    switch operation {
    case "insert":
        atomic.AddInt64(&m.insertions, 1)
    case "search":
        atomic.AddInt64(&m.searches, 1)
    case "delete":
        atomic.AddInt64(&m.deletions, 1)
    case "rotate":
        atomic.AddInt64(&m.rotations, 1)
    }
}
Enter fullscreen mode Exit fullscreen mode

These implementations provide a solid foundation for BST operations in Go. The code is optimized for performance while maintaining readability and thread safety where needed. Regular testing and benchmarking ensure consistent performance across different scenarios.

Remember to choose the appropriate implementation based on your specific requirements, considering factors like data size, access patterns, and concurrency needs. The modular design allows for easy extension and customization as needed.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)