Recursion

Not Started

Recursion is when a function calls itself. That sounds circular, but it works because each call handles a smaller version of the same problem. Eventually the problem is so small it can be answered directly. That direct answer is called the base case, and it is what stops the chain of calls.

1. Picture a Queue of People

The scenario

You are standing at the back of a queue and you want to know how many people are ahead of you. You cannot see the front. So you tap the person in front of you and ask: "How many people are ahead of you?"

That person does not know either. So theyy tap the person in front of them and ask the exact same question. This keeps going, person by person, until someone at the very front says: "Nobody is ahead of me. Zero."

Now the answers pass backwards. The person behind the front says "0 + 1 = 1". The next says "1 + 1 = 2". Each person adds one to whatever they were told, and passes it back. Eventually the answer reaches you.

Let's draw the queue

There are 4 people. You are person D at the back. Watch the question travel forward one person at a time, then the answers travel back:

Step 1: D asks "How many ahead of me?" and waits.
D - asks the question, waits for an answer
Step 2: C doesn't know either. Asks the same question forward. Now D and C are both waiting.
C - asks the question, waits
D - waiting
Step 3: B doesn't know either. Asks forward again.
B - asks the question, waits
C - waiting
D - waiting
Step 4: A is at the front. Nobody ahead. Answers: "Zero."
A - "I'm at the front. Zero."
B - waiting
C - waiting
D - waiting
Step 5: A's answer reaches B. B adds 1: "One."
B - hears 0, adds 1 -> says 1
C - waiting
D - waiting
Step 6: B's answer reaches C. C adds 1: "Two."
C - hears 1, adds 1 -> says 2
D - waiting
Step 7: C's answer reaches D. D adds 1: "Three." That's your answer.
D - hears 2, adds 1 -> answer is 3

Notice two things:

  • Every person did the same job: ask the one in front, add 1, pass it back.
  • Person A did something different: they just answered directly. They stopped the chain.

Two rules hidden in the queue

1. A base case - Person A at the front. They stop the chain and give a direct answer. Without a front person, the question would travel forward forever.

2. A recursive case - Everyone else. They pass the same question forward (a slightly smaller version of the problem, since there are fewer people ahead) and combine the answer when it comes back.

These are the only two rules a recursive function ever needs: a base case to stop, and a recursive case to keep going with a smaller problem.

2. The Queue Is the Call Stack

Here is the key

That queue you just pictured? The computer does the exact same thing when a function calls itself. Each person in the queue becomes a stack frame - a record of one function call, holding its parameter values and where to return to when it finishes.

Just like the queue: each frame waits for the one it created to finish. The newest frame is always on top. When it finishes, it is removed, and the one underneath picks up where it left off. The first frame created is the last to finish - exactly like person D waiting at the back.

The queue, redrawn as a call stack

Let's turn the queue into code and see that it is the same picture:

def people_ahead(person):
    if person is None:        # base case: nobody here
        return 0
    return 1 + people_ahead(person.next)  # 1 for this person + rest
int peopleAhead(Person* person) {
    if (person == nullptr)    // base case: nobody here
        return 0;
    return 1 + peopleAhead(person->next);  // 1 for this person + rest
}
int PeopleAhead(Person person) {
    if (person == null)       // base case: nobody here
        return 0;
    return 1 + PeopleAhead(person.Next);   // 1 for this person + rest
}
int peopleAhead(Person person) {
    if (person == null)       // base case: nobody here
        return 0;
    return 1 + peopleAhead(person.next);   // 1 for this person + rest
}

Each person in the queue is now a function call. Each function call is a frame on the stack. Watch the stack grow one frame at a time as the question travels forward, then shrink one frame at a time as the answers come back:

