My Learning Blog

Understanding Recursion: Return Values, State Management, and Common Misunderstandings - 8/16/2024

In this learning note, I want to delved into the critical aspects of recursion, specifically focusing on how to manage return values and state within recursive functions. Using the example of finding the k-th smallest element in a Binary Search Tree (BST)

283:Given the root of a binary search tree, and an integer k, return the kth smallest value (1-indexed) of all the values of the nodes in the tree.

function kthSmallest(root: TreeNode | null, k: number): number {
  let rank = 0;
  let result = 0;

  function dfs(node: TreeNode | null): void {
    if (node === null) return;

    dfs(node.left);

    rank++;
    if (rank === k) {
      result = node.val;
      return;
    }

    dfs(node.right);
  }

  dfs(root);
  return result;
}

This way is to traverse the tree while keeping a global variable (“result”) that keeps track of the node’value we have encountered. After the dfs, we return the global variable.

The recursive function dfs does not return any value in this case. We “fire-and-forget” the dfs call.

And then, if you try to solve it by “divide and conquer”, Can you code like this?

function kthSmallest(root: TreeNode | null, k: number): number {
  let rank = 0;

  function dfs(node: TreeNode | null): void {
    if (node === null) return;

    dfs(node.left);

    rank++;
    if (rank === k) {
      return node.val;
    }

    dfs(node.right);
  }

  return dfs(root);
}

It return undefined, why?

It’s quite common to run into issues like this when dealing with recursion and state management within recursive functions. Let me explain some of the concepts that might help clarify where things can go wrong and why.

Common Misunderstanding: It’s easy to think that simply returning a value within a recursive function will automatically handle everything. However, unless you explicitly pass that return value up the chain, it gets lost as soon as that specific recursive call finishes.

Common Misunderstanding: It’s common to think that updating a variable (like rank) in one part of the recursive function will automatically “pause” the recursion. However, recursion will continue unless explicitly stopped or controlled.

How to Avoid These Mistakes

When designing a recursive function, start by asking yourself:

When designing a recursive function, you generally need to carefully consider two key aspects:

If you prefer to return values directly from the dfs function instead of using a separate result variable, you need to ensure that the value is propagated correctly up the recursive calls. This involves checking the return value after each recursive call and immediately returning it if it’s not null.

Here’s how you can do it:

function kthSmallest(root: TreeNode | null, k: number): number {
  let rank = 0;

  function dfs(node: TreeNode | null): number {
    if (node === null) return;

    // Traverse the left subtree
    const left = dfs(node.left);
    if (left !== null) return left; // If found in the left subtree, return immediately

    // Visit the current node
    rank++;
    if (rank === k) return node.val; // If current node is the k-th smallest, return its value

    // Traverse the right subtree
    return dfs(node.right); // If not found, continue to the right subtree
  }

  // The result will be the value returned by the DFS function

  return dfs(root);
}