Most Frequent Subtree Sum
MedPrompt
Given the root of a binary tree, return the most frequent subtree sum. If there is a tie, return all the values with the highest frequency in any order.
The subtree sum of a node is defined as the sum of all the node values formed by the subtree rooted at that node (including the node itself).
Example 1:
Input: root = [5,2,-3]
Output: [2,-3,4]Example 2:
Input: root = [5,2,-5]
Output: [2]
Constraints:
- The number of nodes in the tree is in the range
[1, 104]. -105 <= Node.val <= 105
Approaches
2 approaches with complexity analysis and trade-offs.
This approach involves iterating through every node in the tree. For each node, a separate traversal is initiated to calculate the sum of its subtree. The frequencies of these sums are stored in a hash map. This method is straightforward but inefficient due to repeated calculations.
Algorithm
-
- Define a helper function
getAllNodesto traverse the tree (e.g., using pre-order traversal) and store all nodes in a list.
- Define a helper function
-
- Define another helper function
calculateSubtreeSumthat takes a node and recursively calculates the sum of all nodes in its subtree.
- Define another helper function
-
- In the main function, first call
getAllNodesto populate a list of all tree nodes.
- In the main function, first call
-
- Initialize a
HashMap<Integer, Integer>to store the frequency of each subtree sum and an integermaxFreqto track the maximum frequency found.
- Initialize a
-
- Iterate through each node in the list of all nodes.
-
- For each node, call
calculateSubtreeSumto get its subtree sum.
- For each node, call
-
- Update the frequency of this sum in the hash map and update
maxFreqif necessary.
- Update the frequency of this sum in the hash map and update
-
- After iterating through all nodes, create a result list.
-
- Iterate through the hash map and add any sum whose frequency equals
maxFreqto the result list.
- Iterate through the hash map and add any sum whose frequency equals
-
- Convert the result list to an integer array and return it.
Walkthrough
The core idea of this brute-force method is to separate the problem into two main parts: identifying all subtrees and then calculating the sum for each one. First, we perform a full traversal of the tree to get a list of all nodes. Each node in this list is the root of a subtree. Then, we iterate through this list. For each node, we perform a second, independent traversal starting from that node to compute its subtree sum. We use a hash map to keep track of how many times each sum occurs. After calculating all subtree sums, we find the highest frequency and collect all sums that have this frequency.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public int[] findFrequentTreeSum(TreeNode root) { if (root == null) { return new int[0]; } List<TreeNode> nodes = new ArrayList<>(); getAllNodes(root, nodes); Map<Integer, Integer> freqMap = new HashMap<>(); int maxFreq = 0; for (TreeNode node : nodes) { int sum = calculateSubtreeSum(node); int currentFreq = freqMap.getOrDefault(sum, 0) + 1; freqMap.put(sum, currentFreq); maxFreq = Math.max(maxFreq, currentFreq); } List<Integer> resultList = new ArrayList<>(); for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) { if (entry.getValue() == maxFreq) { resultList.add(entry.getKey()); } } return resultList.stream().mapToInt(i -> i).toArray(); } private void getAllNodes(TreeNode node, List<TreeNode> nodes) { if (node == null) { return; } nodes.add(node); getAllNodes(node.left, nodes); getAllNodes(node.right, nodes); } private int calculateSubtreeSum(TreeNode node) { if (node == null) { return 0; } return node.val + calculateSubtreeSum(node.left) + calculateSubtreeSum(node.right); }}Complexity
Time
O(N^2), where N is the number of nodes. The initial traversal to get all nodes is O(N). Then, for each of the N nodes, we perform a subtree sum calculation which can take up to O(N) time in the worst case (a skewed tree). This leads to a total time complexity dominated by N * O(N) = O(N^2).
Space
O(N), where N is the number of nodes. We need O(N) space to store the list of all nodes and up to O(N) for the hash map. The recursion stack for the sum calculation can also go up to O(N) in the worst case of a skewed tree.
Trade-offs
Pros
Conceptually straightforward and easy to break down into smaller, distinct problems.
Cons
Highly inefficient due to redundant computations. The value of a node is included in the sum calculation for itself and for every one of its ancestors, leading to repeated work.
A more efficient approach is to use a single post-order traversal. This traversal order (Left, Right, Root) naturally allows us to compute the sum of a subtree right after we have computed the sums of its children's subtrees. We can calculate each subtree sum and update its frequency in the same pass, avoiding redundant calculations.
Algorithm
-
- Initialize a
HashMap<Integer, Integer>to store sum frequencies and a global or member variablemaxFreqto track the maximum frequency.
- Initialize a
-
- Define a recursive helper function that performs a post-order traversal, let's call it
postOrderSum(node).
- Define a recursive helper function that performs a post-order traversal, let's call it
-
- Base Case: If the current
nodeis null, return 0.
- Base Case: If the current
-
- Recursive Step: Recursively call
postOrderSumfor the left and right children to get their respective subtree sums,leftSumandrightSum.
- Recursive Step: Recursively call
-
- Process Node: Calculate the sum of the subtree rooted at the current node:
currentSum = node.val + leftSum + rightSum.
- Process Node: Calculate the sum of the subtree rooted at the current node:
-
- Update the frequency of
currentSumin the hash map.
- Update the frequency of
-
- Update
maxFreqif the new frequency ofcurrentSumis greater than the currentmaxFreq.
- Update
-
- Return
currentSumto be used by its parent's calculation.
- Return
-
- Start the process by calling
postOrderSum(root).
- Start the process by calling
-
- After the traversal is complete, iterate through the hash map's keys. Collect all sums whose frequency equals
maxFreq.
- After the traversal is complete, iterate through the hash map's keys. Collect all sums whose frequency equals
-
- Return the collected list of sums as an array.
Walkthrough
This optimized approach leverages the properties of post-order traversal. The sum of a subtree at a given node is the node's own value plus the sums of its left and right subtrees. By processing children before the parent, a post-order traversal ensures that when we visit a node, the sums for its left and right subtrees have already been computed.
We can define a recursive function that traverses the tree in a post-order fashion. This function will return the sum of the subtree rooted at the node it's given. Inside this function, after computing the current node's subtree sum, we immediately update a frequency map. We also maintain a variable to keep track of the maximum frequency seen so far. This way, we compute all subtree sums and their frequencies in a single pass over the tree.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { private Map<Integer, Integer> freqMap; private int maxFreq; public int[] findFrequentTreeSum(TreeNode root) { if (root == null) { return new int[0]; } freqMap = new HashMap<>(); maxFreq = 0; postOrderSum(root); List<Integer> resultList = new ArrayList<>(); for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) { if (entry.getValue() == maxFreq) { resultList.add(entry.getKey()); } } return resultList.stream().mapToInt(i -> i).toArray(); } private int postOrderSum(TreeNode node) { if (node == null) { return 0; } int leftSum = postOrderSum(node.left); int rightSum = postOrderSum(node.right); int currentSum = node.val + leftSum + rightSum; int currentFreq = freqMap.getOrDefault(currentSum, 0) + 1; freqMap.put(currentSum, currentFreq); maxFreq = Math.max(maxFreq, currentFreq); return currentSum; }}Complexity
Time
O(N), where N is the number of nodes. We visit each node exactly once during the post-order traversal. Operations inside the recursive function (map access, addition) are O(1) on average. The final step of collecting results from the map takes at most O(N) time.
Space
O(N), where N is the number of nodes. The space is used by the hash map, which can store up to N distinct sums, and by the recursion stack. The depth of the recursion stack is equal to the height of the tree, H, which is O(N) in the worst case (a skewed tree) and O(log N) in the best case (a balanced tree).
Trade-offs
Pros
Optimal time complexity as it processes each node only once.
Combines sum calculation and frequency counting into a single, elegant traversal.
Cons
Requires extra space for the hash map and the recursion stack, which can be O(N) in the worst case for a skewed tree.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private Map < Integer , Integer > counter ; private int mx ; public int [] findFrequentTreeSum ( TreeNode root ) { counter = new HashMap <>(); mx = Integer . MIN_VALUE ; dfs ( root ); List < Integer > res = new ArrayList <>(); for ( Map . Entry < Integer , Integer > entry : counter . entrySet ()) { if ( entry . getValue () == mx ) { res . add ( entry . getKey ()); } } int [] ans = new int [ res . size ()]; for ( int i = 0 ; i < res . size (); ++ i ) { ans [ i ] = res . get ( i ); } return ans ; } private int dfs ( TreeNode root ) { if ( root == null ) { return 0 ; } int s = root . val + dfs ( root . left ) + dfs ( root . right ); counter . put ( s , counter . getOrDefault ( s , 0 ) + 1 ); mx = Math . max ( mx , counter . get ( s )); return s ; } }/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: unordered_map < int , int > counter ; int mx = 0 ; vector < int > findFrequentTreeSum ( TreeNode * root ) { mx = INT_MIN ; dfs ( root ); vector < int > ans ; for ( auto & entry : counter ) if ( entry . second == mx ) ans . push_back ( entry . first ); return ans ; } int dfs ( TreeNode * root ) { if ( ! root ) return 0 ; int s = root -> val + dfs ( root -> left ) + dfs ( root -> right ); ++ counter [ s ]; mx = max ( mx , counter [ s ]); return s ; } };# Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution : def findFrequentTreeSum ( self , root : TreeNode ) -> List [ int ]: def dfs ( root ): if root is None : return 0 left , right = dfs ( root . left ), dfs ( root . right ) s = root . val + left + right counter [ s ] += 1 return s counter = Counter () dfs ( root ) mx = max ( counter . values ()) return [ k for k , v in counter . items () if v == mx ]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.