Notes Unit I Data Structures Using Python Prof - Ajay
Notes Unit I Data Structures Using Python Prof - Ajay
AJAY PASHANKAR
SYLLABUS
UNIT I
Algorithm analysis
Problem, size of problem (symbol n); runtime resources time T(n), space S(n); worst case,
best case, average case. Measuring running time as a function of n with the wall clock
(using function time() of module time); advantages and disadvantages.
7 standard functions: constant c, log n, n, n log n, n2, n3, exponential cn or 2n; growth
of these functions as n grows: for constants c1 and c2, compare c1 * f1(n) with c2 * f2(n)
(for functions f1 and f2 from the set of these 7 functions); conclude that one function grows
faster than another independent of the values of the constants.
Operation count, unit steps (constant time): arithmetic operation (or expression
evaluation or assignment), comparison (with Boolean operators <, ==, >) function call
and/or function return, element access (for compound types); can even treat a single loop
iteration as a constant-time unit step.
Asymptotic analysis: upper bounds with A (at most) and O notation; lower bounds with Ω
Abstract data types (with associated operations and applications) Define the ADTs as
Python classes, and their operations as class methods.
(i) stacks: operations push(), pop(), is_empty(); stacktop(), len() implementation using
lists; applications: reverse a sequence, match parentheses in an expression (or html tags);
evaluate a postfix expression.
Unit II
(ii) queues: operations enqueue() and dequeue(), i.e., enter() and exit(), is_empty(),
first(), last()); implementation using Python lists; applications: simulation of a single-
window queue (uniform, Gaussian and other distributions are available in the Python
module random).
(iii) Singly, doubly and circularly linked lists, with head and optional tail; implementation of
list nodes as Python objects; operations: insertion and deletion at the front and the rear of
the list, search for a value in a list, delete a value in a list; applications: simulate stack and
queue, maintain a set of data in sorted order. Linear search in linked lists.
(iv) trees and binary trees, definitions and properties; insertion and deletion of a tree node
Unit III
(v) trees and binary trees, implementation of binary trees in lists and in linked structures;
applications: preorder, inorder and postorder traversals of binary trees; binary search trees;
breadth-first and depth-first tree traversals.
Page 1 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
(vi) graphs: directed and undirected graphs; implementation using adjacency matrix and
adjacency list; graph traversal algorithms: depth first and breadth first traversals,
application: shortest paths
Textbook(s):
Reference(s)
1) Data Structure and Algorithmic Thinking with Python- Narasimha Karumanchi,
2015, Careermonk Publications
2) Fundamentals of Python: Data Structures, Kenneth Lambert, Delmar Cengage
Learning
Page 2 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
CHAPTER 1: ALGORITHM ANALYSIS
TOPIC COVERED: Problem, size of problem (symbol n); runtime resources time T(n), space
S(n); worst case, best case, average case. Measuring running time as a function of n with
the wall clock (using function time() of module time); advantages and disadvantages.
Experimental Studies
If an algorithm has been implemented, we can study its running time by executing
it on various test inputs and recording the time spent during each execution.
A simple approach for doing this in Python is by using the time function of the time module.
This function reports the number of seconds, or fractions thereof, that have elapsed since a
benchmark time known as the epoch. The choice of the epoch is not significant to our goal,
as we can determine the elapsed time by recording the time just before the algorithm and
the time just after the algorithm, and computing their difference, as follows:
from time import time
start time = time( ) # record the starting time
run algorithm
end time = time( ) # record the ending time
elapsed = end time − start time # compute the elapsed time
We will demonstrate use of this approach, in Chapter 5, to gather experimental data on the
efficiency of Python’s list class. An elapsed time measured in this fashion is a decent
reflection of the algorithm efficiency, but it is by no means perfect.
The time function measures relative to what is known as the “wall clock.” Because many
processes share use of a computer’s central processing unit (or CPU), the elapsed time
will depend on what other processes are running on the computer when the test is
performed. A fairer metric is the number of CPU cycles that are used by the algorithm. This
can be determined using the clock function of the time module, but even this measure
might not be consistent if repeating the identical algorithm on the identical input, and its
granularity will depend upon the computer system. Python includes a more advanced
module, named timeit, to help automate such evaluations with repetition to account for
such variance among trials.
Because we are interested in the general dependence of running time on the size and
structure of the input, we should perform independent experiments on many different test
inputs of various sizes. We can then visualize the results by plotting the performance of
each run of the algorithm as a point with x-coordinate equal to the input size, n, and y-
coordinate equal to the running time, t. Figure 3.1 displays such hypothetical data. This
visualization may provide some intuition regarding the relationship between problem size
and execution time for the algorithm. This may lead to a statistical analysis that seeks to fit
the best function of the input size to the experimental data. To be meaningful, this analysis
requires that we choose good sample inputs and test enough of them to be able to make
sound statistical claims about the algorithm’s running time.
Page 3 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
A dot with coordinates (n, t) indicates that on an input of size n, the running time
While experimental studies of running times are valuable, especially when finetuning
production-quality code, there are three major limitations to their use for algorithm
analysis:
• Experimental running times of two algorithms are difficult to directly compare unless
the experiments are performed in the same hardware and software environments.
• Experiments can be done only on a limited set of test inputs; hence, they leave out
the running times of inputs not included in the experiment (and these inputs may be
important).
• An algorithm must be fully implemented in order to execute it to study its running
time experimentally.
This last requirement is the most serious drawback to the use of experimental studies.
Page 4 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Moving Beyond Experimental Analysis
time that is constant. Ideally, this might be the type of basic operation that is
executed by the hardware, although many of our primitive operations may be translated
execution time of each primitive operation, we will simply count how many primitive
operations are executed, and use this number t as a measure of the running
This operation count will correlate to an actual running time in a specific computer,
and there are only a fixed number of primitive operations. The implicit assumption
in this approach is that the running times of different primitive operations will be
Page 5 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
To capture the order of growth of an algorithm’s running time, we will associate,
with each algorithm, a function f (n) that characterizes the number of primitive
operations that are performed as a function of the input size n. Section 3.2 will introduce
the seven most common functions that arise, and Section 3.3 will introduce
An algorithm may run faster on some inputs than it does on others of the same size.
Thus, we may wish to express the running time of an algorithm as the function of
the input size obtained by taking the average over all possible inputs of the same
a difficult task. Figure 3.2 schematically shows how, depending on the input distribution,
time and the best-case time. For example, what if inputs are really only of types
“A” or “D”?
probability theory. Therefore, for the remainder of this book, unless we specify
otherwise, we will characterize running times in terms of the worst case, as a function
only the ability to identify the worst-case input, which is often simple. Also, this
approach typically leads to better algorithms. Making the standard of success for an
algorithm to perform well in the worst case necessarily requires that it will do well
on every input. That is, designing for the worst case leads to stronger algorithmic
“muscles,” much like a track star who always practices by running up an incline.
Page 6 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Page 7 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
CHAPTER 2: STANDARD FUNCTIONS:
In this section, we briefly discuss the seven most important functions used in the
analysis of algorithms. We will use only these seven simple functions for almost
all the analysis we do in this book. In fact, a section that uses a function other
than one of these seven will be marked with a star (_) to indicate that it is optional.
other useful mathematical facts that apply in the analysis of data structures and
algorithms.
The simplest function we can think of is the constant function. This is the function,
f (n) = c,
for some fixed constant c, such as c = 5, c = 27, or c = 210. That is, for any
argument n, the constant function f (n) assigns the value c. In other words, it does
not matter what the value of n is; f (n) will always be equal to the constant value c.
Because we are most interested in integer functions, the most fundamental constant
function is g(n) = 1, and this is the typical constant function we use in this
book. Note that any other constant function, f (n) = c, can be written as a constant
like adding two numbers, assigning a value to some variable, or comparing two
numbers.
One of the interesting and sometimes even surprising aspects of the analysis of
data structures and algorithms is the ubiquitous presence of the logarithm function,
f (n) = logb n, for some constant b > 1. This function is defined as follows:
Page 8 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
The most common base for the logarithm function in computer science is 2,
that we will typically omit it from the notation when it is 2. That is, for us,
log n = log2 n.
We note that most handheld calculators have a button marked LOG, but this is
typically for calculating the logarithm base-10, not base-two.
Computing the logarithm function exactly for any integer n involves the use
of calculus, but we can use an approximation that is good enough for our purposes
without calculus. In particular, we can easily compute the smallest integer
greater than or equal to logb n (its so-called ceiling, logb n_). For positive integer,
n, this value is equal to the number of times we can divide n by b before we get
a number less than or equal to 1. For example, the evaluation of log3 27_ is 3,
because ((27/3)/3)/3 = 1. Likewise, log4 64_ is 3, because ((64/4)/4)/4 = 1,
and log2 12_ is 4, because (((12/2)/2)/2)/2 = 0.75 ≤ 1.
The following proposition describes several important identities that involve
logarithms for any base greater than 1.
Proposition 3.1 (Logarithm Rules): Given real numbers a > 0, b > 1, c > 0
and d > 1, we have:
1. logb(ac) = logb a+logb c
2. logb(a/c) = logb a−logb c
3. logb(ac) = clogb a
4. logb a = logd a/logd b
5. blogd a = alogd b
By convention, the unparenthesized notation lognc denotes the value log(nc).
We use a notational shorthand, logc n, to denote the quantity, (log n)c, in which the
result of the logarithm is raised to a power.
The above identities can be derived from converse rules for exponentiation that
we will present on page 121. We illustrate these identities with a few examples.
Example 3.2: We demonstrate below some interesting applications of the logarithm
rules from Proposition 3.1 (using the usual convention that the base of a
logarithm is 2 if it is omitted).
• log(2n) = log2+logn = 1+logn, by rule 1
• log(n/2) = log n−log2 = logn−1, by rule 2
• log n3 = 3logn, by rule 3
• log 2n = nlog 2 = n · 1 = n, by rule 3
• log4 n = (log n)/log 4 = (log n)/2, by rule 4
• 2log n = nlog 2 = n1 = n, by rule 5.
As a practical matter, we note that rule 4 gives us a way to compute the base-two
logarithm on a calculator that has a base-10 logarithm button, LOG, for
log2 n = LOG n/LOG 2.
Page 9 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
reading in the n objects already requires n operations.
Page 10 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Figure 3.3: Visual justifications of Proposition 3.3. Both illustrations visualize the
identity in terms of the total area covered by n unit-width rectangles with heights
1,2, . . . ,n. In (a), the rectangles are shown to cover a big triangle of area n2/2
(base
n and height n) plus n small triangles of area 1/2 each (base 1 and height 1). In
(b), which applies only when n is even, the rectangles are shown to cover a big
with nested loops such that the operations in the inner loop increase by one each
time, then the total number of operations is quadratic in the number of times, n,
we perform the outer loop. To be fair, the number of operations is n2/2 + n/2,
and so this is just over half the number of operations than an algorithm that uses n
operations each time the inner loop is performed. But the order of growth is still
quadratic in n.
Page 11 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Continuing our discussion of functions that are powers of the input, we consider
f (n) = n3,
which assigns to an input value n the product of n with itself three times. This
function
appears less frequently in the context of algorithm analysis than the constant,
linear, and quadratic functions previously mentioned, but it does appear from time
to time.
Polynomials
Most of the functions we have listed so far can each be viewed as being part of a
larger class of functions, the polynomials. A polynomial function has the form,
where a0,a1, . . . ,ad are constants, called the coefficients of the polynomial, and
• f (n) = 2+5n+n2
• f (n) = 1+n3
• f (n) = 1
• f (n) = n
• f (n) = n2
Therefore, we could argue that this book presents just four important functions
used
in algorithm analysis, but we will stick to saying that there are seven, since the
constant,
linear, and quadratic functions are too important to be lumped in with other
polynomials. Running times that are polynomials with small degree are generally
Page 12 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Summations
A notation that appears again and again in the analysis of data structures and
algorithms
i=a
where a and b are integers and a ≤ b. Summations arise in data structure and
algorithm
analysis because the running times of loops naturally give rise to summations.
i=1
i=
n(n+1)
Likewise, we can write a polynomial f (n) of degree d with coefficients a0, . . . ,ad
as
f (n) =
i=0
aini.
Page 13 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
The Exponential Function
f (n) = bn,
where b is a positive constant, called the base, and the argument n is the
exponent.
That is, function f (n) assigns to the input argument n the value obtained by
multiplying
the base b by itself n times. As was the case with the logarithm function,
the most common base for the exponential function in algorithm analysis is b = 2.
For example, an integer word containing n bits can represent all the nonnegative
integers less than 2n. If we have a loop that starts by performing one operation
and then doubles the number of operations performed with each iteration, then the
for us to know a few handy rules for working with exponents. In particular, the
To sum up, Table 3.1 shows, in order, each of the seven common functions used in
algorithm analysis.
to the constant or logarithm function, and we would like our algorithms to run in
linear or n-log-n time. Algorithms with quadratic or cubic running times are less
practical, and algorithms with exponential running times are infeasible for all but
the smallest sized inputs. Plots of the seven functions are shown in Figure 3.4.
Page 14 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Page 15 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
In algorithm analysis, we focus on the growth rate of the running time as a function
of the input size n, taking a “big-picture” approach. For example, it is often enough
just to know that the running time of an algorithm grows proportionally to n.
We analyze algorithms using a mathematical notation for functions that disregards
constant factors. Namely, we characterize the running times of algorithms
by using functions that map the size of the input, n, to values that correspond to
the main factor that determines the growth rate in terms of n. This approach reflects
that each basic step in a pseudo-code description or a high-level language
implementation may correspond to a small number of primitive operations. Thus,
we can perform an analysis of an algorithm by estimating the number of primitive
operations executed up to a constant factor, rather than getting bogged down in
language-specific or hardware-specific analysis of the exact number of operations
that execute on the computer.
As a tangible example, we revisit the goal of finding the largest element of a
Python list; we first used this example when introducing for loops on page 21 of
Section 1.4.2. Code Fragment 3.1 presents a function named find max for this task.
This is a classic example of an algorithm with a running time that grows proportional to n,
as the loop executes once for each data element, with some fixed number of primitive
operations executing for each pass. In the remainder of this section, we provide a
framework to formalize this claim.
Big-O Notation
Instead of counting the precise number of operations or steps, computer scientists are more
interested in classifying an algorithm based on the order of magnitude as applied to
execution time or space requirements. This classification approximates the actual number of
required steps for execution or the actual storage requirements in terms of variable-sized
data sets. The term big-O, which is de-rived from the expression \on the order of," is used
to specify an algorithm's classification.
Defining Big-O
Assume we have a function T(n) that represents the approximate number of steps
required by an algorithm for an input of size n. For the second version of our algorithm in
the previous section, this would be written as
Page 16 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Now, suppose there exists a function f(n) defined for the integers n _ 0, such that
for all sufficiently large values of . Then, such an algorithm is said to have a time-
complexity of, or executes on the order of, f(n) relative to the number of operations it
requires. In other words, there is a positive integer m and a constant c (constant of
proportionality) such that for all The function f(n) indicates the
rate of growth at which the run time of an algorithm increases as the input size, n,
increases. To specify the time-complexity of an algorithm, which runs on the order of f(n),
we use the notation
Consider the two versions of our algorithm from earlier. For version one, the time was
computed to be T1(n) = 2n2. If we let c = 2, then
for a result of O(n2). In this case, the choice of c comes from the observation that
Page 17 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Constant of Proportionality
The constant of proportionality is only crucial when two algorithms have the same
f(n). It usually makes no difference when comparing algorithms whose growth
rates are of different magnitudes. Suppose we have two algorithms, L1 and L2,
with run times equal to n2 and 2n respectively. L1 has a time-complexity of O(n2)
with c = 1 and L2 has a time of O(n) with c = 2. Even though L1 has a smaller
constant of proportionality, L1 is still slower and, in fact an order of magnitude
slower, for large values of n. Thus, f(n) dominates the expression cf(n) and the run
time performance of the algorithm. The differences between the run times of these
two algorithms is shown numerically in Table 4.2 and graphically in Figure 4.2.
Constructing T(n)
Instead of counting the number of logical comparisons or arithmetic operations, we
evaluate an algorithm by considering every operation. For simplicity, we assume
that each basic operation or statement, at the abstract level, takes the same amount
of time and, thus, each is assumed to cost constant time. The total number of
Page 18 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
The steps requiring constant time are generally omitted since they eventually
shows a markup of version one of the algorithm from earlier. The basic operations
are marked with a constant time while the loops are marked with the appropriate
total number of iterations. Figure 4.3(b) shows the same algorithm but with the
constant steps omitted since these operations are independent of the data set size.
Page 19 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Classes of Algorithms
We will work with many different algorithms in this text, but most will have a time-
complexity selected from among a common set of functions, which are listed in Table 4.3
and illustrated graphically in Figure 4.4.
Page 20 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Algorithms can be classified based on their big-O function. The various classes are
commonly named based upon the dominant term. A logarithmic algorithm is
any algorithm whose time-complexity is O(loga n). These algorithms are generally very
efficient since loga n will increase more slowly than n. For many problems encountered in
Page 21 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
computer science a will typically equal 2 and thus we use the notation log n to imply log2 n.
Logarithms of other bases will be explicitly stated.
Polynomial algorithms with an efficiency expressed as a polynomial of the form
are characterized by a time-complexity of O(nm) since the dominant term is the highest
power of n. The most common polynomial algorithms are linear (m = 1), quadratic (m = 2),
and cubic (m = 3). An algorithm whose efficiency is characterized by a dominant term in the
form an is called exponential. Exponential algorithms are among the worst algorithms in
terms of time-complexity.
Let f (n) and g(n) be functions mapping positive integers to positive real numbers.
We say that f (n) is O(g(n)) if there is a real constant c > 0 and an integer constant
n0 ≥ 1 such that
This definition is often referred to as the “big-Oh” notation, for it is sometimes pronounced
Justification: By the big-Oh definition, we need to find a real constant c>0 and
an integer constant n0 ≥ 1 such that 8n+5 ≤ cn for every integer n ≥ n0. It is easy
many choices available because there is a trade-off between c and n0. For example,
Page 22 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
we could rely on constants c = 13 and n0 = 1.
The big-Oh notation allows us to say that a function f (n) is “less than or equal
to” another function g(n) up to a constant factor and in the asymptotic sense as n
grows toward infinity. This ability comes from the fact that the definition uses “≤”
to compare f (n) to a g(n) times a constant, c, for the asymptotic cases when n≥n0.
However, it is considered poor taste to say “ f (n) ≤ O(g(n)),” since the big-Oh
it is not fully correct to say “ f (n) = O(g(n)),” with the usual understanding of the
“=” relation, because there is no way to make sense of the symmetric statement,
Alternatively, we can say “ f (n) is order of g(n).” For the more mathematically inclined, it
is also correct to say, “ f (n) ∈ O(g(n)),” for the big-Oh notation, technically speaking,
denotes a whole collection of functions. In this book, we will stick to presenting big-Oh
statements as “ f (n) is O(g(n)).” Even with this interpretation, there is considerable
freedom in how we can use arithmetic operations with the big- Oh notation, and with this
freedom comes a certain amount of responsibility.
The big-Oh notation is used widely to characterize running times and space bounds in terms
of some parameter n, which varies from problem to problem, but is always defined as a
chosen measure of the “size” of the problem. For example, if we are interested in finding
the largest element in a sequence, as with the find max algorithm, we should let n denote
the number of elements in that collection. Using the big-Oh notation, we can write the
following mathematically precise statement on the running time of algorithm find max (Code
Fragment 3.1) for any computer.
Proposition 3.7: The algorithm, find max, for computing the maximum element of a list of n
numbers, runs in O(n) time.
Justification: The initialization before the loop begins requires only a constant number of
primitive operations. Each iteration of the loop also requires only a constant number of
primitive operations, and the loop executes n times. Therefore, we account for the number
of primitive operations being c_ +c__ · n for appropriate constants c_ and c__ that reflect,
respectively, the work performed during initialization and the loop body. Because each
primitive operation runs in constant time, we have that the running time of algorithm find
max on an input of size n is at most a constant times n; that is, we conclude that the
running time of algorithm find max is O(n).
The big-Oh notation allows us to ignore constant factors and lower-order terms and focus on
the main components of a function that affect its growth.
Page 23 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Example 3.8: 5n4 +3n3+2n2 +4n+1 is O(n4).
Complexity Analysis
To determine the efficiency of an algorithm, we can examine the solution itself and
measure those aspects of the algorithm that most critically affect its execution time.
For example, we can count the number of logical comparisons, data interchanges,
or arithmetic operations. Consider the following algorithm for computing the sum
Suppose we want to analyze the algorithm based on the number of additions performed. In
this example, there are only two addition operations, making this a simple task. The
algorithm contains two loops, one nested inside the other. The inner loop is executed n
times and since it contains the two addition operations, there are a total of 2n additions
performed by the inner loop for each iteration of the outer loop. The outer loop is also
performed n times, for a total of 2n2 additions.
Can we improve upon this algorithm to reduce the total number of addition operations
performed? Consider a new version of the algorithm in which the second addition is moved
out of the inner loop and modified to sum the entries in the rowSum array instead of
individual elements of the matrix.
Page 24 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
In this version, the inner loop is again executed n times, but this time, it only
contains one addition operation. That gives a total of n additions for each iteration
of the outer loop, but the outer loop now contains an addition operator of its own.
To calculate the total number of additions for this version, we take the n additions
performed by the inner loop and add one for the addition performed at the bottom
of the outer loop. This gives n + 1 additions for each iteration of the outer loop,
which is performed n times for a total of n2 + n additions.
If we compare the two results, it's obvious the number of additions in the second
version is less than the results for any n greater than 1. Thus, the second version will
execute faster than the results, but the difference in execution times will not be significant.
The reason is that both algorithms execute on the same order of magnitude,
namely n2. Thus, as the size of n increases, both algorithms increase at approximately the
same rate (though one is slightly better), as illustrated numerically in
Table 4.1 and graphically in Figure 4.1.
As indicated earlier, when evaluating the time complexity of an algorithm or code segment,
we assume that basic operations only require constant time. But what exactly is a basic
operation? The basic operations include statements and function calls whose execution time
does not depend on the special c values of the data that is used or manipulated by the
given instruction. For example, the assignment statement x = 5 is a basic instruction since
the time required to assign a reference to the given variable is independent of the value or
type of object specified on the righthand side of the = sign. The evaluation of arithmetic and
logical expressions
y=x
z=x+y*6
Page 25 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
are basic instructions, again since they require the same number of steps to perform
the given operations regardless of the values of their operands. The subscript operator,
when used with Python's sequence types (strings, tuples, and lists) is also a basic
instruction.
The time required to execute a loop depends on the number of iterations per-
formed and the time needed to execute the loop body during each iteration. In this
case, the loop will be executed n times and the loop body only requires constant
time since it contains a single basic instruction. (Note that the underlying mechanism of the
for loop and the range() function are both O(1).) We can compute
the time required by the loop as T(n) = n _ 1 for a result of O(n).
But what about the other statements in the function? The first line of the
function and the return statement only require constant time. Remember, it's
common to omit the steps that only require constant time and instead focus on
the critical operations, those that contribute to the overall time. In most instances,
this means we can limit our evaluation to repetition and selection statements and
function and method calls since those have the greatest impact on the overall time
of an algorithm. Since the loop is the only non-constant step, the function ex1()
has a run time of O(n). That means the statement y = ex1(n) from earlier requires
linear time. Next, consider the following function, which includes two for loops:
To evaluate the function, we have to determine the time required by each loop.
The two loops each require O (n) time as they are just like the loop in function
ex1() earlier. If we combine the times, it yields T(n) = n+n for a result of O(n).
Page 26 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
When presented with nested loops, such as in the following, the time required by
y = ex1(n)
An assignment statement only requires constant time, but that is the time required
to perform the actual assignment and does not include the time required to execute
any function calls used on the right-hand side of the assignment statement.
To determine the run time of the previous statement, we must know the cost of
the function call ex1(n). The time required by a function call is the time it takes
to execute the given function. For example, consider the ex1() function, which
def ex1( n ):
total = 0
for i in range( n ) :
total += i
return total
Both loops will be executed n, but since the inner loop is nested inside the outer
loop, the total time required by the outer loop will be T(n) = n _ n, resulting in
a time of O(n2) for the ex3() function. Not all nested loops result in a quadratic
Page 27 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
def ex4( n ):
count = 0
for i in range( n ) :
for j in range( 25 ) :
count += 1
return count
which has a time-complexity of O(n). The function contains a nested loop, but
the inner loop executes independent of the size variable n. Since the inner loop
loop executes n times, resulting in a linear run time. The next example presents a
def ex5( n ):
count = 0
for i in range( n ) :
count += 1
return count
How many times does the inner loop execute? It depends on the current iteration of the
outer loop. On the first iteration of the outer loop, the inner loop will execute one time; on
the second iteration, it executes two times; on the third iteration, it executes three times,
and so on until the last iteration when the inner loop will execute n times. The time required
to execute the outer loop will be the number of times the increment statement count += 1
is executed. Since the inner loop varies from 1 to n iterations by increments of 1, the total
number of times the increment statement will be executed is equal to the sum of the first n
positive integers:
T(n) = n(n + 1)
2= n2 + n2
Page 28 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Logarithmic Time Examples
The next example contains a single loop, but notices the change to the modification
step. Instead of incrementing (or decrementing) by one, it cuts the loop variable
def ex6( n ):
count = 0
i=n
while i >= 1 :
count += 1
i = i // 2
return count
To determine the run time of this function, we have to determine the number of
loop iterations just like we did with the earlier examples. Since the loop variable is
cut in half each time, this will be less than n. For example, if n equals 16, variable
i will contain the following _ve values during subsequent iterations (16, 8, 4, 2, 1).
Given a small number, it's easy to determine the number of loop iterations.
But how do we compute the number of iterations for any given value of n? When
the size of the input is reduced by half in each subsequent iteration, the number
or the largest integer less than log2 n, plus 1. In our example of n = 16, there are
n, n = ay. Thus, function ex6() requires O(log n) time. Since many problems in
computer science that repeatedly reduce the input size do so by half, it's not un-
common to use log n to imply log2 n when specifying the run time of an algorithm.
Finally, consider the following definition of function ex7(), which calls ex6()
from within a loop. Since the loop is executed n times and function ex6() requires
Page 29 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
logarithmic time, ex7() will have a run time of O(n log n).
def ex7( n ):
count = 0
for i in range( n )
count += ex6( n )
return count
List Traversal
A sequence traversal accesses the individual items, one after the other, in order to
perform some operation on every item. Python provides the built-in iteration for
the list structure, which accesses the items in sequential order starting with the
first item. Consider the following code segment, which iterates over and computes
Page 30 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
sum = 0
To determine the order of complexity for this simple algorithm, we must first
look at the internal implementation of the traversal. Iteration over the contiguous
elements of a 1-D array, which is used to store the elements of a list, requires a
count-controlled loop with an index variable whose value ranges over the indices
sum = 0
Assuming the sequence contains n items, it's obvious the loop performs n iterations. Since
all of the operations within the loop only require constant time, including the element access
operation, a complete list traversal requires O(n) time.
Note, this time establishes a minimum required for a complete list traversal. It
can actually be higher if any operations performed during each iteration are worse
List Allocation
Creating a list, like the creation of any object, is considered an operation whose
create a list:
temp = list()
valueList = [ 0 ] * n
The first example creates an empty list, which can be accomplished in constant
time. The second creates a list containing n elements, with each element initialized
to 0. The actual allocation of the n elements can be done in constant time, but
the initialization of the individual elements requires a list traversal. Since there
are n elements and a traversal requires linear time, the allocation of a vector with
Page 31 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Appending to a List
The append() operation adds a new item to the end of the sequence. If the
underlying array used to implement the list has available capacity to add the new
item, the operation has a best case time of O(1) since it only requires a single
element access. In the worst case, there are no available slots and the array has to
be expanded using the steps described in Section 2.2. Creating the new larger array
and destroying the old array can each be done in O(1) time. To copy the contents
of the old array to the new larger array, the items have to be copied element by
element, which requires O(n) time. Combining the times from the three steps
Extending a List
The extend() operation adds the entire contents of a source list to the end
of the destination list. This operation involves two lists, each of which have
their own collection of items that may be of different lengths. To simplify the
analysis, however, we can assume both lists contain n items. When the destination
list has sufficient capacity to store the new items, the entire contents of the source
list can be copied in O(n) time. But if there is not sufficient capacity, the under-
lying array of the destination list has to be expanded to make room for the new
items. This expansion requires O(n) time since there are currently n items in the
destination list. After the expansion, the n items in the source list are copied to
the expanded array, which also requires O(n) time. Thus, in the worst case the
Inserting a new item into a list is very similar to appending an item except the new
item can be placed anywhere within the list, possibly requiring a shift in elements.
An item can be removed from any element within a list, which may also involve
shifting elements. Both of these operations require linear time in the worst case,
Page 32 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
CHAPTER 4: ABSTRACT DATA TYPES(ADT)
implementation, allowing us to focus on the use of the new data type instead of
how it's implemented. This separation is typically enforced by requiring interaction with the
abstract data type through an interface or defined set of operations.
This is known as information hiding. By hiding the implementation details and
requiring ADTs to be accessed through an interface, we can work with an abstraction and
focus on what functionality the ADT provides instead of how that functionality is
implemented.
Abstract data types can be viewed like black boxes as illustrated in Figure 1.2.
User programs interact with instances of the ADT by invoking one of the several
operations defined by its interface. The set of operations can be grouped into four
categories:
The implementations of the various operations are hidden inside the black box, the contents
of which we do not have to know in order to utilize the ADT. There are several advantages
of working with abstract data types and focusing on the \what" instead of the \how."
. We can focus on solving the problem at hand instead of getting bogged down in the
implementation details. For example, suppose we need to extract a collection of values from
a file on disk and store them for later use in our program. If we focus on the
implementation details, then we have to worry about what type of storage structure to use,
how it should be used, and whether it is the most efficient choice.
. We can reduce logical errors that can occur from accidental misuse of storage structures
and data types by preventing direct access to the implementation. If we used a list to store
the collection of values in the previous example, there is the opportunity to accidentally
modify its contents in a part of our code where it was not intended. This type of logical
Page 33 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
error can be difficult to track down. By using ADTs and requiring access via the interface,
we have fewer access points to debug.
. The implementation of the abstract data type can be changed without having to modify the
program code that uses the ADT. There are many times when we discover the initial
implementation of an ADT is not the most efficient or we need the data organized in a
different way. Suppose our initial approach to the previous problem of storing a collection of
values is to simply append new values to the end of the list. What happens if we later
decide the items should be arranged in a different order than simply appending them to the
end? If we are accessing the list directly, then we will have to modify our code at every
point where values are added and make sure they are not rearranged in other places. By
requiring access via the interface, we can easily \swap out" the black box with a new
implementation with no impact on code segments that use the ADT.
. It's easier to manage and divide larger programs into smaller modules, allowing different
members of a team to work on the separate modules. Large programming projects are
commonly developed by teams of programmers in which the workload is divided among the
members. By working with ADTs and agreeing on their definition, the team can better
ensure the individual modules will work together when all the pieces are combined. Using
our previous example, if each member of the team directly accessed the list storing the
collection of values, they may inadvertently organize the data in different ways or modify
the list in some unexpected way. When the various modules are combined, the results may
be unpredictable.
To illustrate the use of the Date ADT, consider the program in Listing 1.1, which processes a
collection of birth dates. The dates are extracted from standard input and examined. Those
dates that indicate the individual is at least 21 years of age based on a target date are
printed to standard output. The user is continuously prompted to enter a birth date until
zero is entered for the month.
Page 34 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
via the constructor before any operation can be used. Other than the initialization
requirement, an operation may not have any other preconditions. It all depends
on the type of ADT and the respective operation. Likewise, some operations may
not have a postcondition, as is the case for simple access methods, which simply
Page 35 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
return a value without modifying the ADT instance itself. Throughout the text,
we do not explicitly state the precondition and postcondition as such, but they are
easily identified from the description of the ADT operations.
When implementing abstract data types, it's important that we ensure the
proper execution of the various operations by verifying any stated preconditions.
The appropriate mechanism when testing preconditions for abstract data types is
to test the precondition and raise an exception when the precondition fails. You
then allow the user of the ADT to decide how they wish to handle the error, either
catch it or allow the program to abort.
Python, like many other object-oriented programming languages, raises an ex-
ception when an error occurs. An exception is an event that can be triggered
and optionally handled during program execution. When an exception is raised
indicating an error, the program can contain code to catch and gracefully handle
the exception; otherwise, the program will abort. Python also provides the assert
statement, which can be used to raise an AssertionError exception. The assert statement is
used to state what we assume to be true at a given point in the program. If the assertion
fails, Python automatically raises an AssertionError and aborts the program, unless the
exception is caught.
Throughout the text, we use the assert statement to test the preconditions
when implementing abstract data types. This allows us to focus on the implementation of
the ADTs instead of having to spend time selecting the proper exception to raise or creating
new exceptions for use with our ADTs. For more information on exceptions and assertions,
refer to Appendix C.
Date Representations
There are two common approaches to storing a date in an object. One approach stores the
three components|month, day, and year|as three separate fields. With this format, it is
easy to access the individual components, but it's difficult to compare two dates or to
compute the number of days between two dates since the number of days in a month varies
from month to month. The second approach stores the date as an integer value
representing the Julian day, which is the number of days elapsed since the initial date of
November 24, 4713 BC (using the Gregorian calendar notation). Given a Julian day number,
we can compute any of the three Gregorian components and simply subtract the two integer
values to determine which occurs first or how many days separate the two dates. We are
going to use the latter approach as it is very common for storing dates in computer
applications and provides for an easy implementation.
Page 36 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Page 37 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
STACKS:
A stack is a collection of objects that are inserted and removed according to the last-in,
first-out (LIFO) principle.
A user may insert objects into a stack at any time, but may only access or remove the
most recently inserted object that remains (at the so-called “top” of the stack). The name
“stack” is derived from the metaphor of a stack of plates in a spring-loaded, cafeteria
plate dispenser. In this case, the fundamental operations involve the “pushing” and
“popping” of plates on the stack.
When we need a new plate from the dispenser, we “pop” the top plate off the stack,
and when we add a plate, we “push” it down on the stack to become the new top
plate. Perhaps an even more amusing example is a PEZ® candy dispenser, which
stores mint candies in a spring-loaded container that “pops” out the topmost candy
in the stack when the top of the dispenser is lifted (see Figure 6.1). Stacks are
a fundamental data structure. They are used in many applications, including the
following.
Example 6.1: Internet Web browsers store the addresses of recently visited sites
in a stack. Each time a user visits a new site, that site’s address is “pushed” onto the
stack of addresses. The browser then allows the user to “pop” back to previously
visited sites using the “back” button.
Example 6.2: Text editors usually provide an “undo” mechanism that cancels recent
editing operations and reverts to former states of a document. This undo operation
can be accomplished by keeping text changes in a stack.
Page 38 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Example 6.3: The following table shows a series of stack operations and their effects on
an initially empty stack S of integers.
Page 39 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
illustrates new values being added to the top of the stack and one value being
removed from the top.
Page 40 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
When the outer while loop terminates after the negative value is extracted, the
contents of the stack will be as illustrated in Figure 7.2. Notice the last value
entered is at the top and the first is at the base. If we pop the values from the
stack, they will be removed in the reverse order from which they were pushed onto
the stack, producing a reverse ordering.
Page 41 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Page 42 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
The individual stack operations are easy to evaluate for the Python list-based
implementation. isEmpty(), len , and peek() only require O(1) time. The
pop() and push() methods both require O(n) time in the worst case since the
underlying array used to implement the Python list may have to be reallocated
to accommodate the addition or removal of the top stack item. When used in
sequence, both operations have an amortized cost of O(1).
Page 43 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
The class constructor creates two instance variables for each Stack. The top
field is the head reference for maintaining the linked list while size is an integer
value for keeping track of the number of items on the stack. The latter has to be
adjusted when items are pushed onto or popped o_ the stack. Figure 7.3 on the
next page illustrates a sample Stack object for the stack from Figure 7.1(b).
The StackNode class is used to create the linked list nodes. Note the inclusion
of the link argument in the constructor, which is used to initialize the next field of
the new node. By including this argument, we can simplify the prepend operation
of the push() method. The two steps required to prepend a node to a linked list
are combined by passing the head reference top as the second argument of the
StackNode() constructor and assigning a reference to the new node back to top.
Page 44 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
The peek() method simply returns a reference to the data item in the first node after
verifying the stack is not empty. If the method were used on the stack represented by the
linked list in Figure 7.3, a reference to 19 would be returned.
The peek operation is only meant to examine the item on top of the stack. It should not be
used to modify the top item as this would violate the definition of the Stack ADT.
The pop() method always removes the first node in the list. This operation is illustrated in
Figure 7.4(a). This is easy to implement and does not require a search to find the node
containing a specific item. The result of the linked list after popping the top item from the
stack is illustrated in Figure 7.4(b).
The linked list implementation of the Stack ADT is more efficient than the Python-list based
implementation. All of the operations are O(1) in the worst case, the proof of which is left as
an exercise.
Stack Applications
The Stack ADT is required by a number of applications encountered in computer
science. In this section, we examine several basic applications that traditionally
are presented in a data structures course.
Balanced Delimiters
A number of applications use delimiters to group strings of text or simple data
into subparts by marking the beginning and end of the group. Some common examples
include mathematical expressions, programming languages, and the HTML
markup language used by web browsers. There are typically strict rules as to how
the delimiters can be used, which includes the requirement of the delimiters being paired
and balanced. Parentheses can be used in mathematical expressions to
group or override the order of precedence for various operations. To aide in reading
complicated expressions, the writer may choose to use different types of symbol
Page 45 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
pairs, as illustrated here:
{A + (B * C) - (D / [E + F])}
The delimiters must be used in pairs of corresponding types: {}, [], and ().
They must also be positioned such that an opening delimiter within an outer pair
must be closed within the same outer pair. For example, the following expression
would be invalid since the pair of braces [] begin inside the pair of parentheses ()
but end outside.
(A + [B * C)] - {D / E}
Another common use of the three types of braces as delimiters is in the C++
programming language. Consider the following code segment, which implements a
function to compute and return the sum of integer values contained in an array:
int sumList( int theList[], int size )
{
int sum = 0;
int i = 0;
while( i < size ) {
sum += theList[ i ];
i += 1;
}
return sum;
}
As with the arithmetic expression, the delimiters must be paired and balanced.
However, there are additional rules of the language that dictate the proper placement and
use of the symbol pairs. We can design and implement an algorithm
that scans an input text _le containing C++ source code and determines if the
delimiters are properly paired. The algorithm will need to remember not only the
most recent opening delimiter but also all of the preceding ones in order to match
them with closing delimiters. In addition, the opening delimiters will need to be
remembered in reverse order with the most recent one available first.
Consider the C++ code segment from earlier. As the file is scanned, we can push each
opening delimiter onto the stack. When a closing delimiter is encountered, we pop the
opening delimiter from the stack and compare it to the closing delimiter. For properly paired
delimiters, the two should match. Thus, if the top of the stack contains a left bracket [, then
the next closing delimiter should be a right bracket ]. If the two delimiters match, we know
they are properly paired and can continue processing the source code. But if they do not
match, then we know the delimiters are not correct and we can stop processing the file.
Table 7.1
shows the steps performed by our algorithm and the contents of the stack after each
delimiter is encountered in our sample code segment.
Page 46 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
So far, we have assumed the delimiters are balanced with an equal number of
opening and closing delimiters occurring in the proper order. But what happens if
the delimiters are not balanced and we encounter more opening or closing delimiters
than the other? For example, suppose the programmer introduced a typographical
error in the function header:
int sumList( int theList)], int size )
Our algorithm will find the first set of parentheses correct. But what happens
when the closing bracket ] is scanned? The result is illustrated in the top part of
Table 7.2. You will notice the stack is empty since the left parenthesis was popped
and matched with the preceding right parenthesis. Thus, unbalanced delimiters in
which there are more closing delimiters than opening ones can be detected when
trying to pop from the stack and we detect the stack is empty.
Page 47 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
Delimiters can also be out of balance in the reverse case where there are more
opening delimiters than closing ones. Consider another version of the function
header, again containing a typographical error:
int sumList( int (theList[], int size )
The result of applying our algorithm to this code fragment is illustrated in the
bottom chart in Table 7.2. If this were the complete code segment, you can see we
would end up with the stack not being empty since there are opening delimiters
yet to be paired with closing ones. Thus, in order to have a complete algorithm,
we must check for both of these errors.
A Python implementation for the validation algorithm is provided in Listing 7.3.
The function isValidSource() accepts a _le object, which we assume was previously opened
and contains C++ source code. The _le is scanned one line at a time and each line is
scanned one character at a time to determine if it contains properly paired and balanced
delimiters.
A stack is used to store the opening delimiters and either implementation can
be used since the implementation is independent of the definition. Here, we have
chosen to use the linked list version. As the _le is scanned, we need only examine
A*B+C/D
Page 48 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
of the precedence for the operators. But how do we represent that in our string
the characters that correspond to one of the three types of delimiter pairs. All
other characters can be ignored. When an opening delimiter is encountered, we
push it onto the stack. When a closing delimiter occurs, we first check to make sure
the stack is not empty. If it is empty, then the delimiters are not properly paired
and balanced and no further processing is needed. We terminate the function and
return False. When the stack is not empty, the top item is popped and compared
to the closing delimiter. The two delimiters do match corresponding opening and
closing delimiters; we again terminate the function and return False. Finally,
after the entire _le is processed, the stack should be empty when the delimiters are
properly paired and balanced. For the final test, we check to make sure the stack
is empty and return either True or False, accordingly.
Page 49 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
we have to continuously scanned forward and backward through the string in order
to properly evaluate the expression. To simplify the evaluation of a mathematical
expression, we need an alternative representation for the expression. A representation in
which the order the operators are performed is the order they are specified
would allow for a single left-to-right scan of the expression string.
Three different notations can be used to represent a mathematical expression.
The most common is the traditional algebraic or infix notation where the operator
is specified between the operands A+B. The prefix notation places the operator
immediately preceding the two operands +AB, whereas in postfix notation, the
operator follows the two operands AB+.
At first glance, the different notations may seem to be nothing more than different operator
placement. But the postfix and prefix notations have the advantages
that neither uses parentheses to override the order of precedence and both create
expressions in unique form. In other words, each expression is unique and produces
a specific result unlike infix notation in which the same expression can be written in multiple
ways.
Converting from Infix to Postfix
Infix expressions can be easily converted by hand to postfix notation. The expression A + B
- C would be written as AB+C- in postfix form. The evaluation of this
expression would involve first adding A and B and then subtracting C from that
result. We will examine the evaluation of postfix expressions later; for now we
focus on the conversion from infix to postfix.
Short expressions can be easily converted to postfix form, even those using
parentheses. Consider the expression A*(B+C), which would be written in postfix
as ABC+*. Longer expressions, such as the example from earlier, A*B+C/D, are a bit
more involved. To help in this conversion we can use a simple algorithm:
1. Place parentheses around every group of operators in the correct order of
evaluation. There should be one set of parentheses for every operator in the
infix expression.
((A * B) + (C / D))
2. For each set of parentheses, move the operator from the middle to the end
preceding the corresponding closing parenthesis.
((A B *) (C D /) +)
3. Remove all of the parentheses, resulting in the equivalent postfix expression.
AB*CD/+
Compare this result to a modified version of the expression in which parentheses
are used to place the addition as the first operation:
A * (B + C) / D
Using the simple algorithm, we parenthesize the expression:
((A * (B + C)) / D)
and move the operators to the end of each parentheses pair:
((A (B C +) *) D /)
Finally, removing the parentheses yields the postfix expression:
ABC+*D/
A similar algorithm can be used for converting from infix to prefix notation.
The difference is the operators are moved to the front of each group.
Page 50 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
or variables at the beginning of the expression until they are needed. Assume we
are given a valid postfix expression stored in a string consisting of operators and
single-letter variables. We can evaluate the expression by scanning the string, one
character or token at a time. For each token, we perform the following steps:
1. If the current item is an operand, push its value onto the stack.
2. If the current item is an operator:
(a) Pop the top two operands o_ the stack.
(b) Perform the operation. (Note the top value is the right operand while the
next to the top value is the left operand.)
(c) Push the result of this operation back onto the stack.
The final result of the expression will be the last value on the stack. To illustrate
the use of this algorithm, let's evaluate the postfix expression A B C + * D / from
our earlier example. Assume the existence of an empty stack and the following
variable assignments have been made:
A=8C=3
B=2D=4
The complete sequence of algorithm steps and the contents of the stack after
each operation are illustrated in Table 7.3.
The postfix evaluation algorithm assumes a valid expression. But what happens
if the expression is invalid? Consider the following invalid expression in which there
are more operands than available operators:
AB*CD+
After applying the algorithm to this expression, there are two values remaining
on the stack as illustrated in Table 7.4. What happens if there are too many
operators for the given number of operands? Consider such an invalid expression:
AB*+C/
In this case, there are too few operands on the stack when we encounter the
addition operator, as illustrated in Table 7.5. If we attempt to perform two pops
from the stack, an assertion error will be thrown since the stack will be empty
on the second pop. We can modify the algorithm to detect both types of errors.
Page 51 of 52
SYBSCCS PAPER III UNIT I DATA STRUCTURES USING PYTHON BY:[Link] PASHANKAR
In step 2(a), we must first verify the stack is not empty before popping an item.
If the stack is empty, we can stop the evaluation and flag an error. The second
modification occurs after the evaluation of the entire expression. We can pop the
result from the stack and then verify the stack is empty. If the stack is not empty,
the expression was invalid and we must fag an error.
Page 52 of 52