Step 1: We call peopleAhead(D). D is not at the front of the queue (D has someone in front of them), so the base case does not apply. The function calls peopleAhead(C) to ask the person in front. D is now paused, waiting for C's answer.
peopleAhead(D) - calls peopleAhead(C), waiting
Step 2: peopleAhead(C) starts running. C is not at the front either, so it does the same thing - calls peopleAhead(B) and pauses. Now both C and D are waiting. The stack has grown to two frames.
peopleAhead(C) - calls peopleAhead(B), waiting
peopleAhead(D) - waiting
Step 3: peopleAhead(B) starts running. B is not at the front either, so it calls peopleAhead(A) and pauses. Three frames are now stacked up, all waiting.
peopleAhead(B) - calls peopleAhead(A), waiting
peopleAhead(C) - waiting
peopleAhead(D) - waiting
Step 4: peopleAhead(A) starts running. A IS at the front of the queue - there is nobody ahead of A. This is the base case. The function returns 0 immediately. No new call is made. This is the turning point: the stack stops growing and starts shrinking.
peopleAhead(A) - base case, returns 0
peopleAhead(B) - waiting
peopleAhead(C) - waiting
peopleAhead(D) - waiting
Step 5: peopleAhead(A) is finished and removed from the stack. Control returns to peopleAhead(B), which has been waiting for A's answer. B receives the answer 0. It computes 1 + 0 = 1 (one person, A, plus A's count of zero). B returns 1 and is removed from the stack.
peopleAhead(B) - returns 1
peopleAhead(C) - waiting
peopleAhead(D) - waiting
Step 6: peopleAhead(B) is gone. Control returns to peopleAhead(C), which has been waiting for B's answer. C receives the answer 1. It computes 1 + 1 = 2 (one person, B, plus B's count of one). C returns 2 and is removed from the stack.
peopleAhead(C) - returns 2
peopleAhead(D) - waiting
Step 7: peopleAhead(C) is gone. Control returns to peopleAhead(D) - the very first call we made. D receives the answer 2. It computes 1 + 2 = 3 (one person, C, plus C's count of two). D returns 3 and is removed from the stack.
peopleAhead(D) - returns 3
Stack is empty. The final answer is 3 - there are 3 people ahead of D. The first call we made (D) was the last one to finish, just like in the queue.
The first call made (D) is the last to finish. It has to wait for everyone in front of it. The base case (A) finishes first, and the answers bubble backward one frame at a time. This is how every recursive function works. Without understanding this call stack behaviour, it is very difficult to follow how recursive calls operate - particularly for more complex algorithms.

3. Before Turning Up the Dial

Countdown

def countdown(n):
    if n == 0:           # base case
        print("Go!")
        return
    print(n, end="... ") # do something
    countdown(n - 1)     # recursive call (smaller problem)
void countdown(int n) {
    if (n == 0) {          // base case
        cout << "Go!";
        return;
    }
    cout << n << "... ";   // do something
    countdown(n - 1);      // recursive call (smaller problem)
}
void Countdown(int n) {
    if (n == 0) {          // base case
        Console.Write("Go!");
        return;
    }
    Console.Write(n + "... "); // do something
    Countdown(n - 1);         // recursive call (smaller problem)
}
void countdown(int n) {
    if (n == 0) {          // base case
        System.out.print("Go!");
        return;
    }
    System.out.print(n + "... "); // do something
    countdown(n - 1);            // recursive call (smaller problem)
}

Calling countdown(3) prints: 3... 2... 1... Go!

Same pattern as the queue. Each call is a person joining the line:

The call stack for countdown(3)

Step 1: We call countdown(3). n is 3, which is not 0, so the base case does not apply. The function prints "3... " and then calls countdown(2). countdown(3) is now paused - it cannot continue until countdown(2) finishes.
countdown(3) - printed "3... ", calls countdown(2), waiting
Step 2: countdown(2) starts running. n is 2, which is not 0, so again no base case. It prints "2... " and calls countdown(1). Now countdown(2) is also paused. Two frames on the stack, both waiting.
countdown(2) - printed "2... ", calls countdown(1), waiting
countdown(3) - waiting
Step 3: countdown(1) starts running. n is 1, still not 0. It prints "1... " and calls countdown(0). Three frames now stacked up, all waiting.
countdown(1) - printed "1... ", calls countdown(0), waiting
countdown(2) - waiting
countdown(3) - waiting
Step 4: countdown(0) starts running. n is 0 - this IS the base case. It prints "Go!" and returns immediately. No new call is made. Just like person A at the front of the queue, it stops the chain. The stack has reached its deepest point and now starts to shrink.
countdown(0) - base case, prints "Go!", returns
countdown(1) - waiting
countdown(2) - waiting
countdown(3) - waiting
Step 5: countdown(0) is finished and removed from the stack. Control returns to countdown(1), which was waiting for it. countdown(1) already printed "1... " before it made the call, and there is nothing left to do in the function after the recursive call. So it simply returns and is removed from the stack.
countdown(1) - nothing left to do, returns
countdown(2) - waiting
countdown(3) - waiting
Step 6: countdown(1) is gone. Control returns to countdown(2). Same situation - it already printed "2... " before it made its call, and there is nothing left to do. It returns and is removed from the stack.
countdown(2) - nothing left to do, returns
countdown(3) - waiting
Step 7: countdown(2) is gone. Control returns to countdown(3) - the very first call we made. It already printed "3... " at the start, and there is nothing left to do. It returns and is removed from the stack.
countdown(3) - nothing left to do, returns
Stack is empty. The output, in order, was: 3... 2... 1... Go! Each call printed its number before making the next call, so the numbers came out in descending order. The base case printed "Go!" at the end.

Just like the queue: countdown(3) started first but finished last. countdown(0) was the base case - it finished first, and the rest unwound one at a time behind it.

The stack grows on the way down (as calls are made) and shrinks on the way back up (as calls return). The base case is what turns the direction from "going down" to "coming back up". Every example in Level 2 and Level 3 follows this same pattern.

4. Two Calls Instead of One

In Level 1, every example made one recursive call. What happens when a function makes two?

Imagine you are organising a family reunion and you need to count how many living relatives you can invite. You cannot do it all at once. Instead, you ask your Mum: "count everyone on your side who can come." Then you ask your Dad: "count everyone on your side who can come." Each of them does the same thing - they ask their own parents. Eventually someone has no living parents to ask, and that person just reports back "1" - themselves. Each person waits for both answers, adds 1 for themselves, and passes the total back up.

This is the key difference from Level 1. In the queue and countdown examples, each call onlyy made one recursive call and waited for one answer. Here, each call makes two recursive calls - one for mum's side, one for dad's side. It makes the first call, waits for that entire side to finish, then makes the second call and waits for that to finish too. Only then can it add everything together.

The code

def count_family(person):
    if person is None:           # base case: nobody here
        return 0
    left = count_family(person.mum)   # count mum's side
    right = count_family(person.dad)  # count dad's side
    return 1 + left + right            # me + both sides
int countFamily(Person* person) {
    if (person == nullptr)      // base case: nobody here
        return 0;
    int left = countFamily(person->mum);   // count mum's side
    int right = countFamily(person->dad);  // count dad's side
    return 1 + left + right;              // me + both sides
}
int CountFamily(Person person) {
    if (person == null)        // base case: nobody here
        return 0;
    int left = CountFamily(person.Mum);    // count mum's side
    int right = CountFamily(person.Dad); // count dad's side
    return 1 + left + right;              // me + both sides
}
int countFamily(Person person) {
    if (person == null)        // base case: nobody here
        return 0;
    int left = countFamily(person.mum);    // count mum's side
    int right = countFamily(person.dad);   // count dad's side
    return 1 + left + right;              // me + both sides
}

Read that return line as: "To count everyone starting from this person, I need the total from mum's side and the total from dad's side, then I add 1 for myself." The function cannot compute its total until both recursive calls have finished and returned their answers.

The family tree we will trace

        You
       /    \
     Mum    Dad
    /   \
  Gran  Gramps

5 people. You has Mum and Dad. Mum has Gran (her mother) and Gramps (her father). Dad has no living parents to ask - both his mum and dad pointers are null. Gran and Gramps also have no living parents to ask. We call countFamily(You) and expect the answer to be 5.

When someone has no living parents to ask, both their mum and dad pointers are null. The function calls countFamily on both, and both immediately return 0 (base case: no one here). That person then computes 1 + 0 + 0 = 1 and returns - they are counting just themselves.

The call stack for countFamily(You)

The same stack rules from Level 1 apply here. The stack grows as calls are made and shrinks as they return. The difference is that when a call gets an answer back from its first recursive call (mum's side), it does not finish yet. It still needs to make its second recursive call (dad's side), wait for that answer, and only then can it compute its total.

Phase 1 - Going down mum's side. The function always calls countFamily(person->mum) first, so we follow that path as deep as it goes.

Step 1: We call countFamily(You). You is not null, so this is not a base case. To count everyone in the tree, we need two things: the total from Mum's side and the total from Dad's side. The function calls countFamily(Mum) first and pauses, waiting for the answer.
countFamily(You) - calls Mum's side, waiting
Key insight: This paused call - countFamily(You) - is the very first one we made, but it will be the last one to finish. It cannot return its answer until every single call it triggers (directly or indirectly via its children) has finished and reported back. This is the start of its journey - it will wait through every step below, collecting results from both sides, before finally computing and returning the answer we are looking for.
Step 2: countFamily(Mum) starts running. Mum is not null, so not a base case either. Mum has two parents on record: Gran and Gramps. The function needs the total from Gran's side and the total from Gramps' side. It calls countFamily(Gran) first and pauses.
countFamily(Mum) - calls Gran's side, waiting
countFamily(You) - waiting
Step 3: countFamily(Gran) starts running. Gran is not null, so the function tries to count Gran's family. But Gran has no living parents to ask - both her mum and dad pointers are null. The function calls countFamily(Gran's mum), which is null - that hits the base case and returns 0 immediately. Then it calls countFamily(Gran's dad), also null - returns 0 again. Gran now has both answers: 0 and 0. She computes 1 + 0 + 0 = 1 (just herself). Returns 1.
countFamily(Gran) - no living parents, returns 1
countFamily(Mum) - waiting
countFamily(You) - waiting

Phase 2 - Back at Mum. Gran's side answered 1. Now calling Gramps' side.

Step 4: countFamily(Gran) is finished and removed from the stack. We go back to countFamily(Mum), which was waiting. It now has the answer from its first call: Gran's side = 1. But it still needs the answer from its second call. So it calls countFamily(Gramps) now.
countFamily(Mum) - Gran's side = 1, calls Gramps' side
countFamily(You) - waiting
Step 5: countFamily(Gramps) starts running. Same situation as Gran - Gramps has no living parents to ask either. Both his mum and dad pointers are null. The function calls countFamily on both, and both return 0 immediately (base case). Gramps computes 1 + 0 + 0 = 1 (just himself). Returns 1.
countFamily(Gramps) - no living parents, returns 1
countFamily(Mum) - waiting
countFamily(You) - waiting

Phase 3 - Back at Mum. Both sides are done.

Step 6: countFamily(Gramps) is finished and removed. We go back to countFamily(Mum). It now has both answers it was waiting for:
Mum's side (Gran): 1.
Dad's side (Gramps): 1.
Now it can compute the total: 1 + 1 + 1 = 3 (Mum herself, plus Gran, plus Gramps). countFamily(Mum) returns 3 and is removed from the stack.
countFamily(Mum) - returns 3
countFamily(You) - waiting

Phase 4 - Back at You. Mum's side answered 3. Now calling Dad's side.

Step 7: countFamily(Mum) is gone. We go back to countFamily(You), which has been waiting since step 1. It now has the answer from its first call: Mum's side = 3. But it still needs the answer from its second call. So it calls countFamily(Dad) now.
countFamily(You) - Mum's side = 3, calls Dad's side
Step 8: countFamily(Dad) starts running. Dad is not null, so the function tries to count Dad's family. But Dad has no living parents to ask - both pointers are null. Both calls return 0 immediately (base case). Dad computes 1 + 0 + 0 = 1 (just himself). Returns 1.
countFamily(Dad) - no living parents, returns 1
countFamily(You) - waiting

Phase 5 - Back at You for the last time. Both sides are done.

Journey complete: Remember Step 1? countFamily(You) has been waiting patiently through every single step since then. It made its first call (Mum's side), which triggered 4 more calls before returning with the answer 3. Then it made its second call (Dad's side), which returned 1. Now, finally, it has everything it needs. The very first call we made is about to become the very last call to finish.
Step 9: countFamily(Dad) is finished and removed. We go back to countFamily(You) for the last time. It now has both answers:
Mum's side: 3.
Dad's side: 1.
Now it can compute the total: 1 + 3 + 1 = 5 (You, plus 3 people on Mum's side, plus Dad). countFamily(You) returns 5 and is removed from the stack.
countFamily(You) - returns 5
Stack is empty. The final answer is 5 people in the family tree: You, Mum, Gran, Gramps, and Dad. Correct.

Notice the pattern: each call that is not a base case makes its first call (mum's side), waits for it to fully resolve, then makes its second call (dad's side) and waits again. Mum's entire side - all the way down to the leaves and back - finishes completely before Dad's side even begins.

With two recursive calls, the same stack rules apply - but now each frame pauses twice: once for the first call (mum's side), once for the second (dad's side). The first call is fully explored before the second call starts. This is exactly how tree recursion works in Level 3.

5. Applying This to Trees

In Level 2, countFamily made two calls - one for mum's side, one for dad's side. Tree algorithms do the same thing - one call for the left child, one for the right.

In a tree, each node can have a left child and a right child. To process the whole tree, you need to visit both sides. That means each call makes two recursive calls instead of one - exactly like countFamily in Level 2.

Imagine you are at a fork in a corridor. You need to check both paths. You go left first, explore everything down that way, come back to the fork, then go right and explore that side. You onlyy report your findings once you have checked both paths.

Example: height of a binary tree

def height(root):
    if root is None:          # base case: empty tree
        return -1
    left = height(root.left)   # height of left subtree
    right = height(root.right) # height of right subtree
    return 1 + max(left, right) # 1 for this node + max of children
int height(TreeNode* root) {
    if (root == nullptr)       // base case: empty tree
        return -1;
    int left = height(root->left);    // height of left subtree
    int right = height(root->right);  // height of right subtree
    return 1 + max(left, right);       // 1 for this node + max of children
}
int Height(TreeNode root) {
    if (root == null)        // base case: empty tree
        return -1;
    int left = Height(root.Left);   // height of left subtree
    int right = Height(root.Right); // height of right subtree
    return 1 + Math.Max(left, right); // 1 for this node + max of children
}
int height(TreeNode root) {
    if (root == null)        // base case: empty tree
        return -1;
    int left = height(root.left);   // height of left subtree
    int right = height(root.right); // height of right subtree
    return 1 + Math.max(left, right); // 1 for this node + max of children
}

Read that return line as: "My height is 1 (for the edge connecting me to my parent) plus whichever of my two children is taller." Just like countFamily, we cache the results from both recursive calls first, then use them to compute the final answer.

Each node asks its left child and right child the same question. Leaves have no children, so both their calls hit the base case and return -1.

Don't be fooled by compact examples: You might see this written as:
return 1 + max(height(root.left), height(root.right))
return 1 + max(height(root->left), height(root->right));
return 1 + Math.Max(Height(root.Left), Height(root.Right));
return 1 + Math.max(height(root.left), height(root.right));

This is functionally identical to the version above. Both recursive calls still happen - left first, then right - but the results are not stored in variables. The compact version is common in production code, but the cached version makes it clearer that there are two separate recursive calls happening in sequence.

Let's trace this on a small tree

    5
   / \
  3   7
 /
1

Phase 1 - Starting at the root (node 5). We always go left first, as far as we can.

Step 1: We call height(5) on the root. Node 5 has two children (3 on the left, 7 on the right), so it needs to ask both of them how tall they are before it can figure out its own height. It always asks the left child first. So it calls height(3) and pauses, waiting for the answer.
height(5) - calls left child (node 3)
Step 2: height(3) starts running. Node 3 also has children (1 on the left, nothing on the right), so it does the same thing: asks its left child first. It calls height(1) and pauses.
height(3) - calls left child (node 1)
height(5) - waiting
Step 3: height(1) starts running. Node 1 is a leaf - it has no children at all. But the code does not know that yet. It tries to call height on node 1's left child, which is null.
height(1) - calls left child (null)
height(3) - waiting
height(5) - waiting
Step 4: height(null) runs. There is no node here - we have gone past the bottom of the tree. This is the base case. It returns -1 immediately. This is the same as person A in the queue saying "zero" - it stops the chain and sends an answer back.
height(null) - base case, returns -1
height(1) - waiting
height(3) - waiting
height(5) - waiting

Phase 2 - Back at node 1. Its left child has answered (-1). Now it needs to check its right child before it can compute its own result.

Step 5: height(null) is finished and removed from the stack. Control returns to height(1), which was waiting for its left answer. It now knows its left result is -1. But it still needs to ask its right child, so it calls height on node 1's right child, which is also null.
height(1) - left answered -1, now calls right child (null)
height(3) - waiting
height(5) - waiting
Step 6: height(null) again. Same as before - no node here, base case, returns -1 immediately.
height(null) - base case, returns -1
height(1) - waiting
height(3) - waiting
height(5) - waiting
Step 7: height(null) is removed from the stack. Control returns to height(1) again. This time, height(1) has heard back from both its children:
Left answered: -1.
Right answered: -1.
It can now compute its own height: 1 + max(-1, -1) = 1 + (-1) = 0.
Node 1 has a height of 0 (it is a leaf, so the longest path below it has zero edges). It returns 0 and is removed from the stack.
height(1) - returns 0
height(3) - waiting
height(5) - waiting

Phase 3 - Back at node 3. Its left subtree is fullyy explored and returned 0. Now it needs to check its right child.

Step 8: height(1) is gone. Control returns to height(3), which has been waiting this whole time. It now knows its left child (node 1) has a height of 0. But before it can compute its own height, it still needs to hear from its right child. Node 3 has no right child, so it calls height(null).
height(3) - left answered 0, now calls right child (null)
height(5) - waiting
Step 9: height(null) - no node here. Base case. Returns -1.
height(null) - base case, returns -1
height(3) - waiting
height(5) - waiting
Step 10: height(null) is removed. Control returns to height(3). It now has both answers:
Left answered: 0 (that was the height of node 1).
Right answered: -1 (there was no right child).
It computes its own height: 1 + max(0, -1) = 1 + 0 = 1.
Node 3 has a height of 1 (the longest path below it goes one edge down to node 1). It returns 1 and is removed from the stack.
height(3) - returns 1
height(5) - waiting

Phase 4 - Back at node 5 (the root). Its entire left subtree has been explored and returned a height of 1. Now it needs to explore the right subtree.

Step 11: height(3) is gone. Control returns to height(5), the very first call we made. It has been waiting since step 1. It now knows the left subtree has a height of 1. But it cannot compute its own answer yet - it still needs to explore the right side. So it calls height(7).
height(5) - left answered 1, now calls right child (node 7)
Step 12: height(7) starts running. Node 7 is a leaf (no children), but just like node 1 earlier, the code does not know that yet. It tries its left child first, which is null.
height(7) - calls left child (null)
height(5) - waiting
Step 13: height(null) - no node here. Base case. Returns -1. This is the exact same thing that happened with node 1's children.
height(null) - base case, returns -1
height(7) - waiting
height(5) - waiting

Phase 5 - Back at node 7. Its left child answered -1. Now it checks its right child.

Step 14: height(null) is removed. Control returns to height(7). It knows its left result is -1. Now it calls height on its right child, which is also null.
height(7) - left answered -1, now calls right child (null)
height(5) - waiting
Step 15: height(null) - no node here. Base case. Returns -1.
height(null) - base case, returns -1
height(7) - waiting
height(5) - waiting
Step 16: height(null) is removed. Control returns to height(7). It now has both answers:
Left answered: -1.
Right answered: -1.
It computes its own height: 1 + max(-1, -1) = 1 + (-1) = 0.
Node 7 has a height of 0 (it is a leaf, just like node 1). It returns 0 and is removed from the stack.
height(7) - returns 0
height(5) - waiting

Phase 6 - Back at node 5 (the root) for the last time. Both subtrees are fully explored.

Step 17: height(7) is gone. Control returns to height(5). It now has both answers:
Left answered: 1 (the left subtree, rooted at node 3, had a height of 1).
Right answered: 0 (the right subtree, rooted at node 7, had a height of 0).
It computes its own height: 1 + max(1, 0) = 1 + 1 = 2.
The root has a height of 2. It returns 2 and is removed from the stack.
height(5) - returns 2
Stack is empty. The tree has a height of 2. The longest path from the root to any leaf is 5 -> 3 -> 1, which has 2 edges.
The pattern: left side is fully explored before the right side even starts. Each node waits for both children before it can compute its own answer. The deepest nodes finish first. The root finishes last.

6. Passing Values Down vs. Returning Values Up

Height returned values upward. Now let's pass values downward.

Some problems need to carry information down through the calls. Instead of building an answer on the way back up, you modify a value on the way down and check it when you reach a leaf.

Example: hasPathSum

Given a tree and a target sum, return true if any root-to-leaf path adds up to the target.

def has_path_sum(root, target_sum):
    if root is None:          # base case: no node here
        return False
    target_sum -= root.val      # subtract this node's value
    if root.left is None and root.right is None:  # leaf?
        return target_sum == 0    # did we hit exactly zero?
    return (has_path_sum(root.left, target_sum) or
            has_path_sum(root.right, target_sum))
bool hasPathSum(TreeNode* root, int targetSum) {
    if (root == nullptr)     // base case: no node here
        return false;
    targetSum -= root->val;    // subtract this node's value
    if (!root->left && !root->right)  // leaf?
        return targetSum == 0;   // did we hit exactly zero?
    return hasPathSum(root->left, targetSum) ||
           hasPathSum(root->right, targetSum);
}
bool HasPathSum(TreeNode root, int targetSum) {
    if (root == null)        // base case: no node here
        return false;
    targetSum -= root.Val;     // subtract this node's value
    if (root.Left == null && root.Right == null)  // leaf?
        return targetSum == 0;   // did we hit exactly zero?
    return HasPathSum(root.Left, targetSum) ||
           HasPathSum(root.Right, targetSum);
}
boolean hasPathSum(TreeNode root, int targetSum) {
    if (root == null)        // base case: no node here
        return false;
    targetSum -= root.val;     // subtract this node's value
    if (root.left == null && root.right == null)  // leaf?
        return targetSum == 0;   // did we hit exactly zero?
    return hasPathSum(root.left, targetSum) ||
           hasPathSum(root.right, targetSum);
}

Read this as: "Subtract my value from the remaining target. If I am a leaf and the target is now zero, we found a valid path. If not, ask my left child or my right child if they can finish the job."

What does the || (or) do here?

The || operator means: try the left side first. If it returns true, stop immediately and return true (no need to check the right side). If the left side returns false, then try the right side.

So the function explores the left subtree first. Only if no valid path exists on the left does it try the right subtree.

Let's trace this on the same tree with targetSum = 8

    5
   / \
  3   7
 /
1

Phase 1 - Starting at the root (node 5). We go left first, just like the height example.

Step 1: We call hasPathSum(5, 8). The function subtracts this node's value from the target: 8 - 5 = 3. So the remaining target is 3. Node 5 is not a leaf (it has children), so we cannot check the answer yet. The function tries its left child first by calling hasPathSum(3, 3), and pauses.
hasPathSum(5, 8) - remaining 3, calls left child (node 3)
Step 2: hasPathSum(3, 3) starts running. It subtracts node 3's value: 3 - 3 = 0. Remaining target is now 0. But node 3 is not a leaf either - it has a left child (node 1). So we cannot check the answer yet. It calls hasPathSum(1, 0) and pauses.
hasPathSum(3, 3) - remaining 0, calls left child (node 1)
hasPathSum(5, 8) - waiting
Step 3: hasPathSum(1, 0) starts running. It subtracts node 1's value: 0 - 1 = -1. Remaining target is -1. Node 1 IS a leaf (no children). So now we check: is the remaining target zero? No, -1 is not 0. This path (5 -> 3 -> 1) does not sum to 8. The function returns false.
hasPathSum(1, 0) - leaf, remaining -1, returns false
hasPathSum(3, 3) - waiting
hasPathSum(5, 8) - waiting

Phase 2 - Back at node 3. The left child returned false. Because of the || operator, the function now tries the right child to see if there is a valid path on that side.

Step 4: hasPathSum(1, 0) is finished and removed from the stack. Control returns to hasPathSum(3, 3). The left side returned false, so the || operator says "left did not work, try the right side". Node 3 has no right child, so it calls hasPathSum(null, 0).
hasPathSum(3, 3) - left was false, calls right child (null)
hasPathSum(5, 8) - waiting
Step 5: hasPathSum(null, 0) runs. There is no node here. The function returns false immediately (you cannot have a valid path through a node that does not exist).
hasPathSum(null) - no node, returns false
hasPathSum(3, 3) - waiting
hasPathSum(5, 8) - waiting
Step 6: hasPathSum(null) is removed. Control returns to hasPathSum(3, 3). It has now heard from both children: left returned false, right returned false. Neither side found a path that works. hasPathSum(3, 3) returns false and is removed from the stack.
hasPathSum(3, 3) - both sides false, returns false
hasPathSum(5, 8) - waiting

Phase 3 - Back at node 5 (the root). The entire left subtree returned false. The || operator now tries the right subtree.

Step 7: hasPathSum(3, 3) is gone. Control returns to hasPathSum(5, 8). The left subtree returned false, so the || says "try the right side". It calls hasPathSum(7, 3) on node 5's right child.
hasPathSum(5, 8) - left was false, calls right child (node 7)
Step 8: hasPathSum(7, 3) starts running. It subtracts node 7's value: 3 - 7 = -4. Remaining target is -4. Node 7 IS a leaf (no children). Is -4 equal to zero? No. This path (5 -> 7) does not sum to 8 either. Returns false.
hasPathSum(7, 3) - leaf, remaining -4, returns false
hasPathSum(5, 8) - waiting

Phase 4 - Back at node 5 for the last time. Both subtrees have been fullyy explored.

Step 9: hasPathSum(7, 3) is gone. Control returns to hasPathSum(5, 8). It has now heard from both sides: left returned false, right returned false. No path in this tree sums to 8. hasPathSum(5, 8) returns false and is removed from the stack.
hasPathSum(5, 8) - both sides false, returns false
Stack is empty. The answer is false - no root-to-leaf path in this tree sums to 8.

Now let's try with targetSum = 9. The path 5 -> 3 -> 1 adds up to 9, so we expect the function to find it.

Phase 1 - Same as before, we go left from the root.

Step 1: We call hasPathSum(5, 9). Subtracts 5 from the target: 9 - 5 = 4. Remaining target is 4. Node 5 is not a leaf, so it calls its left child: hasPathSum(3, 4).
hasPathSum(5, 9) - remaining 4, calls left child (node 3)
Step 2: hasPathSum(3, 4) starts running. Subtracts 3: 4 - 3 = 1. Remaining target is 1. Node 3 is not a leaf, so it calls its left child: hasPathSum(1, 1).
hasPathSum(3, 4) - remaining 1, calls left child (node 1)
hasPathSum(5, 9) - waiting
Step 3: hasPathSum(1, 1) starts running. Subtracts 1: 1 - 1 = 0. Remaining target is 0. Node 1 IS a leaf. Is the remaining target zero? Yes! We have found a valid path: 5 -> 3 -> 1 sums to exactly 9. The function returns true.
hasPathSum(1, 1) - leaf, remaining 0, returns true
hasPathSum(3, 4) - waiting
hasPathSum(5, 9) - waiting

Phase 2 - A true has been found! The || operator short-circuits: since the left side returned true, there is no need to check the right side at any level. The true travels straight back up the stack.

Step 4: hasPathSum(1, 1) is removed. Control returns to hasPathSum(3, 4). The left side returned true. Because of ||, the function does not bother calling its right child at all - if one side found a valid path, that is good enough. hasPathSum(3, 4) returns true immediately.
hasPathSum(3, 4) - left was true, short-circuits, returns true
hasPathSum(5, 9) - waiting
Step 5: hasPathSum(3, 4) is removed. Control returns to hasPathSum(5, 9). Same thing - the left side returned true, so || short-circuits again. No need to explore the right subtree (node 7) at all. hasPathSum(5, 9) returns true.
hasPathSum(5, 9) - left was true, short-circuits, returns true
Stack is empty. The answer is true - the path 5 -> 3 -> 1 sums to 9. Notice that the right side of the tree (node 7) was never even visited. The || operator saved us from exploring it because the left side already found a valid path.
Short-circuit: once || finds a true on the left side, it does not bother checking the right. The true bubbles all the way back to the root without exploring any further.

Recursion Summary

Base case stops the recursion. Without it, calls go forever and the program crashes (stack overflow).
Each call gets its own copy of the parameters. When height(3) and height(7) run, they each have their own root. They do not interfere with each other.
The stack grows going down, shrinks coming back up. The first call made is the last to finish. The deepest call finishes first.
Return values flow upward. Each call passes its result to the call that created it. This is how the answer builds up.
With trees, left is explored fully before right begins. Each node waits for both children to finish before computing its own result.
|| (or) short-circuits: if the left call returns true, the right call is skipped entirelyy.