Photo by Avery Evans on Unsplash

As a computer science major, at some point in your studies you encounter very fundamental and robust methods for solving complex problems. The study and practice of solving computational problems by designing formalized data structures and efficient algorithms is seen across the entire industry. It is even the reason behind formulations of some of the most influential software corporations leading our time today. To understand these terms and why they are so powerful together one should understand the concept of an Abstract Data Type.

According to my Algorithms / Data Structures professor Dr. Forney, an Abstract Data Type “describes expectations about how data is stored and what operations can be performed on or with the stored data from the viewpoint of the user.” An Abstract Data Type supplies the blueprint while the actual implementation of the design is known as a Data Structure. Those implementations can be applied to create algorithms, or a sequence of procedures designed to execute an objective. Deep understanding of the design and application of core data structures, and the capability to reason one’s way towards a procedural solution, is the basis of most technical interview questions at the top software companies in the world.

In this article I will explain a solution to one of those problems that emphasizes the previously stated fact. The problem is known as “Binary Search Tree to Greater Sum Tree” and it can be found on the platform LeetCode. The given problem description is the following: “Given the root of a binary search tree with distinct values, modify it so that every node has a new value equal to the sum of the values of the original tree that are greater than or equal to node.val.” In this case, “node” is an instance of the struct (Data Structure) “TreeNode” containing the fields “val” (integer), “left” (pointer to a TreeNode), and “right” (pointer to a TreeNode). To tackle this problem, one needs to study the design and application of a Binary Search Tree, a data structure that can help us develop an algorithm to solve the given problem.

Photo by Luke Richardson on Unsplash

A Binary Search Tree is an implementation of the abstract data type “Tree.” A “Tree” provides the blueprint of a hierarchical structure containing a root node and branches, or edges, connecting a set of linked child nodes with parent nodes to form subtrees. A Binary Search Tree not only differs because it is an implementation of a “Tree” but it contains unique constraints that create an advantage over the asymptotic computational complexity of operations, such as search, over data structures such as a Linked List. The constraints, taken from the LeetCode problem, are the following:

1. The left subtree of a node contains only nodes with keys (values) less than the node’s key (value).

2. The right subtree of a node contains only nodes with keys (values) greater than the node’s key (value).

3. Both the left and right subtrees must also be binary search trees.

Understanding the following constraints, we can implement an alternative to a tree traversal algorithm known as “Post Order Traversal” to derive our algorithm to solve the problem. In the “Post Order Traversal” algorithm the following instructions are executed:

1. Traverse the left subtree by passing in as an argument, to the defined traversal method, the current node’s left subtree and recursively invoking that method.

2. Traverse the right subtree by passing in as an argument, to the defined traversal method, the current node’s right subtree and recursively invoking that method.

3. Visit the root.

Photo by Joshua Aragon on Unsplash

Knowing that we can only update a current node’s value by aggregating the values of the original tree that are greater than or equal to the current node’s value, we can apply our knowledge of the known Binary Search Tree constraint that the right subtree of a node contains only nodes with values greater than the current node’s value. Below is a valid algorithm I implemented in both the C programming language and the Python programming language to successfully solve the problem.

C

Python

Ok let us walk through each instruction that the computer must execute to generate our new Binary Search Tree. I will be referring to my implementation in Python.

Photo by David Clode on Unsplash

Referring to the scope of the “bstToGst” method

if root == None: return root

If the given argument for our root tree node parameter “root” obtains a value of “None,” there is no Binary Search Tree to evaluate. Therefore, we can achieve our best-case scenario of a constant time operation for our algorithm’s execution by returning that given value back immediately. Additionally, we can optimize memory allocation by not invoking our recursive function. By invoking our recursive function, we will be allocating memory for our method’s return address, parameters, and arguments onto the call stack which does not need to take place if we know we are not given a Binary Search Tree.

self.bstToGstRecursive(root, 0)

If we are given a tree with at least a root node, we begin our alternative traversal implementation by invoking the recursive “bstToGet” method “bstToGstRecursive” and passing in our root node as an our first argument and our initial sum as our second argument.

Referring to the scope of the “bstToGstRecursive” method

if root == None: return sum

Taken from Wikipedia, “Properties of recursively defined functions and sets can often be proved by an induction principle that follows the recursive definition. In mathematics and computer science, a recursive definition, or inductive definition, is used to define the elements in a set in terms of other elements in the set.” Taking a class on Mathematical Proofs, the mathematical definition of Recursion and Induction should be familiar, and I will not dive deep into it in this article. Here, we are defining our base case to break from our recursive operation. Once we reach a leaf node, or a node containing no children, it’s left and right subtrees will be “None” and we will return the current total sum.

sum = self.bstToGstRecursive(node.right, sum) + node.val

Here, we are defining one of two recursive cases. This instruction examines all of our current node’s right subtree nodes, which may have child nodes with both left and right subtrees that are accounted for in both cases, aggregates their values, and computes the current sum by adding our current node’s value and the aggregated result before updating our current node’s value.

node.val = sum

Next, we assign our new value equal to the sum of the values of the original tree that are greater than or equal to current node’s value.

sum = self.bstToGstRecursive(node.left, sum)

Here, we are defining our second recursive case where we traverse through our current node’s left subtree. We execute this instruction after updating our sum because to satisfy aggregating all our parent’s right subtree nodes, we need to account for any child nodes containing a left subtree as well. The correct total sum will be present for that invocation because all of the current node’s right subtree child nodes are greater than the child node’s in the current node’s left subtree, referring to the known truths about Binary Search Tree constraints; therefore, the correct aggregation is passed as an argument.

return sum

Here we are returning, or “bubbling up,” the updated sum from traversing both our current node’s left and right subtree.

Referring back to the scope of the “bstToGst” method

return root

Here we are returning the updated tree with our alternate node values if any updates took place.

Photo by Joshua Earle on Unsplash

Hopefully, that was not too stressful! I plan to develop and publish solutions to certain problems here and there with free time on my hands. Additionally, I plan to publish articles on a range of thoughts that fill my mind everyday such as history, traveling, software architecture, entrepreneurship, takeaways from books/articles, and anything else that I find interesting in the moment.