Start learning free

LESSON 313 OF 348 · BINARY TREES

Encoded Tree Sums in Kotlin: Recursive Descent Parsing

Practice O(n) Homework Read the full explanation

Given a tree encoded as a string, for every node, compute the sum of its subtree values and return the sums tree encoded as a string.

For example, the following tree is given as an encoded string "1{2{3,4},5{6}}" :

    1
   / \
  2   5
 / \   \
3   4   6
After computing the sums, the tree will look like the following:
    21
   /  \
  9    11
 / \     \
3   4     6
Therefore the output will be "21{9{3,4},11{6}}"

Example 1:

Input:  "1{2{3,4},5{6}}"

Output: "21{9{3,4},11{6}}"

Example 2:

Input:  "1{1,1{1,1,1},1}"

Output: "7{1,4{1,1,1},1}"
SOLVE IT YOURSELF
Ctrl + Enter to check

KOTLIN · BINARY TREES

Encoded Tree Sums in Kotlin, explained

Problem overview

The function encodedTreeSums receives a tree written as a string and returns another string. In the format, a node is a number, optionally followed by its children in braces and separated by commas. So "1{2{3,4},5{6}}" is a root 1 with two children: 2 (with children 3 and 4) and 5 (with the single child 6).

Every number in the output is replaced by the sum of the values in that node's subtree, which includes the node itself. For the example, node 2 becomes 2 + 3 + 4 = 9, node 5 becomes 5 + 6 = 11, and the root becomes 1 + 9 + 11 = 21, whose leaves keep their own values. The result is "21{9{3,4},11{6}}". For "1{1,1{1,1,1},1}" the answer is "7{1,4{1,1,1},1}".

Approach: parse and sum in the same pass

Building a real tree and then walking it would work, but the string already has a recursive shape: a node is a number, then optionally {, a comma-separated list of nodes, and }. A function that parses one node can call itself for each child. That technique is called recursive descent parsing.

The function reads the digits of the node's value, then checks for {. If it finds one, it parses each child in turn. A child's call returns two things: the child's subtree sum and the child's finished output string. The parent adds the sums to its own value and joins the strings. The sum of a subtree is only known after all its children are parsed, so the parent builds its own output last.

Since the parser must know where to continue after a child finishes, all calls share one reading position, which every call advances as it consumes characters.

Step by step

  1. Keep the position in a one-element array. encodedTreeSums creates intArrayOf(0) and passes it down, so every call sees the same current position.
  2. Start the helper parseEncodedTree, which takes the input and the position array, and returns a Pair of the subtree sum and the encoded text.
  3. Read the number. While the position is inside the string and the current character is a digit, multiply value by 10 and add the digit. This handles values with several digits, such as 12.
  4. Start total at value and create an empty list for the children's output strings.
  5. Check for {. A leaf has no braces, so it skips the children entirely.
  6. Parse the children in a loop. Step past {, parse one child, add its sum to total and keep its string. Then, if the next character is ,, step past it and parse another child.
  7. Close the group. When the next character isn't a comma, skip the } if it is there and leave the loop.
  8. Build the output. The text starts as total converted to a string. When there are children, append {, the children joined with commas, and }.
  9. Return the pair of the total and the text. At the top level, encodedTreeSums takes the second part.

The solution in Kotlin

class Solution {
    fun encodedTreeSums(input: String): String {
        val index = intArrayOf(0)
        return parseEncodedTree(input, index).second
    }

    fun parseEncodedTree(input: String, index: IntArray): Pair<Int, String> {
        var value = 0
        while (index[0] < input.length && input[index[0]].isDigit()) {
            value = value * 10 + (input[index[0]] - '0')
            index[0]++
        }
        var total = value
        val children = mutableListOf<String>()
        if (index[0] < input.length && input[index[0]] == '{') {
            index[0]++
            while (true) {
                val (childSum, childTree) = parseEncodedTree(input, index)
                total += childSum
                children.add(childTree)
                if (index[0] < input.length && input[index[0]] == ',') {
                    index[0]++
                    continue
                }
                if (index[0] < input.length && input[index[0]] == '}') {
                    index[0]++
                }
                break
            }
        }
        var encoded = total.toString()
        if (children.isNotEmpty()) {
            encoded += "{" + children.joinToString(",") + "}"
        }
        return Pair(total, encoded)
    }
}

The Kotlin details worth a closer look:

  • Why an IntArray for the position. A Kotlin parameter is read-only, and an Int passed to a function is copied, so a helper can't advance its caller's counter. An array is an object, so every call shares its element index[0].
  • Char arithmetic. input[i] is a Char. Subtracting '0' from a digit character gives an Int from 0 to 9, because characters are numbered consecutively. isDigit() tests the character first.
  • Destructuring. val (childSum, childTree) = ... unpacks a Pair into two variables, in the order of its first and second properties.
  • Short-circuit checks. index[0] < input.length && ... tests the bound first, so input[index[0]] is never read past the end of the string.
  • Building text. joinToString(",") joins the list with commas and no trailing comma. += on a String var creates a new string.
  • Int sums. Sums use 32-bit Int, so totals above 2,147,483,647 would wrap around silently.

Complexity

Let n be the length of the input.

Time. Every character is read once, which is O(n). Building the output strings adds copying: a node's text is copied into its parent's, and that into the grandparent's, so each character is copied once per level above it. The total is O(n) for a shallow tree and up to O(n²) for a very deep one, such as 1{1{1{1{...}}}}.

Space: O(n) for the strings, plus the recursion depth, which equals the depth of the tree.

Edge cases and common mistakes

  • A single leaf. "7" gives "7".
  • Multi-digit values. "12{30}" gives "42{30}". Reading only one digit per node would break this.
  • Deep nesting. "1{2{3{4}}}" gives "10{9{7{4}}}": each level adds its own value to the sum below it.
  • Forgetting the shared position. If each call kept its own copy of the position, the parent would re-read the characters its child already consumed.
  • Adding the wrong number. The parent adds the child's subtree sum, the first part of the pair. Adding only the child's own value would ignore its descendants, so the root of the first example would come out as 1 + 2 + 5 = 8 instead of 21.

Testing

  • "1{2{3,4},5{6}}" gives "21{9{3,4},11{6}}".
  • "1{1,1{1,1,1},1}" gives "7{1,4{1,1,1},1}".
  • "7" gives "7".
  • "12{30}" gives "42{30}".
  • "1{2{3{4}}}" gives "10{9{7{4}}}".
Keep going with KotlinAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
Start learning free