Stacks and Recursion
Stacks and Recursion
Dr Fatimah Adamu-Fika
Faculty of Computing
Kaduna, Nigeria
© 2020-2022
CSC 204: FUNDAMENTALS OF DATA STRUCTURES
TABLE OF CONTENTS
Overview ................................................................................................................................................... 3
Summary ................................................................................................................................................. 17
What Comes Later ............................................................................................................................................ 19
Exercises.................................................................................................................................................. 19
OVERVIEW
Before we study more complex structures, we will briefly discuss the close relationship between stacks
and recursion. Recursion involves operations (or functions) that call themselves . A recursive function
is an operation that either calls itself directly or calls other operations that call it.
• Every recursive function (or group of mutually recursive functions) can be rewritten without
recursion using a stack.
• Every algorithm that uses a stack can be written without a stack using one or more recursive
functions.
Relatedly, when an algorithm would push data on a stack, a recursive function can preserve the same
data in local variables and then call itself to continue processing. The recursion returns just when the
data is going to be pop off the stack, so processing can continue with the data in the local variables just
as it would have been if the data had been popped off the stack.
Recursion and stacks are equivalent is some sense. Some algorithms are easier written as recursive,
and others are easier using stacks. There are some that are just as easy or as hard whether we use
recursion or stacks.
In the remainder of the unit, we will consider the structure of a recursive algorithm, the stack frame
and a few problems that can be solved either by recursion or using stacks.
RECURSIVE ALGORITHM
The idea of recursion is to represent a problem in one or more smaller subproblems and add one or
more base cases that stop the recursion. Hence, the final solution is an aggregate of solutions to these
sub-problems.
Example 1: Consider the problem of determining the 𝑛𝑡ℎ power of 2, i.e., 2𝑛 where 𝑛 is a natural
number. The most natural approach to solving this is by multiplying 2 by itself 𝑛 number of times. So,
the solution will look something as follows:
The subscript denotes the number of times 2 is self-multiplying. For example, when 𝑛 = 10, 2𝑛
means 2 is at the 10𝑡ℎ self-multiplication, 2(𝑛−1) means 2 is at the 9𝑡ℎ self-multiplication and 2(𝑛−2)
means 2 is currently at the 8𝑡ℎ self-multiplication and so forth. When 𝑛 = 1, the result will simply be
2 because 2 does not need to multiply by itself. There is a special case when dealing with exponents,
the rule of raising to the 𝑧𝑒𝑟𝑜𝑡ℎ power: a number raised to power of 0 is simply 1, so 20 = 1.
Example 2: We can use recursion to solve the power of 2 problem. The base cases of this problem
would be when 𝑛 = 0, 𝒓𝒆𝒕𝒖𝒓𝒏 1 and when 𝑛 = 1, 𝒓𝒆𝒕𝒖𝒓𝒏 2, i.e., 20 = 1 and 21 = 2. When the
base cases are correctly defined, the recursion will end when either of the base conditions is met. For
larger values of 𝑛, we will keep breaking the problem into smaller subproblems until a base case is
reached.
𝒓𝒆𝒕𝒖𝒓𝒏 𝑝𝑜𝑤𝑒𝑟2( 𝑛 – 1 ) ∗ 2;
}
If the base condition is not reached or defined, then the stack overflow problem may arise.
Example 3: In our example above, we can use only the base case when 𝑛 = 0 and still get the desired
output. Assuming we decided to do so and incorrectly defined our base case as when 𝑛 = 10 instead
of 𝑛 = 0, and as intended we went ahead and omitted the base case condition for when 𝑛 = 1.
𝒓𝒆𝒕𝒖𝒓𝒏 𝑝𝑜𝑤𝑒𝑟2( 𝑛 – 1 ) ∗ 2;
}
If we call 𝑝𝑜𝑤𝑒𝑟2(7), it will call 𝑝𝑜𝑤𝑒𝑟2(6), 𝑝𝑜𝑤𝑒𝑟2(5), 𝑝𝑜𝑤𝑒𝑟2(4) and so forth, but 𝑛 will never
reduce to or converge at 10. Hence, the base condition will never be met, and if the memory gets
exhausted by these function calls to the stack frame, it will trigger a stack overflow error.
A function, 𝒓𝒆𝒄𝒖𝒓𝑭𝒖𝒏𝒄, is directly recursive if it calls the same function 𝒓𝒆𝒄𝒖𝒓𝑭𝒖𝒏𝒄. A function
𝒓𝒆𝒄𝒖𝒓𝑭𝒖𝒏𝒄 is indirectly recursive if it calls another function 𝒂𝒏𝒐𝒕𝒉𝒆𝒓𝑹𝒆𝒄𝒖𝒓𝑭𝒖𝒏𝒄 and
𝒂𝒏𝒐𝒕𝒉𝒆𝒓𝑹𝒆𝒄𝒖𝒓𝑭𝒖𝒏𝒄 directly or indirectly calls 𝒓𝒆𝒄𝒖𝒓𝑭𝒖𝒏𝒄.
𝒗𝒐𝒊𝒅 𝑑𝑖𝑟𝑒𝑐𝑡𝑅𝑒𝑐𝑢𝑟𝐹𝑢𝑛𝑐( )
{
// 𝑠𝑜𝑚𝑒 𝑐𝑜𝑑𝑒 …
𝑑𝑖𝑟𝑒𝑐𝑡𝑅𝑒𝑐𝑢𝑟𝐹𝑢𝑛𝑐()
// 𝑠𝑜𝑚𝑒 𝑐𝑜𝑑𝑒 …
}
𝑖𝑛𝑑𝑖𝑟𝑒𝑐𝑡𝑅𝑒𝑐𝑢𝑟𝐹𝑢𝑛𝑐2()
// 𝑠𝑜𝑚𝑒 𝑐𝑜𝑑𝑒 …
}
𝒗𝒐𝒊𝒅 𝑖𝑛𝑑𝑖𝑟𝑒𝑐𝑡𝑅𝑒𝑐𝑢𝑟𝐹𝑢𝑛𝑐2( )
{
// 𝑠𝑜𝑚𝑒 𝑐𝑜𝑑𝑒 …
𝑖𝑛𝑑𝑖𝑟𝑒𝑐𝑡𝑅𝑒𝑐𝑢𝑟𝐹𝑢𝑛𝑐1()
// 𝑠𝑜𝑚𝑒 𝑐𝑜𝑑𝑒 …
}
A call stack (or simply “the stack”) is one of the segments of application memory and it is a stack data
structure that stores information pertaining the active subroutines of a program. The stack is also
known as execution stack, program stack, control stack, run-time stack or machine stack. A call stack is
composed of stack frames.
A lot of programming languages support recursion even though it is not supported by computers on
the machine level. To achieve recursion at runtime, compiled programs use the stack to store
information about the current state of the execution of a subroutine. This information is called a stack
frame or an activation record (or activation frame) and the data it contains include the subroutine’s
arguments, local variables and return address (point where the subroutine will get back to when it is
done processing, which is the position where it left from). When a subroutine or function is called, a
new activation record is generated and pushed onto the stack. The activation record remains on the
stack until when the subroutine stops executing (that is when the base condition of that subroutine is
met) and goes back to its return address. The activation record is then popped off the stack and the
thread of execution resumes at the return address. Because every call to a subroutine pushes a new
activation record on the stack, every recursive call of a subroutine has its own stack frame, so data
associated with a particular subroutine is not confused with that of another. This mechanism supports
the unwinding of recursive calls onto the stack, thereby making it possible for the non-recursive
machine implement recursion.
The size of the stack frame is fixed for a specific subroutine. In order for a programming language to
use a stack frame, it maintains a thread with two pointers: the base pointer, BP, (or frame pointer, FP)
and the stack pointer, SP. The frame pointer always points to the top of the frame (i.e., it stores the
address of the stack frame), while the stack pointer points to the top of the stack. The thread also
maintains a program counter, PC, which points to the next instruction to be executed.
Whenever a function call occurs, the following steps takes place in roughly the same order. The exact
order of the steps is dependent on the Application Binary Interface of the system architecture and
platform.
1. The caller saves its local variables and temporary data, by pushing them onto the call stack.
2. The caller pushes the called subroutine (callee)’s arguments (actual parameters) onto the
stack.
3. The caller branches to the callee, pushes the program counter (PC) onto the stack. The PC is
saved as a return address in the stack.
4. The callee pushes the value of the frame pointer (FP) onto the stack.
5. The callee copies the stack pointer (SP) to FP.
6. The callee adjusts SP, by creating storage locations for its variables and temporaries in the
stack.
Steps 4 – 6 above, form the function prologue, since they are the beginning of every function call.
saved temporaries
saved FP
local variables
saved temporaries
saved FP FP
local variables SP
Upon exit from a function, the steps above are performed in reverse order.
1. The callee restores SP, which destroys the storage locations reserved for local variables and
temporaries.
2. The callee restores FP, which returns processing to the previous frame.
3. The callee branches back to the caller by popping PC off the stack.
4. The caller removes the callee’s actual parameters from the stack.
5. The caller restores its local variables and temporaries, by popping them off the stack.
Steps 1 – 3 above, form the function epilogue, since they are at the end of every function.
Figure 1 shows the content of the stack with two frames (a function that has called another function).
Red arrows indicate pointers.
The stack may grow to fill its currently allocated space. If this happens and there is available contiguous
memory space, the operating system may be able to allocate additional chunks of contiguous memory
space. However, if too many subroutine calls occur (possibly due to an infinite loop or a recursive calling
that lasts too long) the stack may not be able to continue to expand. In this case, the operating system
will terminate the process with a stack overflow error.
Now, we will consider a couple of real-life computational problem that could be solved using either
recursion or stacks: checking balanced brackets (i.e., the well-formedness of sequence of brackets) and
expression evaluation.
Checking for balanced brackets is one of the most important tasks of a compiler. Programming
languages are precise. Brackets are part of the syntax of the language. If we miss out any elements of
the language’s syntax elements, the statements become unintelligible to the compiler. For example, in
Java and C, curly brackets {} help the compiler determine set of codes that form a block, e.g., the
function body, the opened curly tells the compiler this is the start of the body of the function, and it
expects to find a closed curly to tell it where function ends. Also, brackets () tells the compiler the order
of precedence of operator evaluation in an arithmetic expression. They are also useful in function
definitions because they help the compile detect the arguments that are passed to the function.
The arithmetic expression we use daily is an infix expression. Infix expressions are readable and solvable
to us because we can easily discern the order of precedence of operators and use brackets to evaluate
the sub-expression within them first when evaluating the expressions. However, computers have
problem understanding this format because they have difficulty differentiating the precedence of the
operators and brackets easily. To evaluate arithmetic expressions, compilers transform the infix
expression into a format that is easier for the computer to understand and evaluate.
Now, we will proceed to illustrate the similarity of stacks and recursion by looking at how to use both
to tackle each of these tasks, in turn.
BALANCED BRACKETS
A bracket is considered to be any one of the following characters: (, ), {, }, [, or ]. Two brackets are
considered to be a matching pair if the opening bracket (i.e., (, {, or [) occurs to the left of the closing
bracket (i.e., ), }, or ]) of the exact type. Thus, there are three kinds of matched pairs of brackets: ( ),
{ }, [ ].
A pair of brackets is not balanced if the set of brackets it encloses are not matched. For example,
( { [ } ] ) is not balanced because the brackets in between { } are not balanced, the pair of curly
brackets encloses a single unbalanced opening square bracket. Similarly, the contents between [ ] are
not balanced, the pair of square brackets encloses a single closing curly bracket.
By this logic, we say a sequence of brackets is balanced if the following conditions are met:
From the first point, it is implicit that an empty string is balanced, so also is a non-empty string that
does not contain a bracket character.
THE PROBLEM
Given a string, based on the conditions stated above determine whether the sequence of brackets
within is balanced. If balanced, return true, return false otherwise.
RECURSIVE SOLUTION
Following is a recursive algorithm that checks the well-formedness of a sequences of brackets. The
algorithm assumes all characters in the sequences are valid bracket characters including empty
characters.
STACK-BASED SOLUTION
The same task can be solved just as well with a non-recursive algorithm using stacks.
EXPRESSIONS
Before we delve into the problem of expression evaluation, we need to know that there are three
different but equivalent notations of writing expressions: infix, prefix and postfix. In this section, we will
first illustrate their differences and likenesses by looking at some examples. Afterwards we will look at
how to evaluate them using recursion and stacks.
INFIX EXPRESSSION
In infix notation, operators are written between their operands. This is the common way we write
expressions. Expressions written using the infix notation are called infix expressions. Hence, an infix
expression is an expression in which the operators appear between their operands.
Infix notation needs extra information to make the order of evaluation of the operators clear: rules built
into the language about operator precedence and associativity, and brackets ( ) to allow users to
override these rules. For example, the usual rules for associativity say that we perform operations from
left to right, so the multiplication by 𝑦 is assumed to come before the division by 𝑧. Similarly, the usual
rules for precedence say that we perform multiplication and division before we perform addition and
subtraction. Additionally, we evaluate sub-expressions within brackets before evaluating those outside
brackets, so the addition of 𝑤 and 𝑥 will be done first.
PREFIX EXPRESSION
In Prefix notation, also called Polish notation, the operators are written before the operands.
Expressions that are written in prefix notation are called prefix expressions. Hence, a prefix expression
is an expression in which the operators appear before their operands.
/∗ +𝑤𝑥𝑦𝑧
The order of evaluation of operators is always left-to-right and brackets cannot be used to change the
order. Operators act on the two nearest values to the right of them. However, if these values involve
computation, then this changes the order the operators have to be evaluated in. In Example 6 above,
although the division is the first operator on the left, it acts on the result of the multiplication, so the
multiplication must be done before the division. And similarly, the multiplication acts on the result of
the addition, so the addition needs to happen before the multiplication. Following we have added
brackets to the expression to help you understand the order of evaluation and explanation better.
(/(∗(+𝑤𝑥)𝑦)𝑧)
POSTFIX EXPRESSION
In a postfix notation, also known as Reverse Polish notation, the operators are written after the
operands. An expression written in postfix notation is called a postfix expression. Hence, a postfix
expression is an expression in which the operators appear after their operands.
𝑤𝑥 + 𝑦 ∗ 𝑧/
Similar to prefix, operators are always evaluated from left-to-right and brackets are redundant.
Operators act on the two nearest value to the left of them. Because postfix operators use values to the
left of them, any values require for computation will already have been calculated as we go from left
to right. Hence, the order of evaluation of operators is not disrupted in the way as in prefix expressions.
In Example 7 above, the addition operation is to the left of the multiplication, so the addition must be
evaluated before the multiplication is performed. Similarly, the multiplication operation must occur
before the division operation. Again, we add brackets to make the order of evaluation of operators
clearer to you.
EQUIVALENT EXPRESSIONS
In all the three notations, the operands occur in the same order, and only the operators are moved to
keep the meaning correct. This is particularly important for asymmetric operators like subtraction and
division: 𝑋– 𝑌 is not equivalent to 𝑌– 𝑋 ; the former is the same as 𝑋𝑌– or – 𝑋𝑌 , the latter
as 𝑌𝑋– or – 𝑌𝑋. In every expression, the number of operands is always one more than the number of
operators.
One expression can be converted into another. The most straightforward and intuitive way to do this
is to start by inserting all the implicit brackets that show the order of evaluations. We can easily convert
between these bracketed forms by simply moving the operators within the brackets. Repeat until all
brackets are processed, then remove any unnecessary brackets.
• As this is an infix expression, we follow the evaluation rules, we are going to start with the sub-
expressions within the inner brackets going from left-to-right.
• In sub-expression, ( 𝑊 ∗ 𝑋 ), move the multiplication operator to the left of the operands,
i.e., ( 𝑊 𝑋 ∗ ).
• In the sub-expression, (𝑌 / 𝑍), move the division to the left of the operands. i.e., ( 𝑌 𝑍 / ).
• Next, move the addition operator, to the left of the sub-expressions, i.e., ( ( 𝑊 𝑋 ∗
) ( 𝑌 𝑍 / ) + ).
• Finally remove the brackets, 𝑊 𝑋 ∗ 𝑌 𝑍 / + .
Example 9: Similarly, to convert to prefix expression, we move the operator within the brackets to the
left of the operands. Let’s convert the following postfix expression to its prefix equivalent,
𝑊 𝑋 𝑌 + ∗ 𝑍 /.
• First, we are going to insert brackets to make clear the order of evaluation.
(( 𝑊 (𝑋𝑌 + ) ∗ )𝑍/)
• As we are going from left-to-right, we will convert the content of the brackets as we come
across them: We move the last division operator to the left of its operands, i.e.,
( / ( 𝑊 ( 𝑋 𝑌 + ) ∗ ) 𝑍 ).
• Next, we move the multiplication operator to the left, ( / ( ∗ 𝑊 ( 𝑋 𝑌 + ) ) 𝑍 ).
• Then we move the addition operation, ( / ( ∗ 𝑊 (+ 𝑋 𝑌 ) ) 𝑍 ).
• Finally, we remove all the brackets: / ∗ 𝑊 + 𝑋 𝑌 𝑍.
Example 10: Similarly, to convert to infix expression, we need to move the operator within the brackets
between the operands. Let’s convert the following to its infix equivalent: ∗ 𝑊 + 𝑋 / 𝑌 𝑍.
Before we move to expression evaluation, let’s look at a more systematic way of doing these
conversions. As mentioned earlier, a computer evaluates an infix expression by first transforming it into
either a prefix expression or a postfix expression. Now, we will consider how to give a step-by-step
instruct to a computer on how to convert between these notations. One possible way of achieving this
is by using stacks.
INFIX TO PREFIX
We can transform an infix expression into a prefix, using a stack, by following these steps:
1. Reverse Traverse the infix expression, i.e., read it from right to left, character by character,
determine whether or not the current character is an operand, an operator, or an opening or
a closing bracket.
2. If the character is an operand, append it to an output string. Operands always appear in the
same order in the prefix expression as they do in the infix expression.
3. If the character is a closing bracket, push it onto the stack. This marks the beginning of an
expression that should be evaluated separately.
4. If the character is an opening bracket, until a closing bracket is encountered at the top of the
stack, pop each operator from the stack and append to the output. This marks the end of the
operands and operators located within the current set of brackets.
5. If the character is an operator and it has greater precedence than the operator on the top of
the stack, push the character onto the stack.
6. If the character is an operator and it has lower precedence than the operator on the top of the
stack, until the operator on top of the stack has lower or equal precedence than the character,
or until the stack is empty, pop each operator from the stack and append them to the output.
Then push the current character onto the stack.
7. If the character is an operator and it has the same precedence as the operator on the top of
the stack, and the operator on top of the stack is left-to-right associative, push the character
onto the stack.
8. If the character is an operator and it has the same precedence as the operator on the top of
the stack, but the operator on top of the stack is right-to-left associative, until the operator on
top of the stack has lower or equal precedence than the character and is left-to-right
associative, or until the stack is empty, pop each operator from the stack and append them to
the output. Then push the current character onto the stack.
9. Once all of the infix characters have been processed, pop any remaining operators from the
stack, one at a time, and append each of them to the output string.
10. Finally, reverse the order of the output string to get the prefix expression.
INFIX TO POSTFIX
Following are the steps to convert infix to postfix using a stack.
1. Traverse from left to right, one character at a time, determine whether or not the current
character is an operand, an operator, or an opening or closing brackets.
2. If the character is an operand, append it to the postfix expression. Operands always appear in
the same order in the postfix expression as they do in the infix expression.
3. If the character is an opening bracket, push it onto the stack. This marks the beginning of an
expression that should be evaluated separately.
4. If the character is a closing bracket, until an opening parenthesis is encountered at the top of
the stack, pop each operator from the stack and append to the postfix expression. This marks
the end of the operands and operators located within the current set of brackets.
5. If the character is an operator and it has greater precedence than the operator on the top of
the stack, push the character onto the stack.
6. If the current character is an operator and it has lower precedence than the operator on the
top of the stack, until the operator on top of the stack has lower precedence than the
character, or until the stack is empty, pop each operator from the stack and append them to
the postfix expression. Then push the current character onto the stack.
7. If the character is an operator and it has the same precedence as the operator on the top of
the stack, and the operator on top of the stack is left-to-right associative, until the operator on
top of the stack has lower precedence than the character, or until the stack is empty, pop each
operator from the stack and append them to the postfix expression. Then push the current
character onto the stack.
8. If the character is an operator and it has the same precedence as the operator on the top of
the stack, but the operator on top of the stack is right-to-left associative, push the current
character to the stack.
9. Once all of the infix characters have been evaluated, pop any remaining operators from the
stack, one at a time, and append each of them to the postfix expression.
PREFIX TO INFIX
We convert a prefix expression into an infix expression using a stack as follows:
1. Reverse Traverse the prefix expression one character at a time, from right to left, i.e., read the
characters starting from the end of the expression.
2. If the current character is an operand, then push it onto the stack.
3. If the current character is an operator, then pop two operands from the stack.
4. Create a string by concatenating the two operands with the operator in between them, in the
format: string = ‘(‘ + operand1 + operator + operand2 + ‘)’. Note: to maintain the precedence
of evaluating operands that are sub-expression first, we enclose each one we come across
within brackets.
5. Push the resultant string in Step 4 back onto the stack
6. Repeat the Steps 1-5 until all characters in the prefix expression have been processed, at which
point the last element remaining in the stack becomes the desired infix expression.
PREFIX TO POSTFIX
Following are the steps to change a prefix expression into a postfix expression using stacks.
1. Reverse Traverse the prefix expression one character at a time, from right to left, i.e., read the
characters starting from the end of the expression.
2. If the current character is an operand, then push it onto the stack.
3. If the current character is an operator, then pop two operands from the stack.
4. Create a string by concatenating the two operands and add the operator after them, in the
format: string = operand1 + operand2 + operator.
5. Push the resultant string in Step 4 back onto the stack
6. Repeat the Steps 1-5 until all characters in the prefix expression have been processed, at which
point the last element remaining in the stack becomes the desired postfix expression.
POSTFIX TO INFIX
We transform a postfix expression into a prefix one by following these steps:
POSTFIX TO PREFIX
The following steps converts a postfix into prefix using a stack.
1. Traverse the postfix expression, one character at a time from left to right.
2. If the current character is an operand, then push it onto the stack
3. If the current character is an operator, then pop two operands from the stack.
4. Create a string by concatenating the two operands and add operator before them, in the
format: string = operator + operand1 + operand2.
5. Push the resultant string in Step 4 back onto the stack
6. Repeat the Steps 1-5 until all characters in the postfix expression have been processed, at which
point the last element remaining in the stack becomes the desired prefix expression.
EXPRESSION EVALUATION
Now that we have understood what expressions are, how they are formatted and how to transform
one format into another, let’s consider the problem of how to evaluate them. Since a computer will
transform an infix expression before solving it, we will study the problem of evaluating prefix and postfix
expressions specifically.
EVALUATING PREFIX
1. Reverse Traverse the prefix expression one character at a time, from right to left, i.e., read the
characters starting from the end of the expression.
2. If the current character is an operand, push it on top of the stack.
3. Otherwise, if the character is an operator, pop the top element from the stack to form the
operator's left operand, and then pop the next top element from the stack to form the
operator's right operand.
4. Solve the expression formed (in Step 3) by the operator and its operands and push the result
on top of the stack.
5. Repeat Steps 1 – 4, until the characters in the expression have been processed, at which point
the last element remaining in the stack becomes the result.
EVALUATING POSTFIX
1. Reverse Traverse the postfix expression one character at a time, from right to left, i.e., read the
characters starting from the end of the expression.
2. If the current character is an operand, return the numeric value of the character. This is the
base case of the recursion.
3. Otherwise, if the current character is an operator, get the right operand recursively, i.e.,
recursively calling the algorithm, until an operand is returned, and then get the left operand
recursively.
4. Return the solution of the sub-expression formed in Step 3 by the operator and the two
operands.
A recursive function is tail-recursive when the recursive call is the last thing executed by the function.
Hence, a tail recursive algorithm is a recursive algorithm with at most one recursive call as the last step
in every execution of its body.
Tail recursive functions can be optimised by the modern compilers, thus making them better than non-
tail recursive functions. Since compilers usually use stacks to execute recursive subroutines, the stack
depth (maximum amount of stack space used at any time during compilation) for a non-tail recursive
function is more. Since the recursive call is the last statement of a tail-recursive, then there is nothing
left do to in the current function. The compiler takes advantage of this notion by not saving the current
function’s stack frame.
Example 11: Consider the function to compute the 𝑛𝑡ℎ power of 2, i.e., 2𝑛 in Example 1. It may look
like a tail recursive function, but It is actually NOT! The value returned by 𝑝𝑜𝑤𝑒𝑟2( 𝑛 – 1) is actually
used as an operand in a computation in 𝑝𝑜𝑤𝑒𝑟2(𝑛). Thus, 𝑝𝑜𝑤𝑒𝑟2(𝑛 − 1) is not the last statement
executed in 𝑝𝑜𝑤𝑒𝑟2(𝑛). However, we can rewrite the function as a tail recursive function. The notion
is to use an additional argument, where we accumulate the power of 2 value in it. When 𝑛 reaches 0,
we then return the accumulated value.
𝒓𝒆𝒕𝒖𝒓𝒏 𝑝𝑜𝑤𝑒𝑟2( 𝑛, 1 );
}
To optimise a tail recursive function, the compiler basically does a tail call elimination by using the a
𝒄𝒐𝒏𝒕𝒓𝒐𝒍 construct, such as 𝒈𝒐𝒕𝒐. This is done in the following steps:
Java does not support the 𝒈𝒐𝒕𝒐 construct. To achieve step 4, we use the 𝒄𝒐𝒏𝒕𝒊𝒏𝒖𝒆 construct together
with a looping construct.
Example 12: The tail recursive program in Example 11 can be transformed to a non-recursive one as
follows.
𝒑𝒖𝒃𝒍𝒊𝒄 𝒊𝒏𝒕 𝑝𝑜𝑤𝑒𝑟2( 𝒊𝒏𝒕 𝑛, 𝒊𝒏𝒕 𝑝𝑤𝑟 )
{
// 𝒕𝒉𝒊𝒔 𝒊𝒔 𝒂 𝒏𝒐𝒏 𝒓𝒆𝒄𝒖𝒓𝒔𝒊𝒗𝒆 𝒇𝒖𝒏𝒄𝒕𝒊𝒐𝒏 𝒕𝒐 𝒄𝒂𝒍𝒄𝒖𝒍𝒂𝒕𝒆 𝟐𝒏 .
𝑠𝑡𝑎𝑟𝑡: 𝒅𝒐 {
𝒊𝒇 ( 𝑛 == 0 )
𝒓𝒆𝒕𝒖𝒓𝒏 𝑝𝑤𝑟;
𝑛 = 𝑛 − 1;
𝑝𝑤𝑟 = 2 ∗ 𝑝𝑤𝑟;
𝒄𝒐𝒏𝒕𝒊𝒏𝒖𝒆 𝑠𝑡𝑎𝑟𝑡;
} 𝒘𝒉𝒊𝒍𝒆 ( 𝒕𝒓𝒖𝒆 );
}
Recursion can always be removed from tail-recursive algorithms without using a stack.
Since with every function call, a new stack frame is pushed on to the call stack, if one frame requires
𝑂(1), i.e., constant memory space, then 𝑵 recursive calls, will need 𝑂(𝑵), i.e., linear memory space.
Tail call elimination improves the space complexity of recursion from 𝑂(𝑵) to 𝑂(1). Since the function
call is eliminated, no new frame is created, and the function is executed in constant memory space.
SUMMARY
• Algorithms that use recursion can always be substituted with algorithms that uses stack, and
vice versa. Hence, stack and recursion are in some sense equivalent.
• Some algorithms are easier written using stacks, while some are easier with recursions. The
nature of the problem determines which may be easier to use in solving it. As a programmer,
you should evaluate both alternatives when deciding how to solve a problem.
• A stack frame is memory management technique used in most programming languages, for
generating and removing temporary variables. Hence, it is a collection of all the information on
the stack pertaining to a particular function/subroutine call.
• Stack frames help programming languages in supporting recursive functionality for
subroutines.
• A stack frame is comprised of:
o Local variables
o Saved copies of registers modified by subroutines that could need restoration.
o Argument parameters
o Return address
• When a function is called, a new stack frame is created for it and pushed on to the stack.
• Stack frames only exists during the runtime process. When a function is done executing, its
corresponding stack frame is removed from the stack.
• The recursion solution of balanced brackets is inefficient compared to the stack solution. The
recursive solution seems to only be useful for recursion practice problem.
• Algorithms that only call themselves as the final step in every execution of their bodies are
called tail-recursive.
• Since in a tail recursive function, there are no statements after the recursive call statement. So,
preserving the state and stack frame of the parent function (caller) is not needed.
• Since no computation is done on the returned value and no statements are left to execute, the
current frame can be modified as per the requirements of the current function call. Thus, it is
possible for the function to be executed in constant memory space, making tail recursive
functions faster and memory efficient.
• Prefix, postfix, and infix expressions list their operands in the same order. The number of
operators in each is always one less than the number of operands.
• The main operator in each expression and sub-expression is easy to find:
o the main operator in an infix expression is the one inside the fewest number of
brackets.
o the main operator of a prefix expression is the first operator.
o the main operator of a postfix expression is the last operator.
• Advantages of prefix and postfix expressions over infix expressions:
o They are unambiguous, there are no precedence rules.
o Easy to parse (translate into free form) for evaluation.
o Supports operators with n-ary arguments with no additional syntax – does not need
brackets.
• The recursive algorithm to determine whether a string of brackets is balanced calls
itself at most once on each activation, but the recursive call is not the last step in the execution
of the body of the algorithm—there must be a check for the closing right bracket after the
recursive call. Hence this operation is not tail-recursive.
• The following table shows the order of precedence and associativity of some common
operators:
• In the next unit, we will take a look at a more complex structure, tree.
• In later unit, we will consider how we can convert expressions and evaluate them using trees.
EXERCISES
𝑓𝑢𝑛𝑐𝑡𝑖𝑜𝑛( 𝑛 / 2 );
𝑺𝒚𝒔𝒕𝒆𝒎. 𝒐𝒖𝒕. 𝒑𝒓𝒊𝒏𝒕𝒍𝒏( 𝑛 % 2 );
}
b. .
𝒄𝒍𝒂𝒔𝒔 𝑅𝑒𝑐𝑢𝑟𝑠𝑖𝑜𝑛 {
𝒔𝒕𝒂𝒕𝒊𝒄 𝒊𝒏𝒕 𝑓𝑢𝑛𝑐𝑡𝑖𝑜𝑛( 𝒊𝒏𝒕 𝑛, 𝒊𝒏𝒕 𝑎 )
{
𝒊𝒇 ( 𝑛 == 0 )
𝒓𝒆𝒕𝒖𝒓𝒏 𝑎;
𝒓𝒆𝒕𝒖𝒓𝒏 𝑓𝑢𝑛𝑐𝑡𝑖𝑜𝑛( 𝑛 – 1, 𝑛 ∗ 𝑎 );
}
/∗ 𝐴𝑠𝑠𝑢𝑚𝑒 𝑡ℎ𝑎𝑡 𝑛 𝑖𝑠 𝑔𝑟𝑒𝑎𝑡𝑒𝑟 𝑡ℎ𝑎𝑛 𝑜𝑟 𝑒𝑞𝑢𝑎𝑙 𝑡𝑜 0 ∗/
𝒔𝒕𝒂𝒕𝒊𝒄 𝒊𝒏𝒕 𝑓𝑢𝑛𝑐𝑡𝑖𝑜𝑛( 𝒊𝒏𝒕 𝑛 )
{
𝒓𝒆𝒕𝒖𝒓𝒏 𝑓𝑢𝑛𝑐𝑡𝑖𝑜𝑛( 𝑛, 1 );
}
}
5. Using the stack-based algorithm in this unit for evaluating expressions, solve the following
expression with pictorial aid:
a. 9 2 − 1 2 + ∗
b. 4 3 3 ∗ ∗ 5 3 ∗ + 6 +
c. / ∗ − 8 2 3 % + 4 5 8
d. + + ∗ 4 ^ 3 2 ∗ 5 3 6
6. Using the stack-based algorithms in this unit convert the following expression into their
equivalent expressions in the other two notations. For example, if an expression is an infix
expression then you are required to find its equivalent prefix and postfix expressions. Use
diagrams to illustrate each step in your solutions.
a. ( ( ( 𝑎 ∗ 𝑏 ) − 𝑐 ) ∗ ( 𝑑 / 𝑒 ) ) + 𝑓
b. 𝑎 𝑥 2 ^ ∗ 𝑏 𝑥 ∗ + 𝑐 +
c. 5 ∗ ( (3 + 7) / 4 ) − 3 ^ 3 ^ 4
d. 3 ∗ ( 2 + ( 5 ∗ ( 3 + 2 ) + 4 ) )
e. % + 𝑎 ∗ 𝑏 𝑐 − 𝑑 𝑒
f. − 𝑎 % 𝑏 𝑐 − / 𝑎 𝑑 𝑒
7. Refer to the recursive algorithm in 2 (a) above.
a. Unwind the recursion using stacks.
b. Using the steps of packing the stack frame in this unit, illustrate how the stack frame
will look during the last recursive call when 𝑛 = 3.
8. The main operator in each expression and sub-expression is easy to find. State how you can
easily identify the main operator in:
a. Infix expressions and sub-expressions.
b. Prefix expressions and sub-expressions.
c. Postfix expressions and sub-expressions.
9. What characteristics is common amongst infix, prefix and postfix expressions?
10. Which is easier:
a. Evaluating a prefix expression with a stack or using recursion?
b. Evaluating a postfix expression with a stack or using recursion?
11. Explain whether the recursive algorithm for determining if a sequence of brackets is balanced,
as discussed in this unit, is tail-recursive or not.
12. Evaluate the following expressions using recursion:
a. 4 3 2 ^ ∗ 5 3 ∗ + 6 +
b. 8 2 − 3 ∗ 45 + 8 % /
c. ∗ − 9 2 + 1 2
d. + + ∗ 4 ∗ 3 3 ∗ 5 3 6