Start learning free

LESSON 298 OF 338 · BINARY TREES

Maximum Sum BST in Go: Checking Every Subtree with a Struct Field

Practice O(n2) Read the full explanation

Given a binary tree root, the task is to return the maximum sum of all keys of any sub-tree which is also a Binary Search Tree (BST).

Assume a BST is defined as follows:

  • The left subtree of a node contains only nodes with keys less than the node's key.
  • The right subtree of a node contains only nodes with keys greater than the node's key.
  • Both the left and right subtrees must also be binary search trees.

 

Example 1:

Input: root = [1,4,3,2,4,2,5,null,null,null,null,null,null,4,6]
Output: 20
Explanation: Maximum sum in a valid Binary search tree is obtained in root node with key equal to 3.

Example 2:

Input: root = [4,3,null,1,2]
Output: 2
Explanation: Maximum sum in a valid Binary search tree is obtained in a single root node with key equal to 2.

Example 3:

Input: root = [-4,-2,-5]
Output: 0
Explanation: All values are negatives. Return an empty BST.

Example 4:

Input: root = [2,1,3]
Output: 6

Example 5:

Input: root = [5,4,8,3,null,6,3]
Output: 7

 

Constraints:

  • Each tree has at most 40000 nodes..
  • Each node's value is between [-4 * 10^4 , 4 * 10^4].
