Data Structures and Algorithms I Pune University
Data Structures and Algorithms I Pune University
AND
ALGORITHMS – I
[As per the New Syllabus CBCS Pattern 2020-21 of SP Pune University for
[S.Y.B.Sc. (Computer Science), Semester III]
Published by : Mrs. Meena Pandey for Himalaya Publishing House Pvt. Ltd.,
“Ramdoot”, Dr. Bhalerao Marg, Girgaon, Mumbai - 400 004.
Phone: 022-23860170, 23863863; Fax: 022-23877178
E-mail: [email protected]; Website: www.himpub.com
Branch Offices :
New Delhi : “Pooja Apartments”, 4-B, Murari Lal Street, Ansari Road, Darya Ganj,
New Delhi - 110 002. Phone: 011-23270392, 23278631; Fax: 011-23256286
Nagpur : Kundanlal Chandak Industrial Estate, Ghat Road, Nagpur - 440 018.
Phone: 0712-2721215, 3296733; Telefax: 0712-2721216
Bengaluru : Plot No. 91-33, 2nd Main Road, Seshadripuram, Behind Nataraja Theatre,
Bengaluru - 560 020. Phone: 080-41138821; Mobile: 09379847017, 09379847005
Hyderabad : No. 3-4-184, Lingampally, Besides Raghavendra Swamy Matham, Kachiguda,
Hyderabad - 500 027. Phone: 040-27560041, 27550139
Chennai : New No. 48/2, Old No. 28/2, Ground Floor, Sarangapani Street, T. Nagar,
Chennai - 600 017. Mobile: 09380460419
Pune : “Laksha” Apartment, First Floor, No. 527, Mehunpura,
Shaniwarpeth (Near Prabhat Theatre), Pune - 411 030.
Phone: 020-24496323, 24496333; Mobile: 09370579333
Lucknow : House No. 731, Shekhupura Colony, Near B.D. Convent School, Aliganj,
Lucknow - 226 022. Phone: 0522-4012353; Mobile: 09307501549
Ahmedabad : 114, “SHAIL”, 1st Floor, Opp. Madhu Sudan House, C.G. Road, Navrang Pura,
Ahmedabad - 380 009. Phone: 079-26560126; Mobile: 09377088847
Ernakulam : 39/176 (New No. 60/251), 1st Floor, Karikkamuri Road, Ernakulam,
Kochi - 682 011. Phone: 0484-2378012, 2378016; Mobile: 09387122121
Cuttack : New LIC Colony, Behind Kamala Mandap, Badambadi,
Cuttack - 753 012, Odisha. Mobile: 09338746007
Kolkata : 108/4, Beliaghata Main Road, Near ID Hospital, Opp. SBI Bank,
Kolkata - 700 010. Phone: 033-32449649; Mobile: 07439040301
DTP by : Sudhakar Shetty (On behalf of HPH Pvt. Ltd.)
Printed at : Geetanjali Press Pvt. Ltd., Nagpur. (On behalf of HPH Pvt. Ltd.)
DEDICATION
I am extremely happy to come out with this book on ‘Data Structures’ for the students. The
primary goal for this book is to acquaint students with fundamental concepts of Data Structures.
First and foremost, I express my sincere thanks to my Parents who showed me the right path
in my career which enabled me to bring forth this book.
It is with due credit that I have to thank my husband Mr. Vishwas and daughters Aarohi,
Vaidehi and son Soham who supported and motivated me to bring out this book.
I cannot think that this book would have been possible without the inputs from my beloved
Principal Dr. Aruna Deoskar (ATSS-CBSCA) and Management members for giving me this
opportunity.
I am indebted to the Himalaya Publishing House Pvt. Ltd. and Mr. S.K. Srivastav and team
for the efforts they have put in making this book a reality and helping in the neat execution of the
text.
Mrs. Vinaya Vishwas Keskar
I am very happy to introduce this book on ‘Data Structures’ for the students. I would like to
thank the Almighty for empowering me with the knowledge and capability to write this book.
The book adheres to the revised syllabus of Data Structures of S.Y.B.Sc. (Computer
Science), Sem. III and has been intended to make students aware of the concepts in easy language,
using diagrams and illustrative examples.
I would like to dedicate this book to my Parents with love and gratitude. I am thankful to my
husband and my daughters for their seamless cooperation and support.
Finally, I take this opportunity to express my deep sense of gratitude towards Mr. S.K.
Srivastav and the entire team of Himalaya Publishing House Pvt. Ltd.
Prof. Rupali Deshpande
PREFACE
We take this opportunity to present this book on “Data Structures and Algorithms – I” to
the Second Year students of B.Sc. (Computer Science) Semester - III as per the new syllabus of
SPPU, June 2019-20.
The main intention of presenting this book to student community is to familiarize them with
the basic concepts of data structures. The book has five chapters which focus on fundamental
concepts of data structures, ADTs, sorting and searching techniques, linear and non-linear data
structures like stack, queues, etc. Each chapter includes explanatory diagrams, examples and
ample practicing questions.
We have tried to give our best inputs to this book. Any suggestions and comments are
always welcome towards the improvement of this book.
– Authors
SYLLABUS
Structure:
1.1 Introduction
1.1.1 Concept
1.1.2 Data Type, Data Object, ADT, Data Structure
1.1.3 Need of Data Structure
1.1.4 Types of Data Structure
1.2 Algorithm Analysis
1.2.1 Space Complexity, Time Complexity
1.2.2 Best, Worst, Average Case Analysis, Asymptotic Notation (Big O, Omega , Theta Ĭ)
1.1 Introduction
1.1.1 Concept
Data is nothing but a collection of numbers, alphabets and symbols combined to represent
information.
Data Structures are defined as the group of data elements which provides an efficient way of
storing and organising data in the computer so that it can be used efficiently. Examples of Data
Structures are arrays, Linked List, Stack, Queue, etc. In Computer Science data structures are
used in Operating System, Compiler Designing, Artificial intelligence, Graphics and many other
areas.
Data Structures are the main part of many computer science algorithms as they enable the
programmers to handle the data in an efficient way. They improve performance of a software or a
program by enabling the software to store and retrieve the user’s data efficiently.
Basic Terminology
Data structures form the main components of any program or software. A programmer has
to choose the appropriate data structure for his program. Let us see some basic terminology used
in relation to data structures.
Data: It is defined as an elementary value or the collection of values, for example,
employee’s name and his ID are the data about the employee.
Group Items: Data items which have sub data items are called Group item, for example,
name of an employee can have first name and the last name.
Record: It is the collection of various data items, for example, if we talk about the
employee’s entity, then name, address, department, date of joining and salary can be grouped
together to form the record for the employee.
2 Data Structures and Algorithms – I
File: A File is a collection of various records of one type of entity, for example, if there are
60 students in the class, then there will be 20 records in the related file where each record contains
the data about each student.
Attribute and Entity: An entity is something that represents the class of certain objects. It
may have various attributes, where each attribute represents the particular property of that entity.
Field: This is a single elementary unit of information that represents the entity’s attribute.
1.1.2 Data Type
A data type is a term which refers to the kinds of data that variables may “hold” in a
programming language. To represent data in computers, basic data types supported by most
programming languages are integer numbers (of varying sizes), Floating-point numbers (which
approximate real numbers), characters and Booleans.
Data types actually define the operations that can be performed on the data, the meaning of
the data, and what type of values can be stored. Data types are generally classified as follows:
Primitive Data Types
z These are the basic data types provided by a programming language as a basic building
block. Complicated, composite data types are constructed from primitive data types.
z These are also called as built-in types, for which many programming languages provide
built-in support.
For example,‘C’ language has primitive data types int, float, char, long and short and
double. In Java, primitive data types are byte, short, int, long, float, double, char and Boolean.
Composite Data Types
Composite data types are derived from more than one primitive type. A primitive type can
be derived to form a compound type which is generally a new data type, for example, array-of-
integer is a different type to integer.
z An array (also called vector, list, or sequence) stores a number of elements of same data
type and provides random access to individual elements. Arrays can be of fixed-length or
variable length.
z Record/tuples are among the simplest data structures. A record is a value that contains
other values, in fixed number and sequence and typically indexed by names. The
elements of records are usually called fields or members.
z Union. A union type may have a number of permitted primitive types stored in its
instances, e.g., “float or long integer”. Unlike a record, which may contain a float and an
integer; in union, there is only one type allowed at a time.
z In object oriented languages, an object contains various data fields, like a record, and one
or more subroutines for accessing or modifying them, called methods.
Data Object
A data object is a region of storage that contains a value or group of values. Each value can
be accessed using its identifier or a more complex expression that refers to the object. In addition,
each object has a unique data type. In object oriented languages, the individual class members are
also called objects.
Introduction to Data Structures and Algorithm Analysis 3
Definition: A data object represents a container for data values, a place where data values
may be stored and later retrieved.
Data objects can be of two types:
1. Programmer-defined: These are available at program execution. For example, variables,
constant, arrays, files etc.
2. System defined: These are not directly accessible to programmer. Examples are run time
storage like stacks, file buffers, free space lists.
Data values can be a single number or a pointer to other objects and characters.
Data object is usually represented as storage in computer memory and a data value is
represented by a pattern of bits. So we can represent the relation between data object and data
value as shown below.
3. Abstraction: Data structure is specified by the ADT which provides a level of abstraction.
Good example of abstraction is a client program which accesses the data structure
through interface only, without getting into the implementation details.
1.1.4 Types of Data Structures
There are various types of data structures based on certain classifications, which are shown
in the diagram below. Two main categories are primitive and non-primitive data structures.
Let’s understand one by one in brief in this chapter, each of which will be studied in detail in
further chapters.
Primitive Data Structures
Primitive Data Structures are the basic data structures that directly operate upon the machine
instructions. These may be represented differently on different computers. Integers, Floating
point numbers, Character constants, String constants and Pointers come under this category.
Non-primitive Data Structures
These data structures are more complicated data structures and are derived from primitive
data structures. Here same or different data items are grouped with relationship between each data
item. Arrays, Lists and Files come under this category.
Data Structures
Primitive Non-Primitive
Data Structures Data Structures
Float Chat
Integer Pointers
Linear Non-linear
Fig. 1.2
Linear Data Structures: Here all of its elements are arranged in the linear order. In linear
data structures, the elements are stored in non-hierarchical way where each element has the
successors and predecessors except the first and last element.
6 Data Structures and Algorithms – I
sum, then we will divide that sum by the number of subjects, i.e., 6, in order to find the
average.
2. Insertion: Insertion can be defined as the process of adding the elements to the data
structure at any location. If the size of data structure is n then we can only insert n-1 data
elements into it.
3. Deletion: The process of removing an element from the data structure is called Deletion.
We can delete an element from the data structure at any random location. If we try to
delete an element from an empty data structure then this situation is called as underflow.
4. Searching: The process of finding the location of an element within the data structure is
called Searching.
5. Sorting: is a process of arranging the data structure in a specific order. There are various
algorithms that can be used to perform sorting like insertion sort, selection sort, bubble
sort, etc.
6. Merging: When two lists List A and List B of size m and n respectively, of similar type
of elements are clubbed or joined to produce the third list, List C of size (m+n), then this
process is called merging.
1.2 Algorithm – Definition and Characteristics
Often computer science is viewed as a study of algorithms. This study covers four distinct
areas:
(i) Machines for executing algorithms — This area includes everything from the smallest
pocket calculator to the largest general-purpose digital computer.
(ii) Languages for describing algorithms — Languages can be closest to the physical
machine and at the other end are languages designed for sophisticated problem solving.
(iii) Foundations of algorithms — This includes checking whether a particular task is
accomplishable by a computing device; or what is the minimum number of operations
necessary for any algorithm which performs a certain function?
(iv) Analysis of algorithms — An algorithm’s behaviour pattern or performance profile is
measured in terms of the computing time and space that are consumed while the
algorithm is processing. Finding the worst and average time and how often they occur are
typical.
Definition: An algorithm is a finite set of instructions which, if followed, accomplish a
particular task.
Characteristics of an Algorithm
Not all procedures can be called an algorithm. An algorithm should have the following
characteristics:
1. Unambiguous/definiteness: An algorithm should be clear and unambiguous. Each of its
steps (or phases), and their inputs/outputs should be clear and must lead to only one
meaning.
2. Input: An algorithm should have 0 or more well-defined inputs which are externally
supplied.
3. Output: An algorithm should have 1 or more well-defined outputs, and should match the
desired output.
8 Data Structures and Algorithms – I
Solution Solution
#1 #2
Problem
Solution Solution
#3 #4
Fig. 1.3
Hence, many solution algorithms can be derived for a given problem. The next step is to
analyse those proposed solution algorithms and implement the best suitable solution.
1.2.1 Space Complexity, Time Complexity
Algorithm Analysis
Efficiency of an algorithm can be analysed at two different stages, before implementation
and after implementation. They are the following:
1. Priori Analysis: Priori analysis consist of checking the algorithm before its
implementation. The efficiency of an algorithm is measured by assuming that factors like
processor speed, are constant and have no effect on the implementation. This is done
usually by the algorithm designer. It is in this method, that the Algorithm Complexity is
determined.
2. Posterior Analysis: Posterior analysis means checking the algorithm after its
implementation. In this, the algorithm is checked by implementing it in any programming
language and executing it. This gives actual analysis report about correctness, space
required, and time consumed etc.
Algorithm analysis deals with the execution or running time of various operations involved.
The running time of an operation can be defined as the number of computer instructions executed
per operation.
Algorithm Complexity
Suppose X is an algorithm and n is the size of input data, the time and space used by the
algorithm X are the two main factors, which decide the efficiency of X.
z Time Factor: Time is measured by counting the number of key operations such as
comparisons in the sorting algorithm.
z Space Factor: Space is measured by counting the maximum memory space required by
the algorithm.
10 Data Structures and Algorithms – I
The complexity of an algorithm f(n) gives the running time and/or the storage space
required by the algorithm in terms of n as the size of input data.
Space Complexity
One needs to find the amount of memory space required by the algorithm during its life
cycle. This represents Space complexity of an algorithm. The space required by an algorithm is
equal to the sum of the following two components –
z A fixed part: This is a space required to store certain data and variables, that are
independent of the size of the problem. For example, simple variables and constants used,
program size, etc.
z A variable part: This is a space required by a variable whose size depends on the size of
the problem. For example, dynamic memory allocation, recursion stack space, etc.
Let us denote Space complexity of any algorithm P by S(P). Then S(P) = C + SP(I), where
C is the fixed part and SP(I) is the variable part of the algorithm, which depends on instance
characteristic I. Let us have a simple example to explain the concept:
Algorithm: To find the sum of two numbers, say A and B
SUM(A, B)
1. START
2. C ĸ A + B + 10
3. Stop
There are three variables A, B, and C and one constant in the above algorithm. Thus, S(P) =
1 + 3. The space depends on data types of given variables and constant types and it will be
multiplied accordingly.
Time Complexity
While solving any problem using a program, there can be infinite number of solutions. For
example, consider two different algorithms to find square of a number as shown below. First
solution runs a loop for n times, starting with the number n and adding n to it, every time.
for i=1 to n
do n=n*n; return n*n;
return n;
Program 1 (Running a loop n times) Program 2 (Using * to find the square)
z For example, in the Program 1 given above, the loop will run n number of times, so the
time complexity will be n at least and as the value of n will increase, the time taken will
also increase.
z While for the second code, time complexity is constant, because it will never be
dependent on the value of n, it will always give the result in 1 step.
z Since the algorithm’s performance may vary with different types of input data, hence for
an algorithm we usually use the worst-case Time complexity of an algorithm because that
is the maximum time taken for any input size.
z Thus, Time complexity of an algorithm represents the amount of time required by the
algorithm to run to completion. Let us define time requirements as a numerical function
T(n), where T(n) can be measured as the number of steps, provided each step consumes
constant time.
z For example, addition of two n-bit integers takes n steps. As a result, the total
computational time is T(n) = c n, where c is the time taken for the addition of two bits.
Here, we observe that T(n) grows linearly as the input size increases.
1.2.2 Best, Worst, Average Case Analysis, Asymptotic Notation (Big O, Omega , Theta Ĭ)
Asymptotic analysis of an algorithm refers to defining the mathematical bounding/framing
of its run-time performance. With the help of asymptotic analysis, one can very well conclude the
best case, average case, and worst-case scenario of an algorithm.
If there’s no input to the algorithm, it is concluded to work in a constant time. Hence
asymptotic analysis is input bound. Other than the “input” all other factors are considered
constant.
Asymptotic analysis is nothing but to compute the running time of any operation in
mathematical units of computation. For example, the running time of one operation is computed
as f(n) and may be for another operation it is computed as g(n2). This means the first operation’s
running time will increase linearly with the increase in n and the running time of the second
operation will increase exponentially as n increases. Similarly, the running time of both
operations will be nearly the same if n is significantly small.
Usually, the time required by an algorithm falls under three types –
z Best Case: Here we analyse the performance of an algorithm for the input, for which the
algorithm takes less time or space.
z Worst Case: Here we analyse the performance of an algorithm for the input, for which
the algorithm takes long time or space.
z Average Case: Here we analyse the performance of an algorithm for the input, for which
the algorithm takes time or space that lies between best and worst case.
Asymptotic notations are the mathematical notations used to describe the running time of an
algorithm when the input tends towards a particular value or a limiting value.
z For example: In bubble sort, when the input array is already sorted, the time taken by the
algorithm is linear, i.e., the best case.
z But, when the input array is in reversed, the algorithm takes the maximum time (quadratic)
to sort the elements this is the worst case.
z When the input array is neither sorted nor in reverse order, then it takes average time.
12 Data Structures and Algorithms – I
These durations are denoted using asymptotic notations. There are mainly three asymptotic
notations used to calculate the running time complexity of an algorithm: Theta notation, Omega
notation and Big-O notation.
z Big-O Notation (ȅ) – Big O notation specifically describes worst case scenario.
z Omega Notation (ȍ) – Omega(ȍ) notation specifically describes best case scenario.
z Theta Notation (ș) – This notation represents the average complexity of an algorithm.
Big-O Notation, ȅ
Big O notation specifically describes worst case scenario. It represents the upper bound
running time complexity of an algorithm. Below we give few examples to understand how we
represent the time and space complexity using Big O notation.
g(n)
f(n)
f(n)
g(n)
k k
O(n)
Big O notation O(N) represents the complexity of an algorithm, whose performance will
grow linearly (in direct proportion) to the size of the input data.
For example, the execution time will depend on the size of array. When the size of the array
increases, the execution time will also increase in the same proportion (linearly).
Traversing an array
Introduction to Data Structures and Algorithm Analysis 13
O(n^2)
Big O notation O(n^2) represents the complexity of an algorithm, whose performance is
directly proportional to the square of the size of the input data.
O(n^2)example
Traversing a 2D array
Other examples: Bubble sort, insertion sort and selection sort algorithms
Similarly, there are other Big O notations such as: logarithmic growth O(logn), log-linear
growth O(nlogn), exponential growth O(2^n) and factorial growth O(n!).The performance of
algorithms denoted by these notations can be compared as:
O(1)< O(log n)< O (n)< O(n log n)< O(n^2)< O (n^3)< O(2^n)< O(n!)
Omega Notation, ȍ
Omega notation ȍ(n) particularly describes best case scenario. It represents the lower bound
running time complexity of an algorithm. So if we represent a complexity of an algorithm in
Omega notation, it means that the algorithm cannot be completed in less time than this, it
would at least take the time represented by Omega notation or it can take more (when not in best
case scenario). Refer to figure 5 shown above.
For example, for a function f(n)
ȍ(f(n)) { g(n): there exists c > 0 and n0 such that g(n) c.f(n) for all n > n0}
Theta Notation, ș
Theta notation describes both upper bound and lower bound of an algorithm. Thus, it defines
exact asymptotic behaviour. In the real case scenario the algorithm not always run on best and
worst cases, the average running time lies between best and worst and can be represented by the
theta notation.
Complexity
Input data
The notation ș(n) is the formal way to express both the lower bound and the upper bound of
an algorithm’s running time. It is represented as follows:
ș(f(n)) = { g(n) if and only if g(n) = ȅ(f(n)) and g(n) = ȍ(f(n)) for all n > n0. }
QUESTIONS
Practice Questions (2 marks)
1. What is data structure?
2. How to calculate count of Best, Worst and Average case?
3. What are the different types of data structures?
4. Explain the following:
(a) Data object
(b) Primitive and non-primitive data structures
(c) Composite data types
5. What are Abstract Data types?
6. What is Big-O notation?
7. Explain different types of asymptotic notations in detail.
8. Explain different types of dynamic memory allocation functions.
9. How to measure performance of an algorithm?
10. What is Space and Time Complexity?
Practice Questions (4 marks)
1. Explain various types of asymptotic notations in detail.
2. What is an algorithm? Explain its characteristics.
3. Explain types of data structures.
2 ARRAY AS A DATA STRUCTURE
Structure:
ADT of Array, Operations
2.1 Searching Linear, Binary, Sentinel, Analysis and Comparison
2.2 Sorting Terminology – Internal, External, Stable, In-place Sorting
2.2.1 Comparison Based Sorting Lower Bound on Comparison Based Sorting, Methods-
Bubble Sort, Insertion Sort, Selection Sort, Quick Sort, Merge Sort
2.2.2 Non Comparison Based Sorting Radix Sort, Counting Sort
2.2.3 Comparison of Sorting Method
There are two popular search methods that are widely used in order to search some item into
the list. However, choice of the algorithm depends upon the arrangement of the list.
z Linear Search
z Binary Search
Linear Search
Linear search is the simplest search algorithm and often called sequential search. In this type
of searching, we simply traverse the list completely and match each element of the list with the
item whose location is to be found. If the match found then location of the item is returned
otherwise the algorithm return NULL.
Linear search is generally used to search an unordered list where the items are not sorted.
The algorithm of linear search is given as follows.
Algorithm
Linear_search(a, n, val)
1: [initialize] set pos = -1
2: [initialize] set i = 1
3: repeat step 4 while i<=n
4: if a[i] = val
set pos = I
print pos, go to step 6
[end of if]
set i = i + 1
[end of loop]
5: if pos = -1
print “ value is not present in the array “
[end of if]
6: exit
Complexity of Algorithm
Time Comlexity: Best case: O(1), worst case: O(n), Worst Case: O(n)
Space Complexity: Worst case: O(1)
C Program: Linear Search
#include<stdio.h>
void main ()
{
int a[10] = {10, 23, 40, 1, 2, 0, 14, 13, 50, 9};
scanf(“%d”,&item);
for (i = 0; i< 10; i++)
{
if(a[i] == item)
{
flag = i+1;
break;
}
else
flag = 0;
}
if(flag != 0)
printf(“Item found at location %d\n”,flag);
else
printf(“\n Item not found\n”);
}
Output:
Enter Item which is to be searched
20
Item not found
Enter Item which is to be searched
23
Item found at location 2
Binary Search
z Binary search technique works efficiently on the sorted lists. Hence, in order to search an
element into some list by using binary search technique, we must ensure that the list is
sorted.
z Binary search follows divide and conquer approach in which, the list is divided into two
halves and the item is compared with the middle element of the list.
z If the match is found then, the location of middle element is returned, otherwise we
search into either of the halves depending upon the result produced through the match.
Binary search algorithm is given below.
BINARY_SEARCH(A, lower_bound, upper_bound, VAL)
Step 1: [initialize] set beg = lower_bound
end = upper_bound, pos = - 1
Step 2: repeat steps 3 and 4 while beg <=end
Step 3: set mid = (beg + end)/2
Step 4: if a[mid] = val
set pos = mid
print pos
go to step 6
else if a[mid] >val
set end = mid - 1
else
set beg = mid + 1
[end of if]
[end of loop]
Step 5: if pos = -1
print “value is not present in the array”
[end of if]
Step 6: exit
Example of Binary Search Algorithm
Consider an example array arr = {1, 5, 7, 8, 13, 19, 20, 23, 29}. Find the location of the item
23 in the array.
In First Step:
BEG = 0
END = 8
MID = 4
a[mid] = a[4] = 13 < 23, therefore
In Second Step:
Beg = mid +1 = 5
End = 8
22 Data Structures and Algorithms – I
mid = 13/2 = 6
a[mid] = a[6] = 20 < 23, therefore;
In Third Step:
beg = mid + 1 = 7
End = 8
mid = 15/2 = 7
a[mid] = a[7]
a[7] = 23 = item;
Therefore, set location = mid;
The location of the item will be 7.
Following figure explains the binary search algorithm for this example.
Return location 7
Fig. 2.2
Array as a Data Structure 23
Output:
Enter the item which you want to search
19
Item found at location 2
Binary Search Algorithm searches an element by comparing it with the middle most element
of the array.
Then, following three cases are possible –
Case-1: If the element is found to be the middle most element, its index is returned.
Case-2: If the element is found to be greater than the middle most element, then its search is
further continued in the right sub array of the middle most element.
24 Data Structures and Algorithms – I
Case-3: If the element is found to be smaller than the middle most element, then the search is
further continued in the left sub array of the middle most element.
The search keeps on repeating on the sub arrays until the desired element is found or size of
the sub array reduces to zero.
Time Complexity Analysis of Binary Search –
Binary Search time complexity analysis is done below –
z In each iteration or in each recursive call, the search gets reduced to half of the array.
z So, for n elements in the array, there are log2n iterations or recursive calls.
Thus, we have – Time Complexity of Binary Search Algorithm is O(log2n). Here, n is the
number of elements in the sorted linear array. This time complexity of binary search remains
unchanged irrespective of the element position even if it is not present in the array.
Time Complexity: Worst case: O(log n), Best case: O(1), Average case: O(log n)
Worst case space complexity: O(1)
Sentinel Search
Sentinel Linear Search of Linear Search where the number of comparisons are reduced as
compared to a traditional linear search. When a linear search is performed on an array of size N
then in the worst case:
1. A total of N comparisons are made when the element to be searched is compared to all
the elements of the array.
2. and (N + 1) comparisons are made for the index of the element to be compared so that the
index is not out of bounds of the array.
The number of comparisons can be reduced in a Sentinel Linear Search. Here, the last
element of the array is replaced with the element to be searched and then the linear search is
carried out on the array without checking whether the current index is inside the index range of
the array, because the element to be searched will definitely be found inside the array even if it
was not present in the original array since the last element got replaced with it. So, the index to be
checked will never be out of bounds of the array. The number of comparisons in the worst case
here will be (N + 2).
Examples:
Input: arr[] = {10, 20, 180, 30, 60, 50, 110, 100, 70}, x = 180
Output: 180 is present at index 2
Input: arr[] = {10, 20, 180, 30, 60, 50, 110, 100, 70}, x = 90
Output: Not found
C Program: Sentinel Search
#include <iostream>
void sentinelSearch(int arr[], int n, int x) // Function to search x in the given array
{
int last = arr[n - 1]; // Last element of the array
arr[n - 1] = x; // Element to be searched is placed at the last index
int i = 0;
Array as a Data Structure 25
while (arr[i] != x)
i++;
arr[n - 1] = last; // Put the last element back
if ((i< n - 1) || (x == arr[n - 1]))
printf( “%d is present at index %d “,x,i);
else
printf(“Not found”);
}
//-----------------------------------------------------------------
int main()
{
int arr[] = {10, 20, 180, 30, 60, 50, 110, 100, 70};
int n = sizeof(arr) / sizeof(arr[0]);
int x = 30;
sentinelSearch(arr, n, x);
return 0;
}
Output:
30 is present at index 3.
Time Complexity Analysis of Linear Search
Best Case –
z The element being searched may be found at the first position.
z In this case, the search terminates in success with just one comparison.
z Thus in best case, linear search algorithm takes O(1) operations.
Worst Case –
The element being searched may be present at the last position or not present in the array at
all.
z In the former case, the search terminates in success with n comparisons.
z In the later case, the search terminates in failure with n comparisons.
z Thus, in worst case, linear search algorithm takes O(n) operations.
Thus, time Complexity of Linear Search Algorithm is O(n),where n is the number of
elements in the linear array.
Linear Search Efficiency
Linear Search is less efficient when compared with other algorithms like Binary Search &
Hash tables.
The other algorithms provide faster searching mechanism.
26 Data Structures and Algorithms – I
Fig. 2.3: Linear Search to find the Element “J” in a given Sorted List from A-X
Fig. 2.4: Binary Search to find the Element “J” in a given Sorted List from A-X
1. Linear search is iterative in nature and uses sequential approach. Whereas, binary search
implements divide and conquer approach.
2. The time complexity of linear search is O(N) ,while binary search has O(log2N).
3. The best case time in linear search is for the first element, i.e., O(1). As against, in binary
search, it is for the middle element, i.e., O(1).
4. In the linear search, worst case for searching an element is N number of comparisons. In
contrast, it is log2N number of comparisons for binary search.
28 Data Structures and Algorithms – I
5. Linear search can be implemented in an array as well as in linked list, whereas binary
search cannot be implemented directly on linked list.
6. As binary search requires the sorted array, it requires inserting a new element at proper
place to maintain a sorted list. Linear search does not require sorted elements, so elements
are easily inserted at the end of the list.
7. Linear search is easy to use, and there is no need for any ordered elements. Binary search
algorithm is bit tricky, and elements need to be necessarily arranged in order.
2.2 Sorting Terminology
A sorting algorithm puts elements of a list in a certain order. The most frequently used
orders are numerical order and lexicographical order. Efficient sorting is important for optimizing
the efficiency of other algorithms (such as search and merge algorithms) that require input data to
be in sorted lists. More formally, the output of any sorting algorithm must satisfy two conditions:
1. The output is in non-decreasing order (each element is no smaller than the previous
element).
2. The output is a permutation, which is nothing but retaining all of the original elements
after reordering of the input.
Sorting algorithms are classified on the basis of various characteristics like computational
complexity, memory usage, recursion, stability.
Internal Sort
In internal sorting, the sorting process takes place entirely within the main memory of a
computer. It is used when the data to be sorted is small enough to all be held in the main memory.
For sorting larger datasets, it may be necessary to hold only a chunk of data in memory at a
time, since main memory is insufficient to hold large datasets. The rest of the data is normally
held on some larger, but slower secondary storage device, like a hard-disk. Reading or writing of
data to and from this slower media can slow down the sorting process considerably.
External Sort
When data that we want to sort cannot be placed in-memory at a time, the sorting is called
external sorting. Data is stored on some external storage like hard-disk, CD. External Sorting is
used for huge or massive amount of data. MergeSort and its variations are generally used for
external sorting.
Stable Sorting
The algorithms that use stable sorting maintain the relative order of records with equal keys
(i.e., values). That is, a sorting algorithm is stable, if whenever there are two records R and S with
the same key and with R appearing before S in the original list, R will appear before S in the
sorted list.
In other words, a stable sort preserves the original order of input set, where the comparison
algorithm does not distinguish between two or more items. A stable sort ensures that the original
order of data having the same rank is preserved in the output.
Some sorting algorithms are stable by nature like Insertion Sort, Merge Sort and Bubble Sort
etc. Sorting algorithm like Quick Sort, Heap Sort etc. are not stable. Following figure illustrates
the stable and unstable sorting.
Array as a Data Structure 29
Fig. 2.5
In-Place Algorithm
A special feature of in-place algorithm is that it does not require an extra space because it
produces an output in the same memory area that contains the data by transforming the input ‘in-
place’. A small constant extra space used for variables is allowed.
1. An ‘in-place’ sorting algorithm directly modifies the list that is received as input instead
of creating a new list.
2. This sorting uses a small amount of extra space to manipulate the input data. In other
words, the output is placed in the correct position while the algorithm is still executing,
which means that the input will be overwritten by the desired output on run-time.
3. In-Place sorting algorithm updates input only through replacement or swapping of
elements.
4. An algorithm which is not in-place is sometimes called not-in-Place or out of Place.
5. An algorithm can only have a constant amount of extra space, counting everything
including function call and pointers. Usually, this space is O(log n).
2.2.1 Comparison Based Sorting
Any sorting problem can be represented as following:
Input: A sequence of n numbers <a1, a2, . . . , an>.
Output: A permutation (reordering) <s1,s2,…..sn> of the input sequence such that
s1<=s2…<= sn.
z A comparison based sorting algorithm uses comparison operators to find the order
between two numbers.
z Comparison sorts can be viewed abstractly in terms of decision trees. A decision tree is a
full binary tree that represents the comparisons between elements that are performed by a
particular sorting algorithm operating on an input of a given size.
z The execution of the sorting algorithm corresponds to tracing a path from the root of the
decision tree to a leaf.
z At each internal node, a comparison ai <= aj is made. The left subtree then dictates
subsequent comparisons for ai <= aj, and the right subtree dictates subsequent
comparisons for ai > aj. When it comes to a leaf, the algorithm has established the
ordering.
z Algorithms merge-sort, quicksort, and heap-sort take an input array and sort the elements
in ascending order in O(n log n) time.
30 Data Structures and Algorithms – I
Consider the problem to sort n distinct numbers. Any sorting algorithm must be able to
distinguish between the n! permutations of these n numbers, since it must treat each permutation
differently in order to sort it.
The number of yes/no decisions (= the number of bits) necessary to distinguish between the
n! permutations is log2(n!); it is a lower bound for the complexity of any sorting algorithm. This
lower bound is known as the information theoretic lower bound.
We have,
n! >= (n/2)n/2
therefore,
log(n!) >= log((n/2)n/2)
= n/2. log(n/2)
€ (n log (n))
Array as a Data Structure 31
A comparison based sorting algorithm derives exactly 1 bit of information from each
comparison (based on whether the two numbers compared are in order or not). Thus, it needs at
least log(n!) ¼ȍ(n log(n)) comparisons to distinguish between the n! possible permutations of n
distinct numbers.
This means that ȍ(n log(n)) is a lower bound for the time complexity of any sorting
algorithm that is based on comparisons.
Sorting Methods
z Bubble Sort
z Insertion Sort
z Merge Sort
z Quick Sort
z Selection Sort
Note:
1. Bubble sort, insertion sort, and selection sort are in-place sorting algorithms, because only
swapping of the element in the input array is required.
2. Bubble sort and insertion sort can be implemented as stable algorithms but selection sort
cannot be (without significant modifications).
3. Merge sort is a stable algorithm but not an in-place algorithm. It requires extra array
storage.
4. Quicksort is not stable but is an in-place algorithm.
Bubble Sort
Bubble sort also called as sinking sort, is a simple sorting algorithm where, each element of
the array is compared with its adjacent element. The algorithm sorts the list in passes. A list with
n elements requires n-1 passes for sorting.
Bubble sort is a comparison sort, the word ‘bubble’ indicates the way the smaller or larger
elements bubble up to the top of the list. If we have an array A of n elements whose elements are
to be sorted using Bubble sort. The algorithm processes in passes as follows.
z Pass 1
A[0] is compared with A[1], A[1] is compared with A[2], A[2] is compared with A[3]
and so on. At the end of pass 1, the largest element of the list is placed at the highest
index of the list.
z Pass 2
A[0] is compared with A[1], A[1] is compared with A[2] and so on. At the end of Pass 2
the second largest element of the list is placed at the second highest index of the list.
z Pass n-1
A[0] is compared with A[1], A[1] is compared with A[2] and so on. At the end of this
pass. The smallest element of the list is placed at the first index of the list.
Below given figure illustrates working of Bubble Sort:
32 Data Structures and Algorithms – I
Fig. 2.7
{
for(j = i+1; j<10; j++)
{
if(a[j] > a[i])
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
printf(“SortedList ...\n”);
for(i = 0; i<10; i++)
printf(“%d\n”,a[i]);
}
Output:
Sorted List . . .
7
9
10
12
23
34
34
44
78
101
z Worst and average case time complexity is O(n*n). Worst case occurs when array is
sorted in reverse order.
z Sorting In Place: Yes
z Stable: Yes
To summarize:
Scenario Complexity
Space O(1)
Worst case running time O(n2)
Average case running time O(n)
Best case running time O(n2)
z Because of its simplicity, bubble sort is mostly used to introduce the concept of a sorting
algorithm to the beginners.
z In computer graphics it is popular for its capability to detect a very small error (like swap
of just two elements) in almost-sorted arrays and fix it with just linear complexity (2n).
Like, it is used in a polygon filling algorithm, where polygon boundary lines are sorted by
their x coordinate at a specific scan line (a line parallel to x axis) and with incrementing y
their order changes (two elements are swapped) only at intersections of two lines.
z Although simplicity is its main feature, bubble sort is not a practical sorting algorithm.
This is because many other sorting algorithms have substantially better worst-case or
average complexity, often O(n log n). Even other Ɉ(n2) sorting algorithms, such as
insertion sort, generally run faster than bubble sort, and are no more complex.
Insertion Sort
Insertion sort is a simple sorting algorithm that builds the final sorted array (or list) one item
at a time. It is also much less efficient on large lists than more advanced algorithms such as
quicksort, heapsort, or merge sort. However, insertion sort provides several advantages:
1. Simple implementation technique.
2. It is efficient for smaller data sets, much like other quadratic sorting algorithms.
3. Also more efficient in practice than most other simple quadratic (i.e., O(n2)) algorithms
such as selection sort or bubble sort.
4. Adaptive, i.e., efficient for data sets that are already substantially sorted: the time
complexity is O(kn) when each element in the input is no more than k places away from
its sorted position.
5. Stable, i.e., does not change the relative order of elements with equal keys.
6. In-place, i.e., only requires a constant amount O(1) of additional memory space
7. Online, i.e., can sort a list as it receives it.
In the Bridge cards game where the players manually sort, a method similar to insertion sort
is used.
Array as a Data Structure 35
a[j+1] = a[j];
j = j-1;
}
a[j+1] = temp;
}
for(i=0;i<10;i++)
printf(“\n%d\n”,a[i]);
}
z The merge() function is used for merging two halves. The merge(arr, l, m, r) is key
process that assumes that arr[l..m] and arr[m+1..r] are sorted and merges the two sorted
sub-arrays into one.
Algorithm: Merge Sort
MergeSort(arr[], l, r)
If r > l
1. Find the middle point to divide the array into two halves:
middle m = (l+r)/2
2. Call mergeSort for first half:
Call mergeSort(arr, l, m)
3. Call mergeSort for second half:
Call mergeSort(arr, m+1, r)
4. Merge the two halves sorted in step 2 and 3:
Call merge(arr, l, m, r)
The following diagram shows the complete merge sort process for an example array {38, 27,
43, 3, 9, 82, 10}. If we take a closer look at the diagram, we can see that the array is recursively
divided in two halves till the size becomes 1. Once the size becomes 1, the merge processes
comes into action and starts merging arrays back till the complete array is merged.
Fig. 2.8
38 Data Structures and Algorithms – I
Output:
Given array is
12 11 13 5 6 7
Sorted array is
5 6 7 11 12 13
Quick Sort
Like Merge Sort, Quick Sort is a Divide and Conquer algorithm. It picks an element as pivot
element and partitions the given array around it. There are many different ways to select pivot
element.
1. Always first element is selected as pivot.
2. Always last element is selected as pivot.
3. Select a random element as pivot.
4. Select a median as pivot.
The key process in quickSort is function partition(). Target of partitions is, given an array
and an element x of array as pivot, put x at its correct position in sorted array and put all smaller
elements (smaller than x) before x, and put all greater elements (greater than x) after x. All this
should be done in linear time.
Array as a Data Structure 41
Partition Algorithm
We start from the leftmost element and keep track of index of smaller (or equal to) elements
as i. While traversing, if we find a smaller element, we swap current element with arr[i].
Otherwise we ignore current element.
Pseudo code for partition()
/* This function takes last element as pivot, places the pivot element at its correct position in
sorted array, and places all smaller (smaller than pivot) to left of pivot and all greater elements
to right of pivot */
partition (arr[], low, high)
{
// pivot (Element to be placed at right position)
pivot = arr[high];
quickSort(arr, 0, 9);
printf(“\n The sorted array is: \n”);
for(i=0;i<10;i++)
printf(“ %d\t”, arr[i]);
}
int partition(int a[], int beg, int end)
{
int left, right, temp, loc, flag;
loc = left = beg;
right = end;
flag = 0;
while(flag!= 1)
{
while((a[loc] <= a[right]) && (loc!=right)) right--;
if(loc==right)
flag =1;
else if(a[loc]>a[right])
{
temp = a[loc];
a[loc] = a[right];
a[right] = temp;
loc = right;
}
if(flag!=1)
{
while((a[loc] >= a[left]) && (loc!=left))
left++;
if(loc==left)
flag =1;
else if(a[loc] <a[left])
{
temp = a[loc];
a[loc] = a[left];
a[left] = temp;
loc = left;
}
}
}
return loc;
}
void quickSort(int a[], int beg, int end)
{
44 Data Structures and Algorithms – I
int loc;
if(beg<end)
{
loc = partition(a, beg, end);
quickSort(a, beg, loc-1);
quickSort(a, loc+1, end);
} }
Selection Sort
In selection sort, the smallest value among the unsorted elements of the array is selected in
every pass and inserted to its appropriate position into the array. First, find the smallest element of
the array and place it on the first position, then, find the second smallest element of the array and
place it on the second position. The process continues until we get the sorted array.
The array with n elements is sorted by using n-1 pass of selection sort algorithm.
z In 1st pass, smallest element of the array is to be found along with its index pos. then,
swap A[0] and A[pos]. Now, A[0] is sorted, we now have n-1 elements which are to be
sorted.
z In 2nd pass, position pos of the smallest element present in the sub-array A[n-1] is found.
A[1] and A[pos] are then swapped. Thus A[0] and A[1] are sorted, we are now left with
n-2 unsorted elements.
z In n-1th pass, position pos of the smaller element between A[n-1] and A[n-2] is to be
found. Then A[pos] and A[n-1] are swapped.
Thus, by following the above explained process, the elements A[0], A[1], A[2],...., A[n-1]
are sorted.
Example: Consider the following array with 6 elements. Sort the elements of the array by
using selection sort.
A = {10, 2, 3, 90, 43, 56}
Pass Pos A[0] A[1] A[2] A[3] A[4] A[5]
1 1 2 10 3 90 43 56
2 2 2 3 10 90 43 56
3 2 2 3 10 90 43 56
4 4 2 3 10 43 90 56
5 5 2 3 10 43 56 90
4: exit
Smallest (arr, k, n, pos)
1: [initialize] set small = arr[k]
2: [initialize] set pos = k
3: repeat for j = k+1 to n -1
if small >arr[j]
set small = arr[j]
set pos = j
[end of if]
[end of loop]
4: return pos
C Program: Selection Sort
#include<stdio.h>
int smallest(int[],int,int);
void main ()
{
int a[10] = {10, 9, 7, 101, 23, 44, 12, 78,
34, 23};
int i,j,k,pos,temp;
for(i=0;i<10;i++)
{
pos = smallest(a,10,i);
temp = a[i];
a[i]=a[pos];
a[pos] = temp;
}
printf(“\n Sorted elements...\n”);
for(i=0;i<10;i++)
{
printf(“%d\n”,a[i]);
}
}
int smallest(int a[], int n, int i)
{
int small,pos,j;
small = a[i];
pos = i;
for(j=i+1;j<10;j++)
{
if(a[j]<small)
46 Data Structures and Algorithms – I
{
small = a[j];
pos=j;
}
}
return pos;
}
int i, j;
int B[15], C[100];
for (i = 0; i <= k; i++)
C[i] = 0;
for (j = 1; j <= n; j++)
C[A[j]] = C[A[j]] + 1;
for (i = 1; i <= k; i++)
C[i] = C[i] + C[i-1];
for (j = n; j >= 1; j--)
{
B[C[A[j]]] = A[j];
C[A[j]] = C[A[j]] - 1;
}
printf(“The Sorted array is:”);
for (i = 1; i <= n; i++)
printf(“%d”, B[i]);
}
/* End of counting_sort() */
//---------------------------------------------------------
int main()
{
int n, k = 0, A[15], i;
printf(“Enter the number of input:”);
scanf(“%d”, &n);
printf(“\nEnter the elements to be sorted :\n”);
for (i = 1; i <= n; i++)
{
scanf(“%d”, &A[i]);
if (A[i] > k) {
k = A[i];
}
}
counting_sort(A, k, n);
printf(“\n”);
return 0;
}
Radix Sort
We have seen that Counting sort is a linear time sorting algorithm that requires O(n+k) time
when elements are in range from 1 to k. But if the elements are in range from 1 to n2, counting
sort can’t be used because it will take O(n2) which is worse than comparison based sorting
algorithms. Such an array can be sorted in linear time by using radix sort. The idea behind radix
48 Data Structures and Algorithms – I
sort is to perform digit by digit sorting starting from least significant digit to most significant digit.
Radix sort uses counting sort as its subroutine to sort.
The Radix Sort Algorithm
1. For each digit i where i varies from least significant digit to the most significant digit,
(a) Sort input array using counting sort (or any stable sort) according to the ith digit.
Example:
Original, unsorted list:
170, 45, 75, 90, 802, 24, 2, 66
z Sorting by least significant digit (1s place) gives: [Note that we keep 802 before 2,
because 802 occurred before 2 in the original list, and similarly for pairs 170 & 90 and 45
& 75.]
z Sorting by next digit (10s place) gives: [Note that 802 again comes before 2 as 802 comes
before 2 in the previous list.]
802, 2, 24, 45, 66, 170, 75, 90
z Sorting by most significant digit (100s place) gives:
2, 24, 45, 66, 75, 90, 170, 802
C Program: Radix Sort
#include<stdio.h>
int exp;
for (exp = 1; m / exp > 0; exp *= 10)
countSort(arr, n, exp);
}
int main() {
int arr[] = { 170, 45, 75, 90, 802, 24, 2, 66 };
int n = sizeof(arr) / sizeof(arr[0]);
radixsort(arr, n);
print(arr, n);
return 0;
}
Output:
2 24 45 66 75 90 170 802
Complexity of Radix Sort
Radix sort operates on n d-digit numbers, where each digit can be one of at most b different
values (since b is the base being used). For example, in base 10, a digit can be 0, 1, 2, 3, 4, 5, 6, 7,
8, \text{or } 90, 1, 2, 3, 4, 5, 6, 7, 8, or 9.
Radix sort uses counting sort on each digit. Each pass over n d-digit numbers will take
O(n+b) time, and there are d passes total. Therefore, the total running time of radix sort is
O(d(n+b)). When d is a constant and b isn’t much larger than n (in other words, b=O(n)), then
radix sort takes linear time.
50 Data Structures and Algorithms – I
Some sorting algorithms are non-comparison based algorithm. Some of them are Radix sort,
Bucket sort, count sort. These are non-comparison based sort because here two elements are not
compared while sorting. Now we will see the difference between radix sort and counting sort
based on different type of analysis.
Analysis Type Radix Sort Counting Sort
(k is maximum digit) (k is size of count array)
Best Case O(nk) O(n + k)
Average Case O(nk) O(n + k)
Worst Case O(nk) O(n + k)
Sorting techniques can also be compared using some other parameters. Like based on
whether they in-place sorting algorithms or out-place sorting algorithms. Algorithms that do not
require any extra space is called in-place sorting algorithm. Quicksort, heapsort algorithms are in-
place. Whereas merge sort is out-place uses sorting mechanism.
QUESTIONS
Practice Questions (2 marks)
1. Compare the efficiency of Bubble Sort with Insertion Sort.
2. What is searching?
3. Compare linear and binary search.
4. Explain:
(a) Internal sorting
(b) Stable sorting
(c) In-Place algorithm
5. What is sorting? State the techniques of sorting.
Practice Questions (4 marks)
1. Explain Quick sort technique with an example.
2. What is efficiency of linear search method?
3. Explain selection sort technique with an example.
Array as a Data Structure 51
3 LINKED LIST
Structure:
3.1 List as a Data Structure
3.2 Dynamic implementation of Linked List
3.3 Types of Linked List – Singly, Doubly, Circular
3.4 Operations on Linked List – create, traverse, insert, delete, search, and reverse
3.5 Applications of Linked List – polynomial manipulation, memory management in OS
Generalized Linked List – concept, representation, multiple-variable polynomial representation
using generalized list.
Fig. 3.2
The linked list also contains a list pointer variable – called HEAD or START, which
contains the address of the first node in the list. This address is required only to trace through the
list. A special case is the list which has no nodes, such a list is called as empty list or null list and
is denoted by the null pointer in the variable START.
Linked List 53
Fig. 3.3
second ĺ data = 2;
second ĺ next = third;
third ĺ data = 3;
third ĺ next = NULL;
return 0;}
Fig. 3.4
As shown in the above figure, the arrow represents the links whereas the data part of every
node contains the marks obtained by the student in the different subjects. The last node in the list
is identified by the null pointer which is present in the address part of the last node.
Time Complexity
Average Worst
Access Search Insertion Deletion Access Search Insertion Deletion
ș(n) ș(n) ș(1) ș(1) O(n) O(n) O(n) O(1)
A doubly linked list containing three nodes having numbers from 1 to 3 in their data part, is
shown in the following image.
Linked List 57
The prev part of the first node and the next part of the last node will always contain null
indicating end in each direction.
Memory Representation of a Doubly Linked List
Memory Representation of a doubly linked list is shown in the following image. Doubly
linked list consumes more space for every node and therefore, causes more expansive basic
operations such as insertion and deletion. But the elements of the list are easily manipulated, since
the list maintains pointers in both the directions (forward and backward).
In the following image, the first element of the list, i.e., 13 stored at address 1. The head
pointer points to the starting address 1. Since this is the first element being added to the list
therefore the prev of the list contains null. The next node of the list resides at address 4 therefore
the first node contains 4 in its next pointer. We can traverse the list in this way until we find any
node containing null or -1 in its next part.
As a circular doubly linked list contains three parts in its structure, it needs more space per
node and more expensive basic operations. However, a circular doubly linked list provides easy
manipulation of the pointers and the searching becomes twice as efficient.
Memory Management of Circular Doubly Linked List
The figure below shows how the memory is allocated for a circular doubly linked list. The
variable head contains the address of the first element of the list, i.e., 1 hence the starting node of
the list contains data A is stored at address 1. Since, each node of the list is supposed to have three
parts therefore, the starting node of the list contains address of the last node, i.e., 8 and the next
node, i.e., 4. The last node of the list that is stored at address 8 and containing data as 6, contains
address of the first node of the list as shown in the image, i.e., 1. In circular doubly linked list, the
next part of the last node the address of the first node.
2. Insertion
The insertion into a singly linked list can be done at different positions. Based on the
position of the new node being inserted, the insertion is categorized into three categories. We can
add an element in the beginning/at the end/at specified location in the linked list.
Adding a new node in a linked list is a multi-step activity. Let us see this with diagrams here.
First, create a node using the same structure and find the location where it has to be inserted.
Suppose this is our original linked list with 3 nodes.
Fig. 3.13: Create a New Empty Node Fig. 3.14: P ĺ DATA =‘N’, P ĺ Next=NULL
Put the value ‘N’ in the data field of new node as in Fig. 3.14.
Step II: Now add P in front of the list so that its Next field points to START and change the
address of START to P as follows:
Linked List 61
Fig. 3.16
Fig. 3.17
62 Data Structures and Algorithms – I
Deletion í Deletes an element at the beginning/end/at specified location in the linked list.
Suppose we have a list of numbers as shown below. Let’s see each of the deletion types on
this list.
(a) Deleting an element at the beginning:
Set P = START
Fig. 3.18
Fig. 3.20
z Display: To display all the nodes in a sequential manner in the list, we need to traverse
the list from the first node and display the data part of each node.
z Search: Searches an element using the given key. Here, to search an element in the list,
we traverse the entire list and compare DATA of each node. If a match is found,
appropriate message can be displayed. If no match is found till last end, the element to be
searched does not exist.
z Reverse: To reverse the given linked list we will use three extra pointers that will be in
the process. The pointers used will be previous, after, current.
We initialize previous and after to NULL initially and current to head of the linked list.
After this, we will iterate until we reach the NULL of the initial (non-reversed linked list). And do
the following:
after ĺ current
next ĺ current
next ĺ previous
previous ĺ current
current ĺ after
Following C program is a menu driven program for singly linked list, which includes all
above mentioned operations on linked list. The code contains functions for each operation. The
readers are requested to note that the C code given below is printed in two columnar text display.
C Program: Singly Linked List
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
64 Data Structures and Algorithms – I
struct node
{
int data;
struct node *next;
};
struct node *head;
void insert_beg();
void insert_last ();
void insert_between();
void begin_delete();
void delete_last();
void delete_between();
void display_list();
void search();
void reverse_list();
void main ()
{
int choice = 0;
while(choice != 9)
{
printf(“\n------Singly Linked List--- \n”);
printf(“\n1.Insert element in beginning\n”
2. Insert at last\n
3. Insert in between the list \n
4. Delete element at beginning\n
5. Delete last element \n
6. Delete node after specified location\n
7. Search for an element\n
8. Display list\n
9. Exit\n”);
printf(“\nEnter your choice?\n”);
scanf(“\n%d”, &choice);
switch(choice)
{
case 1: insert_beg();
break;
case 2: insert_last();
break;
case 3: insert_between();
break;
case 4: delete_beg();
break;
Linked List 65
case 5: delete_last();
break;
case 6: delete_between();
break;
case 7: search();
break;
case 8: display_list();
break;
case 9: reverse_list();
break;
case 10: exit(0);
break;
default: printf(“Please enter valid choice..”); } } }
//----------------------------------------------
void insert_beg()
{
struct node *ptr;
int item;
ptr = (struct node *) malloc(sizeof(struct node *));
if(ptr == NULL)
printf(“\nOVERFLOW”);
else
{
printf(“\nEnter value\n”);
scanf(“%d”, &item);
ptr ĺ data = item;
ptr ĺ next = head;
head = ptr;
printf(“\nNode inserted”);
}
}
//--------------------------------------------------
void insert_last()
{
struct node *ptr,*temp;
int item;
ptr =(struct node*)malloc
(sizeof(struct node));
if(ptr == NULL)
printf(“\nOVERFLOW”);
else
{
66 Data Structures and Algorithms – I
printf(“\nEnter value?\n”);
scanf(“%d”, &item);
ptr ĺ data = item;
if(head == NULL)
{
ptr ĺ next = NULL;
head = ptr;
printf(“\nNode inserted”);
}
else
{
temp = head;
while (temp ĺ next ! = NULL)
{
temp = temp ĺ next;
}
temp ĺ next = ptr;
ptr ĺ next = NULL; printf(“\nNode inserted”);
}
}
}
//----------------------------------------------------
void insert_between()
{
int i,loc,item;
struct node *ptr, *temp;
ptr=(struct node *) malloc
(sizeof(struct node));
if(ptr == NULL)
printf(“\nOVERFLOW”);
else
{
printf(“\nEnter element value”);
scanf(“%d”, &item);
ptr ĺ data = item;
printf(“\nEnter the location after which you want to insert”);
scanf(“\n%d”,&loc);
temp=head;
for(i=0;i<loc;i++)
{
temp = temp ĺ next;
if(temp == NULL)
Linked List 67
{
printf(“\ncan’t insert\n”);
return;
}
}
ptr ĺ next = temp ->next;
temp ĺ next = ptr;
printf(“\nNode inserted”);
}
}
//------------------------------------------------
void delete_beg()
{
struct node *ptr;
if(head == NULL)
printf(“\nList is empty\n”);
else
{
ptr = head;
head = ptr ĺ next;
free(ptr);
printf(“\nNode deleted from the beginning.\n”); }
}
//-----------------------------------------------
void delete_last()
{
struct node *ptr,*ptr1;
if(head == NULL)
printf(“\nlist is empty”);
else if(head ĺ next == NULL)
{
head = NULL;
free(head);
printf(“\nOnly node of the list deleted..\n”);}
else
{
ptr = head;
while(ptr ĺ next != NULL)
{
ptr1 = ptr;
ptr = ptr ĺ next;
}
68 Data Structures and Algorithms – I
}
}
}
Output: (Not all, but some options of run of the program are included here)
Fig. 3.21
Fig. 3.22
Consider that a new node is pointed to by new and first node of the list is pointed to by head.
Following instructions are needed to insert a node at the beginning:
z new ĺ next = head;
z new ĺ prev= NULL;
z head ĺ prev=new;
z new =head;
Fig. 3.23: Adding a Node at the Front of the Doubly Linked List
If a new node is pointed to by new, we traverse the DLL till last and add a new node.
Following are the instructions needed to add a node at the end of DLL:
1. temp=head;
2. while(temp ĺ next != NULL)
temp = temp ĺ next;
3. temp ĺ next = new;
4. new ĺ next= NULL;
The deletion of a node in a doubly linked list can be divided into three main categories:
1. Deletion of head node.
We delete the node at first position pointed to by Head with following instructions:
1. If (P== head)
2. {
3. P ĺ next ĺ prev=NULL;
4. head=P ĺ next;
5. free(P);
6. return(head);
7. }
74 Data Structures and Algorithms – I
Fig. 3.28
//-----------------------------------------------------
insert_middle()
{
int loc;
printf(“\nEnter the location to insert \n”);
scanf(“%d”,&loc);
temp=head;
for(i=0;i<loc;i++)
{
temp = temp->next;
if(temp == NULL)
{
printf(“\ncan’t insert\n”);
return;
}
}
ptr ĺ data = item;
ptr ĺ next = temp ĺ next;
ptr ĺ prev = temp;
temp ĺ next = ptr;
temp ĺ next ĺ prev = ptr;
printf(“Node Inserted\n”);
}
}
//----------------------------------------------------
insert_end()
{
ptr ĺ data=item;
temp = head;
while(temp ĺ next!=NULL)
{
temp = temp ĺ next;
}
temp ĺ next = ptr;
ptr ĺ prev=temp;
ptr ĺ next = NULL;
}
printf(“\nNode Inserted\n”);
}
}
}
//--------------------------------------------------------
Linked List 79
delete_dll()
{
int opt;
do
{
clrscr();
printf(“\nEnter the item to insert?\n”);
scanf(“%d”, &item);
printf(“\n------Deletion Menu in DLL-----\n”);
printf(“\n 1. Delete first node\n”);
printf(“\n 2. Delete middle node \n”);
printf(“\n 3. Delete last node \n”);
printf(“\n 4. Exit \n”);
printf(“Enter your choice\n”);
scanf(“%d”, &opt);
switch(opt)
{
case 1: delete_first();
break;
case 2: delete_middle();
break;
case 3: delete_last();
break;
default: printf(“Incorrect option selected\n”);
break;
}
}while(choice!=4);
}
//---------------------------------------------------
delete_first()
{
if(head->next == NULL)
{
head = NULL;
free(head);
printf(“\nNode Deleted\n”);
}
else
{
ptr = head;
head = head ĺ next;
head ĺ prev = NULL;
80 Data Structures and Algorithms – I
free(ptr);
printf(“\nNode Deleted\n”);
}
}
}
//------------------------------------------------------------
delete_middle()
{
int val;
printf(“Enter the value to be deleted”);
scanf(“%d”,&val);
temp = head;
while(temp ĺ data != val)
temp = temp ĺ next
if(temp ĺ next == NULL)
{
printf(“\nCan’t delete\n”);
}
else if(temp ĺ next ĺ next == NULL)
{
temp ĺ next = NULL;
printf(“\nNode Deleted\n”);
}
else
{
ptr = temp ĺ next;
temp ĺ next = ptr ĺ next;
ptr ĺ next ĺ prev = temp;
free(ptr);
printf(“\nNode Deleted\n”);
}
}
//---------------------------------------------------------------
delete_last()
{
if(head ĺ next == NULL)
{
head = NULL;
free(head);
printf(“\nNode Deleted\n”);
}
else
Linked List 81
{
ptr = head;
while(ptr ĺ next !=NULL)
ptr = ptr ĺ next;
if ptr ĺ prev != NULL
ptr ĺ prev ĺ next = NULL;
free(ptr);
printf(“\nNode Deleted\n”);
}
}
}//------------------------------------------------------search_dll()
{
int item,i=0,flag;
ptr = head;
if(ptr == NULL)
printf(“\n List is empty\n”);
else
{
printf(“\nEnter item to search?\n”);
scanf(“%d”, &item);
while (ptr!=NULL)
{
if(ptr ĺ data == item)
{
printf(“\nitem found at location %d “,i+1); flag=0;
break;
}
else
{
flag=1;
}
i++;
ptr = ptr ĺ next;
}
if(flag==1)
{
printf(“\nItem not found\n”);
}
}
}
//------------------------------------------------------void display_dll(void)
{
82 Data Structures and Algorithms – I
if(head == NULL)
{
head = new;
new ĺ next = head;
Linked List 83
}
In the second situation, the condition head == NULL will become false which means that
the list contains at least one node. In this case, we need to traverse the list in order to reach the last
node of the list. This will be done by using the following statement.
temp = head;
while (temp ĺ next != head)
temp = temp ĺ next;
At the end of the loop, the pointer temp would point to the last node of the list. Since, in a
circular singly linked list, the last node of the list contains a pointer to the first node of the list. we
need to make the next pointer of the last node point to the head node of the list and the new node
which is being inserted into the list will be the new head node of the list therefore the next pointer
of temp will point to the new node ptr. We illustrate by creating a circular linked list with
elements 11, 22, 33.
Fig. 3.29
2. Traverse to N-1 position in the list, in our case since we want to insert node at 3rd
position therefore we would traverse to 3-1 = 2nd position in the list. Say current pointer
points to N-1th node.
current = head;
for(i=2; I ĺ position-1; i++)
current = current ĺ next;
3. Make the following link changes:
new ĺ next = current ĺ next.
4. Connect the next pointer field of current node with the newly created node which means
now next pointer field of current node will point to new and you are done.
new ĺ next = current ĺ next;
current ĺ next = new;
Following figure depicts insertion in CLL at given position:
struct node
{
int data;
struct node *link;
};
Linked List 87
void main()
{
int ch;
search();
break;
case 10:
reverse_CLL(head);
break;
default:
exit(0);
}
}
}
//------------------------------------------------------
void create()
{
int c;
first = (struct node*)malloc
(sizeof(struct node));
printf(“\n Enter the data:”);
scanf(“%d”, &first ĺ data);
first ĺ link = first;
head = first;
printf(“\n If you wish to continue press 1 otherwise 0:”);
scanf(“%d”, &c);
while (c != 0)
{
second = (struct node*)malloc(sizeof(struct node));
printf(“\n Enter the data:”);
scanf(“%d”, &second ĺ data);
first ĺ link = second;
second ĺ link = head;
first = second;
printf(“\n If you wish to continue press 1 otherwise 0:”);
scanf(“%d”, &c);
}
}
//------------------------------------------------------
void ins_at_beg()
{
first = head;
second = (structnode*)malloc(sizeof(struct node));
printf(“\n Enter the data:”);
scanf(“%d”, &second ĺ data);
while (first ĺ link != head)
Linked List 89
{
first = first ĺ link;
}
first ĺ link = second;
second ĺ link = head;
head = second;
}
//-----------------------------------------------------
void ins_at_pos()
{
struct node *ptr;
int c = 1, pos, count = 1;
second = (struct node*)malloc(sizeof(struct node));
if (head == NULL)
{
printf(“cannot enter an element at this place”);
}
printf(“\n Enter the data:”);
scanf(“%d”, &second ĺ data);
printf(“\n Enter the position to be inserted:”);
scanf(“%d”, &pos);
first = head;
ptr = head;
while (ptr ĺ link != head)
{
count++;
ptr = ptr ĺ link;
}
count++;
if (pos ĺ count)
{
printf(“OUT OF BOUND”);
return;
}
while (c <pos)
{
third = first;
first = first->link;
c++;
}
second ĺ link = first;
third ĺ link = second;
90 Data Structures and Algorithms – I
}
//-----------------------------------------------------
void del_at_beg()
{
if (head == NULL)
printf(“\n List is empty”);
else
{
first = head;
second = head;
while (first ĺ link != head)
{
first = first ĺ link;
}
head = second ĺ link;
first ĺ link = head;
free(second);
}
}
//-----------------------------------------------------
void del_at_pos()
{
if (head == NULL)
printf(“\n List is empty”);
else
{
int c = 1, pos;
printf(“\n Enter the position to be deleted:”);
scanf(“%d”, &pos);
first = head;
while (c <pos)
{
second = first;
first = first ĺ link;
c++;
}
second ĺ link = first ĺ link;
free(first);
}
}
//----------------------------------------------------
void traverse()
Linked List 91
{
if (head == NULL)
printf(“\n List is empty”);
else
{
first = head;
while (first ĺ link != head)
{
printf(“%d ĺ ”, first->data);
first = first->link;
}
printf(“%d”, first ĺ data);
}
}
//---------------------------------------------------
void search()
{
int search_val, count = 0, flag = 0;
printf(“\n enter the element to search\n”);
scanf(“%d”, &search_val);
if (head == NULL)
printf(“\nList is empty nothing to search”);
else
{
first = head;
while (first ĺ link != head)
{
if (first ĺ data == search_val)
{
printf(“\n the element is found at %d”, count);
flag = 1;
break;
}
count++;
first = first ĺ link;
}
if (first ĺ data == search_val)
{
printf(“element found at postion %d”, count);
}
if (flag == 0)
{
92 Data Structures and Algorithms – I
Output:
1. Creation
2. Insertion at beginning
3. Insertion at remaining
4. Deletion at beginning
5. Deletion at remaining
6. Traverse
7. Search
10. Exit
Enter your choice: 6
List is empty
Enter your choice: 5
List is empty
Enter your choice: 9
empty list
Linked List 93
Creating a circular doubly linked list is actually inserting in circular doubly linked list at
beginning. There are two scenarios of inserting a node in circular doubly linked list at beginning.
Either the list is empty or it contains more than one element in the list.
Let us understand the insertion in CDLL in various scenarios:
2. Insertion in an Empty List
Consider an empty List (start = NULL): A node (Say N) is inserted with data = 5, so
previous pointer of N points to N and next pointer of N also points to N. But now start pointer
points to the first node the list.
Fig. 3.34
Linked List 95
Fig. 3.36
96 Data Structures and Algorithms – I
4. At the last, as the last node is not visited yet check for the n element if present do step 3.
while(temp ĺ next != start)
{
count++; // Increment count for location
if(temp ĺ data == search) // If it is found raise the flag and break the loop
{
flag = 1;
count--;
break;
}
temp = temp ĺ next; // Increment temp pointer
}
6. Displaying Data in CDLL
Displaying the data in Circular Doubly Linked List is same as in Circular Singly Linked List.
C Program: Operations on Circularly Doubly Linked List (CDLL)
#include<stdio.h>
#include<stdlib.h>
struct node
{
struct node *prev;
struct node *next;
int data;
};
struct node *head;
void insert_at_beginning();
void insert_at_last();
void insert_after();
void delete_at_beginning();
void delete_at_last();
void display();
void search();
void main ()
{
int choice =0;
while(choice != 9)
{
printf(“\n1. Insert in Beginning\n 2. Insert at last 3. Insert after a node\n3. Delete from
Beginning\n 4. Delete from last\n 5. Search\n 6. Show\n7.Exitt\n”);
printf(“\nEnter your choice?\n”);
scanf(“\n%d”, &choice);
98 Data Structures and Algorithms – I
switch(choice)
{
case 1:
insert_at_beginning();
break;
case 2:
insert_at_last();
break;
case 3:
delete_at_beginning();
break;
case 4:
delete_at_last();
break;
case 5:
search();
break;
case 6:
display();
break;
case 7:
exit(0);
break;
default:
printf(“Please enter valid choice..”);
}
}
}
//----------------------------------------------------------
void insert_at_beginning()
{
struct node *ptr,*temp;
int item;
ptr = (struct node *)malloc(sizeof(struct node));
if(ptr == NULL)
{
printf(“\nOVERFLOW”);
}
else
{
printf(“\nEnter Item value”);
scanf(“%d”, &item);
Linked List 99
{
struct node *temp;
if(head == NULL)
{
printf(“\n UNDERFLOW”);
}
else if(head ĺ next == head)
{
head = NULL;
free(head);
printf(“\n node deleted\n”);
}
else
{
temp = head;
while(temp ĺ next != head)
{
temp = temp ĺ next;
}
temp ĺ next = head ĺ next;
head ĺ next ĺ prev = temp;
free(head);
head = temp ĺ next;
}
}
//-----------------------------------------------------------
void delete_at_last()
{
struct node *ptr;
if(head == NULL)
{
printf(“\n UNDERFLOW”);
}
else if(head ĺ next == head)
{
head = NULL;
free(head);
printf(“\nnode deleted\n”);
}
else
{
ptr = head;
102 Data Structures and Algorithms – I
scanf(“%d”, &item);
if(head ĺ data == item)
{
printf(“item found at location %d”, i+1);
flag = 0;
}
else
{
while (ptr ĺ next != head)
{
if(ptr ĺ data == item)
{
printf(“Item found at location %d “, i+1);
flag = 0;
break;
}
else
{
flag = 1;
}
i++;
ptr = ptr ĺ next;
}
}
if(flag != 0)
{
printf(“Item not found\n”);
}
}
}
Output:
1. Insert in Beginning
2. Insert at last
3. Insert after a node
4. Delete from Beginning
5. Delete from last
6. Search
7. Show
8. Exit
104 Data Structures and Algorithms – I
Node inserted
1. Insert in Beginning
2. Insert at last
3. Insert after a node
4. Delete from Beginning
5. Delete from last
6. Search
7. Show
8. Exit
Enter value234
node inserted
1. Insert in Beginning
2. Insert at last
3. Insert after a node
4. Delete from Beginning
5. Delete from last
6. Search
7. Show
8. Exit
Node inserted
1. Insert in Beginning
2. Insert at last
3. Insert after a node
Linked List 105
Enter value80
node inserted
1. Insert in Beginning
2. Insert at last
3. Insert after a node
4. Delete from Beginning
5. Delete from last
6. Search
7. Show
8. Exit
1. Insert in Beginning
2. Insert at last
3. Insert after a node
4. Delete from Beginning
5. Delete from last
6. Search
7. Show
8. Exit
node deleted
1. Insert in Beginning
2. Insert at last
106 Data Structures and Algorithms – I
1. Insert in Beginning
2. Insert at last
3. Insert after a node
4. Delete from Beginning
5. Delete from last
6. Search
7. Show
8. Exit
1. Insert in Beginning
2. Insert at last
3. Insert after a node
4. Delete from Beginning
5. Delete from last
6. Search
7. Show
8. Exit
1. Insert in Beginning
2. Insert at last
3. Insert after a node
4. Delete from Beginning
5. Delete from last
6. Search
7. Show
8. Exit
1. Insert in Beginning
2. Insert at last
3. Insert after a node
4. Delete from Beginning
5. Delete from last
6. Search
7. Show
8. Exit
Enter your choice?
8
3.5 Applications of Linked List
There are various applications of linked list in computer science like:
1. Implementation of stacks and queues.
2. Implementation of graphs: Adjacency list representation of graphs which use linked list to
store adjacent vertices.
3. Dynamic memory allocation: We use linked list of free blocks.
4. Maintaining directory of names.
5. Performing arithmetic operations on long integers
6. Manipulation of polynomials by storing constants in the node of linked list.
7. Representation of sparse matrices
We see here in brief two of them:
108 Data Structures and Algorithms – I
1. Polynomial Manipulation
Given two polynomial numbers represented by a linked list. We write a function that adds
these lists, means we add the coefficients who have same variable powers.
Example:
Input:
1st number = 5x^2 + 4x^1 + 2x^0
2nd number = 5x^1 + 5x^0
Output:
5x^2 + 9x^1 + 7x^0
Input:
1st number = 5x^3 + 4x^2 + 2x^0
2nd number = 5x^1 + 5x^0
Output:
5x^3 + 4x^2 + 5x^1 + 7x^0
Fig. 3.38
{
struct Node *r, *z;
z = *temp;
if(z == NULL)
{
r =(struct Node*)malloc(sizeof(struct Node));
r ĺ coeff = x;
r ĺ pow = y;
*temp = r;
r ĺ next = (struct Node*)malloc(sizeof(struct Node));
r = r ĺ next;
r ĺ next = NULL;
}
else
{
r ĺ coeff = x;
r ĺ pow = y;
r ĺ next = (struct Node*)malloc(sizeof(struct Node));
r = r ĺ next;
r ĺ next = NULL;
}
}
// ------------------------------------------------
void polyadd(struct Node *poly1, struct Node *poly2, struct Node *poly)
{
while(poly1 ĺ next && poly2 ĺ next)
{
// If power of 1st polynomial is greater than 2nd, then store 1st as it is and move its pointer
if(poly1ĺ pow ĺ poly2 ĺ pow)
{
poly ĺ pow = poly1 ĺ pow;
poly ĺ coeff = poly1 ĺ coeff;
poly1 = poly1 ĺ next;
}
// If power of 2nd polynomial is greater than 1st, then store 2nd as it is and move its pointer
else if(poly1 ĺ pow < poly2 ĺ pow)
{
poly ĺ pow = poly2 ĺ pow;
poly ĺ coeff = poly2 ĺ coeff;
poly2 = poly2 ĺ next;
}
// If power of both polynomial numbers is same then add their coefficients
110 Data Structures and Algorithms – I
else
{
poly ĺ pow = poly1 ĺ pow;
poly ĺ coeff = poly1 ĺ coeff+poly2 ĺ coeff;
poly1 = poly1 ĺ next;
poly2 = poly2 ĺ next;
}
// Dynamically create new node
poly ĺ next = (struct Node *)malloc(sizeof(struct Node));
poly = poly ĺ next;
poly ĺ next = NULL;
}
while(poly1 ĺ next || poly2 ĺ next)
{
if(poly1 ĺ next)
{
poly ĺ pow = poly1 ĺ pow;
poly ĺ coeff = poly1 ĺ coeff;
poly1 = poly1 ĺ next;
}
if(poly2 ĺ next)
{
poly ĺ pow = poly2 ĺ pow;
poly ĺ coeff = poly2 ĺ coeff;
poly2 = poly2 ĺ next;
}
poly ĺ next = (struct Node *)malloc(sizeof(struct Node));
poly = poly ĺ next;
poly ĺ next = NULL;
}
}
// Display Linked list
void show(struct Node *node)
{
while(node ĺ next != NULL)
{
printf(“%dx^%d”, node ĺ coeff, node ĺ pow);
node = node ĺ next;
if(node ĺ next != NULL)
printf(“ + ”);
Linked List 111
}
}
//----------------------------------------------------------
int main()
{
struct Node *poly1 = NULL, *poly2 = NULL, *poly = NULL;
// Create first list of 5x^2 + 4x^1 + 2x^0
create_node(5,2,&poly1);
create_node(4,1,&poly1);
create_node(2,0,&poly1);
// Create second list of 5x^1 + 5x^0
create_node(5,1,&poly2);
create_node(5,0,&poly2);
printf(“1st Number:”);
show(poly1);
printf(“\n2nd Number:”);
show(poly2);
poly = (struct Node *)malloc(sizeof(struct Node));
// Function add two polynomial numbers
Poly add(poly1, poly2, poly);
// Display resultant List
printf(“\nAdded polynomial:”);
show(poly);
return 0;
}
Output:
1st Number: 5x^2 + 4x^1 + 2x^0
2nd Number: 5x^1 + 5x^0
Added polynomial: 5x^2 + 9x^1 + 7x^0
Multiplication of two polynomials using Linked list
Given two polynomials in the form of linked list. The task is to find the multiplication of
both polynomials.
Examples:
Input: Poly1: 3x^2 + 5x^1 + 6, Poly2: 6x^1 + 8
Output: 18x^3 + 54x^2 + 76x^1 + 48
On multiplying each element of 1st polynomial with elements of 2nd polynomial, we get
18x^3 + 24x^2 + 30x^2 + 40x^1 + 36x^1 + 48
On adding values with same power of x, we get
18x^3 + 54x^2 + 76x^1 + 48
112 Data Structures and Algorithms – I
Following figures depicts the linked list representation for multiplication of two polynomials:
Fig. 3.39
#include <stdio.h>
struct Node {
int coeff, power;
Node* next;
};
// Function add a new node at the end of list
Node* add node(Node* start, int coeff, int power)
{
// Create a new node
Node* newnode = new Node;
newnode ĺ coeff = coeff;
newnode ĺ power = power;
newnode ĺ next = NULL;
// If linked list is empty
Linked List 113
if (start == NULL)
return new node;
// If linked list has nodes
Node* ptr = start;
while (ptr ĺ next != NULL)
ptr = ptr ĺ next;
ptr ĺ next = newnode;
return start;
}
//----------------------------------------------------------------------------------------------------------
void printList(struct Node* ptr)
{
while (ptr ĺ next != NULL) {
cout<<ptr ĺ coeff<< “x^” <<ptr ĺ power << “ +”;
ptr = ptr ĺ next;
}
cout<<ptr ĺ coeff<< “\n”;
}
// Function to add coefficients of two elements having same power
void remove Duplicates(Node* start)
{
Node *ptr1, *ptr2, *dup;
ptr1 = start;
/* Pick elements one by one */
while (ptr1 != NULL && ptr1 ĺ next != NULL) {
ptr2 = ptr1;
// Compare the picked element
// with rest of the elements
while (ptr2 ĺ next != NULL) {
// If power of two elements are same
if (ptr1 ĺ power == ptr2 ĺ next ĺ power) {
// Add their coefficients and put it in 1st element
ptr1 ĺ coeff = ptr1 ĺ coeff + ptr2 ĺ next ĺ coeff;
dup = ptr2 ĺ next;
ptr2 ĺ next = ptr2 ĺ next ĺ next;
// remove the 2nd element
delete (dup);
}
else
ptr2 = ptr2 ĺ next;
}
ptr1 = ptr1 ĺ next;
114 Data Structures and Algorithms – I
}
}
// Function two Multiply two polynomial Numbers
Node* multiply(Node* poly1, Node* poly2,
Node* poly3)
{
// Create two pointer and store the address of 1st and 2nd polynomials
Node *ptr1, *ptr2;
ptr1 = poly1;
ptr2 = poly2;
while (ptr1 != NULL) {
while (ptr2 != NULL) {
int coeff, power;
// Multiply the coefficient of both polynomials and store it in coeff
coeff = ptr1 ĺ coeff * ptr2 ĺ coeff;
// Add the power of both polynomials and store it in power
power = ptr1 ĺ power + ptr2 ĺ power;
//Invoke add node function to create a new node by passing three parameters
poly3 = add node(poly3, coeff, power);
// move the pointer of 2nd polynomial two get its next term
ptr2 = ptr2 ĺ next;
}
// Move the 2nd pointer to the starting point of 2nd polynomial
ptr2 = poly2;
// move the pointer of 1st polynomial
ptr1 = ptr1 ĺ next;
}
// this function will be invoked to add the coefficient of the elements having same power
from the resultant linked list
removeDuplicates(poly3);
return poly3;
}
//-------------------------------------------------------------------------------------------------------
int main()
{
Node *poly1 = NULL, *poly2 = NULL, *poly3 = NULL;
// Creation of 1st Polynomial: 3x^2 + 5x^1 + 6
poly1 = addnode(poly1, 3, 2);
poly1 = addnode(poly1, 5, 1);
poly1 = addnode(poly1, 6, 0);
// Creation of 2nd polynomial: 6x^1 + 8
poly2 = addnode(poly2, 6, 1);
Linked List 115
Output:
1st Polynomial: 3x^2 + 5x^1 + 6
2nd Polynomial: 6x^1 + 8
Resultant Polynomial: 18x^3 + 54x^2 + 76x^1 + 48
2. Memory Management in OS
The memory is one of the important resources of computer system and plays an important
role in achieving good system performance. A processor cannot achieve its required performance
if it cannot access data at sufficient speed.
The memory manager tries to use the available memory in the most efficient way. It keeps
track of which parts of memory are in use and which parts are free, it allocates memory to
processes when they need it and deallocates it when they are finished.
Operating system keeps track of memory by maintaining a linked list of allocated memory
segments, and free memory segments, where a segment is either a process or a hole between the
two processes.
When processes and holes are kept on a list that is sorted by address, several algorithms can
be used to allocate memory for a newly created process or an existing process being swapped in
from the disk. We assume here that memory manages knows how much memory to allocate.
There are different types of algorithms to perform this.
Following are the different types of algorithms:
First fit:
In first fit algorithm, the memory manager scans along the list of segments until it finds a
hole that is big enough. The hole is broken into two parts, that is, for process and for unused
memory except in likely case of an exact fit. This is a fast algorithm because it searches as little as
possible.
Next fit:
The next fit algorithm works similar to first fit algorithm, except that it keeps track of where
it is whenever it finds a suitable hole
116 Data Structures and Algorithms – I
Best fit:
This algorithm searches the whole list and takes the smallest hole that is adequate. Rather
than breaking up a big hole that might be needed later, this algorithm tries to find a hole that is
close to the actual size that needed.
Worst fit:
This algorithm always takes the largest available hole, so that the hole broken off will be big
enough to be useful.
Quick fit:
This algorithm maintains separate lists for some of the more common sizes requested.
For example, it might have a table with n entries, in which the first entry is a pointer to the
head of a list of 5-KB holes, the second entry is a pointer to a list of 7-KB holes, the third entry a
pointer to 13-KB holes, and so on. Holes of say, 21 KB, could either be put on the 20-KB list or
on a special list of odd-sized holes. With quick fit, finding a hole of the required size is extremely
fast, but it has the same disadvantage as all schemes that sort by hole size, that, when a process
terminates or is swapped out, finding its neighbours to see if a merge is possible is expensive. If
merging is not done, memory will quickly fragment into a large number of small holes into which
no processes fit.
Generalized Linked List
Concept
A Generalized Linked List L, is defined as a finite sequence of n>=0 elements, l1, l2, l3,
l4, …, ln, such that li are either atom or the list of atoms. Elements of the linear list are atomic in
nature.
Thus L = (l1, l2, l3, l4, …, ln), where n is total number of nodes in the list and li are either
atoms or lists.
This is a special type of list, which contains not only individual data items but also pointers
to other list.
Examples:
z A = (): empty or NULL list
z P = (a,b,c): a list of three elements, P(1) = a , P(2) = b, P(3) =c
z In a generalized list L, each element li is either atom or a list itself.
z C = (p,(q,r),t): a list of three elements where,
first element ‘p’ is atom.
Second element (q, r) is a list.
Third element ‘t’ is atom.
Representation
To represent a list of items following are certain assumptions about the node structure:
z Flag = 1 implies that down pointer exists
z Flag = 0 implies that next pointer exists
z Data means the item
Linked List 117
z Down pointer is the address of node which is down of the current node
z Next pointer is the address of node which is attached as the next node
Fig. 3.40
Fig. 3.41
When first field is 0, it indicates that the second field is variable. If first field is 1 means the
second field is a down pointer, means some list is starting.
Multiple-Variable Polynomial Representation Using Generalized Linked List
The typical node structure will be:
Fig. 3.42
118 Data Structures and Algorithms – I
Here
z Flag = 0 means variable is present
z Flag = 1 means down pointer is present
z Flag = 2 means coefficient and exponent is present
Example:
9x5 + 7xy4 + 10xz
Fig. 3.43
In the above example the head node is of variable x. The temp node shows the first field as 2
means coefficient and exponent are present.
Fig. 3.44
Since temp node is attached to head node and head node is having variable x, temp node
having coefficient = 9 and exponent = 5. The above two nodes can be read as 9x5.
Fig. 3.45
Linked List 119
Similarly, in the above figure, the node temp1 can be read as x4.
z The flag field is 1 means down pointer is there
z temp2 = y
z temp3 = coefficient = 7
z exponent = 1
z flag = 2 means the node contains coefficient and exponent values.
z temp2 is attached to temp3 this means 7y1 and temp2 is also attached to temp1 means
z temp1 x temp2
z x4 x 7y1 = 7x4y1 value is represented by above figure.
QUESTIONS
Practice Questions (2 marks)
1. List the advantages of Dynamic Memory Allocation.
2. What are applications of linked lists in computer science?
3. What are applications of linked lists in real world?
4. What are types of linked lists?
5. Give the representation of generalized linked list for the following (1 mark each).
z B= (p, (q, r), s)
z A= (1, 2, (3, (4, 5)), 6)
z G= (a, (b, c, d ),e,f)
17. How are polynomials represented using linked lists? Give a C function for addition of two
polynomials.
18. How are polynomials represented using linked lists? Give a C function for multiplication of two
polynomials.
19. Explain following applications of linked list:
(a) Polynomial manipulation (b) Memory management in OS
20. What is a generalized linked list? What are its advantages?
4 STACKS
Structure:
4.1 Introduction
4.2 Representation – Static and Dynamic
4.3 Operations – init(), push(), pop(), isEmpty(), isFull(), peek()
4.4 Application – String reversal, Function Call, Infix to postfix, Infix to prefix, Postfix Evaluation,
Backtracking – 4 Queens Problem
4.1 Introduction
Stack
z Stack is a linear data structure which performs the operations in a specific order. The
order may be LIFO (Last-In-First-Out) or FILO (First-In-Last-Out).
z In case of Last-In-First-Out (LIFO) lists, the element which is inserted first in the stack,
will be deleted last from the stack.
z Stack can be also called as an ordered list in which, insertion and deletion can be
performed only at one end that is called top.
z Stack is a recursive data structure having pointer to its top element.
There are many real-life examples of a stack. Consider an example of plates stacked over
one another in the canteen. The plate which is at the top is the first one to be removed, i.e., the
plate which has been placed at the bottom most position remains in the stack for the longest
period of time. So, it can be simply seen to follow LIFO (Last-In-First-Out)/FILO (First-In-Last-
Out) order.
Fig. 4.3
bool isempty() {
if(top == -1)
return true;
else
return false;
}
//----------------------------------------------------------------------------------------------------------
bool isfull() {
if(top == MAXSIZE)
return true;
else
return false;
}
{
printf(“How many elements in the stack?\n”);
scanf(“%d”, &n);
printf(“-----STACK -----\n”);
while(ch != 4)
{
printf(“\n1.Push\n 2.Pop\n 3.Display\n 4.Exit”);
printf(“\n Enter your choice \n”);
scanf(“%d”, &choice);
switch(ch)
{
case 1:
{
push();
break;
}
case 2:
{
pop();
break;
}
case 3:
{
display ();
break;
}
case 4:
{
printf(“Exiting....”);
break;
}
default:
printf(“Please Enter valid choice”);
}
}
}
//----------------------------------------------------
void init_stack()
{
top = -1;
}
//---------------------------------------------------
Stacks 127
intisempty()
{
if(top == -1)
return 1;
else
return 0;
}
//-----------------------------------------------------
intisfull() {
if(top == MAXSIZE)
return 1;
else
return 0;
}
//----------------------------------------------------
void push ()
{
int ele;
if (isfull())
printf(“\n Stack Overflow”);
else
{
printf(“Enter the element to push”);
scanf(“%d”,&ele);
top = top +1;
stack[top] = ele;
}
}
//-------------------------------------------------void pop ()
{
if(isempty())
printf(“Stack Underflow”);
else
{
top = top -1;
printf(“Element removed…\n”);
}
//-------------------------------------------------void display()
{
if(isempty())
printf(“Stack is empty”);
else
128 Data Structures and Algorithms – I
{
printf(“Stack elements are…..\n”)
for (i=top;i>=0;i--)
printf(“%d\t”, stack[i]);
}
Output
----------STACK-------
1. Push
2. Pop
3. Display
4. Exit
Enter your choice
1
Enter element to push
10
1. Push
2. Pop
3. Display
4. Exit
Enter your choice
1
Enter element to push
20
1. Push
2. Pop
3. Display
4. Exit
Enter your choice
1
30
1. Push
2. Pop
3. Display
4. Exit
Enter your choice
3
Stack elements are…….
102030
1. Push
2. Pop
3. Display
Stacks 129
4. Exit
Enter your choice
2
Element removed
1. Push
2. Pop
3. Display
4. Exit
Enter your choice
3
Stack elements are…….
1020
Step V: pop()
Display
Displaying all the nodes of a stack needs traversing all the nodes of the linked list organized
in the form of stack. For this purpose, we need to follow the following steps.
1. Copy the head pointer into a temporary pointer.
2. Move the temporary pointer through all the nodes of the list and print the value field
attached to every node.
C Program: Dynamic Implementation of Stack Operations using Linked List:
#include <stdio.h>
#include <stdlib.h>
void push();
void pop();
void display();
struct node
{
int num;
struct node *next;
} *top,*new;
132 Data Structures and Algorithms – I
void main ()
{
int choice=0;
top = NULL;
printf(“\nStack operations using linked list\n”);while(choice != 4)
{
printf(“Chose one from the below options\n”);
printf(“\n1.Push\n2.Pop\n3.Display\n4.Exit”);
printf(“\n Enter your choice \n”);
scanf(“%d”, &choice);
switch(choice)
{
case 1:
push();
break;
case 2:
pop();
break;
case 3:
display();
break;
default:
printf(“Please Enter valid choice”);
} // end of switch
} // end of while loop
} // end of main
//---------------------------------------------------
void push ()
{
int item;
new=(struct node *)malloc(sizeof(struct node));
printf(“Enter the item to push\n”);
scanf(“%d”, &item);
new ĺ num=item;
if(top == NULL)
top=new;
else
{
new ĺ next = top;
top= new;
}
printf(“Item pushed”);
Stacks 133
}
//------------------------------------------------
void pop()
{
int item;
struct node *temp;
if (top == NULL)
printf(“Underflow”);
else
{
item = top ĺ num;
temp = top;
top= top ĺ next;
free(temp);
printf(“Item popped”);
}
}
void display()
{
int i;
struct node *temp;
temp=head;
if(top== NULL)
printf(“Stack is empty\n”);
else
{
printf(“Printing Stack elements \n”);
while(temp!=NULL)
{
printf(“%d\n”, temp->num);
temp= temp ĺ next;
}
}
}
3. Syntax Parsing
Compilers also use stack for parsing the syntax of expressions, program blocks etc. before
translating into machine level code.
4. Backtracking
Backtracking is one of the algorithm designing techniques. In backtracking, we follow
some execution path, if that path is not found efficient at a particular stage, one can come
back to the previous state and go into some other paths.
To get back from current state, we need to store the previous state. Stack is used for this.
Some examples of backtracking are finding the solution for Knight Tour problem or N-
Queen Problem etc.
Another example is finding a path for solving maze problem, we choose a path and after
following it, if we realize that it is wrong, need to go back to the beginning of the path to
start with new path. This can be done with the help of stack.
5. String Reversal
Stack is used to reverse a string. We push the characters of string one by one into stack
and then pop character from stack.
6. Memory Management
Any modern computer environment uses a stack as the primary memory management
model for a running program. Whether it’s native code (x86, Sun, VAX) or JVM, a stack
is at the centre of the run-time environment for Java, C++, Ada, FORTRAN, etc.
7. Function Call, Recursion
Stack is used to keep information about the active functions or subroutines.
When we call a function from one other function, that function call statement may not be
the first statement.
After executing the called function, the control of execution should resume back to the
instruction after the function call statement, where it was left. This is done by storing the
address of the program counter onto the stack. After completion of the called function
execution, it pops out the address from stack and assign it into the program counter to
resume the task again.
8. Checking correctness of nested parenthesis.
9. Browsers: Forward and backward feature in web browsers
10. Editors: Redo-undo features at many places like editors, Photoshop.
11. Tree Traversals
Here we discuss the following applications of stack in detail:
1. String reversal
2. Function Call
3. Infix to postfix
4. Infix to prefix
5. Postfix Evaluation
6. Backtracking – 4 Queens problem
Stacks 135
String Reversal
The logic used to reverse a string is as follows:
1. Read a string.
2. Push all characters until NULL is not found – Characters will be stored in stack variable.
3. Pop all characters until NULL is not found – As we know stack is a LIFO technique, so
last character will be pushed first and finally we will get reversed string in a variable in
which we store inputted string.
C Program to Reverse a String using STACK
#include <stdio.h>
#include <string.h>
#define MAX 100
int top=-1;
int item;
charstack[MAX];
voidpush(char item);
char pop(void);
intisEmpty(void);
intisFull(void);
intmain()
{
charstr[MAX];
inti;
printf(“Input a string:”);
gets(str);
for(i=0;i<strlen(str);i++)
push(str[i]);
for(i=0;i<strlen(str);i++)
str[i]=pop();
printf(“Reversed String is: %s\n”,str);
return 0;
}
//-------------------------------------------
Voidpush (char item)
{
if(isFull())
{
printf(“\nStack is FULL !!!\n”);
return;
}
top=top+1;
stack[top]=item;
136 Data Structures and Algorithms – I
}
charpop()
{
if(isEmpty())
{
printf(“\nStack is EMPTY!!!\n”);
return 0;
}
item = stack[top];
top=top-1;
return item;
}
//----------------------------------
int isEmpty()
{
if(top==-1)
return 1;
else
return 0;
}
//---------------------------------
int isFull()
{
if(top==MAX-1)
return 1;
else
return 0;
}
Output:
Input a string: Hello World!
Reversed String is: !dlroW olleH
2. Function Call
In computer science, a stack data structure is used to store information about the active
subroutines(functions) of a computer program. This kind of stack is also known as a call stack, an
execution stack, program stack, control stack, run-time stack, or machine stack, and is often
shortened to just “the stack”.
A call stack is used mainly to keep track of the point to which each active subroutine
(function) should return control when it finishes executing. Thus, the stack stores the return
addresses. When a function is called, the location (address) of the instruction at which the calling
routine can later resume needs to be saved somewhere.
Stacks 137
A function frequently needs memory space for storing the values of local variables, and do
not retain their values after it returns. This space is allocated by simply moving the top of the
stack. Each separate activation of a function gets its own separate space in the stack for local
variables.
An active function is one that has been called, and is in execution process, after which
control should be returned back to the point of call. Such function calls can be nested to any level
(recursive functions), hence the stack structure is needed.
For example, if a function Draw_Square() calls a function Draw_Line() from four different
places, Draw_Line() must know where to return when its execution gets completed. For this, the
address following the instruction that jumps to Draw_Line(), the return address, is pushed onto
the top of the call stack with each call.
Recursion is a process where a function calls itself. Each recursive call in such function
creates a new copy of that function in the memory. When this call returns its data, the copy is
removed from the memory. The compiler uses a stack to store the values of the variables in each
call. So, recursion has to maintain the stack and track the values of the variables defined in the
stack.
Let us understand the application of stack for handling function calls with the example of
recursive calls. Consider the following recursive definition of the factorial() function.
int factorial(int n)
{
if n=0
return(1);
else
return( n* factorial(n-1));
}
This is the most common way to define factorial function. But for very large values of n, we
get a StackOverflowError. This is because recursion is implemented as growing and shrinking a
stack in memory.
For example, consider the following simulation of the computation of factorial(3).
Fig. 4.4
138 Data Structures and Algorithms – I
That is, to compute factorial(3), we need to compute factorial(2) and then multiply the result
by 3. Since we have very specific instructions to perform after computing factorial(2) (i.e.,
multiplying by 3).
These instructions are to be saved so we can come back to them after computing factorial(2).
The local variables are stored on a stack and then the function factorial(2) is computed. We repeat
this process until the values of factorial(2) is computed.
Here is one more example to explain the memory allocation of the recursive functions.
int print (int n)
{
if(n == 0)
return 0; // terminating condition
else
{
printf(“%d”,n);
return print(n-1); // recursive call
}
}
Let us examine this recursive function for n = 4. First, all the stacks are maintained which
prints the corresponding value of n until n becomes 0, once the termination condition is reached,
the stacks get destroyed one by one by returning 0 to its calling stack. Following image stack
trace for these recursive functions.
3. Infix to Postfix
An arithmetic expression consists of operators and operands. An arithmetic expression can
be written in three different but equivalent notations, i.e., without changing its meaning or output.
These notations are í
z Infix Notation
z Prefix (Polish) Notation
z Postfix (Reverse-Polish) Notation
Above notations are named based on how operators are placed in the expression.
Infix Notation
In infix notation, operators are placed between operands, as in a - b + c. In Mathematics, we
use infix notation, but this notation is not efficient with computing devices. This is because, an
algorithm to process infix notation could be difficult and costly in terms of time and space
consumption.
Suppose we want to divide a number a by difference of b and c, the expression will be a/b-c.
According to the precedence rules of the operators, division will be performed first, which would
create a wrong result. Hence to remove the ambiguity of operators we use parenthesis and the
correct expression will be a/(b-c). This problem of ambiguity is eliminated in the prefix and
postfix notations.
Prefix Notation
In prefix notation, operator is prefixed to operands, i.e., operator is written ahead of
operands. For example, +ab. This is equivalent to its infix notation a + b. Prefix notation is also
known as Polish Notation.
Postfix Notation
This notation style is known as Reversed Polish Notation. The name is given postfix, as the
operator is postfixed to the operands, i.e., the operator is written after the operands. For example,
ab+. This is equivalent to its infix notation a + b.
The following table briefly shows the example expressions in all three notations:
Since infix notation is not very efficient way to design an algorithm or program to parse
infix notations. So, infix notations are first converted into either postfix or prefix notations and
then computed. To parse any arithmetic expression, we need to take care of operator precedence
and associativity also.
140 Data Structures and Algorithms – I
Precedence
When an operand is in between two different operators, which operator will take the operand
first, is decided by the precedence of an operator over others. For example –
a + b * c ĺ a + (b * c)
As multiplication operation has precedence over addition, b * c will be evaluated first. A
table of operator precedence is given in the table below.
Associativity
When more than one operator with the same precedence appear in an expression,
associativity rule of operators needs to be applied. For example, in expression a + b í c, both +
and – have the same precedence. Whether a+b will be evaluated first or b-c should be evaluated
first, is determined by associativity of operators + and í. Here, both + and í are left associative,
so the expression will be evaluated as (a + b) í c.
Precedence and associativity determine the order of evaluation of an expression. Following
is an operator precedence and associativity table (highest to lowest) –
Sr. No. Operator Precedence Associativity
1. Exponentiation ^ Highest Right Associative
2. Multiplication ( ) & Division ( / ) Second Highest Left Associative
3. Addition ( + ) & Subtraction ( í ) Lowest Left Associative
The order of expression evaluation, can be altered by using parenthesis. For example í
In a + b*c, the expression part b*c will be evaluated first, with multiplication as precedence
over addition. We here use parenthesis for a + b to be evaluated first, like (a + b)*c.
Significance of Postfix and Prefix Notations:
z One of the applications of Stack is to convert arithmetic expressions in high-level
programming languages into machine readable form. An infix expression is difficult for
the machine to know and keep track of precedence of operators, whereas a postfix
expression itself determines the precedence of operators as the position of operators in a
postfix expression depends upon its precedence. Therefore, for the machine it is easier to
carry out a postfix expression than an infix expression.
z Computer system can only understand and work on a binary language, it assumes that an
arithmetic operation can take place in two operands only, e.g., a+b, c*d, d/a etc. But an
arithmetic expression may consist of more than one operator and two operands e.g.,
(a+b)*c(d/(j+d)).
z These complex arithmetic operations can be converted into polish notation using stacks
which then can be executed in two operands and an operator form. These expressions thus
become parenthesis free.
Following data structures are required to convert an infix expression to postfix:
1. A string that contains infix expression.
2. A stack which will store the operators.
3. A string that will store resultant postfix expression.
Algorithm to convert the infix to postfix expression.
Stacks 141
//-------------------------------------------------------------------------------------
int check_precedence(char opr)
{
if(opr == ‘^’)
return(3);
elseif(opr == ‘*’ || opr == ‘/’)
return(2);
elseif(opr == ‘+’ || opr == ‘-’)
return(1);
else
return(0);
}
//-----------------------------------------------------------------------------------------
void infix_postfix(char infix_str[], char postfix_str[])
{
int i, j;
char ch;
char x;
push(‘(‘); /* push ‘(‘ onto stack */
strcat(infix_str,”)”); /* add ‘)’ to infix expression */
i=0;
j=0;
item=infix_str[i];
while(ch != ‘\0’) /* to read each character till end of infix expression */
{
if(ch == ‘(‘)
push(ch);
elseif( isdigit(ch) || isalpha(ch))
{
postfix_str[j] = ch;
j++;
}
elseif(check_operator(ch) == 1)
{
x=pop();
while(is_operator(x) == 1 &&check_precedence(x)>= check_precedence(ch))
{
// pop all higher precedence operator and add them to postfix expression
postfix_str[j] = x;
j++;
x = pop();
}
144 Data Structures and Algorithms – I
push(x);
push(ch);
}
elseif(ch == ‘)’) // check if current symbol is ‘)’
{
x = pop(); // pop until ‘(‘ is encountered
while(x != ‘(‘)
{
postfix_str[j] = x;
j++;
x = pop();
}
}
else
{
printf(“\nInvalid infix Expression.\n”);
getchar();
exit(1);
}
i++;
ch = infix_str[i]; /* go to next symbol of infix expression */
} // end of while
if(top>0)
{
printf(“\nInvalid infix Expression.\n”);
getchar();
exit(1);
}
postfix_str[j] = ‘\0’;
}
//-----------------------------------------------------------------------------------------------------
void main()
{
char infix[SIZE], postfix[SIZE];
printf(“\nEnter Infix expression:”);
gets(infix);
infix_postfix(infix,postfix);
printf(“Postfix Expression:”);
puts(postfix);
getch();
}
Stacks 145
Output:
Enter Infix expression: a+(b*c-(d/e^f)*g)*h
Postfix Expression:abc*def^/g*-h*+
Enter Infix expression: (3^2*5)/(3*2-3)+5
Postfix Expression: 32^5*32*3-/5+
4. Infix to Prefix
For writing arithmetic expressions, we use infix notations. Computers do not understand this
format because they need to keep in mind rules of operator precedence and also brackets. Hence
computers find Prefix and Postfix expressions easier to understand and evaluate.
Following data structures are required to convert an infix expression to postfix:
1. A string that contains parenthesized infix expression.
2. A stack which will store the operators.
3. A string that will store resultant prefix expression.
Examples of prefix expression:
Infix: a * b + c/d, Prefix: + * a b/cd
Infix: (a - b/c) * (a/k-l), prefix: *-a/bc-/akl
Algorithm to convert Infix to Prefix
1. Push “)” onto stack, and add “(“ to end of the A.
2. Read each character of infix string from right to left and repeat step 3 to 6 for each
element of infix string until the stack is empty.
3. If an operand is encountered add it to prefix string.
4. If a right parenthesis is encountered push it onto stack.
5. If the character is operator then:
(a) Repeatedly pop from stack and add to prefix string, each operator (on the top of stack)
which has same or higher precedence than the operator.
(b) Push operator to stack.
6. If left parenthesis is encountered then
(a) Repeatedly pop from the stack and add to prefix string (each operator on top of stack =
until a left parenthesis is encountered.)
(b) Remove the left parenthesis.
7. Exit
C Program: Infix to Prefix
#include<stdio.h>
#include<string.h>
#include <ctype.h>
#define MAX 50
char stack[MAX];
int top=-1;
push(char ch)
146 Data Structures and Algorithms – I
{
stack[++top]=ch;
}
//------------------------------------------------------------------------------------------------------------
char pop()
{
return(stack[top--]);
}
//------------------------------------------------------------------------------------------------------------
int chk_precedence(char opr)
{
switch(opr)
{
case ‘#’: return 0;
case ‘)’: return 1;
case ‘+’
case ‘-’:
return 2;
case ‘*’:
case ‘/’: return 3;
}
}
//----------------------------------------------------------------------------------------------------------
main()
{
char infix[50],prefix[50],ch,x;
int i=0,k=0;
printf(“\n\nEnter the Infix Expression”);
scanf(“%s”, infix);
push(‘#’);
strrev(infix);
while( (ch=infix[i++]) != ‘\0’)
{
if( ch == ‘)’)
push(ch);
else if(isalnum(ch))
prefix[k++]=ch;
else if( ch == ‘(‘)
{
while( stack[top] != ‘)’)
prefix[k++]=pop();
x=pop();
Stacks 147
}
else
{
while( chk_precedence(stack[top]) >= chk_precedence (ch) )
perfix[k++]=pop();
push(ch);
}
}
while( stack[top] != ‘#’) /* Pop element from stack till it becomes empty
prefix[k++]=pop();
prefix[k]=‘\0’; // Make prefix a valid string by adding sentinel
strrev(prefix);
strrev(infix);
printf(“\n\n Infix String %s \nPrefix String: %s\n”, infix, prefix);
}
Output:
Enter the infix expression
((a+b) * (c-d))
Infix String: ((a+b) * (c-d))
Prefix String: *+ab-cd
We use the following method to convert an infix to prefix expression:
z Reverse the infix expression, i.e., a+b*c will become c*b+a. Note while reversing each
‘(‘ will become ‘)’ and each ‘)’ becomes ‘(‘.
z Obtain the postfix expression of the modified infix expression, i.e., cb*a+.
z Reverse the postfix expression. Hence in our example prefix is +a*bc.
5. Postfix Evaluation
We use infix expression in Mathematics and evaluate its value. Let’s see now how to
evaluate expression postfix and prefix notation.
Rules used while evaluating a Postfix Expression:
1. While reading the expression from left to right, push the element in the stack if it is an
operand.
2. Pop two operands from the stack, if the element is an operator and then evaluate it.
3. Push back the result of the evaluation. Repeat it till the end of the expression.
Algorithm
1. Add ) to postfix expression.
2. Read postfix expression Left to Right until ‘)’ encountered
3. If operand is encountered, push it onto Stack
4. If operator is encountered, Pop two elements
(i) A ĺ Top element
148 Data Structures and Algorithms – I
push(ch - ‘0’);
}
else if (ch == ‘+’ || ch == ‘-’ || ch == ‘*’ || ch == ‘/’)
{
operand1 = pop();
operand2 = pop();
switch (ch)
{
case ‘+’:
result = operand1 + operand2;
break;
case ‘-’:
result = operand2 - operand1;
break;
case ‘*’:
result = operand1 * operand2;
break;
case ‘/’:
result = operand2/operand1;
break;
}
push(result);
}
}
printf(“ \n Result of expression evaluation: %d \n”, pop());
}
//--------------------------------------------------------------------------------------------------
int main()
{
int i;
char postfix[MAX];
printf(“ \nEnter postfix expression, press right parenthesis ‘)’ for end expression: “);
for (i = 0; i<= MAX - 1; i++)
{
scanf(“%c”, &postfix[i]);
if (postfix[i] == ‘)’)
break;
}
evaluate(postfix);
return 0;
}
Stacks 151
Output:
Enter postfix expression, press right parenthesis ‘)’ for end expression: 248+-8+)
Result of expression evaluation: -2
Enter postfix expression, press right parenthesis ‘)’ for end expression: 255+86-/)
Result of expression evaluation: 2
Evaluation of Prefix Expression
Evaluation of prefix expression can be done by reversing the given expression to obtain
postfix expression and then evaluating it using the algorithm for postfix evaluation. So we have to
include string reversing function in the above program.
A prefix string is converted to equivalent postfix string, for example:
Prefix Expression: -/*2*5+3652
After reversing Postfix equivalent: 2563+5*2*/-
6. Backtracking – 4 Queens problem
Here we give an interesting application of stacks: implementing backtracking to solve the N-
Queens problem.
We will start with a description of a problem which involves queens from a chess game, and
a chess board.
The N-Queens problem
z Some of you may have seen this problem before. The goal is to place all the queens on
the board so that none of the queens are attacking each other.
z Two queens are not allowed in the same row... or in the same column... or along the same
diagonal.
z The number of queens and the size of the board can vary. We give below a program to
solve N-queens problem for varying number of queens. The number of queens must
always equal the size of the chess board. For example, if you have six queens, then the
board will be a six by six chess board.
z Our program tries to find a way to place N queens on an N x N chess board.
z Let us understand N-Queens problem stepwise with N = 4.
1. A stack is used a stack to keep track where each queen is placed. (Fig. 4.7)
2. Each time the program decides to place a queen on the board, the position of the new
queen is pushed onto the stack.
For example, when we place the first queen in the first column of the first row, we record
this placement by pushing a record onto the stack. This record contains both the row and
column number of the newly placed queen (Fig. 4.8).
3. We need to keep track of one more variable: an integer which tells us how many rows
currently have a queen placed (Fig. 4.9).
4. Whenever we try to place a new queen in the next queen in the next row, we start by row,
we start by placing the queen in the first column. When we successfully place a queen in
one row, we move to the next row. We always start by trying to place the queen in the
first column of the new row (Fig. 4.10).
5. If there is a conflict with another queen, then we shift the new queen to the next column
(Fig. 4.11).
6. Sometimes another conflict will occur, and the newly-placed queen must continue
shifting rightward. When there are no conflicts, we stop and add one to the value of filled
(Fig. 4.12).
7. Let’s look at the third row. The first position we try has a conflict. (Fig. 4.13)
8. As shown in Fig. 4.13, there is a conflict with the placement of the new queen, so we
move her rightward to the second column (Fig. 4.14).
9. Again a conflict arises, so we move rightward to the third column (Fig. 4.15).
10. Again another conflict arises, so we move to the fourth column. The key idea is that each
time we try a particular location for the new queen, we should check whether the new
location causes conflicts with our previous queens. If so, then we move the new queen to
the next possible location (Fig.4.16).
154 Data Structures and Algorithms – I
11. But as we see in Fig. 4.16 there’s again a conflict and there’s nowhere else to go.
Sometimes we run out of possible locations for the new queens. This is where
backtracking comes into play.
12. So now, we backtrack with following steps:
倯 pop the stack,
倯 reduce filled by 1
倯 and continue working on the previous row.
Now, at the previous row, we continue shifting the queen rightward.
This is shown in (Fig. 4.17).
13. Now we continue working on row 2, shifting the queen to the right. (Fig. 4.18). Notice
that we continue the previous row from the spot where we left off. The queen shifts from
column 3 to column 4. We don’t return her back to column 1. It is the use of the stack that
lets us easily continue where we left off. The stack contains the position of the previous
queen, so we can just move the queen rightward one more position.
14. This position has no conflicts, so no conflicts, so we can increase filled by 1, and move to
row 3 (Fig. 4.19).
Stacks 155
15. At the new row, we again start at the first column. So the general rules are: When the
algorithm moves forward, it always starts with the first column (Fig. 4.20). But when the
algorithm backtracks, it continues wherever it left off.
Here we give Pseudocode for N-Queens problem:
1. Initialize a stack to keep track of our decisions.
2. Place the first queen, push its position onto the stack and set filled to 0.
3. Repeat the following steps:
(a) If there are no conflicts with the queens then
Increment filled by 1.
If filled is equal to N, then the algorithm is done.
Otherwise, move to the next row and place a queen in the first column.
(b) Else if there is a conflict and there is room to shift the current queen rightward then
Move the current queen rightward and adjust the record on top of the stack to indicate
the new position.
(c) Else if there is a conflict and there is no room to shift the current queen rightward then
Backtrack! Keep popping the stack, and reducing filled by 1, until you reach a row
where the queen can be shifted rightward.
Shift this queen right.
But there is one potential pitfall here!
The potential pitfall: The stack may become empty during this popping. What does this
indicate?
Answer: It means that we backtracked right back to the beginning, and ran out of possible
places to place the first queen. In that case, the problem has no solution.
C Program: N-Queens Problem
#define N 4
#include <stdio.h>
void print_Solution(int board[N][N]) /* A function to print solution */
156 Data Structures and Algorithms – I
{
int i,j;
for (i = 0; i< N; i++)
{
for (j = 0; j < N; j++)
p rintf(“ %d”, board[i][j]);
printf(“\n”);
}
}
//-----------------------------------------------------------------------------------------------------------
/* A function place()checks if a queen can be placed on board[row][col]. It is called when
“col” queens are already placed in columns from 0 to col -1. So check only left side for
attacking queens */
int place(int board[N][N], int row, int col)
{
int i, j;
{
board[i][col] = 1; /* Place this queen in board[i][col] */
if (solve_NQ(board, col + 1)) /* call recursively to place remaining queens */
return 1;
/*If placing queen in board[i][col] doesn’t lead to a solution, then backtrack i.e., remove
queen from board[i][col] */
board[i][col] = 0; // BACKTRACK
}
}
/* If the queen cannot be placed in any row in this column col then return false */
return 0;
}
//----------------------------------------------------------------------------------------------------------
/* FunctionN_Queens() returns false if queens cannot be placed, otherwise, returns true and
prints placement of queens in the form of 1s. Note: There may be more than one solutions, this
function prints one of the feasible solutions.*/
int N_Queens()
{
int board[N][N] = { { 0, 0, 0, 0 },
{0, 0, 0, 0},
{0, 0, 0, 0},
{0, 0, 0, 0} };
if (solve_NQ(board, 0) == 0) {
printf(“Solution does not exist”);
return 0;
}
print_Solution(board);
return 1;
}
// -----------------------------------------------------------------------------------------------------------
int main()
{
N_Queens();
return 0;
}
158 Data Structures and Algorithms – I
Output:
The 1 values indicate placements of queens
0010
1000
0001
0100
QUESTIONS
Practice Questions (2 marks)
1. What are the applications of stack?
2. State the difference between stack and linked list.
3. Explain the basic operations performed on stack.
4. Define the following operations in stack:
(a) Push()
(b) pop()
(c) peek()
(d) isempty()
(e) isfull()
5. Write the postfix form of the following expressions:
(a) A ** B ** C
(b) -A + B - C + D
(c) A ** -B + C
(d) (A + B) * D + E/(F + A * D) + C
6. What is backtracking in N-queens problem?
Practice Questions (4 marks)
1. Write a function to reverse a string using stack.
2. Write a function to check whether a given string is palindrome or not (use stack).
3. Write a ‘C’ program for dynamic implementation of stack.
4. Write a function to reverse a string using stack.
5. Write an algorithm to evaluate postfix expression.
6. Write an algorithm for evaluation of prefix expression.
7. Write a function to check whether given expression is parenthesis or not.
8. Write an algorithm to convert given infix expression to postfix expression.
9. Evaluate the following Postfix expression:
4, 5, 4, 2, ^, +, *, 2, 2, ^, 9, 3, 1, *, -
10. Explain 4-queens problem in detail. Also give an algorithm for it.
11. Discuss 4-queens problem. Give a C function to solve N-queens problem.
5 QUEUES
Structures
5.1 Introduction
5.2 Representation – Static and Dynamic
5.3 Operations – init(), enqueue(), dequeue(), isEmpty(), isFull(), peek()
5.4 Types of Queue
5.4.1 Simple Queue
5.4.2 Circular Queue
5.4.2 Priority Queue
5.4.3 Double Ended Queue
5.5 Applications – Job Scheduling with priority
5.1 Introduction
Queue
A Queue is a linear data structure which performs operations in a particular order. The order
is called as First-In-First-Out (FIFO). Common example of a queue is any queue of consumers for
a resource where the consumer that comes first is served first. People waiting in line for a rail
ticket or bus-stops is another example in day to day life.
The difference between stacks and queues is in deleting an element. In a stack we delete or
remove the item the most recently added; in a queue, we remove the item the least recently added.
1. A queue is defined as an ordered list which enables insertion one end called REAR and
deletion at another end called FRONT.
2. Queue is therefore called as First In-First Out list.
Applications of Queue
A queue performs operations on first in first out basis. This feature of queue makes it
suitable for the ordering of actions in various applications. Below are some applications of queues.
1. Queues are mostly used as waiting lists for a single shared resource like printer, disk,
CPU.
2. In asynchronous transfer of data, where data is not being transferred at the same rate
between two processes, queues are used. For example pipes, file IO, sockets.
3. Queues are used for buffering in most of the applications like MP3 media player, CD
player, etc.
4. The play lists in media players make use of queues to add and remove the songs.
5. Queues are used in operating systems for handling interrupts.
5.2 Representation of Queue
Queue can be implemented in two ways – Static and Dynamic
1. Static Representation of Queue Using Array
Static implementation or array representation of queue requires three entities–
z An array to hold queue elements.
z A variable to hold the index of the front element.
z A variable to hold the index of the rear element.
A queue is easily represented using linear array. Two variables, front and rear, are
implemented in every queue. These two variables point to the position from where insertions and
deletions are performed in a queue. Initially, the value of front and queue is -1 which represents
an empty queue. Insertion and deletion operations in queue are called enqueue and dequeue
respectively. A queue represented using array with 5 elements along with the respective values of
front and rear, is shown in the following figure.
The above figure shows the queue of characters forming the English word “ABCDE”. With
the first element added in the queue, value of front is set to 0 now. The value of rear has become 4
with the insertions performed in the queue. After inserting an element ‘N’ into the queue, the
queue will look as shown in the figure below. The value of rear will become 5 while the value of
front remains same.
Queues 161
exit(0);
break;
default:
printf(“\nEnter valid choice\n”);
}
}
}
//-----------------------------------------------------
void insert()
{
int num;
printf(“\nEnter the element\n”);
scanf(“\n%d”, &num);
if(rear == MAX-1)
{
printf(“\nQueue OVERFLOW\n”);
return;
}
if(front == -1 && rear == -1)
{
front = 0;
rear = 0;
}
else
{
rear = rear+1;
}
queue[rear] = num;
printf(“\nElement added in queue”);
}
//--------------------------------------------------------
void delete()
{
int item;
if (front == -1 || front > rear)
{
printf(“\nQueue UNDERFLOW\n”);
return;
}
else
{
item = queue[front];
if(front == rear)
{
front = -1;
rear = -1 ;
164 Data Structures and Algorithms – I
}
else
{
front = front +1;
}
printf(“\nElement deleted from queue”);
}
}
//------------------------------------------------------
void display()
{
int i;
if(rear == -1)
printf(“Queue is empty\n”);
else
{
printf(“Queue contains.....\n”);
for(i=front;i<=rear;i++)
{
printf(“\n%d\n”,queue[i]);
}
}
}
Output:
-----QUEUE--------
1. Insert
2. Delete
3. Display
4. Exit
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
Queue contains.....
90
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
The above figure shows how the memory space is wasted in the array representation of
queue. In the above figure, a queue of size 10 having 3 elements, is shown. The value of the rear
variable is 5, therefore, we cannot reinsert the values in the place of already deleted element
before the position of front. That much space of the array is wasted and cannot be used in the
future (for this queue).
(b) Deciding the Array Size
Deciding the size of the array is a problem with static implementation. In this
implementation, array size is to be declared in advance. The size of the queue can be extended at
runtime if required. When we use array for implementing queue, extending array size is a time
consuming process and almost impossible to be performed at runtime since a lot of reallocations
take place.
If, to avoid this, we declare the array large enough so that we can store queue elements as
enough as possible, but most of the array slots (nearly half) can never be reused. This will again
lead to memory wastage.
2. Dynamic Implementation of Queue (Using Linked List)
Because of the drawbacks discussed above, the array implementation cannot be used for the
large-scale applications where the queues are implemented. Another alternative for implementing
queues is using linked list. We will call such queue a linked queue.
The storage requirement of linked representation of a queue with n elements is O(n), while
the time requirement for operations is O(1).
In a linked queue,
1. Each node of the queue has two parts, data part and the next part. Each element of the
queue points to its immediate next element in the memory.
2. Two pointers are maintained in the memory, i.e., front pointer and rear pointer. The front
pointer contains the address of the starting element of the queue while the rear pointer
contains the address of the last element of the queue.
3. Insertion is performed at rear end and deletion is performed at front end. If both front and
rear pointers are NULL, it indicates that the queue is empty.
The linked representation of queue is shown in the following figure:
int choice;
printf(“-----Dynamic implementation of queue-----\n”);
while(choice != 4)
{
printf(“\n1. Insert \n 2. Delete \n
3. Display \n 4. Exit\n”);
printf(“\nEnter your choice ?”);
scanf(“%d”, & choice);
switch (choice)
{
case 1:
enqueue ();
break;
case 2:
dequeue ();
break;
case 3:
display();
break;
case 4:
exit(0);
break;
default:
printf(“\nEnter valid choice\n”);
}
}
}
//---------------------------------------------------
void enqueue ()
{
struct node *ptr;
int num;
ptr=(struct node *) malloc(sizeof(struct node)); if(ptr == NULL)
{
printf(“\nOVERFLOW\n”);
return;
}
else
{
printf(“\nEnter element to add\n”);
scanf(“%d”, &num);
ptr ĺ data = num;
if(front == NULL)
{
front = ptr;
rear = ptr;
170 Data Structures and Algorithms – I
printf(“\nUNDERFLOW\n”);
return;
}
else
{
ptr = front;
front = front ĺ next;
free(ptr);
}
}
//------------------------------------------------------
void display()
{
struct node *ptr;
ptr = front;
if(front == NULL)
printf(“\nEmpty queue\n”);
else
{
printf(“\nQueue contains .....\n”);
while(ptr != NULL)
{
printf(“\n%d\n”,ptr ĺ data);
ptr = ptr ĺ next;
}
}
}
Queues 171
5.3 Operations
Queues maintain two data pointers, front and rear. Therefore, its operations are
comparatively difficult to implement than that of stacks. The basic operations performed on queue
are:
1. init() – initializes a queue
2. enqueue() í add (store) an item to the queue. If the queue is full, then it is said to be an
Overflow condition.
The following steps should be taken to enqueue (insert) data into a queue í
1. Check if the queue is full.
2. If the queue is full, print overflow error and exit.
3. If the queue is not full, increment rear pointer to point the next empty space.
4. Add new element to the queue location, pointed to by rear.
5. Return success.
Implementation of enqueue() in C Language í
int enqueue(int data)
{
if(isfull())
return 0;
rear = rear +1;
queue[rear] = data;
return 1;
}
3. dequeue() í remove (access) an item from the queue. The items are popped in the same
order in which they are pushed. If the queue is empty, then it is said to be an Underflow
condition.
Accessing data from the queue is a process of two tasks í access the data where front is
pointing and remove the data after access. The following steps are taken to perform dequeue
operation í
1. Check if the queue is empty.
2. If the queue is empty, produce underflow error and exit.
3. If the queue is not empty, access the data where front is pointing.
4. Increment front pointer to point to the next available data element.
5. Return success.
Implementation of dequeue() in C language í
int dequeue() {
if(isempty())
return 0;
return data;
}
4. peek() í Gets the element at the front of the queue without removing it.
This function helps to see the data at the front of the queue. The algorithm of peek()
function is as follows –
Implementation of peek() function in C language í
int peek() {
return queue[front];
}
5. isfull() í Check if the queue is full.
As we are using single dimension array to implement queue, we just check for the rear
pointer to reach at MAXSIZE to determine that the queue is full. In case we maintain the queue in
a circular linked-list, the algorithm will differ. Algorithm of isfull() function –
Implementation of isfull() function in C Language í
intisfull()
{
if(rear == MAXSIZE - 1)
return 1;
else
return 0;
}
The Queue shown in above figure is completely filled and no element can be inserted any
more due to the condition rear == max - 1 becomes true.
However, if we delete 2 elements at the front end of the queue, we still cannot insert any
element since the condition rear = max -1 still holds. This is the main problem with the linear
queue, although we have space available in the array, we cannot insert new elements in the queue.
This leads to memory wastage and the problem should be overcome.
Fig. 5.9
174 Data Structures and Algorithms – I
Circular queue will be full when front = -1 and rear = max-1. Implementation of circular
queue is similar to that of a linear queue except the logic for insertion and deletion is different
from that of a linear queue.
Working of Circular Queue
Circular queue works by the process of circular increment i.e., when we try to increment any
variable and we reach the end of the queue, we start from the beginning of the queue by modulo
division with the queue size.
i.e.,
if REAR + 1 == 5 (overflow!), REAR = (REAR + 1) % 5 = 0 (start of queue)
Queue operations work as follows:
z Two pointers, front and rear are used to keep track of the first and last elements in the
queue.
z Initialize the queue, set the value of front and rear to -1.
z On enqueuing an element, we circularly increase the value of index and place the new
element in the position pointed to by rear.
z On dequeuing an element, we return the value pointed to by front and circularly increase
the front index.
z Before enqueuing, we check if the queue is already full.
z Before dequeuing, we check if the queue is already empty.
z When enqueuing the first element, we set the value of front to 0.
z When dequeuing the last element, we reset the values of front and rear to -1.
However, the check for full queue has a new additional case:
Case 1: FRONT = 0 && REAR == SIZE - 1
Case 2: FRONT = REAR + 1
Case 2 happens when REAR starts from 0 due to circular increment and when its value is
just 1 less than FRONT, the queue is full.
Following figure explains the working of circular Queue:
Queues 175
}
else if(front == -1 && rear == -1)
{
front = 0;
rear = 0;
}
else if(rear == MAX -1 && front != 0)
rear = 0;
else
rear = (rear+1)%MAX;
queue[rear] = item;
printf(“\nValue inserted”);
}
//---------------------------------------------------
void delete()
{
int item;
if(front == -1 & rear == -1)
{
printf(“\nUNDERFLOW\n”);
return;
}
else if(front == rear)
{
front = -1;
rear = -1;
}
else if(front == max size -1)
{
front = 0;
}
else
front = front + 1;
printf(“\nValue deleted”);
}
//----------------------------------------------------
void display()
{
int i;
if(front == -1)
printf(“\nCircular Queue is Empty!!!\n”);
else
Queues 179
{
i = front;
printf(“\nCircular Queue Elements are : \n”);
if(front <= rear)
{
while (i <= rear)
printf(“%d %d %d\n”, queue[i++], front, rear);
}
else
{
while (i <= MAX - 1)
printf(“%d %d %d\n”, queue[i++], front, rear); i = 0;
while (i <= rear)
printf(“%d %d %d\n”, queue[i++], front, rear);
}
}
}
Output:
---Circular Queue----
1. Insert
2. Delete
3. Display
4. Exit
Enter your choice? 1
Enter the element
1
Value inserted
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
Enter your choice? 1
Enter the element
2
Value inserted
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
Enter your choice? 1
Enter the element
180 Data Structures and Algorithms – I
3
Value inserted
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
Enter your choice? 3
Circular Queue Elements are:
1
2
3
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
Enter your choice? 2
Value deleted
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
Enter your choice? 1
Enter the element
4
Value inserted
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
Enter your choice? 3
Circular Queue Elements are:
2
3
4
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
Enter your choice? 1
Enter the element
1
OVERFLOW
Queues 181
1. Insert an element
2. Delete an element
3. Display the queue
4. Exit
Enter your choice?
4
Fig. 5.11
front=new;
else
rear->next =new;
rear=new;
rear->next =front;
count++;
}
//----------------------------------------------------
int dequeue(void)
{
int n;
cqueue *temp;
if(count==0)
return (-1);
count--;
if(front==rear)
{
n=front->data;
free(front);
front=NULL;
rear=NULL;
}else
{
temp= front;
n = temp->data;
front = front ->next;
rear ->next = front;
free ( temp );
}
return n;
}
//-------------------------------------------------
void display(void)
{
cqueue *temp;
int i;
if(count==0)
pf(“Empty”);
else
{
temp=front;
for(i=0;i<count; i++)
Queues 183
{
printf(“%d “,temp->data);
temp=temp->next;
}
}
printf(“\n”);
}
//---------------------------------------------
int find_size(void)
{
return count;
}
//----------------------------------------------
int main()
{
int n, ch=10;
while(ch!=0)
{
printf(“\n Select the option\n”);
printf (“1. Insert\n”);
printf (“2. Delete\n”);
printf (“3. SizeOfQueue\n”);
printf (“4. Display\n”);
printf (“0. EXIT\n”);
scanf(“%d”,&ch);
switch(ch)
{
case 1:
{
printf(“Enter element to insert \n”);
scanf(“%d”, &n);
enqueue (n);
break;
}
case 2:
{
n=dequeue();
if(n==-1)
printf(“Queue is empty\n”);
else
printf(“Number deleted from queue is%d\n”, n);
break;
184 Data Structures and Algorithms – I
}
case 3:
{
n=find_size();
printf(“Size of queue is %d\n”, n);
break;
}
case 4:
{
printf(“Queue is ….”);
display();
}
case 0:
break;
default:
printf(“Wrong Choice\n”);
break;
}
}
}
Output:
Select the option
1. Insert
2. Delete
3. Size of Queue
4. Display
0. EXIT
1
Enter the element to insert in queue
10
Select the option
1. Insert
2. Delete
3. Size of Queue
4. Display
0. EXIT
1
20
Select the option
1. Insert
2. Delete
Queues 185
3. Size of Queue
4. Display
0. EXIT
3
Size of queue is 2
Select the option
1. Insert
2. Delete
3. Size of Queue
4. Display
0. EXIT
4
Queue is …10 20
Select the option
1. Insert
2. Delete
3. Size of Queue
4. Display
0. EXIT
2
Number deleted from queue is 10
Select the option
1. Insert
2. Delete
3. Size of Queue
4. Display
0. EXIT
4
Queue is …20
Fig. 5.12
z insert() operation can be implemented by adding an item at end of array in O(1) time.
z getHighestPriority() operation can be implemented by linearly searching the highest
priority item in array. This operation takes O(n) time.
z deleteHighestPriority() operation can be implemented by first linearly searching an
item, then removing the item by moving all subsequent items one position back.
Queues 187
1. Using Linked List: The time complexity of all operations with linked list remains same
as array. The advantage with linked list is deleteHighestPriority() can be more efficient as
we don’t have to move items.
2. Using Heaps: Heap is generally preferred for priority queue implementation because
heaps provide better performance compared arrays or linked list. In a Binary Heap,
getHighestPriority() can be implemented in O(1) time, insert() can be implemented in
O(Log n) time and deleteHighestPriority() can also be implemented in O(Log n) time.
Applications of Priority Queue:
Priority Queue is used in all queue applications where priority is involved like:
z In Operating Systems: CPU Scheduling, load balancing on server and interrupt handling.
z Graph algorithms like Dijkstra’s shortest path algorithm, Prim’s Minimum Spanning Tree
etc.
z Artificial Intelligence: A* Search Algorithm : The A* search algorithm finds the shortest
path between two vertices of a weighted graph, trying out the most promising routes first.
The priority queue (also known as the fringe) is used to keep track of unexplored routes,
the one for which a lower bound on the total path length is smallest is given highest
priority.
z Heap Sort: Heap sort is typically implemented using Heap which is an implementation of
Priority Queue.
z Used for data compression in Huffman code.
printf(“\n3. Display”);
printf(“\n4. Exit”);
create();
while(1)
{
188 Data Structures and Algorithms – I
return;
}
else
check(data);
rear++;
}
//-----------------------------------------------------
voidcheck(int data)
{
inti,j;
for(i=0;i<= rear;i++)
{
if(data >=pri_que[i])
{
for(j = rear +1; j >i; j--)
{
pri_que[j]=pri_que[j -1];
}
pri_que[i]= data;
return;
}
}
pri_que[i]= data;
}
//-------------------------------------------------
voiddelete(int data)
{
inti;
if((front==-1)&&(rear==-1))
{
printf(“\nQueue is empty no elements to delete”);
return;
}
for(i=0;i<= rear; i++)
{
if(data ==pri_que[i])
{
for(;i< rear; i++)
{
pri_que[i]=pri_que[i+1];
}
pri_que[i]=-99;
190 Data Structures and Algorithms – I
rear--;
if(rear ==-1)
front =-1;
return;
}
}
printf(“\n%d not found in queue to delete”, data);
}
//-------------------------------------------------
voiddisplay()
{
if((front ==-1)&&(rear ==-1))
{
printf(“\nQueue is empty”);
return;
}
for(; front <= rear; front++)
printf(“%d”, pri_que[front]);
front =0;
}
Output:
1. Insert an element into queue
2. Delete an element from queue
3. Display queue elements
4. Exit
Enter your choice: 1
Enter value to be inserted: 10
Enter your choice: 1
Enter value to be inserted: 14
Enter your choice: 1
Enter value to be inserted: 43
Enter your choice: 3
43 14 10
Enter your choice: 5
Choice is incorrect, Enter a correct choice
Enter your choice: 1
Enter value to be inserted: 5
Enter your choice: 3
43 14 10 5
Enter your choice: 1
Enter value to be inserted: 50
Queues 191
void enQueue(int);
int deQueueFront();
int deQueueRear();
void enQueueRear(int);
void enQueueFront(int);
void display();
int queue[SIZE];
int rear = 0, front = 0;
int main()
{
char ch;
int choice, opt, value;
printf(“\n***Double Ended queue ***\n”);
do
{
printf(“\n1. Input-restricted deque \n”);
printf(“2. Output-restricted deque \n”);
printf(“\nSelect the option\n”);
scanf(“%d”, &opt);
switch(opt)
{
case 1:
printf(“\nSelect the Operation\n”);
printf(“1. Insert\n2. Delete from Rear\n 3. Delete from Front\n 4. Display”);
do
Queues 193
{
printf(“\nEnter your choice for the operation in deque:”);
scanf(“%d”, &choice);
switch(choice)
{
case 1: enQueueRear(value);
display();
break;
case 2:
value = deQueueRear();
printf(“\nThe value deleted is %d”, value);
display();
break;
case 3:
value=deQueueFront();
printf(“\n The value deleted is %d”, value);
display();
break;
case 4:
display();
break;
default:
printf(“Wrong choice”);
}
printf(“\nDo you want to perform another operation (Y/N):”);
ch=getch();
}while(ch== ‘y’||ch==‘Y’);
getch();
break;
case 2:
printf(“\n---- Select the Operation ----\n”);
printf(“1. Insert at Rear\n 2. Insert at Front \n3. Delete\n 4. Display”);
do
{
printf(“\nEnter your choice for the operation:”);
scanf(“%d”,&choice2);
switch(choice2)
{
case 1: enQueueRear(value);
display();
break;
194 Data Structures and Algorithms – I
case 2: enQueueFront(value);
display();
break;
case 3: value = deQueueFront();
printf(“\nThe value deleted is %d”, value);
display();
break;
case 4: display();
break;
default:printf(“Wrong choice”);
}
printf(“\nDo you want to perform another operation (Y/N):”);
ch=getch();
} while(ch==‘y’||ch==‘Y’);
getch();
break;
}
printf(“\nDo you want to continue(y/n):”);
ch=getch();
}while(ch==‘y’||ch==‘Y’);
}
//------------------------------------------------------
void enQueueRear(int value)
{
char ch;
if(front == SIZE/2)
{
printf(“\nQueue is full\n”);
return;
}
do
{
printf(“\nEnter the value to be inserted:”);
scanf(“%d”, &value);
queue[front] = value;
front++;
printf(“Do you want to continue insertion Y/N”);
ch=getch();
}while(ch==‘y’);
}
//------------------------------------------------------
void enQueueFront(int value)
Queues 195
{
char ch;
if(front==SIZE/2)
{
printf(“\nQueue is full \n”);
return;
}
do
{
printf(“\nEnter the value to be inserted:”);
scanf(“%d”, &value);
rear--;
queue[rear] = value;
printf(“Do you want to continue insertion Y/N”);
ch = getch();
}
while(ch == ‘y’);
}
//------------------------------------------------------
int deQueueRear()
{
int x;
if(front == rear)
{
printf(“\nQueue is Empty!!! \n”);
return 0;
}
front--;
x = queue[front+1];
return x;
}
//---------------------------------------------------
int deQueueFront()
{
int x;
if(front == rear)
{
printf(“\nQueue is Empty\n”);
return 0;
}
rear++;
x = queue[rear-1];
196 Data Structures and Algorithms – I
return x;
}
//--------------------------------------------------------
void display()
{
int i;
if(front == rear)
printf(“\nQueue is Empty\n”);
else{
printf(“\nThe Queue elements are:”);
for(i=rear; i< front; i++)
printf(“%d\t”,66 queue[i]);
}
}
Output
*** Double Ended Queue ***
1. Input-restricted deque
2. output-restricted deque
Select the option 1
Select the Operation
1. Insert
2. Delete from Rear
3. Delete from Front
4. Display
Enter your choice for the operation in c deque: 1
Enter the value to be inserted: 10
Do you want to continue insertion Y/N
Enter the value to be inserted: 20
Do you want to continue insertion Y/N
Enter the value to be inserted: 30
Do you want to continue insertion Y/N
The Queue elements are: 10 20 30
5.5 Applications – Job Scheduling with Priority
Job scheduling is an essential part of a multiprogramming operating systems. Such operating
systems allow more than one process (job) to be loaded into the memory at a time and the loaded
processes share the CPU using time multiplexing.
The job scheduling is done by a software called process manager that handles the removal of
the running process from the CPU and the selection of another process on the basis of a particular
strategy.
Queues 197
Fig. 5.16
The operating system (OS) uses different policies to manage each queue (FIFO, Round
Robin, Priority, etc.). The OS scheduler determines how to move processes between the ready and
run queues which can only have one entry per processor core on the system; in the above diagram,
it has been merged with the CPU.
A Process scheduler schedules different processes to be assigned to the CPU based on
particular scheduling algorithms. There are six popular process scheduling algorithms used:
z First-Come, First-Served (FCFS) Scheduling
z Shortest-Job-Next (SJN) Scheduling
z Priority Scheduling
z Shortest Remaining Time
z Round Robin (RR) Scheduling
z Multiple-Level Queues Scheduling
These algorithms are either non-pre-emptive or pre-emptive. Non-pre-emptive algorithms
are designed so that once a process enters the running state, it cannot be pre-empted until it
completes its allotted time. Pre-emptive scheduling is based on priority where a scheduler may
pre-empt a low priority running process anytime when a high priority process enters into a ready
state.
Let us see in detail priority based scheduling.
Priority Based Scheduling
z Priority scheduling is a non-pre-emptive algorithm and one of the most common
scheduling algorithms in batch systems.
z Each process is assigned a priority. Process with highest priority is to be executed first
and so on.
z Processes with same priority are executed on first come first served basis.
z Priority can be decided based on memory requirements, time requirements or any other
resource requirement.
Consider an example that illustrates this algorithm.
Given: Table of processes, and their Arrival time, Execution time, and Priority.
Here we are considering 1 is the lowest priority.
Queues 199
ISBN: 978-93-5433-116-9