← Back to DevBytes

Solving Binary Tree Level Order Traversal in Go: Step-by-Step Guide

Introduction to Binary Tree Level Order Traversal

Binary Tree Level Order Traversal is a fundamental algorithmic problem that every Go developer should master. It involves visiting all nodes of a binary tree level by level, from top to bottom and left to right. This traversal pattern, also known as Breadth-First Search (BFS) for trees, is a common interview question and a building block for many advanced tree-based algorithms.

In this tutorial, you'll learn what level order traversal is, why it matters, how to implement it in Go using a queue-based approach, and the best practices to follow when writing production-ready tree traversal code.

What Is Level Order Traversal?

Level order traversal visits nodes of a binary tree one level at a time. Starting from the root, it processes all nodes at depth 0, then all nodes at depth 1, and so on until every node has been visited. Within each level, nodes are processed from left to right.

Consider the following binary tree:

       3
      / \
     9  20
       /  \
      15   7

The level order traversal would produce the following output:

[
  [3],
  [9, 20],
  [15, 7]
]

Notice that each level is grouped into its own slice, making the result a slice of slices. This grouping is what distinguishes level order traversal from a simple BFS that returns a flat list.

Why Level Order Traversal Matters

Level order traversal is more than an academic exercise. It has practical applications across many domains:

Understanding this traversal also strengthens your grasp of queue data structures and BFS patterns, which appear frequently in coding interviews and real-world systems.

Defining the Binary Tree Structure in Go

Before implementing the traversal, we need a binary tree node structure. In Go, we define this using a struct with a value and pointers to left and right children.

package main

// TreeNode represents a node in a binary tree.
type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

This simple struct is sufficient for most binary tree problems. The Val field stores the node's integer value, while Left and Right are pointers to child nodes. A nil pointer indicates the absence of a child.

Implementing Level Order Traversal

The standard approach to level order traversal uses a queue. We enqueue the root, then repeatedly dequeue nodes while enqueuing their children. To group nodes by level, we track the number of nodes at the current level before processing begins.

Basic Implementation

package main

import "fmt"

type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

// LevelOrder performs a level order traversal and returns
// a slice of slices, where each inner slice contains the
// values of nodes at that level.
func LevelOrder(root *TreeNode) [][]int {
    if root == nil {
        return [][]int{}
    }

    result := [][]int{}
    queue := []*TreeNode{root}

    for len(queue) > 0 {
        levelSize := len(queue)
        level := make([]int, 0, levelSize)

        for i := 0; i < levelSize; i++ {
            node := queue[0]
            queue = queue[1:]

            level = append(level, node.Val)

            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }

        result = append(result, level)
    }

    return result
}

func main() {
    // Build the example tree:
    //        3
    //       / \
    //      9  20
    //        /  \
    //       15   7
    root := &TreeNode{Val: 3}
    root.Left = &TreeNode{Val: 9}
    root.Right = &TreeNode{Val: 20}
    root.Right.Left = &TreeNode{Val: 15}
    root.Right.Right = &TreeNode{Val: 7}

    result := LevelOrder(root)
    fmt.Println(result)
    // Output: [[3] [9 20] [15 7]]
}

How the Algorithm Works

Let's break down the algorithm step by step:

The key insight is capturing levelSize before the inner loop begins. Since we enqueue children during the inner loop, the queue length changes. By saving the size upfront, we ensure we only process nodes belonging to the current level.

Optimizing the Queue Implementation

The basic implementation uses a slice as a queue with queue = queue[1:] to dequeue. While simple, this approach has a subtle inefficiency: it does not free the memory of dequeued elements, and the underlying array keeps growing. For large trees, this can become a problem.

Using a Pointer-Based Queue

A more efficient approach uses head and tail indices to avoid repeated slice reslicing:

func LevelOrderOptimized(root *TreeNode) [][]int {
    if root == nil {
        return [][]int{}
    }

    result := [][]int{}
    queue := []*TreeNode{root}
    head := 0

    for head < len(queue) {
        levelSize := len(queue) - head
        level := make([]int, 0, levelSize)

        for i := 0; i < levelSize; i++ {
            node := queue[head]
            head++

            level = append(level, node.Val)

            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }

        result = append(result, level)
    }

    return result
}

This version uses a head pointer that advances instead of reslicing the queue. The queue slice grows as needed, but we never reallocate due to dequeuing. This is a common pattern in Go for implementing efficient queues without importing a third-party package.

Handling Edge Cases

Robust code must handle edge cases gracefully. Here are the scenarios you should consider:

Here is a test function that covers these cases:

package main

import (
    "reflect"
    "testing"
)

func TestLevelOrder(t *testing.T) {
    tests := []struct {
        name     string
        root     *TreeNode
        expected [][]int
    }{
        {
            name:     "nil root",
            root:     nil,
            expected: [][]int{},
        },
        {
            name:     "single node",
            root:     &TreeNode{Val: 1},
            expected: [][]int{{1}},
        },
        {
            name: "left skewed",
            root: &TreeNode{
                Val:  1,
                Left: &TreeNode{Val: 2, Left: &TreeNode{Val: 3}},
            },
            expected: [][]int{{1}, {2}, {3}},
        },
        {
            name: "full tree",
            root: &TreeNode{
                Val:   3,
                Left:  &TreeNode{Val: 9},
                Right: &TreeNode{Val: 20, Left: &TreeNode{Val: 15}, Right: &TreeNode{Val: 7}},
            },
            expected: [][]int{{3}, {9, 20}, {15, 7}},
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := LevelOrder(tt.root)
            if !reflect.DeepEqual(result, tt.expected) {
                t.Errorf("got %v, want %v", result, tt.expected)
            }
        })
    }
}

Best Practices

When implementing level order traversal in Go, keep these best practices in mind:

Generic Version Using Go Generics

For Go 1.18 and later, you can write a generic version that works with any node value type:

type GenericTreeNode[T any] struct {
    Val   T
    Left  *GenericTreeNode[T]
    Right *GenericTreeNode[T]
}

func LevelOrderGeneric[T any](root *GenericTreeNode[T]) [][]T {
    if root == nil {
        return [][]T{}
    }

    result := [][]T{}
    queue := []*GenericTreeNode[T]{root}
    head := 0

    for head < len(queue) {
        levelSize := len(queue) - head
        level := make([]T, 0, levelSize)

        for i := 0; i < levelSize; i++ {
            node := queue[head]
            head++

            level = append(level, node.Val)

            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }

        result = append(result, level)
    }

    return result
}

This generic implementation provides maximum flexibility without sacrificing performance or readability.

Complexity Analysis

Understanding the time and space complexity of your implementation is essential:

These complexities are optimal for this problem. You cannot do better than O(n) time because every node must be visited, and you cannot do better than O(n) space because the result itself contains all node values.

Conclusion

Binary Tree Level Order Traversal is a foundational algorithm that combines tree traversal with queue-based BFS. By capturing the level size before processing each level, you can cleanly group nodes into the familiar slice-of-slices output format. The Go implementations shown here, from the basic version to the optimized and generic variants, give you the tools to handle this problem efficiently in any context. Remember to handle edge cases, write table-driven tests, and preallocate slices for the best performance. With these techniques in your toolkit, you are well-equipped to tackle level order traversal and the many related problems that build upon it.

— Ad —

Google AdSense will appear here after approval

← Back to all articles