However, it is (strongly) believed that
NP
and
coNP
are different. For
instance, no polynomial solution to the problem of factoring numbers was
found, and this problem is in both
NP
and
coNP
.
8.2
Recursion
The
three laws of recursion
are:
1. A recursive algorithm must have a
base case
.
2. A recursive algorithm must change its state and move toward the base
case.
3. A recursive algorithm must call itself, recursively.
For every recursive call, the recursive function has to allocate memory on
the
stack
for arguments, return address, and local variables, costing time to
push and pop these data onto the stack. Recursive algorithms take at least
O
(
n
) space where
n
is the depth of the recursive call.
Recursion is very costly when there are duplicated calculations and/or
there are overlap among subproblems. In some cases this can cause the stack
166
CHAPTER 8.
ASYMPTOTIC ANALYSIS
to overflow. For this reason, where subproblems overlap, iterative solutions
might be a better approach.
For example, in the case of the Fibonacci
series, the iterative solution runs on
O
(
n
) while the recursive solution runs
on exponential runtime.
Recursive Relations
To describe the running time of recursive functions, we use recursive relations:
T
(
n
) =
a
·
T
(
g
(
n
)) +
f
(
n
)
,
where
a
represents the number of recursive calls,
g
(
n
) is the size of each
subproblem to be solved recursively, and
f
(
n
) is any extra work done in the
function. The following table shows examples of recursive relations:
T
(
n
) =
T
(
n

1) + 1
O
(
n
)
Processing a sequence
T
(
n
) =
T
(
n

1) +
n
O
(
n
2
)
Handshake problem
T
(
n
) = 2
T
(
n

1) + 1
O
(2
n
)
Towers of Hanoi
T
(
n
) =
T
(
n/
2) + 1
O
(ln
n
)
Binary search
T
(
n
) =
T
(
n/
2) +
n
O
(
n
)
Randomized select
T
(
n
) = 2
T
(
n/
2) + 1
O
(
n
)
Tree transversal
T
(
n
) = 2
T
(
n/
2) +
n
O
(
n
ln
n
)
Sort by divide and conquer
Divide and Conquer Algorithms
Recurrences for the
divide and conquer algorithms
have the form:
T
(
n
) =
a
·
T
(
n/b
) +
f
(
n
)
,
where we have
a
recursive calls, each with a percentage 1
/b
of the dataset.
Summing to this, the algorithm does
f
(
n
) of work. To reach the problem of
T(1) = 1 in the final instance (leaf, as we will learn when we study trees),
the
height
is defined as
h
= ln
b
n
, Fig. 8.2.
8.3
Runtime in Functions
We are now ready to estimate algorithm runtimes. First of all, if the algo
rithm does not have any recursive calling, we only need to analyse its data
8.3.
RUNTIME IN FUNCTIONS
167
Figure 8.2: Tree illustrating divide and conquer recurrences.
structures and flow blocks.
In this case, complexities of code blocks exe
cuted one after the other are just added and complexities of nested loops are
multiplied.
If the algorithm has recursive calls, we can use the recursive functions
from the previous section to find the runtime. When we write a recurrence
relation for a function, we must write two equations, one for the general case
and one for the base case (that should be
O
(1), so that
T
(1) = 1). Keeping
this in mind, let us take a look at the example of the algorithm to find the
n
th
element in a Fibonacci sequence, which is known as to be exponential:
[general_poroblems/numbers/find_fibonacci_seq.py]
def
find_fibonacci_seq_rec(n):
if
You've reached the end of your free preview.
Want to read all 244 pages?
 Fall '19
 ObjectOriented Programming, Immutable object