STEP 1 OF 19
    Ctrl + Enter to check

    GO · BINARY TREES

    Max Sum Bst in Go, explained

    Problem overview

    The method maxSumBST receives the root of a binary tree and returns the largest sum of the keys in any subtree that is also a binary search tree (BST). A subtree is a node together with everything below it. It is a BST when, at every node, all keys in the left subtree are strictly less than the node's key, all keys in the right subtree are strictly greater, and both subtrees are BSTs themselves. Keys can be negative, and the tree has up to 40,000 nodes.

    In the tree [2,1,3], the whole tree is a BST, and its sum is 6. In [4,3,null,1,2], node 3 has a right child 2 that is smaller than 3, so the subtree at 3 is not a BST, and neither is the whole tree. The single-node subtrees are, and the best of them is 2. In [-4,-2,-5] every key is negative, and the answer is 0: an empty tree counts as a BST with sum 0. In [5,4,8,3,null,6,3] the node 8 has a right child 3 smaller than itself, so neither it nor the root is a BST. The subtree at node 4, with its child 3, is, and its sum is 7.

    Approach: test every subtree, keep the best sum

    The idea is the direct one. Visit every node. For each, ask whether the subtree rooted there is a BST, and if so, compute its sum and update the best answer so far. That needs three helpers:

    • sum(root): the total of a subtree's keys.
    • isBST(root): whether a subtree is a BST.
    • everythingLess and everythingGreater: whether every key in a subtree is below, or above, a given value.

    The last pair matters because checking only a node against its immediate children is not enough. In [5,4,6,null,null,3,7], node 6 has children 3 and 7, which are on the correct sides of 6. But 3 sits in the right subtree of 5 and is smaller than 5, so the whole tree is not a BST. Comparing every key of the left subtree with the node, and every key of the right subtree with the node, catches this. In that example the subtree at 6 is a BST with sum 16, and the answer is 16.

    The result field starts at 0, which covers the empty-BST case: negative-only trees return 0 because no BST sum beats it.

    This approach is simple but repeats work: each node's check re-walks its subtree. A faster approach does one pass and returns, for every subtree, whether it is a BST together with its smallest key, its largest key and its sum. The lesson keeps the simpler check.

    Step by step

    1. Store the best sum in a struct field. Solution gets a field result of type int. Go gives it the zero value 0, so it needs no initialization.
    2. Write maxSumBST. It calls s.dfs(root) and returns s.result.
    3. Declare dfs with the same pointer receiver s *Solution. A pointer receiver means that updating s.result changes the shared struct, not a copy.
    4. Stop on nil: a bare return ends a method that returns nothing.
    5. Visit the children, left and then right, so every subtree below is examined.
    6. Test this subtree with s.isBST(root).
    7. Update the best sum with s.result = max(s.result, s.sum(root)).
    8. Write sum. An empty subtree sums to 0; otherwise the sum is the node's value plus the sums of its two subtrees.
    9. Write isBST. An empty tree is a BST. For a node, combine four checks with &&: the left subtree is a BST, the right subtree is a BST, everything on the left is less than the node's value, and everything on the right is greater.
    10. Write everythingLess. An empty subtree passes. Otherwise the node's value must be less than val, and both subtrees must pass with the same val.
    11. Write everythingGreater the same way with the comparison reversed.

    The solution in Go

    type Solution struct {
    	result int
    }
    
    func (s *Solution) sum(root *TreeNode) int {
    	if root == nil {
    		return 0
    	}
    	return root.Val + s.sum(root.Left) + s.sum(root.Right)
    }
    
    func (s *Solution) everythingLess(root *TreeNode, val int) bool {
    	if root == nil {
    		return true
    	}
    	return root.Val < val &&
    		s.everythingLess(root.Left, val) &&
    		s.everythingLess(root.Right, val)
    }
    
    func (s *Solution) everythingGreater(root *TreeNode, val int) bool {
    	if root == nil {
    		return true
    	}
    	return root.Val > val &&
    		s.everythingGreater(root.Left, val) &&
    		s.everythingGreater(root.Right, val)
    }
    
    func (s *Solution) isBST(root *TreeNode) bool {
    	if root == nil {
    		return true
    	}
    	return s.isBST(root.Left) &&
    		s.isBST(root.Right) &&
    		s.everythingLess(root.Left, root.Val) &&
    		s.everythingGreater(root.Right, root.Val)
    }
    
    func (s *Solution) dfs(root *TreeNode) {
    	if root == nil {
    		return
    	}
    	s.dfs(root.Left)
    	s.dfs(root.Right)
    	if s.isBST(root) {
    		s.result = max(s.result, s.sum(root))
    	}
    }
    
    func (s *Solution) maxSumBST(root *TreeNode) int {
    	s.dfs(root)
    	return s.result
    }
    

    The Go details worth a closer look:

    • Methods and receivers. func (s *Solution) sum(...) attaches the function to the type Solution, and s plays the role of this or self. Go has no classes; a struct with methods does the job.
    • Why a pointer receiver. With a value receiver, s would be a copy, and dfs would update the copy. The pointer receiver lets every method share one result.
    • Zero values. A struct field that is never assigned holds the zero value of its type, 0 for an int. That is why result needs no constructor.
    • A && chain across lines. Go inserts a semicolon at the end of a line that could end a statement. A line that ends with && can't, so the expression continues on the next line. A line that began with && would be an error.
    • Short-circuit evaluation. && stops at the first false, so a failed check skips the more expensive ones after it.
    • Built-in max. Since Go 1.21 it needs no import.
    • Methods in any order. dfs is written below the helpers it calls, but Go does not require declaration before use at package level.

    Complexity

    Time. dfs makes one call per node. For each node it runs isBST and sum over that node's subtree. In isBST, every node triggers everythingLess or everythingGreater over a part of its own subtree, so one isBST call on a subtree of m nodes costs up to O(m·h), where h is the subtree's height. On balanced trees, h is about log n, and the total is modest. On a tree that degenerates into a chain, h approaches n, and the total grows much faster, up to about O(n³). A single-pass version that returns minimum, maximum and sum from each subtree runs in O(n).

    Space: O(h) for the recursion stack, plus the one integer in the struct.

    Edge cases and common mistakes

    • All keys negative. The answer is 0, not the largest key. Starting result at 0 and updating with max gives that for free.
    • Checking only direct children. The tree [5,4,6,null,null,3,7] fools a check of immediate children. It must compare each key with the ancestors' bounds, which everythingLess and everythingGreater do.
    • Duplicates. The comparisons are strict, so a repeated key breaks the BST property. A node with a right child of the same key is not a BST.
    • A BST inside a non-BST. A bad root doesn't rule out good subtrees. That is why dfs tests every node and does not stop at the first failure.
    • Reusing one Solution. result lives in the struct. Calling maxSumBST twice on the same Solution value keeps the first call's result, so each tree needs a fresh &Solution{} unless the field is reset.
    • A bigger sum from a bigger tree is not guaranteed. A larger BST can have a smaller sum than one of its parts when it contains negative keys. The code takes the maximum over all of them.

    Testing

    • [1,4,3,2,4,2,5,null,null,null,null,null,null,4,6] gives 20, the subtree at node 3.
    • [4,3,null,1,2] gives 2.
    • [-4,-2,-5] gives 0.
    • [2,1,3] gives 6.
    • [5,4,8,3,null,6,3] gives 7.
    • [5,4,6,null,null,3,7] gives 16.
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free