AD3251-DSD Lecture Notes
AD3251-DSD Lecture Notes
Employee 3. INTRODUC
TION TO
OBJECT
ORIENTED
PROGRAMMING
Introduction to Object Oriented Programming
3.1 Goals, Principles and Patterns
• The main ‘actors’ in the object-oriented paradigm are called objects. Each object is an instance of a
class.
• Each class presents to the outside world a concise and consistent view of the objects that are instances
of this class, without going into too much unnecessary detail or giving others access to the inner
workings of the objects.
• The class definition typically specifies instance variables, also known as data members, that the object
contains as well as the methods also known as member functions that the object can execute.
• This view of computing is intended to fulfill several goals and incorporate several design principles.
3.2 Object-Oriented Design Goals
Software implementations should achieve robustness, adaptability, and reusability.
Robustness
• Every good programmer wants to develop software that is correct, which means that a program
produces the right output for all the anticipated inputs in the program’s application. The software is said
to be robust, if it is capable of handling unexpected inputs that are not explicitly defined for its
application.
• For example, if a program is expecting a positive integer and instead is given a negative integer, then
the program should be able to recover gracefully from this error.
Adaptability
• Modern software applications, such as Web browsers and Internet search engines, involve large
programs that are used for many years. Software, therefore, needs to be able to evolve over time in
response to changing conditions in its environment. Thus, another important goal of quality software is
that it achieves adaptability (also called evaluability).
• Related to this concept is portability, which is the ability of software to run with minimal change on
different hardware and operating system platforms. An advantage of writing software in Python is
the portability provided by the language itself.
Reusability
• Going hand in hand with adaptability, the software be reusable. That is, the same code should be usable
as a component of different systems in various applications.
• Developing quality software is designed in a way that makes it easily reusable in future applications.
Such reuse should be done with care.
• Modern software systems consist of several different components that must interact correctly in order
for the entire system to work properly. Keeping these interactions requires that these different
components be well organized. Modularity refers to an organizing principle in which different
components of software system are divided into separate functional units.
• Modularity in a software system can provide a powerful organizing framework that brings clarity
to an implementation.
• In Python, a module is a collection of closely related functions and classes that are defined together in
a single file of source code.
• For example, Python’s standard library math module, provides definitions for key mathematical
constants and functions, and the OS module, provides support for interacting with the operating
system.
Uses of modularity:
• It increases the robustness of the program.
• It is easier to test and debug separate components of the program.
• It enables software reusability.
Abstraction
• Abstraction allows dealing with the complexity of the object. Abstraction allows picking out the relevant
details of the object, and ignoring the non-essential details.
• Applying the abstraction to the design of data structures gives rise to Abstract Data Types (ADTs). An
ADT is a mathematical model of a data structure that specifies the type of data stored, the operations
supported on them, and the types of parameters of the operations. An ADT specifies what each operation
does, but not how it does it.
• Python supports abstract data types using a mechanism known as an abstract base class (ABC). An
abstract base class cannot be instantiated (i.e., you cannot directly create an instance of that class), but it
defines one or more common methods that all implementations of the abstraction must have.
• An ABC is realized by one or more concrete classes that inherit from the abstract base class while
providing implementations for those method declared by the ABC.
Encapsulation
• It hides the data defined in the class and separates implementation of the class from its interface. The
interaction with the class is through the interface provided by the set of methods defined in the class. This
separation of interface from its implementation allows changes to be made in the class without
affecting its interface.
• One of the advantages of encapsulation is that it gives freedom to the programmer to implement the
details of a component, without concern that other programmers will be writing the code that intricately
depends on those internal decisions.
• Encapsulation yields robustness and adaptability, for it allows the implementation details of parts of a
program to change without adversely affecting other parts, thereby making it easier to fix bugs or add
new functionality with relatively local changes to a component
3.4 Object Oriented Design Patterns
• Object-oriented design facilitates reusable, robust, and adaptable software. Designing good code
requires the effective use of object-oriented design techniques.
• Computing researchers and practitioners have developed a variety of organizational concepts and
methodologies for designing quality object-oriented software that is concise, correct, and reusable.
• The concept of a design pattern, describes a solution to a “typical” software design problem. A pattern
provides a general template for a solution that can be applied in many different situations.
• It describes the main elements of a solution in an abstract way that can be specialized for a specific
problem at hand. The design pattern can be consistently applied to implementations of data structures
and algorithms.
The algorithm design patterns include the following:
• Recursion
• Amortization
• Divide and conquer
• Prune and search
• Brute force
• Dynamic Programming
The greedy method The software engineering design patterns include:
• Iterator
• Adapter
• Position
• Composition
• Template method
• Locator
4. CLASSES IN PYTHON
Classes in Python
• A class serves as the primary means for abstraction in object-oriented programming. In python
everything is an object. Everything is an instance of some class. A class also serves as a blueprint for its
instances.
• The data values stored inside an object are called attributes. The state information for each instance is
represented in the form of attributes (also known as fields, instance variables, or data members).
• A class provides a set of behaviors in the form of member functions (also known as methods), with
implementations that are common to all instances of that class.
Defining a class
A class is the definition of data and methods for a specific type of object
Syntax:
class classname:
<statement1>
.
.
.
<statement>
The class definition begins with the keyword class, followed by the name of the class, a colon and an
indented block of code that serves as the body of the class.
The body includes definitions for all methods of the class. These methods are defined as functions, with
a special parameter, named self, that is used to identify the particular instance upon which a member is
invoked.
When a class definition is entered, a new namespace is created, and used as the local scope. Thus, all
assignments to local variables go into this new namespace.
Example:
class customer:
def _ _init_ _(self,name,iden,acno):
self.custName=name
self.custID=iden
self.custAccNo=acno
def display(self):
print("Customer Name = ",self.custName)
print("Customer ID = ",self.custID)
print("Customer Account Number = ",self.custAccNo)
c = customer("Ramesh",10046,327659)
c.display()
• Example:
self.custName, self.custID, self.custAccNo
4.2 Object Creation:
An object is the runtime entity used to provide the functionality to the python class.
The attributes defined inside the class are accessed only using objects of that class.
The user defined functions also accessed by using the object.
As soon as a class is created with attributes and methods, a new class object is created with
the same name as the class.
This class object permits to access the different attributes as well as to instantiate
new objects of that class.
Instance of the object is created using the name same as the class name and it is known as
object instantiation.
One can give any name to a newly created object.
Syntax:
object_name = class_name
The dot(.) operator is used to call the functions.
Syntax:
object_name . function_name()
• In this example, coffee_name and price are the class variables. ram_order and paul_order are the two
instances of this class. Each of these instances has their own values set for the coffee_name and price
instance variables.
• When ram’s order details are printed in the console, the values Espresso and 210 are returned. When
Paul’s order details are printed in the console, the values Latte and 275 are returned.
• This shows that, instance variables can have different values for each instance of the class, whereas class
variables are the same across all instances.
4.4 Constructor
Example:
class Employee:
def _ _init_ _(self):
self.name = "Ramesh"
self.EmpId = 100456
def display(self):
print("Employee Name = ", self.name, " \nEmployee Id = ",self.EmpId) emp =
Employee()
emp.display()
Output:
Employee Name = Ramesh
Employee Id = 100456
Parameterized constructor
Constructor with parameters is known as parameterized constructor. The first parameter to constructor is
self that is a reference to the being constructed, and the rest of the arguments are provided by the
programmer. A parameterized constructor can have any number of arguments.
Example:
class Employee:
def init (self, name, age,
salary): self.name = name
self.age = age
self.salary = salary
def display(self):
print(self.name, self.age, self.salary)
# creating object of the Employee class
emp1 = Employee('Banu', 23, 17500)
emp1.display()
emp2 = Employee('Jack', 25, 18500)
emp2.display()
Output:
Banu 23 17500
Jack 25 18500
Destructor
It is a special method that is called when an object gets destroyed. A class can define a special
method called destructor with the help of del() . In Python, destructor is not called manually but
completely automatic, when the instance(object) is about to be destroyed. It is mostly used to clean up non
memory resources used by an instance(object).
Example: For Destructor
class Student:
# constructor
def init (self, name):
print('Inside Constructor')
self.name = name
print('Object initialized')
def display(self):
print('Hello, my name is', self.name)
# destructor
def del (self):
print('Inside destructor')
print('Object destroyed')
s1 = Student('Raja') # create object
s1.display()
del s1 # delete object
Output:
Inside Constructor
Object initialized Hello, my name is Raja
Inside destructor
Object destroyed
Iterators
Iteration is an important concept in the design of data structures. An iterator is an object that contains a
countable number of elements that can be iterated upon.
Iterators allows to traverse through all the elements of a collection and return one element at a time.
An iterator object must implement two special methods, iter() and next() collectively called iterator
protocol.
An object is called iterable if it gets an iterator from it. Most built-in containers in python are, list,
tuple, string etc.
The _ _iter_ _() method returns the iterator object itself. If required, some initialization can be
performed.
The _ _next_ _() method returns the next element of the collection. On reaching the end, it must raise
stop Iteration exception to indicate that there are no further elements.
ENCAPSULATION
Encapsulation is one of the fundamental concepts in object-oriented programming.
Encapsulation in Python describes the concept of bundling data and methods within a single unit. A
class is an example of encapsulation as it binds all the data members (instance variables) and methods
into a single unit.
Public Member:
Public data members are accessible within and outside of a class. All member variables of the class
are by default public.
Private Member:
The variables can be protected in the class by marking them as private. To define a private
member, prefix the variable name with two underscores. Private members are accessible only within
the class. Protected Member:
Protected members are accessible within the class and also available to its sub-classes. To define a
protected member, prefix the member name with a single underscore. Protected data members are
used in inheritance and to allow data members access to only child classes.
Advantages of Encapsulation:
1. The main advantage of using encapsulation is the security of the data. Encapsulation protects an
object from unauthorized access.
2. Encapsulation hide an object’s internal representation from the outside called data hiding.
3. It simplifies the maintenance of the application by keeping classes separated and preventing them
from tightly coupling with each other.
4. Bundling data and methods within a class makes code more readable and maintainable
OPERATOR OVERLOADING
Operator overloading means giving extended meaning beyond their predefined operational
meaning. For example, operator + is used to add two integers as well as join two strings and merge
two lists. The same built-in operator or function shows different behavior for objects of different
classes, this is called operator overloading.
Example:
# add 2 numbers
print(100 + 200)
# concatenate two strings
print('Python' +
'Programming') # merger
two list
print([10, 20, 30] + ['Data Structures', 'And', 'Algorithms'])
Output:
300
PythonProgramming
[10, 20, 30, 'Data Structures', 'And', 'Algorithms']
The operator + is used to carry out different operations for distinct data types. This is one of the
simplest occurrences of polymorphism in Python.
To perform operator overloading, python provides some special function or magic function that is
automatically invoked when it is associated with the particular object. If + operator is used, the magic
method _add_ is automatically invoked.
Example:
class item:
def _ _init_ _(self,
price): self.price =
price
# Overloading + operator with magic
method def _ _add_ _(self, other):
return self.price +
other.price b1 = item(400)
b2 = item(300)
print("Total Price: ", b1 + b2)
Output:
Total Price: 700
In this example, addition is implemented by a special method in python called the _add_ method. When two
integers are added together, this method is called to create a new integer object
Executing x+y, calls the int class _add_ method when x is an integer, but it calls the float types
_add_ method when x is float. The operand on the left-hand side determines which add method is
called. Thus the + operator is overloaded.
When a binary operator is applied to two instances of different types, as in 5 * ‘Hello’, Python
gives deference to the class of the left operand.
In this example, Python would effectively check if the int class provides a sufficient definition
for how to multiply an instance by a string, via the _ _mul_ _ method. If that class does not implement
such a behavior, Python checks the class definition for the right-hand operand, in the form of a special
method named _ _rmul_ _ (i.e., right multiply). This provides a way for a new user-defined class to
support mixed operations that involve an instance of an existing class.
5. INHERITANCE
A natural way to organize various structural components of a software package is in a hierarchical
fashion. A hierarchical design is useful in software development, as common functionality can be
grouped at the most general level, thereby promoting reuse of code, while differentiated behaviors can be
viewed as extensions of the general case.
In object-oriented programming, the existing class is typically described as the base class, parent class
or super class, while the newly defined class is known as the subclass or child class.
There are two ways in which a subclass can differentiate itself from its superclass. A subclass may
specialize an existing behavior by providing a new implementation that overrides an existing method.
A subclass may also extend its superclass by providing brand new methods.
Inheritance is a mechanism through which we can create a class or object based on another class or
object. In other words, the new objects will have all the features or attributes of the class or object on
which they are based. It supports code reusability.
In Python, based upon the number of child and parent classes involved, there are five types of
inheritance.
The types of inheritance are listed below:
i. Single inheritance
ii. Multiple Inheritance
iii. Multilevel inheritance
iv. Hierarchical Inheritance
v. Hybrid Inheritance
5.1 Single Inheritance
In single inheritance, a child class inherits from a single-parent class. Here is one child class and
one parent class
Example:
# Base class
class Vehicle:
def Vehicle_info(self):
print('Inside Vehicle class')
# Child class
class Car(Vehicle):
def car_info(self):
print('Inside Car class')
# Create object of Car
car = Car()
# access Vehicle's info using car object
car.Vehicle_info()
car.car_info()
Output:
Inside Vehicle class
Inside Car class
5.2 Multiple Inheritance:
In multiple inheritance, one child class can inherit from multiple parent classes. So here is one child class
and multiple parent classes
class SuperClass1:
# features of SuperClass1
class SuperClass2:
# features of SuperClass2
class MultiDerived(SuperClass1, SuperClass2):
# features of SuperClass1 + SuperClass2 + MultiDerived class
In Hierarchical inheritance, more than one child class is derived from a single parent class. In other words,
a single base class is inherited by multiple derived classes. In this scenario, each derived class shares
common attributes and methods from the same base class, forming a hierarchy of classes.
Syntax :
class BaseClass:
# Base class attributes and methods
class DerivedClass1(BaseClass):
# Additional attributes and methods specific to DerivedClass1
class DerivedClass2(BaseClass):
# Additional attributes and methods specific to DerivedClass2
Syntax:
class
Vehi
cle:
def
info(
):
print("This is
Vehicle") class
Car(Vehicle):
def car_info():
print("Car name is
BMW”) class
Truck(Vehicle):
def truck_info():
print("Truck name is
Ford") obj1 = Car()
obj1.info()
obj1.car_info
() obj2 =
Truck()
obj2.info()
obj2.truck_in
fo()
Output:
This is
Vehicle Car
name is
BMW This
is Vehicle
Truck name
is Ford
Syntax:
class BaseClass1:
# Attributes and methods of BaseClass1
class BaseClass2:
# Attributes and methods of BaseClass2
class DerivedClass(BaseClass1, BaseClass2):
# Attributes and methods of DerivedClass
Example:
class Vehicle:
def vehicle_info(self):
print("Inside Vehicle
class")
class Car(Vehicle):
def car_info(self):
print("Inside Car
class") class
Truck(Vehicle):
def truck_info(self):
print("Inside Truck class")
class SportsCar(Car, Vehicle):
def sports_car_info(self):
print("Inside SportsCar class")
s_car = SportsCar()
s_car.vehicle_info()
s_car.car_info()
s_car.sports_car_info()
Output:
Inside Vehicle
class Inside Car
class Inside
SportsCar class
6. NAMESPACE
Namespace
Whenever an identifier is assigned to a value, its definition is made with a specific scope. Top level
assignments are known as global scope. Assignments made with in the body of a function have local
scope to that function call.
In Python, when computing a sum with the syntax x + y, the names x and y must have been previously
associated with objects that serve as values. The process of determining the value associated with an
identifier is known as name resolution.
A namespace manages all of the identifiers that are defined in a particular scope, mapping each name to
its associated value. In Python, functions, classes and modules are class objects and so the value
associated with an identifier in a namespace may be a function, class or module.
A namespace is a collection of currently defined symbolic names along with information about the
object that each name references. We can think of a namespace as a dictionary, in which the keys are the
object names and the values are the objects themselves. Each key-value pair maps a name to its
corresponding object.
Types:
In a Python program, there are three types of namespaces:
1. Built-In
2. Global
3. Local
1) Built-in Namespace
It contains the names of all of Python’s built-in objects. These are available at all times when Python
is running. The Python interpreter creates the built-in namespace when it starts up. This namespace
remains in existence until the interpreter terminates.
Example:
Name=input("Enter your name:") #input() is built-in
function print(Name) #print() is built-in
function
2) Global Namespace
It contains any names defined at the level of the main program. Python creates the global namespace
when the main program body starts, and it remains in existence until the interpreter terminates. The
interpreter creates a global namespace for any module that the program loads with the import statement.
Example:
x=10 # global scope of variable in
python def f1(): #function definition
print(x) #variable accessed inside the function
f1()
3) Local Namespace
In Python, the interpreter creates a new namespace whenever a function executes. That namespace is
local to the function and remains in existence until the function terminates. A local namespace can access
global as well as built-in namespaces.
Example:
def f1(): #function definition
y=20 #Local scope of variable in
python print(y) #variable accessed
inside the function f1()
• The variable ‘y’ is declared in a local namespace and has a local scope of variable in python.
Data structure is a systematic way of organizing and accessing data, and an algorithm is a step-
by-step procedure for performing some task in a finite amount of time. Algorithm analysis helps
us to determine which algorithm is most efficient in terms of time and space consumed.
The running time of an algorithm can be calculated by executing it on various test inputs and
recording the time spent during each execution.
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.
1. Allows us to evaluate the relative efficiency of any two algorithms in a way that is independent of the
hardware and software environment.
2. Takes into account all possible inputs.
3. Is performed by studying a high-level description of the algorithm without need for implementation.
Types of Analysis
Algorithm analysis depends on which inputs the algorithm takes less time (performing wel1) and with
which inputs the algorithm takes a long time.
Worst case
Defines the input for which the algorithm takes a long time (slowest time to complete).
Input is the one for which the algorithm runs the slowest.
Best case
Defines the input for which the algorithm takes the least time (fastest time to complete).
Input is the one for which the algorithm runs the fastest.
Average case
Provides a prediction about the running time of the algorithm.
Run the algorithm many times, using many different inputs and divide by the number of
trials.
Assumes that the input is random.
Lower Bound <= Average Time <= Upper Bound
9. ASYMPTOTIC NOTATIONS
Asymptotic Notation
For the best, average and worst cases, we need to identify the upper and lower bounds. To represent these
upper and lower bounds, we need some kind of syntax, represented in the form of function f(n).
Big-O Notation [Upper Bounding Function]
This notation gives the tight upper bound of the given function. Thus, it gives the worst-case complexity of
an algorithm.
Generally, it is represented as f(n) = O(g(n)). That means, at larger values of n, the upper bound of f(n) is
g(n).
For example, if f(n) = n 4 + 100n 2 + 10n + 50 is the given algorithm, then n 4 is g(n).
That means g(n) gives the maximum rate of growth for f(n) at larger values of n.
Big-O Examples
This notation gives the tighter lower bound of the given algorithm and we represent it as f(n) = Ω(g(n)).
Thus, it provides the best-case complexity of an algorithm.
That means, at larger values of n, the tighter lower bound of f(n) is g(n).
For example, if f(n) = 100n2 + 10n + 50, g(n) is Ω(n2 ).
Example : 3nlog n−2n is Ω(nlog n).
Solution: 3nlog n− 2n = nlog n+ 2n(logn− 1) ≥ nlogn for n ≥ 2; hence, we can take c =1 and n0 = 2 in this
case
Big-Theta
There is a notation that allows us to say that two functions grow at the same rate, up to constant factors(ie.,
It encloses the function from above and below). Since it represents the upper and the lower bound of the
running time of an algorithm, it is used for sanalyzing the average-case complexity of an algorithm.
Θ(g(n)) = { f(n): there exist positive constants c1, c2 and n0 such that 0 ≤ c1g(n) ≤ f(n) ≤ c2g(n) for
all n ≥ n0 }
f(n) is Θ(g(n)), pronounced “ f(n) is big-Theta of g(n),” if f(n) is O(g(n)) and f(n) is Ω(g(n)) , that is, there
are real constants c > 0 and c > 0, and an integer constant n0 ≥1 such that
Example: 3nlog n+4n+5logn is Θ(nlog n).
Solution: 3nlogn ≤ 3nlog n+4n+5logn ≤ (3+4+5) nlogn for n≥2.
Asymptotic Analysis
There are some general rules to help us determine the running time of an algorithm.
1) Loops: The running time of a loop is, at most, the running time of the statements inside the loop
(including tests) multiplied by the number of iterations.
Example: // Executes n times
For(i=1;i<=n;i++)
M=m+2 //constant time, c
2) Nested loops: Analyze from the inside out. Total running time is the product of the sizes of all the loops.
Example:
//outer loop
For(i=1;i<=n;i++)
For(j=1;j<=n;j++)
K=k+1 //constant time, c
For(i=1;i<=n;i++)
M=m+2 //constant time, c
//outer loop
For(i=1;i<=n;i++)
For(j=1;j<=n;j++)
K=k+1 //constant time, c
4) If-then-else statements:
Worst-case running time: the test, plus either the then part or the else part (whichever is the larger).
Recursion
Recursion is a technique by which a function makes one or more calls to itself during execution, until the
condition gets satisfied. Recursion provides a powerful alternative for performing repetitive tasks.
Example
The factorial function (commonly denoted as n!) is a classic mathematical function that
has a natural recursive definition.
An English ruler has a recursive pattern that is a simple example of a fractal structure.
Binary search is among the most important computer algorithms. It allows us to
efficiently locate a desired value in a data set with upwards of billions of entries.
The file system for a computer has a recursive structure in which directories can be
nested arbitrarily deeply within other directories. Recursive algorithms are widely used to
explore and manage these file systems.
1) The Factorial Function
The factorial of a positive integer n, denoted n!, is defined as the product of the integers from 1 to n. If n = 0,
then n! is defined as 1 by convention. More formally, for any integer n ≥ 0.
The factorial function is used to find the number of ways in which n distinct items can be arranged into a
sequence, that is, the number of permutations of n items. For example, the three characters a, b, and c can be
arranged in 3! = 3 · 2 · 1 = 6 ways: abc, acb, bac, bca, cab, and cba.
A Recursive Implementation of the Factorial Function
Recursion is not just a mathematical notation; we can use recursion to design a Python implementation of a
factorial function, as shown in Code Fragment 4.1
def factorial(n): if n == 0:
return 1 else:
return n*factorial(n−1)
Trace for the factorial function is,
Python Code:
"""Draw one line with given tick length (followed by optional label)."""
if tick_label:
print(line)
def drawinterval(centerlength):
drawinterval(centerlength-1)
drawruler(2,4)
Efficiency of the algorithm calculated as big-Oh to summarize the relationship between the number of
operations and the input size for a problem.
With a recursive algorithm, we will account for each operation that is performed based upon the particular
activation of the function that manages the flow of control at the time it is executed. Stated another way,
for each invocation of the function, we only account for the number of operations that are performed within
the body of that activation. We can then account for the overall number of operations that are executed as
part of the recursive algorithm by taking the sum, over all activations, of the number of operations that take
place during each individual activation
Computing Factorials
It is relatively easy to analyze the efficiency of our function for computing factorials.
Sample recursion trace is,
To compute factorial(n), there are a total of n+1 activations, as the parameter decreases from n in the first
call, to n−1 in the second call, and so on, until reaching the base case with parameter 0. Each individual
activation of factorial executes a constant number of operations. Therefore, the overall number of
operations for computing factorial(n) is O(n), as there are n+1 activations, each of which accounts for O(1)
operations.
In analyzing the English ruler application, the fundamental question of how many total lines of output are
generated by an initial call to draw interval(c), where c denotes the center length. This is a reasonable
benchmark for the overall efficiency of the algorithm as each line of output is based upon a call to the draw
line utility, and each recursive call to draw interval with nonzero parameter makes exactly one direct call to
draw line. Some intuition may be gained by examining the source code and the recursion trace. We know that
a call to draw interval(c) for c > 0 spawns two calls to draw interval(c−1) and a single call to draw line. We
will rely on this intuition to prove the following claim. Proposition 4.1: For c ≥ 0, a call to draw interval(c)
results in precisely 2c − 1 lines of output
Justification:
In fact, induction is a natural mathematical technique for proving the correctness and efficiency of a
recursive process. In the case of the ruler, we note that an application of draw interval (0) generates no
output, and that 20 −1 = 1−1 = 0. This serves as a base case for our claim. More generally, the number of
lines printed by draw interval(c) is one more than twice the number generated by a call to draw
interval(c−1), as one center line is printed between two such recursive calls. By induction, we have that the
number of lines is thus 1+2 ·(2c−1 −1) = 1+2c −2 = 2c −1. This proof is indicative of a more
mathematically rigorous tool, known as a recurrence equation that can be used to analyze the running time
of a recursive algorithm.
Performing a Binary Search
Considering the running time of the binary search algorithm, a constant number of primitive operations are
executed at each recursive call of method of a binary search. Hence, the running time is proportional to the
number of recursive calls performed. The most log n+1 recursive calls are made during a binary search of a
sequence having n elements, leading to the following claim. The binary search algorithm runs in O(logn)
time for a sorted sequence with n elements.
Justification:
Each recursive call the number of candidate entries still to be searched is given by the value high−low+1.
Moreover, the number of remaining candidates is reduced by at least one half with each recursive call.
Specifically, from the definition of mid, the number of remaining candidates is either.
Initially, the number of candidates is n; after the first call in a binary search, it is at most n/2; after the
second call, it is at most n/4; and so on. In general, after the j th call in a binary search, the number of
candidate entries remaining is at most n/2j . In the worst case (an unsuccessful search), the recursive calls
stop when there are no more candidate entries. Hence, the maximum number of recursive calls performed,
is the smallest integer r such that n 2r < 1. In other words (recalling that we omit a logarithm’s base when it
is 2), r > logn. Thus, we have r = logn+1, which implies that binary search runs in O(logn) time
Computing Disk Space Usage
To characterize the “problem size” for our analysis, we let n denote the number of file- system entries in
the portion of the file system that is considered. (For example, the file system portrayed in Figure 4.6 has n
= 19 entries.) To characterize the cumulative time spent for an initial call to the disk usage function, we
must analyze the total number of recursive invocations that are made, as well as the number of operations
that are executed within those invocations.
Intuitively, a call to disk usage for a particular entry ‘e’ of the file system is only made from ‘e’, and that
entry will only be explored once.
The fact that each iteration of that loop makes a recursive call to disk usage, and yet we have already
concluded that there are a total of n calls to disk usage (including the original call). We therefore conclude
that there are O(n) recursive calls, each of which uses O(1) time outside the loop, and that the overall
number of operations due to the loop is O(n). Summing all of these bounds, the overall number of
operations is O(n)