1.1 Introduction
1.1 Introduction
Text Books
Aaron M. Tenenbaum, Yedidyah Langsam and Moshe J.
Augenstein, “Data Structures Using C and C++”, PHI
Learning Private Limited, Delhi India
Horowitz and Sahani, “Fundamentals of Data Structures”,
Galgotia Publications Pvt Ltd Delhi India.
Lipschutz, “Data Structures” Schaum’s Outline Series, Tata
McGraw-hill Education (India) Pvt. Ltd.
Thareja, “Data Structure Using C” Oxford Higher
Education.
Course Outcomes [CO]
CO1: Understand and analyze the time and space
complexity of an algorithm.
CO2 : Understand and implement linear structures such as
Stack, Queue and Linked list.
CO3 : Discuss various tree representation of data and
techniques for its traversals.
CO4 : Understand and implement various graph
representation of data and graph traversal algorithms.
CO5 : Apply various searching and sorting techniques.
Syllabus [Unit-1]
Introduction: Basic Terminology, Elementary Data
Organization, Built in Data Types in C. Algorithm, Efficiency
of an Algorithm, Time and Space Complexity, Asymptotic
notations: Big Oh, Big Theta and Big Omega, Time-Space
trade-off. Abstract Data Types (ADT) Arrays: Definition,
Single and Multidimensional Arrays, Representation of
Arrays: Row Major Order, and Column Major Order,
Derivation of Index Formulae for 1-D,2-D,3-D and n-D Array
Application of arrays, Sparse Matrices and their representations.
Linked lists: Array Implementation and Pointer Implementation
of Singly Linked Lists, Doubly Linked List, Circularly Linked
List, Operations on a Linked List. Insertion, Deletion, Traversal,
Polynomial Representation and Addition Subtraction &
Multiplications of Single variable & Two variables Polynomial.
Data Structure is a way of collecting and organising
data in such a way that we can perform operations on
these data in an effective way.
The idea is to reduce the space and time complexities
of different tasks.
The choice of a good data structure makes it possible to
perform a variety of critical operations effectively.
An efficient data structure also uses minimum memory
space and execution time to process the structure.
Data Structures is about rendering data elements in
terms of some relationship, for better organization and
storage.
If you are aware of Object Oriented
programming concepts, then a class also does the
same thing, it collects different type of data
under one single entity.
In simple language, Data Structures are
structures programmed to store ordered data, so
that various operations can be performed on it
easily.
It should be designed and implemented in such a
way that it reduces the complexity and increases
the efficiency.
Why is Data Structure important?
Data structure is important because it is used in
almost every program or software system.
It helps to write efficient code, structures the code and
solve problems.
Data can be maintained more easily by encouraging a
better design or implementation.
Data structure is just a container for the data that is
used to store, manipulate and arrange. It can be
processed by algorithms.
Basic types of Data Structures
As we have discussed above, anything that can store data can be
called as a data structure, hence Integer, Float, Boolean, Char etc, all
are data structures. They are known as Primitive Data Structures.
Then we also have some complex Data Structures, which are used to
store large and connected data. Some example of Abstract Data
Structure are :
Arrays
Linked List
Tree
Graph
Stack, Queue etc.
All these data structures allow us to perform different operations on
data. We select these data structures based on which type of
operation is required.
Characteristic Description
counter = 0;
for (i = 1; i < = n; i++)
{
if (A[i] == 1)
counter++;
else {
f(counter);
counter = 0;
}
}
The complexity of this program fragment is θ(n)
Asymptotic Notations
When it comes to analyzing the complexity of any algorithm in
terms of time and space, we can never provide an exact number
to define the time required and the space required by the
algorithm, instead we express it using some standard notations,
also known as Asymptotic Notations.
When we analyse any algorithm, we generally get a formula to
represent the amount of time required for execution or the time
required by the computer to run the lines of code of the
algorithm, number of memory accesses, number of
comparisons, temporary variables occupying memory space
etc.
Asymptotic Notations allow us to analyze an algorithm's
running time by identifying its behavior as its input size grows.
This is also referred to as an algorithm's growth rate.
Let us take an example, if some algorithm has a time complexity
of
T(n) = (n2 + 3n + 4), which is a quadratic equation. For large
values of n, the 3n + 4 part will become insignificant compared to
the n2 part.
For n = 1000, n2 will be 1000000 while 3n + 4 will be 3004.
Also, When we compare the execution times of two
algorithms the constant coefficients of higher order terms
are also neglected.
An algorithm that takes a time of 200n2 will be faster than
some other algorithm that takes n3 time, for any value
of n larger than 200. Since we're only interested in the
asymptotic behavior of the growth of the function, the
constant factor can be ignored too.
Types of functions
Logarithmic Function - log n
Linear Function - an + b
Quadratic Function - an2 + bn + c
Polynomial Function - anz + . . . + an2 + an1 + an0, where z is
some constant
Exponential Function - an, where a is some constant
GATE-CS-2021
Consider the following three functions.
f1=10n
f2=nlogn
f3=n√n
Arranges the functions in the increasing order of asymptotic growth rate:
f2<f3<f1
What is Asymptotic Behavior?
The word Asymptotic means approaching a value or curve
arbitrarily closely (i.e., as some sort of limit is taken).
In asymptotic notations, we use the same model to ignore
the constant factors and insignificant parts of an expression,
to device a better way of representing complexities of
algorithms, in a single term, so that comparison between
algorithms can be done easily.
Let's take an example to understand this:
If we have two algorithms with the following expressions
representing the time required by them for execution:
Expression 1: (20n2 + 3n - 4)
Expression 2: (n3 + 100n - 2)
Now, as per asymptotic notations, we should just worry about how
the function will grow as the value of n (input) will grow, and that
will entirely depend on n2 for the Expression 1, and on n3 for
Expression 2. Hence, we can clearly say that the algorithm for
which running time is represented by the Expression 2, will grow
faster than the other one, simply by analyzing the highest power
coefficient and ignoring the other constants(20 in 20n2) and
insignificant parts of the expression(3n - 4 and 100n - 2).
The main idea behind casting aside the less important part is to
make things manageable.
All we need to do is, first analyze the algorithm to find out an
expression to define it's time requirements and then analyse how
that expression will grow as the input (n) will grow.
Types of Asymptotic Notations
We use three types of asymptotic notations to represent the
growth of any algorithm, as input increases:
Big Theta (Θ)
Big Oh(O)
Big Omega (Ω)
Tight Bounds: Theta
When we say tight bounds, we mean that the time complexity
represented by the Big-Θ notation is like the average value or range
within which the actual time of execution of the algorithm will be.
For example, if for some algorithm the time complexity is
represented by the expression 3n 2 + 5n, and we use the Big-Θ
notation to represent this, then the time complexity would be Θ(n 2),
ignoring the constant coefficient and removing the insignificant
part, which is 5n.
Here, in the example above, complexity of Θ(n 2) means, that the
average time for any input n will remain in
between, c1*n2 and c2*n2, where c1, c2 are two constants, thereby
tightly binding the expression representing the growth of the
algorithm.
Let f(n) and g(n) are two nonnegative functions indicating
running time of two algorithms. We say the function g(n) is tight
bound of function f(n) if there exist some positive constants c1,
c2, and n0 such that 0 ≤ c1 g(n) ≤ f(n) ≤ c2 g(n) for all n ≥ n0. It is
denoted as f(n) = Θ (g(n)).
Upper Bounds: Big-O
This notation is known as the upper bound of the algorithm, or a
Worst Case of an algorithm.
It tells us that a certain function will never exceed a specified time for
any value of input n.
The question is why we need this representation when we already
have the big-Θ notation, which represents the tightly bound running
time for any algorithm. Let's take a small example to understand this.
Consider Linear Search algorithm, in which we traverse an array
elements, one by one to search a given number.
In Worst case, starting from the front of the array, we find the
element or number we are searching for at the end, which will lead to
a time complexity of n, where n represents the number of total
elements.
But it can happen, that the element that we are searching
for is the first element of the array, in which case the time
complexity will be 1.
Now in this case, saying that the big-Θ or tight bound time
complexity for Linear search is Θ(n), will mean that the
time required will always be related to n, as this is the right
way to represent the average time complexity, but when we
use the big-O notation, which means that the time
complexity will never exceed n, defining the upper bound,
hence saying that it can be less than or equal to n, which is
the correct representation.
This is the reason, most of the time you will see Big-O
notation being used to represent the time complexity of
any algorithm, because it makes more sense.
Let f(n) and g(n) are two nonnegative functions indicating the
running time of two algorithms. We say, g(n) is upper bound of
f(n) if there exist some positive constants c and n0 such that 0
≤ f(n) ≤ c.g(n) for all n ≥ n0. It is denoted as f(n) = Ο(g(n)).
Lower Bounds: Omega
Big Omega notation is used to define the lower bound of
any algorithm or we can say the best case of any
algorithm.
This always indicates the minimum time required for any
algorithm for all input values, therefore the best case of any
algorithm.
In simple words, when we represent a time complexity for
any algorithm in the form of big-Ω, we mean that the
algorithm will take at least this much time to complete it's
execution. It can definitely take more time than this too.
Let f(n) and g(n) are two nonnegative functions indicating the
running time of two algorithms. We say the function g(n) is
lower bound of function f(n) if there exist some positive
constants c and n0 such that 0 ≤ c.g(n) ≤ f(n) for all n ≥ n0. It is
denoted as f(n) = Ω (g(n)).
ISRO CS 2015
int recursive (int n)
{
if(n == 1)
return (1);
else return (recursive (n-1) + recursive (n-1));
}