Start learning free

LESSON 295 OF 338 · BINARY TREES

Post Order Tree Traversal in Go: Recursion with a Slice Pointer

Practice O(n) Read the full explanation

Given a binary tree, return the postorder traversal of its nodes' values.

Example:

Input: [1,null,2,3]
   1
    \
     2
    /
   3

Output: [3,2,1]

Follow up: Recursive solution is trivial, could you do it iteratively?

STEP 1 OF 6
    Ctrl + Enter to check

    GO · BINARY TREES

    Post Order Tree Traversal in Go, explained

    Problem overview

    The function postOrderTraversal receives the root of a binary tree, a *TreeNode, and returns the node values in post order: the left subtree first, then the right subtree, and the node itself last. Each node has an int field Val and two child pointers, Left and Right, which are nil when the child is missing.

    In the example, the tree [1,null,2,3] has root 1 with no left child and a right child 2, whose left child is 3. Post order reaches 3 first, then 2, then 1, so the result is [3, 2, 1]. The statement's follow-up asks for an iterative version; this lesson is the recursive one, and a second lesson on the same problem does it iteratively.

    Approach: visit the children, then the node

    A tree is a node with two smaller trees below it, so recursion matches its shape. To traverse a tree in post order:

    1. Traverse the left subtree.
    2. Traverse the right subtree.
    3. Record the node's own value.

    The only difference between the three classic orders is where step 3 sits. Recording the value first gives pre order, recording it between the two traversals gives in order, and recording it last gives post order. A missing child is an empty tree, and an empty tree contributes nothing, which gives the base case.

    Each call needs somewhere to put its value. Rather than building and merging a new slice in every call, the lesson shares one result slice among all the recursive calls. In Go that raises one question: how can a helper grow a slice that belongs to its caller?

    Step by step

    1. In postOrderTraversal, create result as an empty []int, call the helper postOrderDfs(root, &result), and return result. The & passes the address of the slice variable.
    2. Declare the helper outside it, with the parameters root *TreeNode and result *[]int and no return value.
    3. Stop on nil. If root is nil, return at once. This check also handles missing children, so the calls below need no checks of their own.
    4. Recurse left with root.Left, passing the same result pointer along.
    5. Recurse right with root.Right.
    6. Record the value last with *result = append(*result, root.Val). Because this line comes after both recursive calls, the order is post order.

    The solution in Go

    func postOrderTraversal(root *TreeNode) []int {
    	result := []int{}
    	postOrderDfs(root, &result)
    	return result
    }
    
    func postOrderDfs(root *TreeNode, result *[]int) {
    	if root == nil {
    		return
    	}
    	postOrderDfs(root.Left, result)
    	postOrderDfs(root.Right, result)
    	*result = append(*result, root.Val)
    }
    

    The Go details worth a closer look:

    • Why a pointer to the slice. A slice value is a small header holding a pointer to an array, a length and a capacity. Passing it to a function copies that header. append returns a new header, and the helper would assign it to its own copy, so the caller would never see the new element. With result *[]int the helper changes the caller's header through the pointer. For example, a function func byValue(r []int) { r = append(r, 1) } called on an empty slice leaves it with length 0, while func byPointer(r *[]int) { *r = append(*r, 1) } leaves it with length 1.
    • *result and &result. &result takes an address, and *result follows one. In the helper, result alone is the pointer and *result is the slice it points to. The recursive calls pass the pointer on unchanged.
    • Nil pointers as empty trees. A *TreeNode can be nil, and root == nil is the correct test. Reading root.Val on a nil pointer panics, so the check must come before any field access.
    • []int{} versus nil. The result starts as an empty, non-nil slice, so an empty tree returns [] rather than nil.
    • Declaration order. The helper is declared after the function that calls it, which Go allows at package level.

    Complexity

    Time: O(n). Every node is visited once, and each visit does a constant amount of work, plus the missing-child calls that return immediately. append is amortized constant time.

    Space: O(h) for the call stack, plus O(n) for the output, where h is the height of the tree. A balanced tree has h around log n, and a tree where every node has one child has h = n.

    Edge cases and common mistakes

    • Empty tree. A nil root returns [] immediately.
    • Single node. The tree [1] gives [1]: both children are empty, and the node is recorded.
    • Passing the slice by value. The code compiles and runs but returns an empty slice, a silent bug.
    • Recording the value in the wrong place. Moving the append line to the top gives pre order, and for [1,null,2,3] that returns [1, 2, 3].
    • Skipping the nil check. Without it, the first leaf calls the helper on a missing child, and root.Left on a nil pointer panics.
    • Forgetting the dereference. Writing result = append(result, root.Val) in the helper does not compile, since result is a pointer, not a slice.

    Testing

    • [1,null,2,3] gives [3, 2, 1].
    • [1] gives [1].
    • An empty tree gives [].
    • [1,2,3,4,5,6,7], a full tree of three levels, gives [4, 5, 2, 6, 7, 3, 1].
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free