0% found this document useful (0 votes)
635 views324 pages

Introduction To Data Structures

This document provides an introduction to data structures using C++. It defines data structures as a way to organize data to allow for efficient operations. There are primitive data structures like integers and complex data structures like linked lists and trees. The document then describes several common data structures - arrays, linked lists, trees, stacks, queues, and graphs - and provides examples of each. It also defines algorithms and pseudo code.
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
Download as docx, pdf, or txt
0% found this document useful (0 votes)
635 views324 pages

Introduction To Data Structures

This document provides an introduction to data structures using C++. It defines data structures as a way to organize data to allow for efficient operations. There are primitive data structures like integers and complex data structures like linked lists and trees. The document then describes several common data structures - arrays, linked lists, trees, stacks, queues, and graphs - and provides examples of each. It also defines algorithms and pseudo code.
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1/ 324

UNIT I

DATA STRUCTURES WITH C++

1.0. Introduction to Data Structures


Data Structure is a way of collecting and organising data in such a way that we can perform
operations on these data in an effective way. Data Structures is about rendering data
elements in terms of some relationship, for better organization and storage.
In simple language, Data Structures are structure programmed to store ordered data, so
that various operations can be performed on it easily.
1.1. Basic types of Data Structures
As we discussed above, anything that can store data can be called as a data structure, hence
Integer, Float, Boolean, Char etc, all are data structures. They are known as Primitive Data
Structures.
Then we also have some complex Data Structures, which are used to store large and
connected data. Some example of Abstract Data Structure are :

 Linked List
 Tree
 Graph
 Stack, Queue etc.

All these data structures allow us to perform different operations on data. We select these
data structures based on which type of operation is required. We will look into these data
structures in more details in our later lessons.

 Arrays:
The simplest type of data structure is a linear array. It means a list of finite number n of
elements referenced respectively by a set of n consecutive numbers, usually 1,2,3,…,n. If we
choose the name A for the array then the elements of A are denoted by subscript notation
as a1, a2, a3,…, an , or by parenthesis notation A(1), A(2), A(3),…, A(n) or by bracket notation
A[1], A[2], A[3],… ,A[n]. For example, considering the following array named STUDENTS
containing 6 student names,

Here, STUDENTS[1] = Tanmay, STUDENTS[2] = H and so on.

 Linked List:
In computer science, a linked list is a data structure that consists of a sequence of data
records such that in each record there isa field that contains a reference (that is, a link) to
the next record in the sequence. In other words, A simple linear datastructure, each of
whose nodes includes pointers to the previous and subsequent nodes in the list, enabling
traversal of thestructure from any starting point. For example, considering following two
lists.

These are list of patients and doctors indicating which patient met which doctors and which
doctors saw which patient.

 Trees:
Data frequently contain a hierarchical relationship between various elements. The data
structure which reflects this relationship is called a rooted tree graph or simply a tree. This is
sometimes like the properties of an object. For example, properties to store of an employee
in the office directory can be name, age, sex, salary etc. Again the properties “name” can
have sub-properties like first name, middle name, last name etc. This can be showed as
follows.

 Stack:
A stack, also called a Last-In-First-Out (LIFO) system, where insertion and deletion can take
place only at one end called the top. This structure is similar in its operation to stack of
dishes in a spring system as in the following figure.
 Queue:
A queue, also called a First-In-First-Out (FIFO) system is a linear list where deletions will take
place only at the front end and insertions will take place only at the rear end. In the
following figure we can see a pipe with two ends, left end is the rear end and right end is the
front end. We are inserting some balls and those balls can only be inserted from the rear
and they can come out (deletions) from the front end.

 Graph
A graph data structure consists of a finite (and possibly mutable) set of vertices or nodes or
points, together with a set of unordered pairs of these vertices for an undirected graph or a
set of ordered pairs for a directed graph.

1.2 What is Algorithm ?


An algorithm is a finite set of instructions or logic, written in order, to accomplish a certain
predefined task. Algorithm is not the complete code or program, it is just the core
logic(solution) of a problem, which can be expressed either as an informal high level
description as pseudocode or using a flowchart.
From the data structure point of view, following are some important categories of
algorithms −

 Search − Algorithm to search an item in a data structure.

 Sort − Algorithm to sort items in a certain order.

 Insert − Algorithm to insert item in a data structure.

 Update − Algorithm to update an existing item in a data structure.

 Delete − Algorithm to delete an existing item from a data structure.

1.2.1. Characteristics of an Algorithm


Not all procedures can be called an algorithm. An algorithm should have the following
characteristics −
 Unambiguous − 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.

 Input − An algorithm should have 0 or more well-defined inputs.

 Output − An algorithm should have 1 or more well-defined outputs, and should


match the desired output.

 Finiteness − Algorithms must terminate after a finite number of steps.

 Feasibility − Should be feasible with the available resources.

 Independent − An algorithm should have step-by-step directions, which should be


independent of any programming code.

Example of Algorithm

Let us consider the following problem for finding the largest value in a given list of values.

Problem Statement : Find the largest number in the given list of numbers?
Input : A list of positive integer numbers. (List must contain at least one number).
Output : The largest number in the given list of positive integer numbers.

Consider the given list of numbers as 'L' (input), and the largest number as 'max' (Output).

Algorithm

 Step 1: Define a variable 'max' and initialize with '0'.


 Step 2: Compare first number (say 'x') in the list 'L' with 'max', if 'x' is larger than
'max', set 'max' to 'x'.
 Step 3: Repeat step 2 for all numbers in the list 'L'.
 Step 4: Display the value of 'max' as a result.

1.3. Pseudo code

Pseudo code is a simple way of writing programming code in English. Pseudo code is not
actual programming language. It uses short phrases to write code for programs before you
actually create it in a specific language.
1.3.1. RULES FOR PSEUDOCODE

 1.3.1.1.Pseudocode Notations
Pseudocode is a precise description of a solution as compared to a flowchart. To get a
complete description of the solution with respect to problem definition, pre–post conditions
and return value details are to be included in the algorithm header. In addition,
information about the variables used and the purpose are to be viewed clearly. To help
anyone get all this information at a glance, the pseudocode uses various notations such
as header, purpose, pre–post conditions, return, variables, statement numbers, and
subalgorithms.

Let us discuss the details of each.

 1.3.1.2 Algorithm Header


A header includes the name of the algorithm, the parameters, and the list of pre and post
conditions. This information is important to know about the algorithm just by reading the
header, not the complete algorithm.

 1.3.1.3 Purpose
The purpose is a brief description about what the algorithm does.

 1.3.1.4 Condition and Return Statements


The pre condition states the pre-requirements for the parameters, if any. For example, in an
algorithm for set operations, the pre condition may state that the input should be a group of
elements without duplicates. Sometimes, there are no pre conditions, in which case, we still
list the pre condition with the statement Nothing, as shown here.

 Pre Nothing
If there are several input parameters, then the pre condition should be shown for each.
For example, a simple array search Algorithm 1.2 has the following header:
algorithm search (val list<array>, val argument<integer>)
Search array for specific item and return index location.
 Pre list containing data array to be searched, argument containing data to be located
in the list
 Post None
Return Location if found else return −1 indicating that the element is not found

 1.3.1.5 Statement Numbers


The statements in an algorithm are numbered sequentially. For conditional or un-
conditional jumps and also for iteration statements, numbering helps identify the
statements uniquely.

4 while(i < 10) do


begin
4.1 x = x * y
4.2 i = i + 1
end

 1.3.1.6 Variables
Variables are needed in algorithms. We need not define every variable used in the
algorithm.
The use of meaningful variable names is appreciated as the context of the data is
indicated by its name.

 1.3.1.7 Statement Constructs


There are three statement constructs used for developing an algorithm. The objective is
that an algorithm should be made up of a combination of lesser constructs, say three, as
in the following:

1. sequence
2. decision
3. repetition

The use of only these constructs makes an algorithm easy to understand, debug, and
modify.

 Sequence
An algorithm is a sequence of instructions, which can be a simple instruction (input, output,
or assignment) or either of the other two constructs. Figure 1.3 shows an example of
such a sequence construct. Algorithm 1.3 computes the area of a circle.

Sequence construct
do action 1
do action 2
.
.
.
do action n
Algorithm 1.3
Pre None
Post None
Return None
1. Read Radius
2. AreaOfCircle = 2 * 3.142 * Radius * Radius
3. Print AreaOfCircle
4. Stop

 Decision
Some problems cannot be solved with the help of just a sequence of simple instructions.
Sometimes, we need to test the conditions. If the result of the testing is true, we follow a
sequence of instructions; if it is false, we follow a different sequence of instructions. This
is called decision or selection construct .

Series of actions
If a condition is true,
Then
Else

example 1.1 Compare two numbers to print the maximum among them.
Solution The algorithm for comparing two numbers is listed in Algorithm 1.4.
algorithm 1.4
Pre None
Post None
Return None
1. Read two numbers Num1 and Num2
2. If Num1 > Num2
Then Print Num1
Else Print Num2
3. Stop

 Repetition
In some problems, we need to repeat a set of instructions. We can use repetition construct
for this purpose. Figure 1.5 shows a repetition construct and an example of computing
the sum of first N numbers, N (Algorithm 1.5).

1.3.1.8 Subalgorithms
We studied three constructs — sequence, decision, and iteration — for developing
an algorithm for solvable problems. A solvable problem is a problem that has a solution
that can be described in the form of an algorithm.

while a condition is true do


Action 1
Action 2
.
.
.
Action n
end while

Algorithm 1.5
Pre None
Post None
Return SUM
1. Read N
2. Let SUM = 0 and Index = 1
3. while Index <= N do
SUM = SUM + Index
Index = Index + 1
end while
4. Return SUM
Repetition construct

In structured programming, the problem solution is described in the form of smaller


modules. This modular design breaks an algorithm into smaller units called subalgorithms.
These units are referred by various names in programming languages such as
functions, subroutines, procedures, methods, and modules.

example 1.2 Write an algorithm to compute the following:


P = n!/(n − r)!
Solution Algorithms 1.6 computes the number of possible ways of arranging any
r of n elements.
algorithm 1.6
Pre None
Post None
Return Result
1. Read n and r
2. Let
(a) A = FACT(n) and
(b) B = FACT(n − r)
3. Result = A / B

4. Print Result
5. Stop
Here FACT is the subalgorithm to compute the factorial of a number as
n! = n × (n - 1) × (n - 2) × ... × 1
subalgorithm FACT
1. Read n
2. Let Result = 1
3. while(n not equal to 1) do
Result = Result × n
n=n–1
end while
4. Return Result

1.4. ADVANTAGES AND DISADVANTAGES OF PSEUDOCODE

Pseudocode Advantages
 Can be done easily on a word processor
 Easily modified
 Implements structured concepts well
Pseudocode Disadvantages
 It’s not visual
 There is no accepted standard, so it varies widely from company to company

1.5. Algorithm vs Pseudo code:

 An algorithm is a well defined sequence of steps that provides a solution for a given
problem,
 while a pseudocode is one of the methods that can be used to represent an
algorithm.
 While algorithms can be written in natural language
 pseudocode is written in a format that is closely related to high level programming
language structures.
 pseudocode does not use specific programming language syntax and therefore could
be understood by programmers who are familiar with different programming
languages.
 Transforming an algorithm presented in pseudocode to programming code could be
much easier than converting an algorithm written in natural language.

1.6. Relationship among data , data structure and algorithm

There is an intimate relationship between the structuring of data and analysis of algorithms.
In fact, a data structure and an algorithm should be thought of as one single unit;
neither one making sense without the other.
Let us consider the example of searching for a person’s phone number in a directory. The
procedure we follow to search a person and get his/her phone number critically depends on
how the phone number and names are arranged in the directory. Let us consider two ways
of organizing the data (phone numbers and names) in the directory.

1. The data is organized randomly. Then to search a person by name, one has to
linearly start from the first name till the last name in the directory. There is no other
option.
2. If the data is organized by sorting the names (alphabetically sorted in ascending
order), then the search is much easier. Instead of linearly searching through all
records, one may search in a particular area for particular alphabets, similar to using
a dictionary.
As the data is in sorted order, both the binary search and a typical directory search methods
work. Hence our ideas for algorithms become possible when we realize that we can
organize the data as we wish.

1.7 Implementation of Data Structures

A data structure is an aggregation of atomic and composite data types into a set with the
relationship among them defined.
Hence, implementation of data structures can be viewed in terms of two phases:
specification and implementation. Such a division of tasks is useful as it helps to control the
complexity of the entire process.

Phase I: Specification At the first stage, a data structure should be designed so that we
know what it does and not necessarily how it will do it.

Phase II: Implementation At this stage, we define all functions with respect to
the description of how to manipulate data. This can be done with algorithms so that
the details of the operation can be understood easily, and the reader can implement
them easily and effectively with the help of any programming language. Either of the
design tools, that is, an algorithm or a flowchart, can be used at this phase.

1.8 Algorithm Analysis


Efficiency of an algorithm can be analyzed at two different stages, before implementation
and after implementation. They are the following −

 A Priori Analysis − This is a theoretical analysis of an algorithm. Efficiency of an


algorithm is measured by assuming that all other factors, for example, processor
speed, are constant and have no effect on the implementation.
 A Posterior Analysis − This is an empirical analysis of an algorithm. The selected
algorithm is implemented using programming language. This is then executed on
target computer machine. In this analysis, actual statistics like running time and
space required, are collected.

We shall learn about a priori algorithm analysis. 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.

1.9. 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.

 Time Factor − Time is measured by counting the number of key operations such as
comparisons in the sorting algorithm.

 Space Factor − Space is measured by counting the maximum memory space


required by the algorithm.

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.

1.9.1 Space Complexity


Space complexity of an algorithm represents the amount of memory space required by the
algorithm in its life cycle. The space required by an algorithm is equal to the sum of the
following two components −

 A fixed part that 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.

 A variable part is a space required by variables, whose size depends on the size of
the problem. For example, dynamic memory allocation, recursion stack space, etc.

Space complexity S(P) of any algorithm P is S(P) = C + SP(I), where C is the fixed part and S(I)
is the variable part of the algorithm, which depends on instance characteristic I. Following
is a simple example that tries to explain the concept −

Algorithm: SUM(A, B)
Step 1 - START
Step 2 - C ← A + B + 10
Step 3 - Stop
Here we have three variables A, B, and C and one constant. Hence S(P) = 1 + 3. Now, space
depends on data types of given variables and constant types and it will be multiplied
accordingly.

1.9.2 Time Complexity


Time complexity of an algorithm represents the amount of time required by the algorithm
to run to completion. Time requirements can be defined as a numerical function T(n),
where T(n) can be measured as the number of steps, provided each step consumes
constant time.

For example, addition of two n-bit integers takes n steps. Consequently, 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.10. Asymptotic Notations

Majorly, we use THREE types of Asymptotic Notations and those are as follows...

1. Big - Oh (O)

2. Big - Omega (Ω)

3. Big - Theta (Θ)

1.10.1. Big - Oh notation

Big - Oh notation is used to define the upper bound of an algorithm in terms of Time
Complexity.

That means Big - Oh notation always indicates the maximum time required by an algorithm
for all input values. That means Big - Oh notation describes the worst case of an algorithm
time complexity.

Big - Oh Notation can be defined as follows...

Consider function f(n) the time complexity of an algorithm and g(n) is the most significant
term. If f(n) <= C g(n) for all n >= n0, C > 0 and n0 >= 1. Then we can
represent f(n) as O(g(n)).
f(n) = O(g(n))

Consider the following graph drawn for the values of f(n) and C g(n) for input (n) value on X-
Axis and time required is on Y-Axis

In above graph after a particular input value n0, always C g(n) is greater than f(n) which
indicates the algorithm's upper bound.
Example

Consider the following f(n) and g(n)... f(n) = 3n + 2 , g(n) = n


If we want to represent f(n) as O(g(n)) then it must satisfy f(n) <= C x g(n) for all values of C
> 0 and n0>= 1

f(n) <= C g(n) ⇒3n + 2 <= C n

Above condition is always TRUE for all values of C = 4 and n >= 2.


By using Big - Oh notation we can represent the time complexity as follows...
3n + 2 = O(n)

1.10.2. Big - Omege Notation (Ω)

Big - Omega notation is used to define the lower bound of an algorithm in terms of Time
Complexity.

That means Big - Omega notation always indicates the minimum time required by an
algorithm for all input values. That means Big - Omega notation describes the best case of
an algorithm time complexity.

Big - Omega Notation can be defined as follows...

Consider function f(n) the time complexity of an algorithm and g(n) is the most significant
term. If f(n) >= C x g(n) for all n >= n0,C > 0 and n0 >= 1. Then we can
represent f(n) as Ω(g(n)).

f(n) = Ω(g(n))

Consider the following graph drawn for the values of f(n) and C g(n) for input (n) value on X-
Axis and time required is on Y-Axis

In above graph after a particular input value n0, always C x g(n) is less than f(n) which
indicates the algorithm's lower bound.
Example

Consider the following f(n) and g(n)... f(n) = 3n + 2, g(n) = n


If we want to represent f(n) as Ω(g(n)) then it must satisfy f(n) >= C g(n) for all values of C >
0 and n0>= 1

f(n) >= C g(n) ⇒3n + 2 <= C n

Above condition is always TRUE for all values of C = 1 and n >= 1.


By using Big - Omega notation we can represent the time complexity as follows...
3n + 2 = Ω(n)

1.10.3. Big - Theta Notation (Θ)

Big - Theta notation is used to define the average bound of an algorithm in terms of Time
Complexity.

That means Big - Theta notation always indicates the average time required by an algorithm
for all input values. That means Big - Theta notation describes the average case of an
algorithm time complexity.

Big - Theta Notation can be defined as follows...

Consider function f(n) the time complexity of an algorithm and g(n) is the most significant
term. If C1 g(n) <= f(n) >= C2 g(n) for all n >= n0, C1, C2 > 0 and n0 >= 1. Then we can
represent f(n) as Θ(g(n)).

f(n) = Θ(g(n))

Consider the following graph drawn for the values of f(n) and C g(n) for input (n) value on X-Axis and
time required is on Y-Axis

In above graph after a particular input value n0, always C1 g(n) is less than f(n) and C2 g(n) is
greater than f(n) which indicates the algorithm's average bound.
Example

Consider the following f(n) and g(n)... f(n) = 3n + 2, g(n) = n


If we want to represent f(n) as Θ(g(n)) then it must satisfy C1 g(n) <= f(n) >= C2 g(n) for all
values of C1, C2 > 0 and n0>= 1

C1 g(n) <= f(n) >= ⇒C2 g(n) , C1 n <= 3n + 2 >= C2 n

Above condition is always TRUE for all values of C1 = 1, C2 = 4 and n >= 1.


By using Big - Theta notation we can represent the time compexity as follows...
3n + 2 = Θ(n)

1.11. Concept of Stacks ans Queues

Stacks and queues are the two data structures where insert and delete operations are
applied at specifi c ends only. These are special cases of ordered lists and are also called
controlled linear lists. There is a wide variety of software applications where we need
these restricted data structure operations. The following are some examples where stacks
and queues are generally used:

1. Queues are widely used in applications that maintain a list of printing jobs waiting at a
network printer. Here, one queue that can hold all print requests from different users is
kept.
2. Handling function calls in programs very often restricts access at one end to keep track of
the returning position. In such implementation, we need to use stacks. We can keep track of
the return address to earlier function after furnishing/fi nishing a function call using stacks.
1.11.1. Stack
A stack is an Abstract Data Type (ADT), commonly used in most programming languages. It is
named stack as it behaves like a real-world stack, for example – a deck of cards or a pile of
plates, etc.
Stack Representation
The following diagram depicts a stack and its operations −
A stack can be implemented by means of Array, Structure, Pointer, and Linked List. Stack
can either be a fixed size one or it may have a sense of dynamic resizing. Here, we are going
to implement stack using arrays, which makes it a fixed size stack implementation.

We know that Stack can be represented using an array. Stack is open at one end and
operations can be performed on single end. We can have different primitive operations on
Stack Data Structure.

Creating Stack Data Structure :

typedef struct stack {


int data[MAX];
int top;
}stack;

Basic Operations Performed on Stack :

1. Create
2. Push
3. Pop
4. Empty

A. Creating Stack :

1. Stack can be created by declaring the structure with two members.


2. One Member can store the actual data in the form of array.
3. Another Member can store the position of the topmost element.

typedef struct stack {


int data[MAX];
int top;
}stack;

B. Push Operation on Stack :

We have declared data array in the above declaration. Whenever we add any element in the
‘data’ array then it will be called as “Pushing Data on the Stack”.
Suppose “top” is a pointer to the top element in a stack. After every push operation, the
value of “top” is incremented by one.
 Step 1 − Checks if the stack is full.

 Step 2 − If the stack is full, produces an error and exit.

 Step 3 − If the stack is not full, increments top to point next empty space.

 Step 4 − Adds data element to the stack location, where top is pointing.
Step 5 − Returns success.\ Algorithm for PUSH Operation
A simple algorithm for Push operation can be derived as follows −

begin procedure push: stack, data


if stack is full
return null
endif
top ← top + 1
stack[top] ← data
end procedure
Example

void push(int data) {


if(!isFull()) {
top = top + 1;
stack[top] = data;
} else {
printf("Could not insert data, Stack is full.\n");
}
}

C. Pop Operation on Stack :

Whenever we try to remove element from the stack then the operation is called as POP
Operation on Stack.
A Pop operation may involve the following steps −

 Step 1 − Checks if the stack is empty.

 Step 2 − If the stack is empty, produces an error and exit.

 Step 3 − If the stack is not empty, accesses the data element at whichtop is pointing.

 Step 4 − Decreases the value of top by 1.

 Step 5 − Returns success.


Algorithm for Pop Operation
A simple algorithm for Pop operation can be derived as follows −

begin procedure pop: stack


if stack is empty
return null
endif
data ← stack[top]
top ← top - 1
return data
end procedure
Example

int pop(int data) {


if(!isempty()) {
data = stack[top];
top = top - 1;
return data;
} else {
printf("Could not retrieve data, Stack is empty.\n");
}
}

Some basic Terms :

Concept Definition

The procedure of inserting a new element to the top of the


Stack Push
stack is known as Push Operation
Concept Definition

Any attempt to insert a new element in already full stack is


Stack Overflow
results into Stack Overflow.

The procedure of removing element from the top of the stack is


Stack Pop
called Pop Operation.

Any attempt to delete an element from already empty stack


Stack Underflow
results into Stack Underflow.

1.11.2 Stack ADT Operations

• push(o): Insert o at top of stack


– Input: Object; Output: None
• pop(): Remove top object; error if empty
– Input: None; Output: Object removed
• size(): Return number of objects in stack
– Input: None; Output: Integer
• isEmpty(): Return a boolean indicating stack empty
– Input: None; Output: Boolean
• top(): Return top object without removing; error if empty
– Input: None; Output: Object
1.11.3. Representation of Stacks using arrays

A stack data structure can be implemented using one dimensional array. But stack
implemented using array, can store only fixed number of data values. This implementation
is very simple, just define a one dimensional array of specific size and insert or delete the
values into that array by using LIFO principle with the help of a variable 'top'. Initially top is
set to -1. Whenever we want to insert a value into the stack, increment the top value by one
and then insert. Whenever we want to delete a value from the stack, then delete the top
value and decrement the top value by one.

Stack Operations using Array

A stack can be implemented using array as follows...

Before implementing actual operations, first follow the below steps to create an empty
stack.

 Step 1: Include all the header files which are used in the program and define a
constant 'SIZE' with specific value.
 Step 2: Declare all the functions used in stack implementation.
 Step 3: Create a one dimensional array with fixed size (int stack[SIZE])
 Step 4: Define a integer variable 'top' and initialize with '-1'. (int top = -1)
 Step 5: In main method display menu with list of operations and make suitable
function calls to perform operation selected by the user on the stack.

 push(value) - Inserting value into the stack

In a stack, push() is a function used to insert an element into the stack. In a stack, the new
element is always inserted at top position. Push function takes one integer value as
parameter and inserts that value into the stack. We can use the following steps to push an
element on to the stack...

 Step 1: Check whether stack is FULL. (top == SIZE-1)


 Step 2: If it is FULL, then display "Stack is FULL!!! Insertion is not possible!!!" and
terminate the function.
 Step 3: If it is NOT FULL, then increment top value by one (top++) and set stack[top]
to value (stack[top] = value).

 pop() - Delete a value from the Stack

In a stack, pop() is a function used to delete an element from the stack. In a stack, the
element is always deleted from top position. Pop function does not take any value as
parameter. We can use the following steps to pop an element from the stack...

 Step 1: Check whether stack is EMPTY. (top == -1)


 Step 2: If it is EMPTY, then display "Stack is EMPTY!!! Deletion is not
possible!!!" and terminate the function.
 Step 3: If it is NOT EMPTY, then delete stack[top] and decrement top value by one
(top--).

 display() - Displays the elements of a Stack

We can use the following steps to display the elements of a stack...

 Step 1: Check whether stack is EMPTY. (top == -1)


 Step 2: If it is EMPTY, then display "Stack is EMPTY!!!" and terminate the function.
 Step 3: If it is NOT EMPTY, then define a variable 'i' and initialize with top.
Display stack[i] value and decrement i value by one (i--).
 Step 3: Repeat above step until i value becomes '0'.

Demonstrate the implementation of a simple stack using arrays


#include <iostream>
using namespace std;
const int MAX_SIZE = 100;
class StackOverFlowException
{
public:
StackOverFlowException()
{
cout << "Stack overflow" << endl;
}
};

class StackUnderFlowException
{
public:
StackUnderFlowException()
{
cout << "Stack underflow" << endl;
}
};

class ArrayStack
{
private:
int data[MAX_SIZE];
int top;
public:
ArrayStack()
{
top = -1;
}

void Push(int element)


{
if ( top >= MAX_SIZE )
{
throw new StackOverFlowException();
}
data[++top] = element;
}

int Pop()
{
if ( top == -1 )
{
throw new StackUnderFlowException();
}
return data[top--];
}

int Top()
{
return data[top];
}
int Size()
{
return top + 1;
}
bool isEmpty()
{
return ( top == -1 ) ? true : false;
}
};

int main()
{
ArrayStack s;
try {
if ( s.isEmpty() )
{
cout << "Stack is empty" << endl;
}
// Push elements
s.Push(100);
s.Push(200);
// Size of stack
cout << "Size of stack = " << s.Size() << endl;
// Top element
cout << s.Top() << endl;
// Pop element
cout << s.Pop() << endl;
// Pop element
cout << s.Pop() << endl;
// Pop element
cout << s.Pop() << endl;
}
catch (...) {
cout << "Some exception occured" << endl;
}
}
OUTPUT:-
Stack is empty

Size of stack = 2
200
200
100
Stack underflow
Some exception occured
1.11.4. Multiple Stacks

Often, data is represented using several stacks. The contiguous stack (stack using an
array) uses separate arrays for more than one stack, if needed. The use of a contiguous
stack when more than one stack is needed is not a space-effi cient approach, because many
locations in the stacks are often left unused. An effi cient solution to this problem is to use
a single array to store more than one stack.

Multiple stacks can be implemented by sequentially mapping these stacks into


A[0], ..., A[n − 1]. The solution is simple if we implement only two stacks. The
first stack grows towards A[n 1] from A[0] and the second stack grows towards
A[0] from A[n − 1].
This way, we can make use of the space most efficiently so that the stack is full only
when the top of one stack reaches the top of other stack.
The difficulty arises when we have to represent m stacks in the memory. We can divide
A[0, ..., n -1] into m segments and allocate one of these segments to each of the
m stacks.
Implementation of multiple stacks using arrays
#include<iostream>
#include<stdlib.h>
using namespace std;
class twoStacks
{
int *arr;
int size;
int top1, top2;
public:
twoStacks(int n) // constructor
{
size = n;
arr = new int[n];
top1 = -1;
top2 = size;
}

// Method to push an element x to stack1


void push1(int x)
{
// There is at least one empty space for new element
if (top1 < top2 - 1)
{
top1++;
arr[top1] = x;
}
else
{
cout << "Stack Overflow";
exit(1);
}
}

// Method to push an element x to stack2


void push2(int x)
{
// There is at least one empty space for new element
if (top1 < top2 - 1)
{
top2--;
arr[top2] = x;
}
else
{
cout << "Stack Overflow";
exit(1);
}
}

// Method to pop an element from first stack


int pop1()
{
if (top1 >= 0 )
{
int x = arr[top1];
top1--;
return x;
}
else
{
cout << "Stack UnderFlow";
exit(1);
}
}

// Method to pop an element from second stack


int pop2()
{
if (top2 < size)
{
int x = arr[top2];
top2++;
return x;
}
else
{
cout << "Stack UnderFlow";
exit(1);
}
}
};

/* Driver program to test two stacks class */


int main()
{
twoStacks ts(5);
ts.push1(5);
ts.push2(10);
ts.push2(15);
ts.push1(11);
ts.push2(7);
cout << "Popped element from stack1 is " << ts.pop1();
ts.push2(40);
cout << "\nPopped element from stack2 is " << ts.pop2();
return 0;
}

Output:
Popped element from stack1 is 11
Popped element from stack2 is 40

1.11.5 Applications of stack

The stack data structure is used in a wide range of applications. A few of them are the
following:
1. Converting infix expression to postfix and prefix expressions
2. Evaluating the postfix expression
3. Checking well-formed (nested) parenthesis
4. Reversing a string
5. Processing function calls
6. Parsing (analyse the structure) of computer programs
7. Simulating recursion
8. In computations such as decimal to binary conversion
9. In backtracking algorithms (often used in optimizations and in games)
1.12. EXPRESSION EVALUATI ON AND CONVERSION

The most frequent application of stacks is in the evaluation of arithmetic expressions.


An arithmetic expression is made of operands, operators, and delimiters.
For instance, consider the following expression:
X = a/b \ c - d
Let a = 1, b = 2, c = 3, and d = 4.
One of the meanings that can be drawn from this expression could be
X = (1/2) \ (3 - 4) = -1/2

Another way to evaluate the same expression could be


X = (1/(2 \ 3)) - 4 = -23/6
To avoid more than one meaning being drawn out of an expression, we have to specify
the order of operation by using parentheses. For instance,
X = (a/b) \ (c - d)
The following operators are written in descending order of their precedence:

1. Exponentiation (^), Unary (+), Unary (-), and not (~)


2. Multiplication (\) and division (/)
3. Addition (+) and subtraction (-)
4. Relational operators <, £ , =, π, ≥, >
5. Logical AND
6. Logical OR
1.12.1. Polish Notation and expression conversion
Polish notation also known as prefix notation is defined as: In this notation system operator
is written before the operands. Example of polish notation or prefix notation is +AB or /CD.
In these two examples we can see that here A and B are two operands and + is an operator
which is written before operands, similarly we can say for the second example. The
conversion of an infix expression into prefix notation is shown belowes

Based on the operator position, expressions are divided into THREE types. They are as
follows...

1. Infix Expression

2. Postfix Expression

3. Prefix Expression
1.12.1.1. Infix Expression

In infix expression, operator is used in between operands.

The general structure of an Infix expression is as follows...

Operand1 Operator Operand2


 Operators are written in between their operands
 This is used in our common mathematical expressions.
 The operations(order of evaluation) are performed from left to right. and it obeys
precedence rules ie multiplication and division are performed before addition and
subtraction.
 Brackets can be used to change the order of evaluation
 examples

(a) A+B (b) X*(Y+Z)

Example

1.12.1.2.Prefix Notation(Polish notation)

In postfix expression, operator is used after operands. We can say that "Operator follows the
Operands".

The general structure of Postfix expression is as follows...


Operand1 Operand2 Operator
 Operators are written before their operands
 order of evaluation from right to left.
 example

(a) +AB (b)*X+YZ

Example

1.12.1.3.Postfix Notation(Reverse Polish notation)

In prefix expression, operator is used before operands. We can say that "Operands follows the
Operator".
The general structure of Prefix expression is as follows...
Operator Operand1 Operand2
 Operators are written after their operands
 The order of evaluation of operators is always from left to right
 brackets cannot be used to change the order of evaluation.
 Example
(a) AB+, (b) XYZ+*

Example

Any expression can be represented using the above three different types of expressions.
And we can convert an expression from one form to another form like Infix to Postfix, Infix
to Prefix, Prefix to Postfix and vice versa.

1.12.2. Expression Conversion

Any expression can be represented using three types of expressions (Infix, Postfix and
Prefix). We can also convert one type of expression to another type of expression like Infix
to Postfix, Infix to Prefix, Postfix to Prefix and vice versa.

1.12.2.1.Infix to Postfix Conversion

Procedure for Postfix Conversion


1. Scan the Infix string from left to right.
2. Initialize an empty stack.
3. If the scanned character is an operand, add it to the Postfix string.
If the scanned character is an operator and if the stack is empty push the character to
4.
stack.
If the scanned character is an Operator and the stack is not empty, compare the
5.
precedence of the character with the element on top of the stack.
If top Stack has higher precedence over the scanned character pop the stack else push
6. the scanned character to stack. Repeat this step until the stack is not empty and top
Stack has precedence over the character.
7. Repeat 4 and 5 steps till all the characters are scanned.
After all characters are scanned, we have to add any character that the stack may have
8.
to the Postfix string.
9. If stack is not empty add top Stack to Postfix string and Pop the stack.
10.Repeat this step as long as stack is not empty.
Algorithm for Postfix Conversion
S:stack
while(more tokens)
x<=next token
if(x == operand)
print x
else
while(precedence(x)<=precedence(top(s)))
print(pop(s))
push(s,x)
while(! empty (s))
print(pop(s))

Conversion To Postfix

EXAMPLE: A+(B*C-(D/E-F)*G)*H

Stack Input Output

Empty A+(B*C-(D/E-F)*G)*H -

Empty +(B*C-(D/E-F)*G)*H A

+ (B*C-(D/E-F)*G)*H A

+( B*C-(D/E-F)*G)*H A

+( *C-(D/E-F)*G)*H AB

+(* C-(D/E-F)*G)*H AB

+(* -(D/E-F)*G)*H ABC

+(- (D/E-F)*G)*H ABC*

+(-( D/E-F)*G)*H ABC*

+(-( /E-F)*G)*H ABC*D

+(-(/ E-F)*G)*H ABC*D


+(-(/ -F)*G)*H ABC*DE

+(-(- F)*G)*H ABC*DE/

+(-(- F)*G)*H ABC*DE/

+(-(- )*G)*H ABC*DE/F

+(- *G)*H ABC*DE/F-

+(-* G)*H ABC*DE/F-

+(-* )*H ABC*DE/F-G

+ *H ABC*DE/F-G*-

+* H ABC*DE/F-G*-

+* End ABC*DE/F-G*-H

Empty End ABC*DE/F-G*-H*+

Implemetation of infix to postfox evaluation

#include<iostream>

#include<cstring>

#include<stack>

using namespace std;

// get weight of operators as per precedence

// higher weight given to operators with higher precedence

// for non operators, return 0

int getWeight(char ch) {

switch (ch) {

case '/':

case '*': return 2;


case '+':

case '-': return 1;

default : return 0;

// convert infix expression to postfix using a stack

void infix2postfix(char infix[], char postfix[], int size) {

stack<char> s;

int weight;

int i = 0;

int k = 0;

char ch;

// iterate over the infix expression

while (i < size) {

ch = infix[i];

if (ch == '(') {

// simply push the opening parenthesis

s.push(ch);

i++;

continue;

if (ch == ')') {

// if we see a closing parenthesis,

// pop of all the elements and append it to

// the postfix expression till we encounter

// a opening parenthesis

while (!s.empty() && s.top() != '(') {


postfix[k++] = s.top();

s.pop();

// pop off the opening parenthesis also

if (!s.empty()) {

s.pop();

i++;

continue;

weight = getWeight(ch);

if (weight == 0) {

// we saw an operand

// simply append it to postfix expression

postfix[k++] = ch;

else {

// we saw an operator

if (s.empty()) {

// simply push the operator onto stack if

// stack is empty

s.push(ch);

else {

// pop of all the operators from the stack and

// append it to the postfix expression till we

// see an operator with a lower precedence that


// the current operator

while (!s.empty() && s.top() != '(' &&

weight <= getWeight(s.top())) {

postfix[k++] = s.top();

s.pop();

// push the current operator onto stack

s.push(ch);

i++;

// pop of the remaining operators present in the stack

// and append it to postfix expression

while (!s.empty()) {

postfix[k++] = s.top();

s.pop();

postfix[k] = 0; // null terminate the postfix expression

// main

int main() {

char infix[] = "A*(B+C)/D";

int size = strlen(infix);

char postfix[size];

infix2postfix(infix,postfix,size);

cout<<"\nInfix Expression :: "<<infix;

cout<<"\nPostfix Expression :: "<<postfix;


cout<<endl;

return 0;

1.12.2.2. Infix to Prefix Conversion


Step 1. Push “)” onto STACK, and add “(“ to end of the A
Step 2. Scan A from right to left and repeat step 3 to 6 for each element of A until the STACK is e
mpty
Step 3. If an operand is encountered add it to B
Step 4. If a right parenthesis is encountered push it onto STACK
Step 5. If an operator is encountered then:
a. Repeatedly pop from STACK and add to B each operator (on the top of STACK)
which has same or higher precedence than the operator.
b. Add operator to STACK
Step 6. If left parenthesis is encontered then
a. Repeatedly pop from the STACK and add to B (each operator on top of stack unt
il a left parenthesis is encounterd)
b. Remove the left parenthesis
Step 7. Exit

Example
Expression = (A+B^C)*D+E^5
Step 1. Reverse the infix expression.
5^E+D*)C^B+A(
Step 2. Make Every '(' as ')' and every ')' as '('
5^E+D*(C^B+A)
Step 3. Convert expression to postfix form.
A+(B*C-(D/E-F)*G)*H

Expression Stack Output Comment


5^E+D*(C^B+A) Empty - Initial

^E+D*(C^B+A) Empty 5 Print

E+D*(C^B+A) ^ 5 Push

+D*(C^B+A) ^ 5E Push

D*(C^B+A) + 5E^ Pop And Push

*(C^B+A) + 5E^D Print

(C^B+A) +* 5E^D Push

C^B+A) +*( 5E^D Push

^B+A) +*( 5E^DC Print

B+A) +*(^ 5E^DC Push

+A) +*(^ 5E^DCB Print

A) +*(+ 5E^DCB^ Pop And Push

) +*(+ 5E^DCB^A Print

End +* 5E^DCB^A+ Pop Until '('

End Empty 5E^DCB^A+*+ Pop Every element

Implementation of Infix to prefix evaluation

#include <iostream.h>
#include <string.h>
#include <ctype.h>
const int MAX = 50 ;
class infix
{
private :
char target[MAX], stack[MAX] ;
char *s, *t ;
int top, l ;
public :
infix( ) ;
void setexpr ( char *str ) ;
void push ( char c ) ;
char pop( ) ;
void convert( ) ;
int priority ( char c ) ;
void show( ) ;
};
infix :: infix( )
{
top = -1 ;
strcpy ( target, "" ) ;
strcpy ( stack, "" ) ;
l=0;
}
void infix :: setexpr ( char *str )
{
s = str ;
strrev ( s ) ;
l = strlen ( s ) ;
* ( target + l ) = '\0' ;
t = target + ( l - 1 ) ;
}
void infix :: push ( char c )
{
if ( top == MAX - 1 )
cout << "\nStack is full\n" ;
else
{
top++ ;
stack[top] = c ;
}
}
char infix :: pop( )
{
if ( top == -1 )
{
cout << "Stack is empty\n" ;
return -1 ;
}
else
{
char item = stack[top] ;
top-- ;
return item ;
}
}
void infix :: convert( )
{
char opr ;

while ( *s )
{
if ( *s == ' ' || *s == '\t' )
{
s++ ;
continue ;
}

if ( isdigit ( *s ) || isalpha ( *s ) )
{
while ( isdigit ( *s ) || isalpha ( *s ) )
{
*t = *s ;
s++ ;
t-- ;
}
}

if ( *s == ')' )
{
push ( *s ) ;
s++ ;
}

if ( *s == '*' || *s == '+' || *s == '/' ||


*s == '%' || *s == '-' || *s == '$' )
{
if ( top != -1 )
{
opr = pop( ) ;

while ( priority ( opr ) > priority ( *s ) )


{
*t = opr ;
t-- ;
opr = pop( ) ;
}
push ( opr ) ;
push ( *s ) ;
}
else
push ( *s ) ;
s++ ;
}

if ( *s == '(' )
{
opr = pop( ) ;
while ( ( opr ) != ')' )
{
*t = opr ;
t-- ;
opr = pop ( ) ;
}
s++ ;
}
}

while ( top != -1 )
{
opr = pop( ) ;
*t = opr ;
t-- ;
}
t++ ;
}
int infix :: priority ( char c )
{
if ( c == '$' )
return 3 ;
if ( c == '*' || c == '/' || c == '%' )
return 2 ;
else
{
if ( c == '+' || c == '-' )
return 1 ;
else
return 0 ;
}
}
void infix :: show( )
{
while ( *t )
{
cout << " " << *t ;
t++ ;
}
}

void main( )
{
char expr[MAX] ;
infix q ;

cout << "\nEnter an expression in infix form: " ;


cin.getline ( expr, MAX ) ;

q.setexpr( expr ) ;
q.convert( ) ;

cout << "The Prefix expression is: " ;


q.show( ) ;
}

1.12.2.3. Postfix to Infix Conversion


Algorithm of Postfix to Infix
Expression = abc-+de-fg-h+/*

1.While there are input symbol left


2. Read the next symbol from input.
3. If the symbol is an operand
Push it onto the stack.
4. Otherwise,
the symbol is an operator.
5. If there are fewer than 2 values on the stack
Show Error /* input not sufficient values in the expression */
6. Else
Pop the top 2 values from the stack.
Put the operator, with the values as arguments and form a string.
Encapsulate the resulted string with parenthesis.
Push the resulted string back to stack.
7. If there is only one value in the stack
That value in the stack is the desired infix string.
8. If there are more values in the stack
Show Error /* The user input has too many values */

Postfix to Infix conversion Example


abc-+de-fg-h+/*

Expression Stack

abc-+de-fg-h+/* NuLL
bc-+de-fg-h+/* "a"

c-+de-fg-h+/* "b"
"a"

"c"
-+de-fg-h+/*
"b"
"a"

+de-fg-h+/* "b - c"


"a"
de-fg-h+/*
"a+b-c"

e-fg-h+/* "d"
"a+b-c"

"e"
-fg-h+/*
"d"
"a+b-c"

fg-h+/* "d - e"


"a+b-c"

"f"
g-h+/*
"d - e"
"a+b-c"

"g"
-h+/* "f"
"d - e"
"a+b-c"

"f-g"
h+/*
"d - e"
"a+b-c"

"h"
+/* "f-g"
"d - e"
"a+b-c"
"f-g+h"
/*
"d - e"
"a+b-c"

* "(d-e)/(f-g-h)"
"a+b-c"
Null
"(a+b-c)*(d-e)/(f-g+h)"

Ans = (a+b-c)*(d-e)/(f-g+h)

1.12.2.4. Prefix to Infix Conversion


Algorithm of Prefix to Infix
This algorithm is a non-tail recursive method.
1.The reversed input string is completely pushed into a stack.
prefixToInfix(stack)
2.IF stack is not empty
a. Temp -->pop the stack
b. IF temp is a operator
Write a opening parenthesis to output
prefixToInfix(stack)
Write temp to output
prefixToInfix(stack)
Write a closing parenthesis to output
c. ELSE IF temp is a space -->prefixToInfix(stack)
d. ELSE
Write temp to output
IF stack.top NOT EQUAL to space -->prefixToInfix(stack)

Prefix to Infix conversionExample


*+a-bc/-de+-fgh

Expression Stack

*+a-bc/-de+-fgh NuLL

*+a-bc/-de+-fg
"h"

*+a-bc/-de+-f "g"
"h"

*+a-bc/-de+- "f"
"g"
"h"

*+a-bc/-de+ "f - g"


"h"
*+a-bc/-de
"f-g+h"

*+a-bc/-d "e"
"f-g+h"

"d"
*+a-bc/-
"e"
"f-g+h"

*+a-bc/ "d - e"


"f-g+h"
*+a-bc
"(d-e)/(f-g+h)"

*+a-b "c"
"(d-e)/(f-g+h)"

"b"
*+a-
"c"
"(d-e)/(f-g+h)"

*+a "b-c"
"(d-e)/(f-g+h)"

"a"
*+
"b-c"
"(d-e)/(f-g+h)"

* "a+b-c"
"(d-e)/(f-g+h)"
End
"(a+b-c)*(d-e)/(f-g+h)"

Ans = (a+b-c)*(d-e)/(f-g+h)

1.13. Processing of Function calls


One natural application of stacks, which arises in computer programming, is the
processing of function calls and their terminations. The program must remember the
place where the call was made so that it can return there after the function is complete.
Suppose we have three functions, say, A, B, and C, and one main program. Let the main
invoke A, A invoke B, and B in turn invoke C. Then, B will not have fi nished its work until
C has fi nished and returned. Similarly, main is the fi rst to start work, but it is the last
to be fi nished, not until sometime after A has fi nished and returned. Thus, the sequence
by which a function actively proceeds is summed up as the LIFO or FILO property, as
shown in Fig. 3.14. The output is shown in Fig. 3.15.
From the output in Fig. 3.15, it can be observed that the main program is invoked
fi rst but fi nished last, whereas the function C is invoked last but fi nished fi rst. Hence, to
keep track of the return addresses ra, rb, and rc the only data structure required here

is the stack.

Reversing a string using stack

Suppose a sequence of elements is presented and it is desired to reverse the sequence.


Various methods could be used for this, and in the beginning, the programmer will
usually suggest a solution using an array. A conceptually simple solution, however,
is based on using a stack. The LIFO property of the stack access guarantees the
reversal.
Suppose the sequence ABCDEF is to be reversed. With a stack, one simply scans the
sequence, pushing each element onto the stack as it is encountered, until the end of the
sequence is reached. The stack is then popped repeatedly, with each popped element sent

to the output, until the stack is empty. Table 3.13 illustrates this algorithm:

Reading a string character and writing it backward can be accomplished by pushing


each character on to a stack as it is read. When the string is fi nished, pop the characters

off the stack, and they will come out in the reverse order.

Program to reverse string using stack

#include<iostream>

#include<string.h>

#include<stack> //Use Standard template library to create Stack data


structure

using namespace std;

void Reverse(char *p);

int main()

char string[] = "www.firmcodes.com";

Reverse(string);

printf(" %s",string);

return 0;

void Reverse(char *p)

stack<char> S;
/* Pushing to stack */

for(int i=0; i<strlen(p); i++)

S.push(p[i]);

/* Poping from stack */

for(int i=0; i<strlen(p); i++)

p[i] = S.top();

S.pop();

1. 14. RECURSION
In C++, a function can call itself, that is, one of the statements of the function is a call
to itself. Such functions are called recursive functions and can be used to implement
recursive problems in an elegant manner.
To solve a recursive problem using functions, the problem must have an end condition
that can be stated in non-recursive terms. For example, in the case of factorials, we know
that 1! =1. If no such condition exists, then the recursive calls will indefinitely continue
until the computer runs or the program is terminated by the operating system.
Consider the recursive implementation of factorial given that
1! = 1 and n! = n (n +1)!
The recursive function in C++is given by the following statement:
long int factorial (unsigned int n)
{
if(n <= 1)
return(1);
else
return(n * factorial(n − 1));
}
As we can see, the C++ function represents the recursive mathematical definition of
n!. To see how it works, consider the computation of 5!.
The function calls will proceed as follows:
factorial(5) = 5 * factorial(4)
= 5 * (4* factorial (3))
= 5 * (4* (3 * factorial (2)))
= 5 * (4 *(3 * (2 * factorial (1))))
= 5 * (4 * (3 * (2 * 1)))
= 5 * (4 * (3 * 2))
= 5 * (4 * 6)
= 5 * 24
= 120
As the starting number is not 1, the function calls itself with the value 5 1, that is, 4.
Therefore, the original function call is kept incomplete and pending, and a second call
is made to the factorial with value 4. This process continues until the fifth call is made,
with the value 1. In this call, the function terminates without any further recursion and
returns the desired value of 1!, which is 1. Subsequently, each of the pending function
calls is completed upto the original factorial (5) function call, which returns the
computed value as 120. In the preceding piece of code, parentheses have been used to
show how the recursive calls proceed from left to right and the computations are made
from right to left.

1.15. Memory management

1.15.1 Managing Equal-Sized Blocks

Let us imagine we have a program that manipulates cells each consisting of a pair of fields;
each field can be a pointer to a cell or can hold an "atom." The situation, of course, is like
that of a program written in Lisp, but the program may be written in almost any
programming language, even Pascal, if we define cells to be of a variant record type. Empty
cells available for incorporation into a data structure are kept on an available space list, and
each program variable is represented by a pointer to a cell. The cell pointed to may be one
cell of a large data structure.

Let us assume that as the program runs, new cells may on occasion be seized from the
available space list. For example, we might wish to have the null pointer in the cell with
atom c in Fig. 12.3 replaced by a pointer to a new cell that holds atom i and a null pointer.
This cell will be removed from the top of the available space list. It is also possible that from
time to time, pointers will change in such a way that cells become detached from the
program variables, as the cells holding g and h in Fig. 12.3 have been. For example, the cell
holding c may, at one time, have pointed to the cell holding g. As another example, the
value of variable B may at some time change, which would, if nothing else has changed,
detach the cell now pointed to by B in Fig. 12.3 and also detach the cell holding d and e (but
not the cell holding c, since it would still be reached from A). We call cells not reachable
from any variable and not on the available space list inaccessible.

Fig. 1.15.1. A network of cells.

When cells are detached, and therefore are no longer needed by the program, it would
be nice if they found their way back onto the available space list, so they could be reused. If
we don't reclaim such cells, we shall eventually reach the unacceptable situation where the
program is not using all the cells, yet it wants a cell from available space, and the available
space list is empty. It is then that a time-consuming garbage collection must be performed.
This garbage collection step is "implicit," in the sense that it was not explicitly called for by
the request for space.

1.15.2.Garbage Collection Algorithms for Equal-Sized Blocks

Let us now give an algorithm for finding which of a collection of cells of the types suggested
in Fig. 1.15.1 are accessible from the program variables. We shall define the setting for the
problem precisely by defining a cell type in Pascal that is a variant record type; the four
variants, which we call PP, PA, AP, and AA, are determined by which of the two data fields
are pointers and which are atoms. For example, PA means the left field is a pointer and the
right field an atom. An additional boolean field in cells, called mark, indicates whether the
cell has been found accessible. That is, by setting mark to true when garbage collecting, we
"mark" the cell, indicating it is accessible. The important type definitions are shown in Fig.
1.15.2.

type
atomtype = { some appropriate type;
preferably of the same size as pointers }
patterns = (PP, PA, AP, AA);
celltype = record
mark: boolean;
case pattern: patterns of
PP: (left: celltype; right: celltype);
PA: (left: celltype; right: atomtype);
AP: (left: atomtype; right: celltype);
AA: (left: atomtype; right: atomtype);
end;
Fig. 1.15.2. Definition of the type for cells.

We assume there is an array of cells, taking up most of the memory, and some collection
of variables, that are pointers to cells. For simplicity, we assume there is only one variable,
called source, pointing to a cell, but the extension to many variables is not hard. † That is,
we declare

var
source: celltype;
memory: array [1..memorysize ] of celltype;

To mark the cells accessible from source, we first "unmark" all cells, accessible or not, by
running down the array memory and setting the mark field to false. Then we perform a
depth-first search of the graph emanating from source, marking all cells visited. The cells
visited are exactly those that are accessible. We then traverse the array memory and add to
the available space list all unmarked cells. Figure 12.5 shows a procedure dfs to perform the
depth-first search; dfs is called by the procedure collect that unmarks all cells, and then
marks accessible cells by calling dfs. We do not show the code linking the available space list
because of the peculiarities of Pascal. For example, while we could link available cells using
either all left or all right cells, since pointers and atoms are assumed the same size, we are
not permitted to replace atoms by pointers in cells of variant type AA.

(1) procedure dfs ( currentcell: celltype );


{ If current cell was marked, do nothing. Otherwise, mark
it and call dfs on any cells pointed to by the current cell }
begin
(2) with currentcell do
(3) if mark = false then begin
(4) mark := true;
(5) if (pattern = PP ) or (pattern = PA ) then
(6) if left <> nil then
(7) dfs (left);
(8) if (pattern = PP ) or (pattern = AP ) then
(9) if right <> nil then
(10) dfs (right)
end
end; { dfs }

(11) procedure collect;


var
i: integer;
begin
(12) for i := 1 to memorysize do { "unmark" all cells }
(13) memory[i].mark := false;
(14) dfs(source); { mark accessible cells }
(15) { the code for the collection goes here }
end; { collect }
Fig. 1.15.3. Algorithm for marking accessible cells.

UNIT 2

Recursion in C++

2.0. Introduction

When method is call within same method is called Recursion. The method which call
same method is called recursive method. In other words when a method call itself then
that method is called Recursive method.

Recursive method are very useful to solve many mathematical problems like to calculate
factorial of a number, generating Fibonacci series, etc.

2.1. Advantage of Recursion


 method calling related information will be maintained by recursion.
 Stack evaluation will be take place by using recursion.
 In fix prefix, post-fix notation will be evaluated by using recursion.

Disadvantage of Recursion
 It is a very slow process due to stack overlapping.
 Recursive programs can create stack overflow.
 Recursive method can create as loops.

Implementation of Recursion

#include<iostream.h>
#include<conio.h>

void main()
{
int fact(int);
int i,f,num;
clrscr();
cout<<"Enter any number: ";
cin>>num;
f=fact(num);
cout<<"Factorial: "<<f;
getch();
}

int fact(int n)
{
if(a<0)
return(-1);
if(a==0)
return(1);
else
{
return(n*fact(n-1));
}
}
Output
Enter any number: 5
Factorial: 120

Find the Table of any number using recursion

Example
#include<iostream.h>
#include<conio.h>

void main()
{
int table(int,int);
int n,i; // local variable
clrscr();
cout<<"Enter any num : ";
cin>>n;
for(i=1;i< =10;i++)
{
cout<<n<<"*"<<i<<= <<table(n,i)<<endl;
}
getch();
}
int table(n,i)
{
int t;
if(i==1)
{
return(n);
}
else
{
t=(table(n,i-1)+n);
return(t);
//return(table(n,i-1)+n);
}
}
Output
Enter any number: 5
5*1= 5
5*2= 10
5*3= 15
5*4= 20
5*5= 25
5*6= 30
5*7= 35
5*8= 40
5*9= 45
5*10= 50
2.2. Using of stacks in Recursion

2.2.1. Variants of Recursion

Depending on the following characterization, the recursive functions are categorized as


direct, indirect, linear, tree, and tail recursions. Recursion may have any one of the following
forms:
1. A function calls itself.
2. A function calls another function which in turn calls the caller function.
3. The function call is part of the same processing instruction that makes a recursive
function call.
A few more terms that are used with respect to recursion are explained in the following
section.

In C++, Recursion can be divided into two types:


(a) Run- Time Recursion: Normal as in C

( Compile- Time Recursion: By using Template

Each of these can be also divided into following types:

1. Linear Recursion
2. Binary Recursion
3. Tail Recursion
4. Mutual Recursion
5. Nested Recursion

1. Linear Recursion: This recursion is the most commonly used. In this recursion a function
call itself in a simple manner and by termination condition it terminates. This process called
'Winding' and when it returns to caller that is called 'Un-Winding'. Termination condition
also known as Base condition.

Example: Factorial calculation by linear recursion

Run-Time Version

int Fact(long n)
{
if(0>n)
return -1;
if(0 == n)
return 1;
else
{
return ( n* Fact(n-1));
}
}
Winding Process:

Function called Function return

Fact(6) 6*Fact(5)
Fact(5) 5*Fact(4)
Fact(4) 4*Fact(3)
Fact(3) 3* Fact(2)
Fact(2) 2* Fact(1)
Fact(1) 1* Fact(0)
Terminating Point
Fact(0) 1

Unwinding Process

Fact(1) 1*1
Fact(2) 2*1
Fact(3) 3*2*1
Fact(4) 4*3*2*1
Fact(5) 5*4*3*2*1
Fact(6) 6*5*4*3*2*1

2.2.1 Binary Recursion: Binary Recursion is a process where function is called twice at a time
inplace of once at a time. Mostly it's using in data structure like operations for tree as
traversal, finding height, merging, etc.

Example: Fibonacci number

Run Time Version Code:

int FibNum(int n)
{
// Base conditions
if (n < 1)
return -1;
if (1 == n || 2 == n)
return 1;

// Recursive call by Binary Method


return FibNum(n - 1) + FibNum(n - 2); // At a time two recursive function called
so
// binary
}
3. Tail Recursion: In this method, recursive function is called at the last. So it's more efficient
than linear recursion method. Means you can say termination point will come(100%) only
you have to put that condition.

Example: Fibonacci number

Run Time Version Code:


01 int FibNum(int n, int x, int y)
02 {
03 if (1 == n) // Base Condition
04 {
05 return y;
06 }
07 else // Recursive call by Tail method
08 {
09 return FibNum(n-1, y, x+y);
10 }
11 }

4. Mutual Recursion: Functions calling each other. Let's say FunA calling FunB and FunB
calling FunA recursively. This is not actually not recursive but it's doing same as recursive. So
you can say Programming languages which are not supporting recursive calls, mutual
recursion can be applied there to fulfill the requirement of recursion. Base condition can be
applied to any into one or more than one or all functions.

Example: To find Even Or Odd number

Run Time Version Code:

view source

print?

01 bool IsOddNumber(int n)

02 {

03 // Base or Termination Condition

04 if (0 == n)

05 return 0;

06 else
07 // Recursive call by Mutual Method

08 return IsEvenNumber(n - 1);

09 }

10

11 bool IsEvenNumber(int n)

12 {

13 // Base or Termination Condition

14 if (0 == n)

15 return 1;

16 else

17 // Recursive call by Mutual Method

18 return IsOddNumber(n - 1);

19 }

3. Nested Recursion: It's very different than all recursions. All recursion can be converted to
iterative (loop) except nested recursion. You can understand this recursion by example of
Ackermann function.

Example: Ackermann function

Run Time Version Code:


// Base or Termination Condition
if (0 == x)
{
return y + 1;
}
// Error Handling condition
if (x < 0 || y < 0)
{
return -1;
}
// Recursive call by Linear method
else if (x > 0 && 0 == y)
15 {
16 return Ackermann(x-1, 1);
17 }
18 // Recursive call by Nested method
19 else
20 {
21 return Ackermann(x-1, Ackermann(x, y-1));
22 }
23 }

Recursive Functions

A recursive function (DEF) is a function which either calls itself or is in a potential cycle of
function calls. As the definition specifies, there are two types of recursive
functions. Consider a function which calls itself: we call this type of recursion immediate
recursion.

void A() {
A();
return;
}

A() is a recursive function since it directly calls itself.

The second part of the defintion refers to a cycle (or potential cycle if we use conditional
statements) which involves other functions.

Consider the following directed call graph

A ---------> B
^ |
| |
| |
|---- C <----|

This can be viewed in the following three functions:

void C() {
A();
return;
}
void B() {
C();
return;
}

void A() {
B();
return;
}

Recursive functions are an inefficient means of solving problems in terms of run times but
are interesting to study nonetheless. For our purposes we will only consider immediate
recursion since this will cause enough difficulty.

2.2.3. Writing Recursive Functions


A recursive function has the following general form (it is simply a specification of the general
function we have seen many times):

ReturnType Function( Pass appropriate arguments ) {

if a simple case, return the simple value // base case / stopping condition

else call function with simpler version of problem


}

For a recursive function to stop calling itself we require some type of stopping condition. If
it is not the base case, then we simplify our computation using the general formula.

Example: n!
Compute n! Recursively.

We are given the mathematical function for computing the factorial:

n! = n * (n - 1)!

Why this is a recursive function? To compute n! we are required to compute (n - 1)!.

We are also given the simple case. We define mathematically 0!

0! = 1

To undersatnd factorial in a recursive sense, consider a few simple cases:

1! = 1 * 0! = 1 * 1 = 1
2! = 2 * 1! = 2 * 1 = 2

3! = 3 * 2! = 3 * 2 = 6

4! = 4 * (3 * 2 * 1) = 4 * 3! = 24

Consider a more complicated instance:

12! = 12 * 11! = 12 * (11 * 10 * ... * 1) = = ?

Now let us write the C++ function which will calculate the factorial of a given integer. Apply
the simple case and the general factorial function to the general recursive function
established previously.

int Factorial(int n) {
if (n == 0) return 1; // Simple case: 0! = 1
return (n * Factorial(n - 1)); // General function: n! = n * (n - 1)!
}

Here, 0! = 1 is the base case and our general recursive function forms the simplification of
the original problem.

Example 1: Fibonacci Sequence

Consider the following program which contains a recursive function. What is the complete
output?

#include <iostream.h>

int Fibonacci(int x) {
if (x == 0) return 0; // Stopping conditions
if (x == 1) return 1;
return Fibonacci(x - 1) + Fibonacci(x - 2);
}

int main() {
int num;

cin >> num;


cout << Fibonacci(num) << endl;

return 0;
}

Let's input the value 5 for this program.


The following recursive calls are made. Notice the (upside-down) tree structure created by
the recursive calls.

Fibonacci(5)
/ \
/ \
/ \
/ \
/ \
F(4) + F(3)
/ \ / \
/ \ / \
/ \ / \
/ \ / \
/ \ / \
F(3) + F(2) F(2) + F(1)
/\ /\ | \ \
/ \ / \ | \ \
/ \ / \ | \ \
/ \ / \ | \ \
F(2) + F(1) F(1) + F(0) F(1) + F(0) 1
/\ | | | | |
/ \ | | | | |
/ \ | | | | |
/ \ | | | | |
F(1) + F(0) 1 1 0 1 0
| |
| |
| |
| |
1 0

If we evaluate these recursive calls we have

(((1 + 0) + 1) + (1 + 0)) + ((1 + 0) + 1) = 5

Thus, Fibonacci(5) = 5.

Input:
5

Output:
5

You may have noticed the name of the function and recognized it as a well-known
mathematical function: the fibonacci sequance.
Working backward, we could have formulated the function given the mathematical
equations:

General form:
Fibonacci(n) = Fibonacci(n - 1) + Fibonacci(n - 2)

Stopping conditions:

Fibonacci(0) = 1
Fibonacci(1) = 1

Example 2: F()

#include <iostream.h>

//F(0) = 1; F(1) = 2
//F(n) = F(n-1) * F(n-2)

int Recursive(int n) {
cout << n << endl;
if (n == 0) return 1;
if (n == 1) return 2;
return (Recursive(n - 1) * Recursive(n - 2));
}

int main()
{
int Result, num;

cin >> num;

cout << "Result: " << Recursive(num) << endl;

return 0;
}

Inputting: num = 3.

Output:

3
2
1
0
1
Result: 4
If we evaluate these recursive calls we have

((2 * 1) * 2) = 4

Thus, Recursive(3) = 4.

Recursive(3)
/ \
/ \
/ \
/ \
/ \
R(2) * R(1)
/ \ \
/ \ \
/ \ \
/ \ \
R(1) * R(0) 2
| |
| |
| |
| |
2 1

Running the program with num = 5;

5
4
3
2
1
0
1
2
1
0
3
2
1
0
1
Result: 32

We can generate the recursive calls.


Recursive(5)
/ \
/ \
/ \
/ \
/ \
R(4) * R(3)
/ \ / \
/ \ / \
/ \ / \
/ \ / \
/ \ / \
R(3) * R(2) R(2) * R(1)
/\ /\ | \ \
/ \ / \ | \ \
/ \ / \ | \ \
/ \ / \ | \ \
R(2) * R(1) R(1) * R(0) R(1) * R(0) 2
/\ | | | | |
/ \ | | | | |
/ \ | | | | |
/ \ | | | | |
F(1) * F(0) 2 2 1 2 1
| |
| |
| |
| |
2 1

If we evaluate these recursive calls we have

(((2 * 1) * 2) * (2 * 1)) * ((2 * 1) * 2) = 32

Thus, Recursive(5) = 32.

One thing you may have noticed from Recursive(5) is that one of the subtrees is
Recursive(3).

2.3. Iteration Vs. Recursion

If a recursive method is called with a base case, the method returns a result. If a method is
called with a more complex problem, the method divides the problem into two or more
conceptual pieces: a piece that the method knows how to do and a slightly smaller version
of the original problem. Because this new problem looks like the original problem, the
method launches a recursive call to work on the smaller problem.
For recursion to terminate, each time the recursion method calls itself with a slightly simpler
version of the original problem, the sequence of smaller and smaller problems must
converge on the base case. When the method recognizes the base case, the result is
returned to the previous method call and a sequence of returns ensures all the way up the
line until the original call of the method eventually returns the final result.

Both iteration and recursion are based on a control structure: Iteration uses a repetition
structure; recursion uses a selection structure.

Both iteration and recursion involve repetition: Iteration explicitly uses a repetition
structure; recursion achieves repetition through repeated method calls.

Iteration and recursion each involve a termination test: Iteration terminates when the loop-
continuation condition fails; recursion terminates when a base case is recognized.

Iteration and recursion can occur infinitely: An infinite loop occurs with iteration if the loop-
continuation test never becomes false; infinite recursion occurs if the recursion step does
not reduce the problem in a manner that converges on the base case.

Recursion repeatedly invokes the mechanism, and consequently the overhead, of method
calls. This can be expensive in both processor time and memory space.

2.4.Queue Data Structures


Queue is also an abstract data type or a linear data structure, in which the first element is
inserted from one end called REAR(also called tail), and the deletion of exisiting element
takes place from the other end called as FRONT(also called head). This makes queue as FIFO
data structure, which means that element inserted first will also be removed first.
The process to add an element into queue is called Enqueue and the process of removal of
an element from queue is called Dequeue.

2.4.1. Basic features of Queue

1. Like Stack, Queue is also an ordered list of elements of similar data types.
2. Queue is a FIFO( First in First Out ) structure.
3. Once a new element is inserted into the Queue, all the elements inserted before the
new element in the queue must be removed, to remove the new element.
4. peek( ) function is oftenly used to return the value of first element without dequeuing it.

Basic Operations
Queue operations may involve initializing or defining the queue, utilizing it, and then
completely erasing it from the memory. Here we shall try to understand the basic
operations associated with queues −

 enqueue() − add (store) an item to the queue.

 dequeue() − remove (access) an item from the queue.

Few more functions are required to make the above-mentioned queue operation efficient.
These are −

 peek() − Gets the element at the front of the queue without removing it.

 isfull() − Checks if the queue is full.

 isempty() − Checks if the queue is empty.

In queue, we always dequeue (or access) data, pointed by front pointer and while
enqueing (or storing) data in the queue we take help of rear pointer.

Let's first learn about supportive functions of a queue −

 peek()
This function helps to see the data at the front of the queue. The algorithm of peek()
function is as follows −

Algorithm

begin procedure peek

return queue[front]

end procedure
Implementation of peek() function in C programming language −

Example

int peek() {
return queue[front];
}
 isfull()
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 −

Algorithm

begin procedure isfull


if rear equals to MAXSIZE
return true
else
return false
endif
end procedure
Implementation of isfull() function in C programming language −

Example

bool isfull() {
if(rear == MAXSIZE - 1)
return true;
else
return false;
}
 isempty()
Algorithm of isempty() function −

Algorithm

begin procedure isempty


if front is less than MIN OR front is greater than rear
return true
else
return false
endif
end procedure
If the value of front is less than MIN or 0, it tells that the queue is not yet initialized, hence
empty.
Example

bool isempty() {
if(front < 0 || front > rear)
return true;
else
return false;
}
2.4.2. Enqueue Operation
Queues maintain two data pointers, front and rear. Therefore, its operations are
comparatively difficult to implement than that of stacks.

The following steps should be taken to enqueue (insert) data into a queue −

 Step 1 − Check if the queue is full.

 Step 2 − If the queue is full, produce overflow error and exit.

 Step 3 − If the queue is not full, increment rear pointer to point the next empty
space.

 Step 4 − Add data element to the queue location, where the rear is pointing.

 Step 5 − return success.

Sometimes, we also check to see if a queue is initialized or not, to handle any unforeseen
situations.

Algorithm for enqueue operation

procedure enqueue(data)
if queue is full
return overflow
endif
rear ← rear + 1
queue[rear] ← data
return true
end procedure
Example

int enqueue(int data)


if(isfull())
return 0;
rear = rear + 1;
queue[rear] = data;
return 1;
end procedure
2.4.3. Dequeue Operation
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 −

 Step 1 − Check if the queue is empty.

 Step 2 − If the queue is empty, produce underflow error and exit.

 Step 3 − If the queue is not empty, access the data where front is pointing.

 Step 3 − Increment front pointer to point to the next available data element.

 Step 5 − Return success.

Algorithm for dequeue operation

procedure dequeue
if queue is empty
return underflow
end if
data = queue[front]
front ← front + 1
return true
end procedure
Implementation of dequeue() in C programming language −

Example

int dequeue() {
if(isempty())
return 0;
int data = queue[front];
front = front + 1;
return data;
}
Applications of Queue
Queue, as the name suggests is used whenever we need to have any group of objects in an
order in which the first one coming in, also gets out first while the others wait for there
turn, like in the following scenarios :

1. Serving requests on a single shared resource, like a printer, CPU task scheduling etc.
2. In real life, Call Center phone systems will use Queues, to hold people calling them in an
order, until a service representative is free.
3. Handling of interrupts in real-time systems. The interrupts are handled in the same
order as they arrive, First come first served.

2.4.4 Implementation of Queue


Queue can be implemented using an Array, Stack or Linked List. The easiest way of
implementing a queue is by using an Array. Initially the head(FRONT) and the tail(REAR) of
the queue points at the first index of the array (starting the index of array from 0). As we
add elements to the queue, the tail keeps on moving ahead, always pointing to the position
where the next element will be inserted, while the head remains at the first index.
When we remove element from Queue, we can follow two possible approaches (mentioned
[A] and [B] in above diagram). In [A] approach, we remove the element at head position,
and then one by one move all the other elements on position forward. In approach [B] we
remove the element from head position and then move head to the next position.
In approach [A] there is an overhead of shifting the elements one position forward every
time we remove the first element. In approach [B] there is no such overhead, but whener
we move head one position ahead, after removal of first element, the size on Queue is
reduced by one space each time

2.4.5. The Queue Abstract Data Type


The queue abstract data type is defined by the following structure and operations. A queue
is structured, as described above, as an ordered collection of items which are added at one
end, called the “rear,” and removed from the other end, called the “front.” Queues maintain
a FIFO ordering property. The queue operations are given below.

 Queue() creates a new queue that is empty. It needs no parameters and returns an empty
queue.
 enqueue(item) adds a new item to the rear of the queue. It needs the item and returns
nothing.
 dequeue() removes the front item from the queue. It needs no parameters and returns the
item. The queue is modified.
 isEmpty() tests to see whether the queue is empty. It needs no parameters and returns a
boolean value.
 size() returns the number of items in the queue. It needs no parameters and returns an
integer.
 first(): Return object at front without removing; error if empty.

As an example, if we assume that q is a queue that has been created and is currently empty,

2.4.6. Queue Using Array

A queue data structure can be implemented using one dimensional array. But, queue
implemented using array can store only fixed number of data values. The implementation of
queue data structure using array is very simple, just define a one dimensional array of
specific size and insert or delete the values into that array by using FIFO (First In First Out)
principle with the help of variables 'front' and 'rear'. Initially both 'front' and 'rear' are set
to -1. Whenever, we want to insert a new value into the queue, increment 'rear' value by
one and then insert at that position. Whenever we want to delete a value from the queue,
then increment 'front' value by one and then display the value at 'front' position as deleted
element.

Queue Operations using Array

Queue data structure using array can be implemented as follows...

Before we implement actual operations, first follow the below steps to create an empty
queue.

1. Step 1: Include all the header files which are used in the program and define a
constant 'SIZE' with specific value.
2. Step 2: Declare all the user defined functions which are used in queue
implementation.
3. Step 3: Create a one dimensional array with above defined SIZE (int queue[SIZE])
4. Step 4: Define two integer variables 'front' and 'rear' and initialize both with '-1'. (int
front = -1, rear = -1)
5. Step 5: Then implement main method by displaying menu of operations list and
make suitable function calls to perform operation selected by the user on queue.

 enQueue(value) - Inserting value into the queue

In a queue data structure, enQueue() is a function used to insert a new element into the
queue. In a queue, the new element is always inserted at rear position. The enQueue()
function takes one integer value as parameter and inserts that value into the queue. We can
use the following steps to insert an element into the queue...

 Step 1: Check whether queue is FULL. (rear == SIZE-1)


 Step 2: If it is FULL, then display "Queue is FULL!!! Insertion is not possible!!!" and
terminate the function.
 Step 3: If it is NOT FULL, then increment rear value by one (rear++) and
set queue[rear] = value.
 deQueue() - Deleting a value from the Queue

In a queue data structure, deQueue() is a function used to delete an element from the
queue. In a queue, the element is always deleted from front position. The deQueue()
function does not take any value as parameter. We can use the following steps to delete an
element from the queue...

 Step 1: Check whether queue is EMPTY. (front == rear)


 Step 2: If it is EMPTY, then display "Queue is EMPTY!!! Deletion is not
possible!!!" and terminate the function.
 Step 3: If it is NOT EMPTY, then increment the front value by one (front ++). Then
display queue[front] as deleted element. Then check whether
both front and rear are equal (front == rear), if it TRUE, then set
both front and rear to '-1' (front = rear = -1).

 display() - Displays the elements of a Queue

We can use the following steps to display the elements of a queue...

 Step 1: Check whether queue is EMPTY. (front == rear)


 Step 2: If it is EMPTY, then display "Queue is EMPTY!!!" and terminate the function.
 Step 3: If it is NOT EMPTY, then define an integer variable 'i' and set 'i = front+1'.
 Step 3: Display 'queue[i]' value and increment 'i' value by one (i++). Repeat the same
until 'i' value is equal to rear (i <= rear)

Algorithm to insert item into the queue


 Insert ( ):
Description: Here QUEUE is an array with N locations. FRONT and REAR points to the front
and rear of
the QUEUE. ITEM is the value to be inserted.
If (REAR == N) Then [Check for overflow]
Print: Overflow
Else
If (FRONT and REAR == 0) Then [Check if QUEUE is empty]
(a) Set FRONT = 1
(b) Set REAR = 1
Else
Set REAR = REAR + 1 [Increment REAR by 1]
[End of Step 4 If]
QUEUE[REAR] = ITEM
Print: ITEM inserted
[End of Step 1 If]
Exit

Algorithm to delete the item from the queue

 Delete ( ):
Description: Here QUEUE is an array with N locations. FRONT and REAR points to the front
and rear of
the QUEUE.
If (FRONT == 0) Then [Check for underflow]
Print: Underflow
Else
ITEM = QUEUE[FRONT]
If (FRONT == REAR) Then [Check if only one element is left]
Set FRONT = 0
Set REAR = 0
Else
Set FRONT = FRONT + 1 [Increment FRONT by 1]
[End of Step 5 If]
Print: ITEM deleted
[End of Step 1 If]
Exit

IMPLEMENTATION OF QUEUE ADT USING ARRAY


“qarray.h” File:

#include<stdio.h>
#include<conio.h>
#include<alloc.h>
#include<stdlib.h>
struct Queue
{
int capacity;
int size;
int front;
int rear;
int *array;
};
typedef struct Queue *PtrToNode;
typedef PtrToNode QUEUE;
QUEUE CreateQueue(int max)
{
QUEUE Q;
Q=(struct Queue*)malloc(sizeof(struct Queue));
if(Q==NULL)
printf("\nFatal error");
else
{
Q->size=0;
Q->capacity=max;
Q->front=0;
Q->rear=-1;
Q->array=(int*)malloc(sizeof(int)*max);
if(Q->array==NULL)
printf("\nFatal error");
else
printf("\nQueue is created successfully");
}
return Q;
}
void MakeEmpty(QUEUE Q)
{
Q->size=0;
Q->front=0;
Q->rear=-1;
}
int IsEmpty(QUEUE Q)
{
return Q->size==0;
}
int IsFull(QUEUE Q)
{
return Q->size==Q->capacity;
}
void EnQueue(int x,QUEUE Q)
{
if(IsFull(Q))
printf("\nQueue is Full");
else
{
Q->rear++;
Q->array[Q->rear]=x;
Q->size++;
}
}
void DeQueue(QUEUE Q)
{
if(IsEmpty(Q))
printf("\nQueue is empty");
else
{
Q->front++;
Q->size--;
}
}
int Front(QUEUE Q)
{
return Q->array[Q->front];
}
QUEUE DisposeQueue(QUEUE Q)
{
MakeEmpty(Q);
free(Q->array);
free(Q);
Q=NULL;
return Q;
}
void Display(QUEUE Q)
{
int i;
for(i=Q->front;i<=Q->rear;i++)
printf("\n%d",Q->array[i]);
}

“qarray.c” File:

#include<stdio.h>
#include"qarray.h"
void main()
{
QUEUE Q=NULL;
int a,size,ch;
printf("\n\n1.CreateQueue\n 2.Enqueue\n 3.Dequeue\n 4.Front\n 5.MakeEmpty\n
6.IsEmpty\n 7.IsFull\n 8.DisposeQueue\n 9.Display\n 10.Exit\n");
X:
printf("\nEnter ur choice:\t");
scanf("%d",&ch);
switch(ch)
{
case 1:
if(Q==NULL)
{
printf("\nEnter the size of queue");
scanf("%d",&size);
Q=CreateQueue(size);
}
else
printf("\nQueue is already created");
break;
case 2:
if(Q==NULL)
printf("\nQueue is not yet created");
else
{
printf("\nEnter the element to insert");
scanf("%d",&a);
EnQueue(a,Q);
}
break;
case 3:
if(Q==NULL)
printf("\nQueue is not yet created");
else
DeQueue(Q);
break;
case 4:
if(Q==NULL)
printf("\nQueue is not yet created");
else
{
a=Front(Q);
printf("\n Front element present in the queue is:\t%d",a);
}
break;
case 5:
if(Q==NULL)
printf("\n Queue is not yet created");
else
{
MakeEmpty(Q);
printf("\n Now Queue becomes empty");
}
break;
case 6:
if(Q==NULL)
printf("\n Queue is not yet created");
else if(IsEmpty(Q))
printf("\n Queue is empty");

else
printf("\n Queue contains some element");
break;
case 7:
if(Q==NULL)
printf("\n Queue is not yet created");
else if(IsFull(Q))
printf("\n Queue is full");
else
printf("\n Queue is not full");
break;
case 8:
if(Q==NULL)
printf("\n Queue is not yet created");
else
{
Q=DisposeQueue(Q);
printf("\n Queue is disposed");
}
break;
case 9:
if(Q==NULL)
printf("\n Queue is not yet created");
else
{
printf("\n The elements in the Queue are:");
Display(Q);
}
break;
case 10:
exit(0);
default:
printf("\n*******WRONG CHOICE********");
}
goto X;
}

OUTPUT:

1.CreateQueue
2.Enqueue
3.Dequeue
4.Front
5.MakeEmpty
6.IsEmpty
7.ISFull
8.DisposeQueue
9.Display
10.Exit

Enter ur choice: 1
Enter the size of queue: 3
Queue is created successfully

Enter ur choice: 2
Enter the element to insert: 100

Enter ur choice: 2
Enter the element to insert: 200
Enter ur choice: 2
Enter the element to insert: 300

Enter ur choice: 2
Enter the element to insert: 400
Queue is Full

Enter ur choice: 6
Queue contains some element

Enter ur choice: 4
Front element present in the queue is: 100

Enter ur choice: 7
Queue is Full

Enter ur choice: 9
The Elements in the queue are:
100
200
300

Enter ur choice: 3

Enter ur choice: 9
The Elements in the queue are:
200
300

Enter ur choice: 7
Queue is not Full

Enter ur choice: 4
Front element present in the queue is: 200

Enter ur choice: 5
Now Queue becomes empty

Enter ur choice: 8
Queue is Disposed

Enter ur choice: 12
***********WRONG ENTRY********

Enter ur choice: 10
2.5. Circular Queue

In a normal Queue Data Structure, we can insert elements until queue becomes full. But
once if queue becomes full, we can not insert the next element until all the elements are
deleted from the queue. For example consider the queue below...

After inserting all the elements into the queue.

Now consider the following situation after deleting three elements from the queue...

This situation also says that Queue is Full and we can not insert the new element because,
'rear' is still at last position. In above situation, even though we have empty positions in the
queue we can not make use of them to insert new element. This is the major problem in
normal queue data structure. To overcome this problem we use circular queue data
structure.

Circular Queue is a linear data structure in which the operations are performed based on
FIFO (First In First Out) principle and the last position is connected back to the first position
to make a circle.

Graphical representation of a circular queue is as follows...

2.5.1. Implementation of Circular Queue

To implement a circular queue data structure using array, we first perform the following
steps before we implement actual operations.
 Step 1: Include all the header files which are used in the program and define a
constant 'SIZE' with specific value.
 Step 2: Declare all user defined functions used in circular queue implementation.
 Step 3: Create a one dimensional array with above defined SIZE (int cQueue[SIZE])
 Step 4: Define two integer variables 'front' and 'rear' and initialize both with '-1'. (int
front = -1, rear = -1)
 Step 5: Implement main method by displaying menu of operations list and make
suitable function calls to perform operation selected by the user on circular queue.

 enQueue(value) - Inserting value into the Circular Queue

In a circular queue, enQueue() is a function which is used to insert an element into the
circular queue. In a circular queue, the new element is always inserted at rear position. The
enQueue() function takes one integer value as parameter and inserts that value into the
circular queue. We can use the following steps to insert an element into the circular queue...

 Step 1: Check whether queue is FULL. ((rear == SIZE-1 && front == 0) || (front ==
rear+1))
 Step 2: If it is FULL, then display "Queue is FULL!!! Insertion is not possible!!!" and
terminate the function.
 Step 3: If it is NOT FULL, then check rear == SIZE - 1 && front != 0 if it is TRUE, then
set rear = -1.
 Step 4: Increment rear value by one (rear++), set queue[rear] = value and check
'front == -1' if it is TRUE, then set front = 0.

 deQueue() - Deleting a value from the Circular Queue

In a circular queue, deQueue() is a function used to delete an element from the circular
queue. In a circular queue, the element is always deleted from front position. The
deQueue() function doesn't take any value as parameter. We can use the following steps to
delete an element from the circular queue...

 Step 1: Check whether queue is EMPTY. (front == -1 && rear == -1)


 Step 2: If it is EMPTY, then display "Queue is EMPTY!!! Deletion is not
possible!!!" and terminate the function.
 Step 3: If it is NOT EMPTY, then display queue[front] as deleted element and
increment the front value by one (front ++). Then check whether front == SIZE, if it
is TRUE, then set front = 0. Then check whether both front - 1 and rear are equal
(front -1 == rear), if it TRUE, then set both front and rear to '-1' (front = rear = -1).

 display() - Displays the elements of a Circular Queue

We can use the following steps to display the elements of a circular queue...

 Step 1: Check whether queue is EMPTY. (front == -1)


 Step 2: If it is EMPTY, then display "Queue is EMPTY!!!" and terminate the function.
 Step 3: If it is NOT EMPTY, then define an integer variable 'i' and set 'i = front'.
 Step 4: Check whether 'front <= rear', if it is TRUE, then display 'queue[i]' value and
increment 'i' value by one (i++). Repeat the same until 'i <= rear' becomes FALSE.
 Step 5: If 'front <= rear' is FALSE, then display 'queue[i]' value and increment 'i' value
by one (i++). Repeat the same until'i <= SIZE - 1' becomes FALSE.
 Step 6: Set i to 0.
 Step 7: Again display 'cQueue[i]' value and increment i value by one (i++). Repeat
the same until 'i <= rear' becomes FALSE.

Consider the example with Circular Queue implementation

See the logical circularity of the queue. Addition causes the increment in REAR. It means
that when REAR reaches MAX-1 position then Increment in REAR causes REAR to reach at
first position that is 0.

if( rear == MAX -1 )


rear = 0;
else
rear = rear + 1;
The short-hand equivalent representation may be
rear = ( rear + 1) % MAX;
As we know that, Deletion causes the increment in FRONT. It means that when FRONT
reaches the MAX-1 position, then increment in FRONT, causes FRONT to reach at first
position that is 0.

if( front == MAX -1 )


front = 0;
else
front = front + 1;
The short-hand equivalent representation may be
front = ( front + 1) % MAX;

Drawback of Circular Queue

The drawback of circular queue is , difficult to distinguished the full and empty cases. It is
also known as boundary case problem.

In circular queue it is necessary that:

 Before insertion, fullness of Queue must be checked (for overflow).


 Before deletion, emptiness of Queue must be checked (for underflow).

Condition to check FULL Circular Queue


if( ( front == MAX-1) || ( front ==0 && rear == MAX -1 ) )
Condition to check EMPTY Circular Queue
if( ( front == MAX-1) || ( front ==0 && rear == MAX -1 ) )

Solution of the drawback of circular Queue

Use count variable to hold the current position ( in case of insertion or deletion).

Operation of Circular Queue using count

1. Initialize Operation.
2. Is_Full check.
3. Is_Empty check.
4. Addition or Insertion operation.
5. Deletion operation.

Algorithms

1. INIT(QUEUE,FRONT,REAR,COUNT)
2. INSERT-ITEM(QUEUE, FRONT, REAR, MAX, COUNT, ITEM)
3. REMOVE-ITEM(QUEUE, FRONT, REAR, COUNT, ITEM)
4. FULL-CHECK(QUEUE,FRONT,REAR,MAX,COUNT,FULL)
5. EMPTY-CHECK(QUEUE,FRONT,REAR,MAX,COUNT,EMPTY)

INIT(QUEUE,FORNT,REAR,COUNT)
This algorithm is used to initialize circular queue.
1. FRONT := 1;
2. REAR := 0;
3. COUNT := 0;
4. Return;
INSERT-ITEM( QUEUE, FRONT, REAR, MAX, COUNT, ITEM)
This algorithm is used to insert or add item into circular queue.
1. If ( COUNT = MAX ) then
a. Display “Queue overflow”;
b. Return;
2. Otherwise
a. If ( REAR = MAX ) then
i. REAR := 1;
b. Otherwise
i. REAR := REAR + 1;
c. QUEUE(REAR) := ITEM;
d. COUNT := COUNT + 1;
3. Return;
REMOVE-ITEM( QUEUE, FRONT, REAR, COUNT, ITEM)
This algorithm is used to remove or delete item from circular queue.
1. If ( COUNT = 0 ) then
a. Display “Queue underflow”;
b. Return;
2. Otherwise
a. ITEM := QUEUE(FRONT)l
b. If ( FRONT =MAX ) then
i. FRONT := 1;
c. Otherwise
i. FRONT := FRONT + 1;
d. COUNT := COUNT + 1;
3. Return;
EMPTY-CHECK(QUEUE,FRONT,REAR,MAX,COUNT,EMPTY)
This is used to check queue is empty or not.
1. If( COUNT = 0 ) then
a. EMPTY := true;
2. Otherwise
a. EMPTY := false;
3. Return ;
FULL-CHECK(QUEUE,FRONT,REAR,MAX,COUNT,FULL)
This algorithm is used to check queue is full or not.
1. If ( COUNT = MAX ) then
a. FULL := true;
2. Otherwise
a. FULL := false;
3. Return ;

Declaration of Circular Queue


#define MAX 5

//Declaration of Circular Queue

class CirQueue

private:

int front ;

int rear ;

int count ;

int ele[MAX] ;

public:

CirQueue();

int isFull();

int isEmpty();

void insertQueue(int item);

int deleteQueue(int *item);

};

Circular Queue Implementation for all above queue operations

#include <iostream>

using namespace std;

#define MAX 5

//Declaration of Circular Queue

class CirQueue

private:

int front ;

int rear ;

int count ;

int ele[MAX] ;
public:

CirQueue();

int isFull();

int isEmpty();

void insertQueue(int item);

int deleteQueue(int *item);

};

//Initalize Circular Queue

CirQueue:: CirQueue()

front = 0;

rear = -1;

count = 0;

//To check queue is full or not

int CirQueue:: isFull()

int full=0;

if( count == MAX )

full = 1;

return full;

//To check queue is empty or not

int CirQueue:: isEmpty()

int empty=0;
if( count == 0 )

empty = 1;

return empty;

//Insert item into circular queue

void CirQueue:: insertQueue(int item)

if( isFull() )

cout<<"\nQueue Overflow";

return;

rear= (rear+1) % MAX;

ele[rear] = item;

count++;

cout<<"\nInserted item : "<< item;

//Delete item from circular queue

int CirQueue:: deleteQueue(int *item)

if( isEmpty() )

cout<<"\nQueue Underflow";

return -1;

*item = ele[front];

front = (front+1) % MAX;

count--;
return 0;

int main()

int item;

CirQueue q = CirQueue();

q.insertQueue(10);

q.insertQueue(20);

q.insertQueue(30);

q.insertQueue(40);

q.insertQueue(50);

q.insertQueue(60);

if( q.deleteQueue(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

if( q.deleteQueue(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

if( q.deleteQueue(&item) == 0 )

cout<<"\nDeleted item:"<< item << endl;

if( q.deleteQueue(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

}
if( q.deleteQueue(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

q.insertQueue(60);

if( q.deleteQueue(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

if( q.deleteQueue(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

cout<<"\n";

return 0;

Inserted item : 10

Inserted item : 20

Inserted item : 30

Inserted item : 40

Inserted item : 50

Queue Overflow

Deleted item:10

Deleted item:20

Deleted item:30

Deleted item:40
Deleted item:50

Inserted item : 60

Deleted item:60

Queue Underflow

2.6.Multi-queues
If more number of queues is required to be implemented, then an efficient data structure
to handle multiple queues is required. It is possible to utilize all the available spaces in
a single array. When more than two queues, say n, are represented sequentially, we can
divide the available memory A[size] into n segments and allocate these segments to n
queues, one to each. For each queue i we shall use Front[i] and Rear[i]. We shall use
the condition Front[i] = Rear[i] if and only if the ith queue is empty, and the condition
Rear[i] = Front[i] if and only if the ith queue is full.
If we want five queues, then we can divide the array A[100] into equal parts of 20
and initialize front and rear for each queue, that is, Front[0] = Rear[0] = 0 and
Front[1] = Rear[1] = 20, and so on for other queues (Fig. 5.3).

Double Ended Queue (Dequeue)

Double Ended Queue is also a Queue data structure in which the insertion and deletion
operations are performed at both the ends (frontand rear). That means, we can insert at
both front and rear positions and can delete from both front and rear positions.

Double Ended Queue can be represented in TWO ways, those are as follows...

1. Input Restricted Double Ended Queue


2. Output Restricted Double Ended Queue
 Input Restricted Double Ended Queue

In input restricted double ended queue, the insertion operation is performed at only one
end and deletion operation is performed at both the ends.

 Output Restricted Double Ended Queue

In output restricted double ended queue, the deletion operation is performed at only one
end and insertion operation is performed at both the ends.

The Operations in DeQueue are

So there are 5 operations for deque.

1. Insert element at front end.


2. Insert element at rear end.
3. Delete element from front end.
4. Delete element from rear end.
5. Display the elements of the deque.

Algorithm for Insertion at rear end

Step -1: [Check for overflow]

if(rear==MAX)

Print("Queue is Overflow”);

return;

Step-2: [Insert element]

else
rear=rear+1;

q[rear]=no;

[Set rear and front pointer]

if rear=0

rear=1;

if front=0

front=1;

Step-3: return

Algorithm for Insertion at font end

Step-1 : [Check for the front position]

if(front<=1)

Print (“Cannot add item at front end”);

return;

Step-2 : [Insert at front]

else

front=front-1;

q[front]=no;

Step-3 : Return

Algorithm for Deletion from front end

Step-1 [ Check for front pointer]

if front=0

print(" Queue is Underflow”);

return;
Step-2 [Perform deletion]

else

no=q[front];

print(“Deleted element is”,no);

[Set front and rear pointer]

if front=rear

front=0;

rear=0;

else

front=front+1;

Step-3 : Return

Algorithm for Deletion from rear end

Step-1 : [Check for the rear pointer]

if rear=0

print(“Cannot delete value at rear end”);

return;

Step-2: [ perform deletion]

else

no=q[rear];

[Check for the front and rear pointer]

if front= rear

front=0;

rear=0;
else

rear=rear-1;

print(“Deleted element is”,no);

Step-3 : Return

Declaration of DeQueue

#define MAX 5

typedef struct DQ

int front ;

int rear ;

int count ;

int ele[MAX];

};

DeQueue Implementation with all above Queue operations

#include <iostream>

using namespace std;

#define MAX 5

//Declaration of DQueue

class DQueue

private:

int front ;

int rear ;

int count ;

int ele[MAX] ;

public:
DQueue();

int isFull();

int isEmpty();

void insertDQueueAtRear(int item);

int deleteDQueueAtFont(int *item);

void insertDQueueAtFront(int item);

int deleteDQueueAtRear(int *item);

};

//Initalize DQueue

DQueue:: DQueue()

front = 0;

rear = -1;

count = 0;

//To check DQueue is full or not

int DQueue:: isFull()

int full=0;

if( count == MAX )

full = 1;

return full;

//To check DQueue is empty or not

int DQueue:: isEmpty()

int empty=0;

if( count == 0 )
empty = 1;

return empty;

//Insert item into DQueue

void DQueue:: insertDQueueAtRear(int item)

if( isFull() )

cout<<"\nQueue Overflow";

return;

rear= (rear+1) % MAX;

ele[rear] = item;

count++;

cout<<"\nAfterInsert At Rear FRONT : "<< front<<", REAR : "<< rear;

cout<<"\nInserted item : "<< item << endl;

//Delete item from DQueue

int DQueue:: deleteDQueueAtFont(int *item)

if( isEmpty() )

cout<<"\nQueue Underflow";

return -1;

*item = ele[front];

front = (front+1) % MAX;

cout<<"\nAfter Delete At Front FRONT : "<< front<<", REAR : "<< rear;


count--;

return 0;

//To insert item into Dequeue at front.

void DQueue:: insertDQueueAtFront(int item)

if( isFull() )

cout<<"\nQueue Overflow";

return;

if ( front == 0)

front = MAX - 1;

else

front = front - 1;

ele[front] = item;

count++;

cout<<"\nAfter Insert At Front FRONT :"<< front<<", REAR : "<< rear;

cout<<"\nInserted item : "<< item<< endl;

//To delete item from Dequeue at rear.

int DQueue:: deleteDQueueAtRear(int *item)

if( isEmpty() )

cout<<"\nQueue Underflow";

return -1;

}
*item = ele[rear];

if(rear == 0)

rear = MAX - 1;

else

rear = rear - 1;

cout<<"\nAfterDeleteAtRear FRONT : "<< front<<", REAR : "<< rear;

count--;

return 0;

int main()

int item;

DQueue q = DQueue();

q.insertDQueueAtRear(10);

q.insertDQueueAtRear(20);

q.insertDQueueAtRear(30);

q.insertDQueueAtFront(40);

q.insertDQueueAtFront(50);

q.insertDQueueAtFront(60);

if( q.deleteDQueueAtFont(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

if( q.deleteDQueueAtFont(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

if( q.deleteDQueueAtFont(&item) == 0 )
{

cout<<"\nDeleted item:"<< item<< endl;

if( q.deleteDQueueAtRear(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

if( q.deleteDQueueAtRear(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

if( q.deleteDQueueAtRear(&item) == 0 )

cout<<"\nDeleted item:"<< item<< endl;

cout<<"\n";

return 0;

AfterInsert At Rear FRONT : 0, REAR : 0

Inserted item : 10

AfterInsert At Rear FRONT : 0, REAR : 1

Inserted item : 20

AfterInsert At Rear FRONT : 0, REAR : 2

Inserted item : 30

After Insert At Front FRONT :4, REAR : 2

Inserted item : 40
After Insert At Front FRONT :3, REAR : 2

Inserted item : 50

Queue Overflow

After Delete At Front FRONT : 4, REAR : 2

Deleted item:50

After Delete At Front FRONT : 0, REAR : 2

Deleted item:40

After Delete At Front FRONT : 1, REAR : 2

Deleted item:10

AfterDeleteAtRear FRONT : 1, REAR : 1

Deleted item:30

AfterDeleteAtRear FRONT : 1, REAR : 0

Deleted item:20

Queue Underflow

2.6. Priority Queue

In normal queue data structure, insertion is performed at the end of the queue and deletion
is performed based on the FIFO principle. This queue implementation may not be suitable
for all situations.

Consider a networking application where server has to respond for requests from multiple
clients using queue data structure. Assume four requests arrived to the queue in the order
of R1 requires 20 units of time, R2 requires 2 units of time, R3 requires 10 units of time and
R4 requires 5 units of time. Queue is as follows...

Now, check waiting time for each request to be complete.


1. R1 : 20 units of time
2. R2 : 22 units of time (R2 must wait till R1 complete - 20 units and R2 itself requeres 2
units. Total 22 units)
3. R3 : 32 units of time (R3 must wait till R2 complete - 22 units and R3 itself requeres
10 units. Total 32 units)
4. R4 : 37 units of time (R4 must wait till R3 complete - 35 units and R4 itself requeres 5
units. Total 37 units)
Here, average waiting time for all requests (R1, R2, R3 and R4) is (20+22+32+37)/4 ≈ 27 units
of time.

That means, if we use a normal queue data structure to serve these requests the average
waiting time for each request is 27 units of time.

Now, consider another way of serving these requests. If we serve according to their
required amount of time. That means, first we serve R2 which has minimum time required
(2) then serve R4 which has second minimum time required (5) then serve R3 which has
third minimum time required (10) and finnaly R1 which has maximum time required (20).

Now, check waiting time for each request to be complete.

1. R2 : 2 units of time

2. R4 : 7 units of time (R4 must wait till R2 complete 2 units and R4 itself requeres 5
units. Total 7 units)

3. R3 : 17 units of time (R3 must wait till R4 complete 7 units and R3 itself requeres 10
units. Total 17 units)

4. R1 : 37 units of time (R1 must wait till R3 complete 17 units and R1 itself requeres 20
units. Total 37 units)

Here, average waiting time for all requests (R1, R2, R3 and R4) is (2+7+17+37)/4 ≈ 15 units of
time.

From above two situations, it is very clear that, by using second method server can
complete all four requests with very less time compared to the first method. This is what
exactly done by the priority queue.

Priority queue is a variant of queue data structure in which insertion is performed in the
order of arrival and deletion is performed based on the priority.

There are two types of priority queues they are as follows...

1. Max Priority Queue

2. Min Priority Queue


1. Max Priority Queue

In max priority queue, elements are inserted in the order in which they arrive the queue and
always maximum value is removed first from the queue. For example assume that we insert
in order 8, 3, 2, 5 and they are removed in the order 8, 5, 3, 2.

The following are the operations performed in a Max priority queue...

1. isEmpty() - Check whether queue is Empty.

2. insert() - Inserts a new value into the queue.

3. findMax() - Find maximum value in the queue.

4. remove() - Delete maximum value from the queue.

Max Priority Queue Representations

There are 6 representations of max priority queue.

1. Using an Unordered Array (Dynamic Array)

2. Using an Unordered Array (Dynamic Array) with the index of the maximum value

3. Using an Array (Dynamic Array) in Decreasing Order

4. Using an Array (Dynamic Array) in Increasing Order

5. Using Linked List in Increasing Order

6. Using Unordered Linked List with reference to node with the maximum value

#1. Using an Unordered Array (Dynamic Array)

In this representation elements are inserted according to their arrival order and maximum
element is deleted first from max priority queue.

For example, assume that elements are inserted in the order of 8, 2, 3 and 5. And they are
removed in the order 8, 5, 3 and 2.

Now, let us analyse each operation according to this representation...


isEmpty() - If 'front == -1' queue is Empty. This operation requires O(1) time complexity that
means constant time.

insert() - New element is added at the end of the queue. This operation requires O(1) time
complexity that means constant time.

findMax() - To find maximum element in the queue, we need to compare with all the
elements in the queue. This operation requires O(n)time complexity.

remove() - To remove an element from the queue first we need to perform findMax() which
requires O(n) and removal of particular element requires constant time O(1). This operation
requires O(n) time complexity.

#2. Using an Unordered Array (Dynamic Array) with the index of the maximum value

In this representation elements are inserted according to their arrival order and maximum
element is deleted first from max priority queue.

For example, assume that elements are inserted in the order of 8, 2, 3 and 5. And they are
removed in the order 8, 5, 3 and 2.

Now, let us analyse each operation according to this representation...

isEmpty() - If 'front == -1' queue is Empty. This operation requires O(1) time complexity that
means constant time.

insert() - New element is added at the end of the queue with O(1) and for each insertion we
need to update maxIndex with O(1). This operation requires O(1) time complexity that
means constant time.

findMax() - To find maximum element in the queue is very simple as maxIndex has
maximum element index. This operation requires O(1)time complexity.

remove() - To remove an element from the queue first we need to perform findMax() which
requires O(1) , removal of particular element requires constant time O(1) and update
maxIndex value which requires O(n). This operation requires O(n) time complexity.

#3. Using an Array (Dynamic Array) in Decreasing Order

In this representation elements are inserted according to their value in decreasing order and
maximum element is deleted first from max priority queue.
For example, assume that elements are inserted in the order of 8, 5, 3 and 2. And they are
removed in the order 8, 5, 3 and 2.

Now, let us analyse each operation according to this representation...

isEmpty() - If 'front == -1' queue is Empty. This operation requires O(1) time complexity that
means constant time.

insert() - New element is added at a particular position in the decreasing order into the
queue with O(n), because we need to shift existing elements inorder to insert new element
in decreasing order. This operation requires O(n) time complexity.

findMax() - To find maximum element in the queue is very simple as maximum element is at
the beginning of the queue. This operation requires O(1) time complexity.

remove() - To remove an element from the queue first we need to perform findMax() which
requires O(1), removal of particular element requires constant time O(1) and rearrange
remaining elements which requires O(n). This operation requires O(n) time complexity.

#4. Using an Array (Dynamic Array) in Increasing Order

In this representation elements are inserted according to their value in increasing order and
maximum element is deleted first from max priority queue.

For example, assume that elements are inserted in the order of 2, 3, 5 and 8. And they are
removed in the order 8, 5, 3 and 2.

Now, let us analyse each operation according to this representation...

isEmpty() - If 'front == -1' queue is Empty. This operation requires O(1) time complexity that
means constant time.

insert() - New element is added at a particular position in the increasing order into the
queue with O(n), because we need to shift existing elements inorder to insert new element
in increasing order. This operation requires O(n) time complexity.

findMax() - To find maximum element in the queue is very simple as maximum element is at
the end of the queue. This operation requires O(1) time complexity.
remove() - To remove an element from the queue first we need to perform findMax() which
requires O(1), removal of particular element requires constant time O(1) and rearrange
remaining elements which requires O(n). This operation requires O(n) time complexity.

#5. Using Linked List in Increasing Order

In this representation, we use a single linked list to represent max priority queue. In this
representation elements are inserted according to their value in increasing order and node
with maximum value is deleted first from max priority queue.

For example, assume that elements are inserted in the order of 2, 3, 5 and 8. And they are
removed in the order 8, 5, 3 and 2.

Now, let us analyse each operation according to this representation...

isEmpty() - If 'head == NULL' queue is Empty. This operation requires O(1) time complexity
that means constant time.

insert() - New element is added at a particular position in the increasing order into the
queue with O(n), because we need to the position where new element has to be inserted.
This operation requires O(n) time complexity.

findMax() - To find maximum element in the queue is very simple as maximum element is at
the end of the queue. This operation requires O(1) time complexity.

remove() - To remove an element from the queue is simply removing the last node in the
queue which requires O(1). This operation requires O(1) time complexity.

#6. Using Unordered Linked List with reference to node with the maximum value

In this representation, we use a single linked list to represent max priority queue. Always we
maitain a reference (maxValue) to the node with maximum value. In this representation
elements are inserted according to their arrival and node with maximum value is deleted
first from max priority queue.

For example, assume that elements are inserted in the order of 2, 8, 3 and 5. And they are
removed in the order 8, 5, 3 and 2.
Now, let us analyse each operation according to this representation...

isEmpty() - If 'head == NULL' queue is Empty. This operation requires O(1) time complexity
that means constant time.

insert() - New element is added at end the queue with O(1) and update maxValue reference
with O(1). This operation requires O(1) time complexity.

findMax() - To find maximum element in the queue is very simple as maxValue is referenced
to the node with maximum value in the queue. This operation requires O(1) time
complexity.

remove() - To remove an element from the queue is deleting the node which referenced by
maxValue which requires O(1) and update maxValue reference to new node with maximum
value in the queue which requires O(n) time complexity. This operation requires O(n)time
complexity.

2. Min Priority Queue Representations

Min Priority Queue is similar to max priority queue except removing maximum element first,
we remove minimum element first in min priority queue.

The following operations are performed in Min Priority Queue...

1. isEmpty() - Check whether queue is Empty.


2. insert() - Inserts a new value into the queue.
3. findMin() - Find minimum value in the queue.
4. remove() - Delete minimum value from the queue.

Min priority queue is also has same representations as Max priority queue with minimum
value removal.

Priority Queue Implementation with all above queue operations

#include <iostream>

using namespace std;

#define MAX 5

//Declaration of priority queue

class PQueue

private:

int ele[MAX][MAX];
int count;

public:

PQueue();

void insertWithPriority(int priority, int item );

int GetNext(int *item);

int PeekAtNext(int *item); };

//Initialize priority queue

PQueue:: PQueue()

int i=0;

count = 0;

//All priority value set to -1

for( i = 0; i < MAX ; i++ )

ele[i][1] = -1 ;

//Insert item with priority in queue

void PQueue:: insertWithPriority(int priority, int item )

int i = 0;

if( count == MAX )

cout<< "\nPriority Queue is overflow";

return;

}
for ( i = 0; i < MAX; i++ )

if( ele[i][1] == -1)

break;

ele[i][0] = item;

ele[i][1] = priority;

count++;

cout<<"\nInserted item is : "<< item;

//Remove & get element with highest priority in queue

int PQueue:: GetNext(int *item)

int i = 0,max,pos=0;

if( count == 0 )

cout<<"\nPriority Queue is underflow";

return -1;

max = ele[0][1];

for ( i = 1; i < MAX; i++ )

if( max < ele[i][1] )

{
pos = i;

max = ele[i][1];

*item = ele[pos][0];

ele[pos][1] = -1;

count--;

return 0;

//Get element with highest priority without removing it from queue

int PQueue:: PeekAtNext(int *item)

int i = 0,max,pos=0;

if( count == 0 )

cout<<"\nPriority Queue is underflow";

return -1;

max = ele[0][1];

for ( i = 1; i < MAX; i++ )

if( max < ele[i][1] )

pos = i;

max = ele[i][1];

}
}

*item = ele[pos][0];

return 0;

int main()

int item;

PQueue q = PQueue();

q.insertWithPriority( 1, 10 );

q.insertWithPriority( 2, 20 );

q.insertWithPriority( 3, 30 );

q.insertWithPriority( 4, 40 );

q.insertWithPriority( 5, 50 );

q.insertWithPriority( 6, 60 );

if( q.PeekAtNext( &item) == 0 )

cout<<"\nPeek Item : "<< item;

if( q.GetNext( &item) == 0 )

cout<<"\nItem : "<< item;

if( q.PeekAtNext( &item) == 0 )

cout<<"\nPeek Item : "<< item;

if( q.GetNext( &item) == 0 )

cout<<"\nItem : "<< item;

if( q.GetNext( &item) == 0 )

cout<<"\nItem : "<< item;

if( q.PeekAtNext( &item) == 0 )

cout<<"\nPeek Item : "<< item;

if( q.GetNext( &item) == 0 )

cout<<"\nItem : "<< item;


if( q.GetNext( &item) == 0 )

cout<<"\nItem : "<< item;

if( q.GetNext( &item) == 0 )

cout<<"\nItem : "<< item;

cout<< endl;

return 0;

OUTPUT

Inserted item is : 10

Inserted item is : 20

Inserted item is : 30

Inserted item is : 40

Inserted item is : 50

Priority Queue is overflow

Peek Item : 50

Item : 50

Peek Item : 40

Item : 40

Item : 30

Peek Item : 20

Item : 20

Item : 10

Priority Queue is underflow


2.7. Applications of Queue Data Structure
Queue is used when things don’t have to be processed immediatly, but have to be
processed in First In First Out order like Breadth First Search. This property of Queue makes
it also useful in following kind of scenarios.
1) When a resource is shared among multiple consumers. Examples include CPU scheduling,
Disk Scheduling.
2) When data is transferred asynchronously (data not necessarily received at same rate as
sent) between two processes. Examples include IO Buffers, pipes, file IO, etc.

2.7.1 Josephus Problem

This is an interesting programming ,problem know in the literature as Josephus problem.


The problem is given as:

1. Suppose there are n children standing in a queue.

2. Students are numbered from 1 to n in the clockwise direction.

3. Students choose a lucky number say m.

They start counting in clockwise direction from the child designated as 1. The counting
proceeds until the mth child is identified. mth child is eliminated from the queue.
Counting for the next round begins from the child next to the eliminated one and proceeds
until the mth child is identified. This child is then eliminated and the process continues.
After few rounds of counting only one child is left and this child is declared as winner.

algorithm
1. Let n be the number of members
2. Get the first member
3. Add all members to the queue
4. while (there is more than one member in the queue)
begin
count through n − 1 members in the queue;
print the name of the nth member;
Remove the nth member from the queue;
end
5. Print the name of the only member in the list.

2.7.2 Job Scheduling

In the job-scheduling problem, we are given a list of n jobs. Every job i is associated with
an integer deadline di ≥0 and a profit pi ≥0. For any job i, profit is earned if and only if
the job is completed within its deadline. A feasible solution with the maximum sum of
profits is to be obtained.
To find the optimal solution and feasibility of jobs, we are required to find a subset J
such that each job of this subset can be completed by its deadline. The value of a feasible
solution J is the sum of profits of all the jobs in J.
The steps in finding the subset J are as follows:
1.  pi X i J is the objective function chosen for the optimization measure.
2. Using this measure, the next job to be included should be the one that increases .  pi X i
J.
3. Begin with J = , pi =0, and i J.
4. Add a job to J, which has the largest profit.
5. Add another job to J bearing in mind the following conditions:
(a) Search for the job that has the next maximum profit.
(b) See if this job in union with J is feasible.
(c) If yes, go to step 5 and continue; else go to (d).
(d) Search for the job with the next maximum profit and go to step 2.
6. Terminate when addition of no more jobs is feasible.
Example 1 shows a job scheduling algorithm that works to yield an optimized high
profit solution.
example 1 Consider five jobs with profits (p1, p2, p3, p4, p5) =(20, 15, 10, 5, 1) and
maximum delay allowed (d1, d2, d3, d4, d5) =(2, 2, 1, 3, 3).
Here, the maximum number of jobs that can be completed is
Min(n, maxdelay(di)) =Min(5, 3)=3
Hence, there is a possibility of doing 3 jobs, and there are 3 units of time, as shown
in Table 5.2.

In the first unit of time, job 1 is done and a profit of 20 is gained; in the second unit, job 2 is
done and a profit of 15 is obtained. However, in the third unit of time, job 3 is not available,
so job 4 is done with a gain of 5. Further, the deadline of job 5 has also passed; hence three
jobs 1, 2, and 4 are completed with a total profit of 40.

2.8 Introduction to Linked Lists


Linked List is a linear data structure and it is very common data structure which consists of
group of nodes in a sequence which is divided in two parts. Each node consists of its own
data and the address of the next node and forms a chain. Linked Lists are used to create
trees and graphs.

2.8.1. Advantages of Linked Lists

 They are a dynamic in nature which allocates the memory when required.
 Insertion and deletion operations can be easily implemented.
 Stacks and queues can be easily executed.
 Linked List reduces the access time.

Disadvantages of Linked Lists

 The memory is wasted as pointers require extra memory for storage.


 No element can be accessed randomly; it has to access each node sequentially.
 Reverse Traversing is difficult in linked list.

Applications of Linked Lists

 Linked lists are used to implement stacks, queues, graphs, etc.


 Linked lists let you insert elements at the beginning and end of the list.
 In Linked Lists we don’t need to know the size in advance.

2.9. Linked list Vs array : Difference

1. Access :Random / Sequential

 Stack elements can be randomly Accessed using Subscript Variable


 e.g a[0],a[1],a[3] can be randomly accessed
 While In Linked List We have to Traverse Through the Linked List for Accessing
Element. So O(n) Time required for Accessing Element .
 Generally In linked List Elements are accessed Sequentially.

2 . Memory Structure :

 Stack is stored in contiguous Memory Locations , i.e Suppose first element is Stored
at 2000 then Second Integer element will be stored at 2002 .
 But It is not necessary to store next element at the Consecutive memory Location .
 Element is stored at any available Location , but the Pointer to that memory location
is stored in Previous Node.

3 . Insertion / Deletion

 As the Array elements are stored in Consecutive memory Locations , so While


Inserting elements ,we have to create space for Insertion.
 So More time required for Creating space and Inserting Element
 Similarly We have to Delete the Element from given Location and then Shift All
successive elements up by 1 position
 In Linked List we have to Just Change the Pointer address field (Pointer),So Insertion
and Deletion Operations are quite easy to implement

4 . Memory Allocation :

 Memory Should be allocated at Compile-Time in Stack . i.e at the time when


Programmer is Writing Program
 In Linked list memory can be allocated at Run-Time , i.e After executing Program
 Stack uses Static Memory Allocation and Linked List Uses Dynamic Memory
Allocation
 Dynamic Memory allocation functions – malloc,calloc,delete etc…

2.10. Basic Operations of Linked list


Following are the basic operations supported by a list.

 Insertion − Adds an element at the beginning of the list.

 Deletion − Deletes an element at the beginning of the list.

 Display − Displays the complete list.

 Search − Searches an element using the given key.

 Delete − Deletes an element using the given key.

2.10.1. Insertion Operation


Adding a new node in linked list is a more than one step activity. We shall learn this with
diagrams here. First, create a node using the same structure and find the location where it
has to be inserted.

Imagine that we are inserting a node B (NewNode), between A (LeftNode)


and C (RightNode). Then point B.next to C −

NewNode.next −> RightNode;


It should look like this −

Now, the next node at the left should point to the new node.

LeftNode.next −> NewNode;

This will put the new node in the middle of the two. The new list should look like this −
Similar steps should be taken if the node is being inserted at the beginning of the list. While
inserting it at the end, the second last node of the list should point to the new node and
the new node will point to NULL.

2.10.2. Deletion Operation


Deletion is also a more than one step process. We shall learn with pictorial representation.
First, locate the target node to be removed, by using searching algorithms.

The left (previous) node of the target node now should point to the next node of the target
node −

LeftNode.next −> TargetNode.next;

This will remove the link that was pointing to the target node. Now, using the following
code, we will remove what the target node is pointing at.

TargetNode.next −> NULL;

We need to use the deleted node. We can keep that in memory otherwise we can simply
deallocate memory and wipe off the target node completely.

2.10.3. Reverse Operation


This operation is a thorough one. We need to make the last node to be pointed by the head
node and reverse the whole linked list.
First, we traverse to the end of the list. It should be pointing to NULL. Now, we shall make
it point to its previous node −

We have to make sure that the last node is not the lost node. So we'll have some temp
node, which looks like the head node pointing to the last node. Now, we shall make all left
side nodes point to their previous nodes one by one.

Except the node (first node) pointed by the head node, all nodes should point to their
predecessor, making them their new successor. The first node will point to NULL.

We'll make the head node point to the new first node by using the temp node.

2.11.Linked List abstrct data type

Although a linked list can be implemented in a variety of ways, the most fl exible
implementation is by using pointers.

 Create() – it creates a linked list


 Traverse()- it traverse the entire list
 Insert( int data, position) - it insert the new element to the location at a given
position.
 append(int data)- appends the new data element at the end of the lis
 Delete(int position) – deletes the list from the given position
 Reverse() – it reverses the list

2.12. Types of Linked Lists


 Singly Linked List : Singly linked lists contain nodes which have a data part as well as an
address part i.e. next, which points to the next node in sequence of nodes. The
operations we can perform on singly linked lists are insertion, deletion and traversal.

 Doubly Linked List : In a doubly linked list, each node contains two links the first link
points to the previous node and the next link points to the next node in the sequence.

 Circular Linked List : In the circular linked list the last node of the list contains the
address of the first node and forms a circular chain.

2.12. Singly linked list

Simply a list is a sequence of data, and linked list is a sequence of data linked with each
other.

The formal definition of a single linked list is as follows...

Single linked list is a sequence of elements in which every element has link to its next
element in the sequence.

In any single linked list, the individual element is called as "Node". Every "Node" contains
two fields, data and next. The data field is used to store actual value of that node and next
field is used to store the address of the next node in the sequence.

The graphical representation of a node in a single linked list is as follows...


In a single linked list, the address of the first node is always stored in a reference node
known as "front" (Some times it is also known as "head").
Example

Operations

In a single linked list we perform the following operations...

1. Insertion
2. Deletion
3. Display

Before we implement actual operations, first we need to setup empty list. First perform the
following steps before implementing actual operations.

 Step 1: Include all the header files which are used in the program.
 Step 2: Declare all the user defined functions.
 Step 3: Define a Node structure with two members data and next
 Step 4: Define a Node pointer 'head' and set it to NULL.
 Step 4: Implement the main method by displaying operations menu and make
suitable function calls in the main method to perform user selected operation.

Insertion

In a single linked list, the insertion operation can be performed in three ways. They are as
follows...

1. Inserting At Beginning of the list


2. Inserting At End of the list
3. Inserting At Specific location in the list

Inserting At Beginning of the list

We can use the following steps to insert a new node at beginning of the single linked list...
 Step 1: Create a newNode with given value.
 Step 2: Check whether list is Empty (head == NULL)
 Step 3: If it is Empty then, set newNode→next = NULL and head = newNode.
 Step 4: If it is Not Empty then, set newNode→next = head and head = newNode.

Inserting At End of the list

We can use the following steps to insert a new node at end of the single linked list...

 Step 1: Create a newNode with given value and newNode → next as NULL.
 Step 2: Check whether list is Empty (head == NULL).
 Step 3: If it is Empty then, set head = newNode.
 Step 4: If it is Not Empty then, define a node pointer temp and initialize with head.
 Step 5: Keep moving the temp to its next node until it reaches to the last node in the
list (until temp → next is equal to NULL).
 Step 6: Set temp → next = newNode.

Inserting At Specific location in the list (After a Node)

We can use the following steps to insert a new node after a node in the single linked list...

 Step 1: Create a newNode with given value.


 Step 2: Check whether list is Empty (head == NULL)
 Step 3: If it is Empty then, set newNode → next = NULL and head = newNode.
 Step 4: If it is Not Empty then, define a node pointer temp and initialize with head.
 Step 5: Keep moving the temp to its next node until it reaches to the node after
which we want to insert the newNode (until temp1 → data is equal to location, here
location is the node value after which we want to insert the newNode).
 Step 6: Every time check whether temp is reached to last node or not. If it is reached
to last node then display 'Given node is not found in the list!!! Insertion not
possible!!!' and terminate the function. Otherwise move the temp to next node.
 Step 7: Finally, Set 'newNode → next = temp → next' and 'temp → next = newNode'

Deletion

In a single linked list, the deletion operation can be performed in three ways. They are as
follows...

1. Deleting from Beginning of the list


2. Deleting from End of the list
3. Deleting a Specific Node

Deleting from Beginning of the list

We can use the following steps to delete a node from beginning of the single linked list...
 Step 1: Check whether list is Empty (head == NULL)
 Step 2: If it is Empty then, display 'List is Empty!!! Deletion is not possible' and
terminate the function.
 Step 3: If it is Not Empty then, define a Node pointer 'temp' and initialize with head.
 Step 4: Check whether list is having only one node (temp → next == NULL)
 Step 5: If it is TRUE then set head = NULL and delete temp (Setting Empty list
conditions)
 Step 6: If it is FALSE then set head = temp → next, and delete temp.

Deleting from End of the list

We can use the following steps to delete a node from end of the single linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty then, display 'List is Empty!!! Deletion is not possible' and
terminate the function.
 Step 3: If it is Not Empty then, define two Node pointers 'temp1' and 'temp2' and
initialize 'temp1' with head.
 Step 4: Check whether list has only one Node (temp1 → next == NULL)
 Step 5: If it is TRUE. Then, set head = NULL and delete temp1. And terminate the
function. (Setting Empty list condition)
 Step 6: If it is FALSE. Then, set 'temp2 = temp1 ' and move temp1 to its next node.
Repeat the same until it reaches to the last node in the list. (until temp1 →
next == NULL)
 Step 7: Finally, Set temp2 → next = NULL and delete temp1.

Deleting a Specific Node from the list

We can use the following steps to delete a specific node from the single linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty then, display 'List is Empty!!! Deletion is not possible' and
terminate the function.
 Step 3: If it is Not Empty then, define two Node pointers 'temp1' and 'temp2' and
initialize 'temp1' with head.
 Step 4: Keep moving the temp1 until it reaches to the exact node to be deleted or to
the last node. And every time set 'temp2 = temp1' before moving the 'temp1' to its
next node.
 Step 5: If it is reached to the last node then display 'Given node not found in the list!
Deletion not possible!!!'. And terminate the function.
 Step 6: If it is reached to the exact node which we want to delete, then check
whether list is having only one node or not
 Step 7: If list has only one node and that is the node to be deleted, then
set head = NULL and delete temp1 (free(temp1)).
 Step 8: If list contains multiple nodes, then check whether temp1 is the first node in
the list (temp1 == head).
 Step 9: If temp1 is the first node then move the head to the next node (head = head
→ next) and delete temp1.
 Step 10: If temp1 is not first node then check whether it is last node in the list
(temp1 → next == NULL).
 Step 11: If temp1 is last node then set temp2 → next = NULL and
delete temp1 (free(temp1)).
 Step 12: If temp1 is not first node and not last node then set temp2 → next = temp1
→ next and delete temp1 (free(temp1)).

Displaying a Single Linked List

We can use the following steps to display the elements of a single linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty then, display 'List is Empty!!!' and terminate the function.
 Step 3: If it is Not Empty then, define a Node pointer 'temp' and initialize with head.
 Step 4: Keep displaying temp → data with an arrow (--->) until temp reaches to the
last node
 Step 5: Finally display temp → data with arrow pointing to NULL (temp → data --->
NULL).

/*
* C++ Program to Implement Singly Linked List
*/
#include<iostream>
#include<cstdio>
#include<cstdlib>
using namespace std;
/*
* Node Declaration
*/
struct node
{
int info;
struct node *next;
}*start;

/*
* Class Declaration
*/
class single_llist
{
public:
node* create_node(int);
void insert_begin();
void insert_pos();
void insert_last();
void delete_pos();
void sort();
void search();
void update();
void reverse();
void display();
single_llist()
{
start = NULL;
}
};

/*
* Main :contains menu
*/
main()
{
int choice, nodes, element, position, i;
single_llist sl;
start = NULL;
while (1)
{
cout<<endl<<"---------------------------------"<<endl;
cout<<endl<<"Operations on singly linked list"<<endl;
cout<<endl<<"---------------------------------"<<endl;
cout<<"1.Insert Node at beginning"<<endl;
cout<<"2.Insert node at last"<<endl;
cout<<"3.Insert node at position"<<endl;
cout<<"4.Sort Link List"<<endl;
cout<<"5.Delete a Particular Node"<<endl;
cout<<"6.Update Node Value"<<endl;
cout<<"7.Search Element"<<endl;
cout<<"8.Display Linked List"<<endl;
cout<<"9.Reverse Linked List "<<endl;
cout<<"10.Exit "<<endl;
cout<<"Enter your choice : ";
cin>>choice;
switch(choice)
{
case 1:
cout<<"Inserting Node at Beginning: "<<endl;
sl.insert_begin();
cout<<endl;
break;
case 2:
cout<<"Inserting Node at Last: "<<endl;
sl.insert_last();
cout<<endl;
break;
case 3:
cout<<"Inserting Node at a given position:"<<endl;
sl.insert_pos();
cout<<endl;
break;
case 4:
cout<<"Sort Link List: "<<endl;
sl.sort();
cout<<endl;
break;
case 5:
cout<<"Delete a particular node: "<<endl;
sl.delete_pos();
break;
case 6:
cout<<"Update Node Value:"<<endl;
sl.update();
cout<<endl;
break;
case 7:
cout<<"Search element in Link List: "<<endl;
sl.search();
cout<<endl;
break;
case 8:
cout<<"Display elements of link list"<<endl;
sl.display();
cout<<endl;
break;
case 9:
cout<<"Reverse elements of Link List"<<endl;
sl.reverse();
cout<<endl;
break;
case 10:
cout<<"Exiting..."<<endl;
exit(1);
break;
default:
cout<<"Wrong choice"<<endl;
}
}
}
/*
* Creating Node
*/
node *single_llist::create_node(int value)
{
struct node *temp, *s;
temp = new(struct node);
if (temp == NULL)
{
cout<<"Memory not allocated "<<endl;
return 0;
}
else
{
temp->info = value;
temp->next = NULL;
return temp;
}
}

/*
* Inserting element in beginning
*/
void single_llist::insert_begin()
{
int value;
cout<<"Enter the value to be inserted: ";
cin>>value;
struct node *temp, *p;
temp = create_node(value);
if (start == NULL)
{
start = temp;
start->next = NULL;
}
else
{
p = start;
start = temp;
start->next = p;
}
cout<<"Element Inserted at beginning"<<endl;
}

/*
* Inserting Node at last
*/
void single_llist::insert_last()
{
int value;
cout<<"Enter the value to be inserted: ";
cin>>value;
struct node *temp, *s;
temp = create_node(value);
s = start;
while (s->next != NULL)
{
s = s->next;
}
temp->next = NULL;
s->next = temp;
cout<<"Element Inserted at last"<<endl;
}

/*
* Insertion of node at a given position
*/
void single_llist::insert_pos()
{
int value, pos, counter = 0;
cout<<"Enter the value to be inserted: ";
cin>>value;
struct node *temp, *s, *ptr;
temp = create_node(value);
cout<<"Enter the postion at which node to be inserted: ";
cin>>pos;
int i;
s = start;
while (s != NULL)
{
s = s->next;
counter++;
}
if (pos == 1)
{
if (start == NULL)
{
start = temp;
start->next = NULL;
}
else
{
ptr = start;
start = temp;
start->next = ptr;
}
}
else if (pos > 1 && pos <= counter)
{
s = start;
for (i = 1; i < pos; i++)
{
ptr = s;
s = s->next;
}
ptr->next = temp;
temp->next = s;
}
else
{
cout<<"Positon out of range"<<endl;
}
}

/*
* Sorting Link List
*/
void single_llist::sort()
{
struct node *ptr, *s;
int value;
if (start == NULL)
{
cout<<"The List is empty"<<endl;
return;
}
ptr = start;
while (ptr != NULL)
{
for (s = ptr->next;s !=NULL;s = s->next)
{
if (ptr->info > s->info)
{
value = ptr->info;
ptr->info = s->info;
s->info = value;
}
}
ptr = ptr->next;
}
}
/*
* Delete element at a given position
*/
void single_llist::delete_pos()
{
int pos, i, counter = 0;
if (start == NULL)
{
cout<<"List is empty"<<endl;
return;
}
cout<<"Enter the position of value to be deleted: ";
cin>>pos;
struct node *s, *ptr;
s = start;
if (pos == 1)
{
start = s->next;
}
else
{
while (s != NULL)
{
s = s->next;
counter++;
}
if (pos > 0 && pos <= counter)
{
s = start;
for (i = 1;i < pos;i++)
{
ptr = s;
s = s->next;
}
ptr->next = s->next;
}
else
{
cout<<"Position out of range"<<endl;
}
free(s);
cout<<"Element Deleted"<<endl;
}
}

/*
* Update a given Node
*/
void single_llist::update()
{
int value, pos, i;
if (start == NULL)
{
cout<<"List is empty"<<endl;
return;
}
cout<<"Enter the node postion to be updated: ";
cin>>pos;
cout<<"Enter the new value: ";
cin>>value;
struct node *s, *ptr;
s = start;
if (pos == 1)
{
start->info = value;
}
else
{
for (i = 0;i < pos - 1;i++)
{
if (s == NULL)
{
cout<<"There are less than "<<pos<<" elements";
return;
}
s = s->next;
}
s->info = value;
}
cout<<"Node Updated"<<endl;
}

/*
* Searching an element
*/
void single_llist::search()
{
int value, pos = 0;
bool flag = false;
if (start == NULL)
{
cout<<"List is empty"<<endl;
return;
}
cout<<"Enter the value to be searched: ";
cin>>value;
struct node *s;
s = start;
while (s != NULL)
{
pos++;
if (s->info == value)
{
flag = true;
cout<<"Element "<<value<<" is found at position "<<pos<<endl;
}
s = s->next;
}
if (!flag)
cout<<"Element "<<value<<" not found in the list"<<endl;
}

/*
* Reverse Link List
*/
void single_llist::reverse()
{
struct node *ptr1, *ptr2, *ptr3;
if (start == NULL)
{
cout<<"List is empty"<<endl;
return;
}
if (start->next == NULL)
{
return;
}
ptr1 = start;
ptr2 = ptr1->next;
ptr3 = ptr2->next;
ptr1->next = NULL;
ptr2->next = ptr1;
while (ptr3 != NULL)
{
ptr1 = ptr2;
ptr2 = ptr3;
ptr3 = ptr3->next;
ptr2->next = ptr1;
}
start = ptr2;
}

/*
* Display Elements of a link list
*/
void single_llist::display()
{
struct node *temp;
if (start == NULL)
{
cout<<"The List is Empty"<<endl;
return;
}
temp = start;
cout<<"Elements of list are: "<<endl;
while (temp != NULL)
{
cout<<temp->info<<"->";
temp = temp->next;
}
cout<<"NULL"<<endl;
}

OUTPUT
Operations on singly linked list

---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 8
Display elements of link list
The List is Empty.

---------------------------------

Operations on singly linked list

---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 5
Delete a particular node:
List is empty

---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 6
Update Node Value:
List is empty

---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 7
Search element in Link List:
List is empty
---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 3
Inserting Node at a given position:
Enter the value to be inserted: 1010
Enter the postion at which node to be inserted: 5
Positon out of range

---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 1
Inserting Node at Beginning:
Enter the value to be inserted: 100
Element Inserted at beginning

---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 1
Inserting Node at Beginning:
Enter the value to be inserted: 200
Element Inserted at beginning

---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 8
Display elements of link list
Elements of list are:
200->100->NULL

---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 2
Inserting node at last:
Enter the value to be inserted: 50
Element Inserted at last

---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 2
Inserting node at last:
Enter the value to be inserted: 150
Element Inserted at last
---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 8
Display elements of link list
Elements of list are:
200->100->50->150->NULL

---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 3
Inserting node at a given position:
Enter the value to be inserted: 1111
Enter the position at which node to be inserted: 4

---------------------------------
Operations on singly linked list
---------------------------------
1.Insert Node at beginning
2.Insert node at last
3.Insert node at position
4.Sort Link List
5.Delete a Particular Node
6.Update Node Value
7.Search Element
8.Display Linked List
9.Reverse Linked List
10.Exit
Enter your choice : 8
Display elements of link list
Elements of list are:
200->100->50->1111->150->NULL

2.13. Double Linked List

In a single linked list, every node has link to its next node in the sequence. So, we can
traverse from one node to other node only in one direction and we can not traverse back.
We can solve this kind of problem by using double linked list. Double linked list can be
defined as follows...

Double linked list is a sequence of elements in which every element has links to its
previous element and next element in the sequence.

In double linked list, every node has link to its previous node and next node. So, we can
traverse forward by using next field and can traverse backward by using previous field.
Every node in a double linked list contains three fields and they are shown in the following
figure...

Here, 'link1' field is used to store the address of the previous node in the
sequence, 'link2' field is used to store the address of the next node in the sequence
and 'data' field is used to store the actual value of that node.
Example

 In double linked list, the first node must be always pointed by head.
 Always the previous field of the first node must be NULL.
 Always the next field of the last node must be NULL.

Operations

In a double linked list, we perform the following operations...

1. Insertion
2. Deletion
3. Display

Insertion

In a double linked list, the insertion operation can be performed in three ways as follows...

1. Inserting At Beginning of the list


2. Inserting At End of the list
3. Inserting At Specific location in the list

Inserting At Beginning of the list

We can use the following steps to insert a new node at beginning of the double linked list...

 Step 1: Create a newNode with given value and newNode → previous as NULL.
 Step 2: Check whether list is Empty (head == NULL)
 Step 3: If it is Empty then, assign NULL to newNode → next and newNode to head.
 Step 4: If it is not Empty then, assign head to newNode →
next and newNode to head.

Inserting At End of the list

We can use the following steps to insert a new node at end of the double linked list...

 Step 1: Create a newNode with given value and newNode → next as NULL.
 Step 2: Check whether list is Empty (head == NULL)
 Step 3: If it is Empty, then assign NULL to newNode →
previous and newNode to head.
 Step 4: If it is not Empty, then, define a node pointer temp and initialize with head.
 Step 5: Keep moving the temp to its next node until it reaches to the last node in the
list (until temp → next is equal to NULL).
 Step 6: Assign newNode to temp → next and temp to newNode → previous.

Inserting At Specific location in the list (After a Node)

We can use the following steps to insert a new node after a node in the double linked list...

 Step 1: Create a newNode with given value.


 Step 2: Check whether list is Empty (head == NULL)
 Step 3: If it is Empty then, assign NULL to newNode → previous & newNode →
next and newNode to head.
 Step 4: If it is not Empty then, define two node pointers temp1 & temp2 and
initialize temp1 with head.
 Step 5: Keep moving the temp1 to its next node until it reaches to the node after
which we want to insert the newNode (until temp1 → data is equal to location, here
location is the node value after which we want to insert the newNode).
 Step 6: Every time check whether temp1 is reached to the last node. If it is reached
to the last node then display 'Given node is not found in the list!!! Insertion not
possible!!!' and terminate the function. Otherwise move the temp1 to next node.
 Step 7: Assign temp1 → next to temp2, newNode to temp1 →
next, temp1 to newNode → previous, temp2 to newNode →
nextand newNode to temp2 → previous.

Deletion

In a double linked list, the deletion operation can be performed in three ways as follows...

1. Deleting from Beginning of the list


2. Deleting from End of the list
3. Deleting a Specific Node

Deleting from Beginning of the list

We can use the following steps to delete a node from beginning of the double linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty then, display 'List is Empty!!! Deletion is not possible' and
terminate the function.
 Step 3: If it is not Empty then, define a Node pointer 'temp' and initialize with head.
 Step 4: Check whether list is having only one node (temp → previous is equal
to temp → next)
 Step 5: If it is TRUE, then set head to NULL and delete temp (Setting Empty list
conditions)
 Step 6: If it is FALSE, then assign temp → next to head, NULL to head →
previous and delete temp.

Deleting from End of the list

We can use the following steps to delete a node from end of the double linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty, then display 'List is Empty!!! Deletion is not possible' and
terminate the function.
 Step 3: If it is not Empty then, define a Node pointer 'temp' and initialize with head.
 Step 4: Check whether list has only one Node (temp → previous and temp →
next both are NULL)
 Step 5: If it is TRUE, then assign NULL to head and delete temp. And terminate from
the function. (Setting Empty list condition)
 Step 6: If it is FALSE, then keep moving temp until it reaches to the last node in the
list. (until temp → next is equal to NULL)
 Step 7: Assign NULL to temp → previous → next and delete temp.

Deleting a Specific Node from the list

We can use the following steps to delete a specific node from the double linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty then, display 'List is Empty!!! Deletion is not possible' and
terminate the function.
 Step 3: If it is not Empty, then define a Node pointer 'temp' and initialize with head.
 Step 4: Keep moving the temp until it reaches to the exact node to be deleted or to
the last node.
 Step 5: If it is reached to the last node, then display 'Given node not found in the
list! Deletion not possible!!!' and terminate the fuction.
 Step 6: If it is reached to the exact node which we want to delete, then check
whether list is having only one node or not
 Step 7: If list has only one node and that is the node which is to be deleted then
set head to NULL and delete temp (free(temp)).
 Step 8: If list contains multiple nodes, then check whether temp is the first node in
the list (temp == head).
 Step 9: If temp is the first node, then move the head to the next node (head = head
→ next), set head of previous to NULL (head → previous = NULL) and delete temp.
 Step 10: If temp is not the first node, then check whether it is the last node in the list
(temp → next == NULL).
 Step 11: If temp is the last node then set temp of previous of next to NULL (temp →
previous → next = NULL) and delete temp(free(temp)).
 Step 12: If temp is not the first node and not the last node, then
set temp of previous of next to temp of next (temp → previous → next = temp →
next), temp of next of previous to temp of previous (temp → next → previous =
temp → previous) and delete temp (free(temp)).

Displaying a Double Linked List

We can use the following steps to display the elements of a double linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty, then display 'List is Empty!!!' and terminate the function.
 Step 3: If it is not Empty, then define a Node pointer 'temp' and initialize with head.
 Step 4: Display 'NULL <--- '.
 Step 5: Keep displaying temp → data with an arrow (<===>) until temp reaches to
the last node
 Step 6: Finally, display temp → data with arrow pointing to NULL (temp → data --->
NULL).

/*
* C++ Program to Implement Doubly Linked List
*/
#include<iostream>
#include<cstdio>
#include<cstdlib>
/*
* Node Declaration
*/
using namespace std;
struct node
{
int info;
struct node *next;
struct node *prev;
}*start;

/*
Class Declaration
*/
class double_llist
{
public:
void create_list(int value);
void add_begin(int value);
void add_after(int value, int position);
void delete_element(int value);
void search_element(int value);
void display_dlist();
void count();
void reverse();
double_llist()
{
start = NULL;
}
};

/*
* Main: Conatins Menu
*/
int main()
{
int choice, element, position;
double_llist dl;
while (1)
{
cout<<endl<<"----------------------------"<<endl;
cout<<endl<<"Operations on Doubly linked list"<<endl;
cout<<endl<<"----------------------------"<<endl;
cout<<"1.Create Node"<<endl;
cout<<"2.Add at begining"<<endl;
cout<<"3.Add after position"<<endl;
cout<<"4.Delete"<<endl;
cout<<"5.Display"<<endl;
cout<<"6.Count"<<endl;
cout<<"7.Reverse"<<endl;
cout<<"8.Quit"<<endl;
cout<<"Enter your choice : ";
cin>>choice;
switch ( choice )
{
case 1:
cout<<"Enter the element: ";
cin>>element;
dl.create_list(element);
cout<<endl;
break;
case 2:
cout<<"Enter the element: ";
cin>>element;
dl.add_begin(element);
cout<<endl;
break;
case 3:
cout<<"Enter the element: ";
cin>>element;
cout<<"Insert Element after postion: ";
cin>>position;
dl.add_after(element, position);
cout<<endl;
break;
case 4:
if (start == NULL)
{
cout<<"List empty,nothing to delete"<<endl;
break;
}
cout<<"Enter the element for deletion: ";
cin>>element;
dl.delete_element(element);
cout<<endl;
break;
case 5:
dl.display_dlist();
cout<<endl;
break;
case 6:
dl.count();
break;
case 7:
if (start == NULL)
{
cout<<"List empty,nothing to reverse"<<endl;
break;
}
dl.reverse();
cout<<endl;
break;
case 8:
exit(1);
default:
cout<<"Wrong choice"<<endl;
}
}
return 0;
}

/*
* Create Double Link List
*/
void double_llist::create_list(int value)
{
struct node *s, *temp;
temp = new(struct node);
temp->info = value;
temp->next = NULL;
if (start == NULL)
{
temp->prev = NULL;
start = temp;
}
else
{
s = start;
while (s->next != NULL)
s = s->next;
s->next = temp;
temp->prev = s;
}
}

/*
* Insertion at the beginning
*/
void double_llist::add_begin(int value)
{
if (start == NULL)
{
cout<<"First Create the list."<<endl;
return;
}
struct node *temp;
temp = new(struct node);
temp->prev = NULL;
temp->info = value;
temp->next = start;
start->prev = temp;
start = temp;
cout<<"Element Inserted"<<endl;
}

/*
* Insertion of element at a particular position
*/
void double_llist::add_after(int value, int pos)
{
if (start == NULL)
{
cout<<"First Create the list."<<endl;
return;
}
struct node *tmp, *q;
int i;
q = start;
for (i = 0;i < pos - 1;i++)
{
q = q->next;
if (q == NULL)
{
cout<<"There are less than ";
cout<<pos<<" elements."<<endl;
return;
}
}
tmp = new(struct node);
tmp->info = value;
if (q->next == NULL)
{
q->next = tmp;
tmp->next = NULL;
tmp->prev = q;
}
else
{
tmp->next = q->next;
tmp->next->prev = tmp;
q->next = tmp;
tmp->prev = q;
}
cout<<"Element Inserted"<<endl;
}

/*
* Deletion of element from the list
*/
void double_llist::delete_element(int value)
{
struct node *tmp, *q;
/*first element deletion*/
if (start->info == value)
{
tmp = start;
start = start->next;
start->prev = NULL;
cout<<"Element Deleted"<<endl;
free(tmp);
return;
}
q = start;
while (q->next->next != NULL)
{
/*Element deleted in between*/
if (q->next->info == value)
{
tmp = q->next;
q->next = tmp->next;
tmp->next->prev = q;
cout<<"Element Deleted"<<endl;
free(tmp);
return;
}
q = q->next;
}
/*last element deleted*/
if (q->next->info == value)
{
tmp = q->next;
free(tmp);
q->next = NULL;
cout<<"Element Deleted"<<endl;
return;
}
cout<<"Element "<<value<<" not found"<<endl;
}

/*
* Display elements of Doubly Link List
*/
void double_llist::display_dlist()
{
struct node *q;
if (start == NULL)
{
cout<<"List empty,nothing to display"<<endl;
return;
}
q = start;
cout<<"The Doubly Link List is :"<<endl;
while (q != NULL)
{
cout<<q->info<<" <-> ";
q = q->next;
}
cout<<"NULL"<<endl;
}
/*
* Number of elements in Doubly Link List
*/
void double_llist::count()
{
struct node *q = start;
int cnt = 0;
while (q != NULL)
{
q = q->next;
cnt++;
}
cout<<"Number of elements are: "<<cnt<<endl;
}

/*
* Reverse Doubly Link List
*/
void double_llist::reverse()
{
struct node *p1, *p2;
p1 = start;
p2 = p1->next;
p1->next = NULL;
p1->prev = p2;
while (p2 != NULL)
{
p2->prev = p2->next;
p2->next = p1;
p1 = p2;
p2 = p2->prev;
}
start = p1;
cout<<"List Reversed"<<endl;
}

OUTPUT
---------------------------------
Operations on Doubly linked list
---------------------------------
1.Create Node
2.Add at begining
3.Add after
4.Delete
5.Display
6.Count
7.Reverse
8.Quit
Enter your choice : 2
Enter the element: 100
First Create the list.

---------------------------------
Operations on Doubly linked list
---------------------------------
1.Create Node
2.Add at begining
3.Add after
4.Delete
5.Display
6.Count
7.Reverse
8.Quit
Enter your choice : 3
Enter the element: 200
Insert Element after postion: 1
First Create the list.

---------------------------------
Operations on Doubly linked list
---------------------------------
1.Create Node
2.Add at begining
3.Add after
4.Delete
5.Display
6.Count
7.Reverse
8.Quit
Enter your choice : 4
List empty,nothing to delete

---------------------------------
Operations on Doubly linked list
---------------------------------
1.Create Node
2.Add at begining
3.Add after
4.Delete
5.Display
6.Count
7.Reverse
8.Quit
Enter your choice : 5
List empty,nothing to display

---------------------------------
Operations on Doubly linked list
---------------------------------
1.Create Node
2.Add at begining
3.Add after
4.Delete
5.Display
6.Count
7.Reverse
8.Quit
Enter your choice : 6
Number of elements are: 0

---------------------------------
Operations on Doubly linked list
---------------------------------
1.Create Node
2.Add at begining
3.Add after
4.Delete
5.Display
6.Count
7.Reverse
8.Quit
Enter your choice : 7
List empty,nothing to reverse

2.14. Circular Linked List

In single linked list, every node points to its next node in the sequence and the last node
points NULL. But in circular linked list, every node points to its next node in the sequence
but the last node points to the first node in the list.

Circular linked list is a sequence of elements in which every element has link to its next
element in the sequence and the last element has a link to the first element in the
sequence.

That means circular linked list is similar to the single linked list except that the last node
points to the first node in the list
Example

Operations

In a circular linked list, we perform the following operations...

1. Insertion
2. Deletion
3. Display

Before we implement actual operations, first we need to setup empty list. First perform the
following steps before implementing actual operations.

 Step 1: Include all the header files which are used in the program.
 Step 2: Declare all the user defined functions.
 Step 3: Define a Node structure with two members data and next
 Step 4: Define a Node pointer 'head' and set it to NULL.
 Step 4: Implement the main method by displaying operations menu and make
suitable function calls in the main method to perform user selected operation.

Insertion

In a circular linked list, the insertion operation can be performed in three ways. They are as
follows...

1. Inserting At Beginning of the list


2. Inserting At End of the list
3. Inserting At Specific location in the list

Inserting At Beginning of the list

We can use the following steps to insert a new node at beginning of the circular linked list...

 Step 1: Create a newNode with given value.


 Step 2: Check whether list is Empty (head == NULL)
 Step 3: If it is Empty then, set head = newNode and newNode→next = head .
 Step 4: If it is Not Empty then, define a Node pointer 'temp' and initialize with
'head'.
 Step 5: Keep moving the 'temp' to its next node until it reaches to the last node
(until 'temp → next == head').
 Step 6: Set 'newNode → next =head', 'head = newNode' and 'temp → next = head'.

Inserting At End of the list

We can use the following steps to insert a new node at end of the circular linked list...

 Step 1: Create a newNode with given value.


 Step 2: Check whether list is Empty (head == NULL).
 Step 3: If it is Empty then, set head = newNode and newNode → next = head.
 Step 4: If it is Not Empty then, define a node pointer temp and initialize with head.
 Step 5: Keep moving the temp to its next node until it reaches to the last node in the
list (until temp → next == head).
 Step 6: Set temp → next = newNode and newNode → next = head.

Inserting At Specific location in the list (After a Node)

We can use the following steps to insert a new node after a node in the circular linked list...

 Step 1: Create a newNode with given value.


 Step 2: Check whether list is Empty (head == NULL)
 Step 3: If it is Empty then, set head = newNode and newNode → next = head.
 Step 4: If it is Not Empty then, define a node pointer temp and initialize with head.
 Step 5: Keep moving the temp to its next node until it reaches to the node after
which we want to insert the newNode (until temp1 → data is equal to location, here
location is the node value after which we want to insert the newNode).
 Step 6: Every time check whether temp is reached to the last node or not. If it is
reached to last node then display 'Given node is not found in the list!!! Insertion not
possible!!!' and terminate the function. Otherwise move the temp to next node.
 Step 7: If temp is reached to the exact node after which we want to insert the
newNode then check whether it is last node (temp → next == head).
 Step 8: If temp is last node then set temp → next = newNode and newNode →
next = head.
 Step 8: If temp is not last node then set newNode → next = temp → next and temp
→ next = newNode.

Deletion

In a circular linked list, the deletion operation can be performed in three ways those are as
follows...

1. Deleting from Beginning of the list


2. Deleting from End of the list
3. Deleting a Specific Node
Deleting from Beginning of the list

We can use the following steps to delete a node from beginning of the circular linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty then, display 'List is Empty!!! Deletion is not possible' and
terminate the function.
 Step 3: If it is Not Empty then, define two Node pointers 'temp1' and 'temp2' and
initialize both 'temp1' and 'temp2' with head.
 Step 4: Check whether list is having only one node (temp1 → next == head)
 Step 5: If it is TRUE then set head = NULL and delete temp1 (Setting Empty list
conditions)
 Step 6: If it is FALSE move the temp1 until it reaches to the last node. (until temp1 →
next == head )
 Step 7: Then set head = temp2 → next, temp1 → next = head and delete temp2.

Deleting from End of the list

We can use the following steps to delete a node from end of the circular linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty then, display 'List is Empty!!! Deletion is not possible' and
terminate the function.
 Step 3: If it is Not Empty then, define two Node pointers 'temp1' and 'temp2' and
initialize 'temp1' with head.
 Step 4: Check whether list has only one Node (temp1 → next == head)
 Step 5: If it is TRUE. Then, set head = NULL and delete temp1. And terminate from
the function. (Setting Empty list condition)
 Step 6: If it is FALSE. Then, set 'temp2 = temp1 ' and move temp1 to its next node.
Repeat the same until temp1 reaches to the last node in the list. (until temp1 →
next == head)
 Step 7: Set temp2 → next = head and delete temp1.

Deleting a Specific Node from the list

We can use the following steps to delete a specific node from the circular linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty then, display 'List is Empty!!! Deletion is not possible' and
terminate the function.
 Step 3: If it is Not Empty then, define two Node pointers 'temp1' and 'temp2' and
initialize 'temp1' with head.
 Step 4: Keep moving the temp1 until it reaches to the exact node to be deleted or to
the last node. And every time set 'temp2 = temp1' before moving the 'temp1' to its
next node.
 Step 5: If it is reached to the last node then display 'Given node not found in the list!
Deletion not possible!!!'. And terminate the function.
 Step 6: If it is reached to the exact node which we want to delete, then check
whether list is having only one node (temp1 → next == head)
 Step 7: If list has only one node and that is the node to be deleted then
set head = NULL and delete temp1 (free(temp1)).
 Step 8: If list contains multiple nodes then check whether temp1 is the first node in
the list (temp1 == head).
 Step 9: If temp1 is the first node then set temp2 = head and keep moving temp2 to
its next node until temp2 reaches to the last node. Then set head = head →
next, temp2 → next = head and delete temp1.
 Step 10: If temp1 is not first node then check whether it is last node in the list
(temp1 → next == head).
 Step 11: If temp1 is last node then set temp2 → next = head and
delete temp1 (free(temp1)).
 Step 12: If temp1 is not first node and not last node then set temp2 → next = temp1
→ next and delete temp1 (free(temp1)).

Displaying a circular Linked List

We can use the following steps to display the elements of a circular linked list...

 Step 1: Check whether list is Empty (head == NULL)


 Step 2: If it is Empty, then display 'List is Empty!!!' and terminate the function.
 Step 3: If it is Not Empty then, define a Node pointer 'temp' and initialize with head.
 Step 4: Keep displaying temp → data with an arrow (--->) until temp reaches to the
last node
 Step 5: Finally display temp → data with arrow pointing to head → data.

/*
* C++ Program to Implement Circular Linked List
*/
#include<iostream>
#include<cstdio>
#include<cstdlib>
using namespace std;
/*
* Node Declaration
*/
struct node
{
int info;
struct node *next;
}*last;

/*
* Class Declaration
*/
class circular_llist
{
public:
void create_node(int value);
void add_begin(int value);
void add_after(int value, int position);
void delete_element(int value);
void search_element(int value);
void display_list();
void update();
void sort();
circular_llist()
{
last = NULL;
}
};

/*
* Main :contains menu
*/
int main()
{
int choice, element, position;
circular_llist cl;
while (1)
{
cout<<endl<<"---------------------------"<<endl;
cout<<endl<<"Circular singly linked list"<<endl;
cout<<endl<<"---------------------------"<<endl;
cout<<"1.Create Node"<<endl;
cout<<"2.Add at beginning"<<endl;
cout<<"3.Add after"<<endl;
cout<<"4.Delete"<<endl;
cout<<"5.Search"<<endl;
cout<<"6.Display"<<endl;
cout<<"7.Update"<<endl;
cout<<"8.Sort"<<endl;
cout<<"9.Quit"<<endl;
cout<<"Enter your choice : ";
cin>>choice;
switch(choice)
{
case 1:
cout<<"Enter the element: ";
cin>>element;
cl.create_node(element);
cout<<endl;
break;
case 2:
cout<<"Enter the element: ";
cin>>element;
cl.add_begin(element);
cout<<endl;
break;
case 3:
cout<<"Enter the element: ";
cin>>element;
cout<<"Insert element after position: ";
cin>>position;
cl.add_after(element, position);
cout<<endl;
break;
case 4:
if (last == NULL)
{
cout<<"List is empty, nothing to delete"<<endl;
break;
}
cout<<"Enter the element for deletion: ";
cin>>element;
cl.delete_element(element);
cout<<endl;
break;
case 5:
if (last == NULL)
{
cout<<"List Empty!! Can't search"<<endl;
break;
}
cout<<"Enter the element to be searched: ";
cin>>element;
cl.search_element(element);
cout<<endl;
break;
case 6:
cl.display_list();
break;
case 7:
cl.update();
break;
case 8:
cl.sort();
break;
case 9:
exit(1);
break;
default:
cout<<"Wrong choice"<<endl;
}
}
return 0;
}

/*
* Create Circular Link List
*/
void circular_llist::create_node(int value)
{
struct node *temp;
temp = new(struct node);
temp->info = value;
if (last == NULL)
{
last = temp;
temp->next = last;
}
else
{
temp->next = last->next;
last->next = temp;
last = temp;
}
}

/*
* Insertion of element at beginning
*/
void circular_llist::add_begin(int value)
{
if (last == NULL)
{
cout<<"First Create the list."<<endl;
return;
}
struct node *temp;
temp = new(struct node);
temp->info = value;
temp->next = last->next;
last->next = temp;
}

/*
* Insertion of element at a particular place
*/
void circular_llist::add_after(int value, int pos)
{
if (last == NULL)
{
cout<<"First Create the list."<<endl;
return;
}
struct node *temp, *s;
s = last->next;
for (int i = 0;i < pos-1;i++)
{
s = s->next;
if (s == last->next)
{
cout<<"There are less than ";
cout<<pos<<" in the list"<<endl;
return;
}
}
temp = new(struct node);
temp->next = s->next;
temp->info = value;
s->next = temp;
/*Element inserted at the end*/
if (s == last)
{
last=temp;
}
}

/*
* Deletion of element from the list
*/
void circular_llist::delete_element(int value)
{
struct node *temp, *s;
s = last->next;
/* If List has only one element*/
if (last->next == last && last->info == value)
{
temp = last;
last = NULL;
free(temp);
return;
}
if (s->info == value) /*First Element Deletion*/
{
temp = s;
last->next = s->next;
free(temp);
return;
}
while (s->next != last)
{
/*Deletion of Element in between*/
if (s->next->info == value)
{
temp = s->next;
s->next = temp->next;
free(temp);
cout<<"Element "<<value;
cout<<" deleted from the list"<<endl;
return;
}
s = s->next;
}
/*Deletion of last element*/
if (s->next->info == value)
{
temp = s->next;
s->next = last->next;
free(temp);
last = s;
return;
}
cout<<"Element "<<value<<" not found in the list"<<endl;
}

/*
* Search element in the list
*/
void circular_llist::search_element(int value)
{
struct node *s;
int counter = 0;
s = last->next;
while (s != last)
{
counter++;
if (s->info == value)
{
cout<<"Element "<<value;
cout<<" found at position "<<counter<<endl;
return;
}
s = s->next;
}
if (s->info == value)
{
counter++;
cout<<"Element "<<value;
cout<<" found at position "<<counter<<endl;
return;
}
cout<<"Element "<<value<<" not found in the list"<<endl;
}

/*
* Display Circular Link List
*/
void circular_llist::display_list()
{
struct node *s;
if (last == NULL)
{
cout<<"List is empty, nothing to display"<<endl;
return;
}
s = last->next;
cout<<"Circular Link List: "<<endl;
while (s != last)
{
cout<<s->info<<"->";
s = s->next;
}
cout<<s->info<<endl;
}

/*
* Update Circular Link List
*/
void circular_llist::update()
{
int value, pos, i;
if (last == NULL)
{
cout<<"List is empty, nothing to update"<<endl;
return;
}
cout<<"Enter the node position to be updated: ";
cin>>pos;
cout<<"Enter the new value: ";
cin>>value;
struct node *s;
s = last->next;
for (i = 0;i < pos - 1;i++)
{
if (s == last)
{
cout<<"There are less than "<<pos<<" elements.";
cout<<endl;
return;
}
s = s->next;
}
s->info = value;
cout<<"Node Updated"<<endl;
}

/*
* Sort Circular Link List
*/
void circular_llist::sort()
{
struct node *s, *ptr;
int temp;
if (last == NULL)
{
cout<<"List is empty, nothing to sort"<<endl;
return;
}
s = last->next;
while (s != last)
{
ptr = s->next;
while (ptr != last->next)
{
if (ptr != last->next)
{
if (s->info > ptr->info)
{
temp = s->info;
s->info = ptr->info;
ptr->info = temp;
}
}
else
{
break;
}
ptr = ptr->next;
}
s = s->next;
}
}

OUTPUT

---------------------------------
Operations on Circular singly linked list
---------------------------------
1.Create Node
2.Add at beginning
3.Add after
4.Delete
5.Search
6.Display
7.Update
8.Sort
9.Quit
Enter your choice : 1
Enter the element: 100

---------------------------------
Operations on Circular singly linked list
---------------------------------
1.Create Node
2.Add at beginning
3.Add after
4.Delete
5.Search
6.Display
7.Update
8.Sort
9.Quit
Enter your choice : 2
Enter the element: 200

---------------------------------
Operations on Circular singly linked list
---------------------------------
1.Create Node
2.Add at beginning
3.Add after
4.Delete
5.Search
6.Display
7.Update
8.Sort
9.Quit
Enter your choice : 6
Circular Link List:
200->100

2.15.Sparse Matrix

In computer programming, a matrix can be defined with a 2-dimensional array. Any array
with 'm' columns and 'n' rows represents a mXn matrix. There may be a situation in which a
matrix contains more number of ZERO values than NON-ZERO values. Such matrix is known
as sparse matrix.

Sparse matrix is a matrix which contains very few non-zero elements.

When a sparse matrix is represented with 2-dimensional array, we waste lot of space to
represent that matrix. For example, consider a matrix of size 100 X 100 containing only 10
non-zero elements. In this matrix, only 10 spaces are filled with non-zero values and
remaining spaces of matrix are filled with zero. That means, totally we allocate 100 X 100 X
2 = 20000 bytes of space to store this integer matrix. And to access these 10 non-zero
elements we have to make scanning for 10000 times.

Sparse Matrix Representations

A sparse matrix can be represented by using TWO representations, those are as follows...

1. Triplet Representation
2. Linked Representation

Triplet Representation

In this representation, we consider only non-zero values along with their row and column
index values. In this representation, the 0th row stores total rows, total columns and total
non-zero values in the matrix.

For example, consider a matrix of size 5 X 6 containing 6 number of non-zero values. This
matrix can be represented as shown in the image...
In above example matrix, there are only 6 non-zero elements ( those are 9, 8, 4, 2, 5 & 2)
and matrix size is 5 X 6. We represent this matrix as shown in the above image. Here the
first row in the right side table is filled with values 5, 6 & 6 which indicates that it is a sparse
matrix with 5 rows, 6 columns & 6 non-zero values. Second row is filled with 0, 4, & 9 which
indicates the value in the matrix at 0th row, 4th column is 9. In the same way the remaining
non-zero values also follows the similar pattern.

2.15.1. Linked Representation

In linked representation, we use linked list data structure to represent a sparse matrix. In
this linked list, we use two different nodes namely header node and element node. Header
node consists of three fields and element node consists of five fields as shown in the
image...

Consider the above same sparse matrix used in the Triplet representation. This sparse
matrix can be represented using linked representation as shown in the below image...

In above representation, H0, H1,...,H5 indicates the header nodes which are used to
represent indexes. Remaining nodes are used to represent non-zero elements in the matrix,
except the very first node which is used to represent abstract information of the sparse
matrix (i.e., It is a matrix of 5 X 6 with 6 non-zero elements).

In this representation, in each row and column, the last node right field points to it's
respective header node.

Representation of Sparce matrix using linked lists


/*
* C++ Program to Implement Sparse Matrix
*/
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;

/*
* Class List Declaration
*/
class List
{
private:
int index;
int value;
List *nextindex;
public:
List(int index)
{
this->index = index;
nextindex = NULL;
value = NULL;
}
List()
{
index = -1;
value = NULL;
nextindex = NULL;
}
void store(int index, int value)
{
List *current = this;
List *previous = NULL;
List *node = new List(index);
node->value = value;
while (current != NULL && current->index < index)
{
previous = current;
current = current->nextindex;
}
if (current == NULL)
{
previous->nextindex = node;
}
else
{
if (current->index == index)
{
cout<<"DUPLICATE INDEX"<<endl;
return;
}
previous->nextindex = node;
node->nextindex = current;
}
return;
}

int fetch(int index)


{
List *current = this;
int value = NULL;
while (current != NULL && current->index != index)
{
current = current->nextindex;
}
if (current != NULL)
{
value = current->value;
}
else
{
value = NULL;
}
return value;
}

int elementCount()
{
int elementCount = 0;
List *current = this->nextindex;
for ( ; (current != NULL); current = current->nextindex)
{
elementCount++;
}
return elementCount;
}
};
/*
* Class SpareArray Declaration
*/
class SparseArray
{
private:
List *start;
int index;
public:
SparseArray(int index)
{
start = new List();
this->index = index;
}
void store(int index, int value)
{
if (index >= 0 && index < this->index)
{
if (value != NULL)
start->store(index, value);
}
else
{
cout<<"INDEX OUT OF BOUNDS"<<endl;
}
}
int fetch(int index)
{
if (index >= 0 && index < this->index)
return start->fetch(index);
else
{
cout<<"INDEX OUT OF BOUNDS"<<endl;
return NULL;
}
}
int elementCount()
{
return start->elementCount();
}
};
/*
* Class SparseMatrix Declaration
*/
class SparseMatrix
{
private:
int N;
SparseArray **sparsearray;
public:
SparseMatrix(int N)
{
this->N = N;
sparsearray = new SparseArray* [N];
for (int index = 0; index < N; index++)
{
sparsearray[index] = new SparseArray(N);
}
}
void store(int rowindex, int colindex, int value)
{
if (rowindex < 0 || rowindex > N)
{
cout<<"row index out of bounds"<<endl;
return;
}
if (colindex < 0 || colindex > N)
{
cout<<"col index out of bounds"<<endl;
return;
}
sparsearray[rowindex]->store(colindex, value);
}

int get(int rowindex, int colindex)


{
if (rowindex < 0 || colindex > N)
{
cout<<"row index out of bounds"<<endl;
return 0;
}
if (rowindex < 0 || colindex > N)
{
cout<<"col index out of bounds"<<endl;
return 0;
}
return (sparsearray[rowindex]->fetch(colindex));
}
int elementCount()
{
int count = 0;
for (int index = 0; index < N; index++)
{
count += sparsearray[index]->elementCount();
}
return count;
}
};
/*
* Main
*/
int main()
{
int iarray[3][3];
iarray[0][0] = 1;
iarray[0][1] = NULL;
iarray[0][2] = 2;
iarray[1][0] = NULL;
iarray[1][1] = 3;
iarray[1][2] = NULL;
iarray[2][0] = 4;
iarray[2][1] = 6;
iarray[2][2] = NULL;
SparseMatrix sparseMatrix(3);
for (int rowindex = 0; rowindex < 3; rowindex++)
{
for (int colindex = 0; colindex < 3; colindex++)
{
sparseMatrix.store(rowindex, colindex, iarray[rowindex][colindex]);
}
}

cout<<"the sparse Matrix is: "<<endl;


for (int rowindex = 0; rowindex < 3; rowindex++)
{
for (int colindex = 0; colindex < 3; colindex++)
{
if (sparseMatrix.get(rowindex, colindex) == NULL)
cout<<"NULL"<< "\t";
else
cout<<sparseMatrix.get(rowindex, colindex) << "\t";
}
cout<<endl;
}
cout<<"The Size of Sparse Matrix is "<<sparseMatrix.elementCount()<<endl;

OUTPUT
the sparse Matrix is:
1 NULL 2
NULL 3 NULL
4 6 NULL
The Size of Sparse Matrix is 5

2.16. Stack using Linked List ( Linked Stack)

The major problem with the stack implemented using array is, it works only for fixed
number of data values. That means the amount of data must be specified at the beginning
of the implementation itself. Stack implemented using array is not suitable, when we don't
know the size of data which we are going to use. A stack data structure can be implemented
by using linked list data structure. The stack implemented using linked list can work for
unlimited number of values. That means, stack implemented using linked list works for
variable size of data. So, there is no need to fix the size at the beginning of the
implementation. The Stack implemented using linked list can organize as many data values
as we want.

In linked list implementation of a stack, every new element is inserted as 'top' element. That
means every newly inserted element is pointed by 'top'. Whenever we want to remove an
element from the stack, simply remove the node which is pointed by 'top' by moving 'top' to
its next node in the list. The next field of the first element must be always NULL.

Example

In above example, the last inserted node is 99 and the first inserted node is 25. The order of
elements inserted is 25, 32,50 and 99.

Operations

To implement stack using linked list, we need to set the following things before
implementing actual operations.

 Step 1: Include all the header files which are used in the program. And declare all
the user defined functions.
 Step 2: Define a 'Node' structure with two members data and next.
 Step 3: Define a Node pointer 'top' and set it to NULL.
 Step 4: Implement the main method by displaying Menu with list of operations and
make suitable function calls in the main method.
push(value) - Inserting an element into the Stack

We can use the following steps to insert a new node into the stack...

 Step 1: Create a newNode with given value.


 Step 2: Check whether stack is Empty (top == NULL)
 Step 3: If it is Empty, then set newNode → next = NULL.
 Step 4: If it is Not Empty, then set newNode → next = top.
 Step 5: Finally, set top = newNode.

pop() - Deleting an Element from a Stack

We can use the following steps to delete a node from the stack...

 Step 1: Check whether stack is Empty (top == NULL).


 Step 2: If it is Empty, then display "Stack is Empty!!! Deletion is not possible!!!" and
terminate the function
 Step 3: If it is Not Empty, then define a Node pointer 'temp' and set it to 'top'.
 Step 4: Then set 'top = top → next'.
 Step 7: Finally, delete 'temp' (free(temp)).

display() - Displaying stack of elements

We can use the following steps to display the elements (nodes) of a stack...

 Step 1: Check whether stack is Empty (top == NULL).


 Step 2: If it is Empty, then display 'Stack is Empty!!!' and terminate the function.
 Step 3: If it is Not Empty, then define a Node pointer 'temp' and initialize with top.
 Step 4: Display 'temp → data --->' and move it to the next node. Repeat the same
until temp reaches to the first node in the stack (temp → next != NULL).
 Step 4: Finally! Display 'temp → data ---> NULL'.

Implementation of linked list stack

#include<iostream>

using namespace std;

//Define node pointer

struct node{

int data;

node* next;

};

//Head and tail pointers


node* head;

node*tail;

//Push element in stack

void push(node *& head, node *&tail, int data)

if (head==NULL)

{ node* n= new node;

n->data=data;

n->next=NULL;

head=n;

tail=n;

else if (head!=NULL)

node* n= new node;

n->data=data;

n->next=head;

head=n;

//Show elements in stack

void showdata(node *& head)

{ node* temp=new node;

temp=head;

if (temp==NULL)

cout<<"The stack is empty"<<endl;

}
else

cout<<"The elements in the stack are: "<<endl;

while(temp!=NULL)

cout<<temp->data<<endl;

temp=temp->next;

// Pop first element in stack

void pop(node *&head, node *& tail)

if (head==NULL)

cout<<"The stack is empty"<<endl;

else if (head==tail)

{ cout<<"The value "<<head->data<<" was popped"<<endl;

delete head;

head=NULL;

tail=NULL;

else

node* delptr=new node;

delptr=head;

head=head->next;
cout<<"The value "<<delptr->data<<" was popped"<<endl;

delete delptr;

//Show top-most element in stack

void top(node *& head)

node* temp=new node;

temp=head;

if (temp==NULL)

cout<<"The stack is empty"<<endl;

else

cout<<"The value at the top of the stack is "<<temp->data<<endl;

//Show number of elements in stack

void sizeofstack(node *& head)

node* temp = new node;

temp=head;

int count=0;

if (temp==NULL)

cout<<"The size of the stack is "<<count<<endl;

}
else

while(temp!=NULL)

count++;

temp=temp->next;

cout<<"The size of the stack is "<<count<<endl;

//Destroy stack

void destroystack(node *& head)

node* delptr=new node;

if (head==NULL)

cout<<"The stack is empty"<<endl;

else

while(head!=NULL)

delptr=head;

head=head->next;

delete delptr;

cout<<"Stack has been destroyed!"<<endl;

}
}

//Menu function

char menu()

char choice;

cout<<"Menu: \n";

cout<<"1. Push values in stack."<<endl;

cout<<"2. Pop value from top of stack."<<endl;

cout<<"3. Show the stack."<<endl;

cout<<"4. Show the top of the stack."<<endl;

cout<<"5. Number of elements in stack."<<endl;

cout<<"6. Destroy stack."<<endl;

cout<<"7. Exit."<<endl;

cout<<"Enter your choice (1-6) from above: ";

cin>>choice;

return choice;

//Main function

int main()

node* head;

node* tail;

head=NULL;

tail=NULL;

char choice;

do{

cout<<"-----------------------------------------------------"<<endl;

choice=menu();
cout<<"-----------------------------------------------------"<<endl;

switch(choice)

case '1':

int data;

cout<<"Enter value to be stored in stack: ";

cin>>data;

push(head,tail,data);

break;

case '2':

pop(head,tail);

break;

case '3':

showdata(head);

break;

case '4':

top(head);

break;

case '5':

sizeofstack(head);

break;

case '6':

destroystack(head);

break;

default:

cout<<"Exiting Menu";

}
}while(choice!='7');

return 0;

2.17. Queue using Linked List

The major problem with the queue implemented using array is, It will work for only fixed
number of data. That means, the amount of data must be specified in the beginning itself.
Queue using array is not suitable when we don't know the size of data which we are going
to use. A queue data structure can be implemented using linked list data structure. The
queue which is implemented using linked list can work for unlimited number of values. That
means, queue using linked list can work for variable size of data (No need to fix the size at
beginning of the implementation). The Queue implemented using linked list can organize as
many data values as we want.

In linked list implementation of a queue, the last inserted node is always pointed by 'rear'
and the first node is always pointed by 'front'.

Example

In above example, the last inserted node is 50 and it is pointed by 'rear' and the first
inserted node is 10 and it is pointed by 'front'. The order of elements inserted is 10, 15, 22
and 50.

Operations

To implement queue using linked list, we need to set the following things before
implementing actual operations.

 Step 1: Include all the header files which are used in the program. And declare all
the user defined functions.
 Step 2: Define a 'Node' structure with two members data and next.
 Step 3: Define two Node pointers 'front' and 'rear' and set both to NULL.
 Step 4: Implement the main method by displaying Menu of list of operations and
make suitable function calls in the main method to perform user selected operation.

enQueue(value) - Inserting an element into the Queue

We can use the following steps to insert a new node into the queue...
 Step 1: Create a newNode with given value and set 'newNode → next' to NULL.
 Step 2: Check whether queue is Empty (rear == NULL)
 Step 3: If it is Empty then, set front = newNode and rear = newNode.
 Step 4: If it is Not Empty then, set rear → next = newNode and rear = newNode.

deQueue() - Deleting an Element from Queue

We can use the following steps to delete a node from the queue...

 Step 1: Check whether queue is Empty (front == NULL).


 Step 2: If it is Empty, then display "Queue is Empty!!! Deletion is not
possible!!!" and terminate from the function
 Step 3: If it is Not Empty then, define a Node pointer 'temp' and set it to 'front'.
 Step 4: Then set 'front = front → next' and delete 'temp' (free(temp)).

display() - Displaying the elements of Queue

We can use the following steps to display the elements (nodes) of a queue...

 Step 1: Check whether queue is Empty (front == NULL).


 Step 2: If it is Empty then, display 'Queue is Empty!!!' and terminate the function.
 Step 3: If it is Not Empty then, define a Node pointer 'temp' and initialize with front.
 Step 4: Display 'temp → data --->' and move it to the next node. Repeat the same
until 'temp' reaches to 'rear' (temp → next != NULL).
 Step 4: Finally! Display 'temp → data ---> NULL'.

2.18. Generalized Lists

A generalized linked list contains structures or elements with every one containing its own
pointer. It's generalized if the list can have any deletions, insertions, and similar inserted
effectively into it.

• Definition : A generalized list, A, is a finite sequence of n ≥ 0 elements, a0, a1, a2, …,


an-1, where i, is either an atom or a list. The elements i,0 ≤ i ≤ n – 1, that are not
atoms are said to be the sublists of A.
• A list A is written as A = (a0, …, an-1 ), and the length of the list is n.
• A list name is represented by a capital letter and an atom is represented by a
lowercase letter.
• a0 is the head of list A and the rest (a1, …, an-1) is the tail of list A.

Examples of Generalized Lists

Some examples of generalized lists are the following:


1. A =() The empty (or null) list.
2. B =(a, (b, c), d) List of three elements—the first element is a, the second element is list (b,
c), and the third element is d.
3. C=(B, B, A) List of length 3 with the first and the second element as list B and the third
element as list A, which is a null list.
4. D =(a, b, D) List of length 3 which is recursive as it includes itself as one of the elements. It
can also be written as D=(a, b, (a, b, (a, b, ...) ...
In example 2, A is a list made up of three components. The first component is an atom, the
second component is a list made up of two atoms, and the third component is the atom d.
One of the better approaches to visualize the generalized lists is using a header node. In this
approach, each generalized list has a header node labelled Head. Figure 6.53 shows the
pictorial representation of list B.

In example 3, the list C has three components: the first component is list B, the second
component is again list B, and the third component is list A. This can be pictorially viewed
as in Fig. 6.54.

2.20. More On Linked Lists

2.20.1 Copying a Linked List


Consider the Copy_List() function, shown in Program Code 6.25, that takes a list and
returns a complete copy of that list. One pointer can iterate over the original list in the
usual way. Two other pointers can keep track of the new list: one head pointer and one tail
pointer, which always points to the last node in the new list. The fi rst node is done as a
special case, and then the tail pointer is used in the standard way for the others.

Node *Llist :: CopyList()


{
Node *current = Head;
Node *newList = Null;
Node *Tail = Null;
while(current != Null)
{
if(newList == Null)
{
newList = new Node;
newList->Data = current->Data;
newList->link = Null;
Tail = newList;
}
else
{
Tail->link = new Node;
Tail = Tail->link;
Tail->Data = current->Data;
Tail->link = Null;
}
current = current->link;
}
return(newList);
}

2.20.2 Computing the Length of a Linked List


The Length() function, in Program Code 6.26, takes a linked list and computes the number
of elements in the list.
Length() is a simple list function, but it demonstrates several concepts, which will be
used later in more complex list functions.

int Llist :: Length()


{
Node *current = Head;
int count = 0;
while(current != Null)
{
count++;
current = current->link;
}
return count;
}

2.20.3. Calling Length()


Program Code 6.27 is a typical code that calls Length(). It fi rst calls create() to make
a list and store the head pointer in a local variable. It then calls Length() on the list and
catches the int result in a local variable.

void LengthTest()
{
Llist myList;
mylist.Create();
int len = mylist.Length();
}
6.14.3 Reversing singly Linked List Without temporary storage
The procedure for reversing a singly linked list without temporary storage is illustrated

void Llist :: Reverse()


{
Node *curr, *prev, *next;
prev = Head;
curr = Head->link;
prev->link = Null
while(temp != Null)
{
next = temp->link;
temp->link = prev;
prev = temp;
temp = next;
}
head = prev;
}

2.20.4 Concatenating two Linked Lists


Concatenation of two linked lists is illustrated.

void Llist :: concatanate(Llist A)


{
Node *X, *Y;
X = Head;
Y = A.Head;
while(X->link != Null)
{
X = X->link;
}
X->link = Y;
Head = X;
}
//A call to concatenate:
{
Llist L1, L2;
L1.Create(); L2.Create();
L1.Concatanate(L2);
}
Here, X and Y are concatenated, and X is the pointer to the fi rst node of the resultant list.

2.20.5 erasing the Linked List


The procedure for erasing a linked list and returning all nodes to the free pool of memory
is illustrated

void Llist :: ~Llist()


{
Node *temp;
while(Head != Null)
{
temp = Head;
Head = Head->link;
delete temp;
}
}

UNIT III

3.0. Introduction to Trees

In linear data structure, data is organized in sequential order and in non-linear data
structure, data is organized in random order. Tree is a very popular data structure used in
wide range of applications. A tree data structure can be defined as follows...

Tree is a non-linear data structure which organizes data in hierarchical structure and this
is a recursive definition.

A tree data structure can also be defined as follows...

Tree data structure is a collection of data (Node) which is organized in hierarchical


structure and this is a recursive definition

In tree data structure, every individual element is called as Node. Node in a tree data
structure, stores the actual data of that particular element and link to next element in
hierarchical structure.

In a tree data structure, if we have N number of nodes then we can have a maximum of N-
1 number of links.

Example

3.1. Terminology

In a tree data structure, we use the following terminology...


1. Root

In a tree data structure, the first node is called as Root Node. Every tree must have root
node. We can say that root node is the origin of tree data structure. In any tree, there must
be only one root node. We never have multiple root nodes in a tree.

2. Edge

In a tree data structure, the connecting link between any two nodes is called as EDGE. In a
tree with 'N' number of nodes there will be a maximum of 'N-1' number of edges.

3. Parent

In a tree data structure, the node which is predecessor of any node is called as PARENT
NODE. In simple words, the node which has branch from it to any other node is called as
parent node. Parent node can also be defined as "The node which has child / children".
4. Child

In a tree data structure, the node which is descendant of any node is called as CHILD Node.
In simple words, the node which has a link from its parent node is called as child node. In a
tree, any parent node can have any number of child nodes. In a tree, all the nodes except
root are child nodes.

5. Siblings

In a tree data structure, nodes which belong to same Parent are called as SIBLINGS. In
simple words, the nodes with same parent are called as Sibling nodes.

6. Leaf

In a tree data structure, the node which does not have a child is called as LEAF Node. In
simple words, a leaf is a node with no child.

In a tree data structure, the leaf nodes are also called as External Nodes. External node is
also a node with no child. In a tree, leaf node is also called as 'Terminal' node.
7. Internal Nodes

In a tree data structure, the node which has atleast one child is called as INTERNAL Node. In
simple words, an internal node is a node with atleast one child.

In a tree data structure, nodes other than leaf nodes are called as Internal Nodes. The root
node is also said to be Internal Node if the tree has more than one node. Internal nodes are
also called as 'Non-Terminal' nodes.

8. Degree

In a tree data structure, the total number of children of a node is called as DEGREE of that
Node. In simple words, the Degree of a node is total number of children it has. The highest
degree of a node among all the nodes in a tree is called as 'Degree of Tree'
9. Level

In a tree data structure, the root node is said to be at Level 0 and the children of root node
are at Level 1 and the children of the nodes which are at Level 1 will be at Level 2 and so
on... In simple words, in a tree each step from top to bottom is called as a Level and the
Level count starts with '0' and incremented by one at each level (Step).

10. Height

In a tree data structure, the total number of egdes from leaf node to a particular node in the
longest path is called as HEIGHT of that Node. In a tree, height of the root node is said to
be height of the tree. In a tree, height of all leaf nodes is '0'.

11. Depth

In a tree data structure, the total number of egdes from root node to a particular node is
called as DEPTH of that Node. In a tree, the total number of edges from root node to a leaf
node in the longest path is said to be Depth of the tree. In simple words, the highest depth
of any leaf node in a tree is said to be depth of that tree. In a tree, depth of the root node is

'0'.
12. Path

In a tree data structure, the sequence of Nodes and Edges from one node to another node is
called as PATH between that two Nodes. Length of a Path is total number of nodes in that
path. In below example the path A - B - E - J has length 4.

13. Sub Tree

In a tree data structure, each child from a node forms a subtree recursively. Every child node
will form a subtree on its parent node.

3.2. Tree Representations

A tree data structure can be represented in two methods. Those methods are as follows...

1. List Representation

2. Left Child - Right Sibling Representation

Consider the following tree...


1. List Representation

In this representation, we use two types of nodes one for representing the node with data
and another for representing only references. We start with a node with data from root
node in the tree. Then it is linked to an internal node through a reference node and is linked
to any other node directly. This process repeats for all the nodes in the tree.

The above tree example can be represented using List representation as follows...

2. Left Child - Right Sibling Representation

In this representation, we use list with one type of node which consists of three fields
namely Data field, Left child reference field and Right sibling reference field. Data field
stores the actual value of a node, left reference field stores the address of the left child and
right reference field stores the address of the right sibling node. Graphical representation of
that node is as follows...

In this representation, every node's data field stores the actual value of that node. If that
node has left child, then left reference field stores the address of that left child node
otherwise that field stores NULL. If that node has right sibling then right reference field
stores the address of right sibling node otherwise that field stores NULL.

The above tree example can be represented using Left Child - Right Sibling representation as
follows...

3.3. Types of Trees


1. Free tree
2. Rooted tree
3. Ordered tree
4. Regular tree
5. Binary tree
6. Complete tree
7. Position tree
Free tree A free tree is a connected, acyclic graph. It is an undirected graph. It has no node
designated as a root. As it is connected, any node can be reached from any other node
through a unique path. The following is an example of a free tree.

Rooted tree Unlike free tree, a rooted tree is a directed graph where one node is designated
as root, whose incoming degree is zero, whereas for all other nodes, the incoming
degree is one.

Ordered tree In many applications, the relative order of the nodes at any particular level
assumes some significance. It is easy to impose an order on the nodes at a level by referring
to a particular node as the first node, to another node as the second, and so on. Such
ordering can be done from left to right . Just like nodes at each level, we can prescribe order
to edges. If in a directed tree, an ordering of a node at each level is prescribed, then such a
tree is called an ordered tree.

Regular tree A tree where each branch node vertex has the same outdegree is called a
regular tree. If in a directed tree, the outdegree of every node is less than or equal to m,
then the tree is called an m-ary tree. If the outdegree of every node is exactly equal to m
(the branch nodes) or zero (the leaf nodes), then the tree is called a regular m-ary tree.

Binary tree A binary tree is a special form of an m-ary tree. Since a binary tree is
important, it is frequently used in various applications of computer science.
We have defined an m-ary tree (general tree). A binary tree is an m-ary position tree
when m =2. In a binary tree, no node has more than two children.

Complete tree A tree with n nodes and of depth k is complete if and only if its nodes
correspond to the nodes that are numbered from 1 to n in the full tree of depth k.
A binary tree of height h is complete if and only if one of the following holds good:
1. It is empty.
2. Its left subtree is complete of height h 1 and its right subtree is completely full of
height h =2.
3. Its left subtree is completely full of height h 1 and its right subtree is complete of
height h =1.
A binary tree is completely full if it is of height h and has (2h+1 -1) nodes.
Full binary tree A binary tree is a full binary tree if it contains the maximum possible
number of nodes in all levels. Figure 7.15 shows a full binary tree of height 2.
In a full binary tree, each node has two children or no child at all. The total number of nodes
in a full binary tree of heighth is 2h+1-1 considering the root at level 0.
It can be calculated by adding the number of nodes of each level as in the following
equation: 20 <21 <22 <... <2h <2h+1 -1

Complete binary tree A binary tree is said to be a complete binary tree if all its levels
except the last level have the maximum number of possible nodes, and all the nodes of the
last level appear as far left as possible. In a complete binary tree, all the leaf nodes are at
the last and the second last level, and the levels are filled from left to right.

Left skewed binary tree If the right subtree is missing in every node of a tree, we call it
a left skewed tree . If the left subtree is missing in every node of a tree, we call it as right
subtree.

Strictly binary tree If every non-terminal node in a binary tree consists of non-empty left
and right subtrees, then such a tree is called a strictly binary tree.
In Fig. 7.19, the non-empty nodes D and E have left and right subtrees. Such expression
trees are known as strictly binary trees.

Extended binary tree A binary tree T with each node having zero or two children is called
an extended binary tree. The nodes with two children are called internal nodes, and those
with zero children are called external nodes. Trees can be converted into extended trees by
adding a node.
Position tree A position tree, also known as a suffix tree, is one that represents the suffixes
of a string S and such representation facilitates string operations being performed faster.
Such a tree’s edges are labelled with strings, such that each suffix of S corresponds to
exactly one path from the tree’s root to a leaf node. The space and time requirement is
linear in the length of S. After its construction, several operations can be performed quickly,
such as locating a substring in S, locating a substring if a certain number of mistakes are
allowed, locating matches for a regular expression pattern, and so

3.4. Binary Tree

In a normal tree, every node can have any number of children. Binary tree is a special type
of tree data structure in which every node can have a maximum of 2 children. One is known
as left child and the other is known as right child.

A tree in which every node can have a maximum of two children is called as Binary Tree.

In a binary tree, every node can have either 0 children or 1 child or 2 children but not more
than 2 children.

Example

There are different types of binary trees and they are...

1. Strictly Binary Tree

In a binary tree, every node can have a maximum of two children. But in strictly binary tree,
every node should have exactly two children or none. That means every internal node must
have exactly two children. A strictly Binary Tree can be defined as follows...

A binary tree in which every node has either two or zero number of children is called
Strictly Binary Tree

Strictly binary tree is also called as Full Binary Tree or Proper Binary Tree or 2-Tree
Strictly binary tree data structure is used to represent mathematical expressions.

Example

2. Complete Binary Tree

In a binary tree, every node can have a maximum of two children. But in strictly binary tree,
every node should have exactly two children or none and in complete binary tree all the
nodes must have exactly two children and at every level of complete binary tree there must
be 2level number of nodes. For example at level 2 there must be 2 2 = 4 nodes and at level 3
there must be 23 = 8 nodes.

A binary tree in which every internal node has exactly two children and all leaf nodes are
at same level is called Complete Binary Tree.

Complete binary tree is also called as Perfect Binary Tree

3. Extended Binary Tree

A binary tree can be converted into Full Binary tree by adding dummy nodes to existing
nodes wherever required.
The full binary tree obtained by adding dummy nodes to a binary tree is called as
Extended Binary Tree.

In above figure, a normal binary tree is converted into full binary tree by adding dummy
nodes (In pink colour).

3.5. The Binary Tree ADT

A binary tree consists of nodes with one predecessor and at most two successors (called the
left child and the right child). The only exception is the root node of the tree that does not
have a predecessor. A node can contain any amount and any type of data.

 create - A binary tree may be created in several states. The most common include ...
o an empty tree (i.e. no nodes) and hence the constructor will have no parameters
o a tree with one node (i.e. the root) and hence the constructor will have a single
parameter (the data for the root node)
o a tree with a new root whose children are other existing trees and hence the
constructor will have three parameters (the data for the root node and references to
its subtrees)
 isEmpty() - Returns true if there are no nodes in the tree, false otherwise.
o isFull() - May be required by some implementations. Returns true if the tree is full,
false otherwise.
 clear() - Removes all of the nodes from the tree (essentially reinitializing it to a new
empty tree).
 add(value) - Adds a new node to the tree with the given value. The actual
implementation of this method is determined by the purpose for the tree and how the
tree is to be maintained and/or processed. To begin with, we will assume no particular
purpose or order and therefore add new nodes in such a way that the tree will remain
nearly balanced.
 remove() - Removes the root node of the tree and returns its data. The actual
implementation of this method and other forms of removal will be determined by the
purpose for the tree and how the tree is to be maintained and/or processed. For
example, you might need a remove() method with a parameter that indicates which
node of the tree is to be removed (based on position, key data, or reference). To begin
with, we will assume no particular purpose or order and therefore remove the root in
such a way that the tree will remain nearly balanced.

Other operations that may be included as needed:

 height() - Determines the height of the tree. An empty tree will have height 0.
 size() - Determines the number of nodes in the tree.
 getRootData() - Returns the data (primative data) or reference to the data (objects) of
the tree's root.
 getLeftSubtree() - Returns a reference to the left subtree of this tree.
 getRightSubtree() - Returns a reference to the right subtree of this tree.

3.6. Realization of Binary tree

A binary tree data structure is represented using two methods. Those methods are as
follows...

1. Array Representation

2. Linked List Representation

Consider the following binary tree...

1. Array Representation

In array representation of binary tree, we use a one dimensional array (1-D Array) to
represent a binary tree.
Consider the above example of binary tree and it is represented as follows...

To represent a binary tree of depth 'n' using array representation, we need one dimensional
array with a maximum size of 2n+1 - 1.

Advantages The various merits of representing binary trees using arrays are as follows:
1. Any node can be accessed from any other node by calculating the index.
2. Here, the data is stored without any pointers to its successor or predecessor.
3. In the programming languages, where dynamic memory allocation is not possible
(such as BASIC, fortran ), array representation is the only means to store a tree.

Disadvantages The various demerits when representing binary trees using arrays are
as follows:
1. Other than full binary trees, majority of the array entries may be empty.
2. It allows only static representation. The array size cannot be changed during the
execution.
3. Inserting a new node to it or deleting a node from it is inefficient with this representation,
because it requires considerable data movement up and down the array, which demand
excessive amount of processing time.

2. Linked List Representation

We use double linked list to represent a binary tree. In a double linked list, every node
consists of three fields. First field for storing left child address, second for storing actual data
and third for storing right child address.
In this linked list representation, a node has the following structure...

The above example of binary tree represented using Linked list representation is shown as
follows...

// C++ program to create a Complete Binary tree from its Linked List
// Representation
#include <iostream>
#include <string>
#include <queue>
using namespace std;
// Linked list node
struct ListNode
{
int data;
ListNode* next;
};

// Binary tree node structure


struct BinaryTreeNode
{
int data;
BinaryTreeNode *left, *right;
};

// Function to insert a node at the beginning of the Linked List


void push(struct ListNode** head_ref, int new_data)
{
// allocate node and assign data
struct ListNode* new_node = new ListNode;
new_node->data = new_data;

// link the old list off the new node


new_node->next = (*head_ref);

// move the head to point to the new node


(*head_ref) = new_node;
}

// method to create a new binary tree node from the given data
BinaryTreeNode* newBinaryTreeNode(int data)
{
BinaryTreeNode *temp = new BinaryTreeNode;
temp->data = data;
temp->left = temp->right = NULL;
return temp;
}

// converts a given linked list representing a complete binary tree into the
// linked representation of binary tree.
void convertList2Binary(ListNode *head, BinaryTreeNode* &root)
{
// queue to store the parent nodes
queue<BinaryTreeNode *> q;

// Base Case
if (head == NULL)
{
root = NULL; // Note that root is passed by reference
return;
}

// 1.) The first node is always the root node, and add it to the queue
root = newBinaryTreeNode(head->data);
q.push(root);

// advance the pointer to the next node


head = head->next;
// until the end of linked list is reached, do the following steps
while (head)
{
// 2.a) take the parent node from the q and remove it from q
BinaryTreeNode* parent = q.front();
q.pop();

// 2.c) take next two nodes from the linked list. We will add
// them as children of the current parent node in step 2.b. Push them
// into the queue so that they will be parents to the future nodes
BinaryTreeNode *leftChild = NULL, *rightChild = NULL;
leftChild = newBinaryTreeNode(head->data);
q.push(leftChild);
head = head->next;
if (head)
{
rightChild = newBinaryTreeNode(head->data);
q.push(rightChild);
head = head->next;
}

// 2.b) assign the left and right children of parent


parent->left = leftChild;
parent->right = rightChild;
}
}

// Utility function to traverse the binary tree after conversion


void inorderTraversal(BinaryTreeNode* root)
{
if (root)
{
inorderTraversal( root->left );
cout << root->data << " ";
inorderTraversal( root->right );
}
}

// Driver program to test above functions


int main()
{
// create a linked list shown in above diagram
struct ListNode* head = NULL;
push(&head, 36); /* Last node of Linked List */
push(&head, 30);
push(&head, 25);
push(&head, 15);
push(&head, 12);
push(&head, 10); /* First node of Linked List */

BinaryTreeNode *root;
convertList2Binary(head, root);

cout << "Inorder Traversal of the constructed Binary Tree is: \n";
inorderTraversal(root);
return 0;
}
Run on IDE

Output:
Inorder Traversal of the constructed Binary Tree is:
25 12 30 10 36 15
3.7. Insert ion of a node in Binary tree

The insert() operation inserts a new node at any position in a binary tree. The node to
be inserted could be a branch node or a leaf node. The branch node insertion is generally
based on some criteria that are usually in the context of a special form of a binary tree.
Let us study a commonly used case of inserting a node as a leaf node.
The insertion procedure is a two-step process.
1. Search for the node whose child node is to be inserted. This is a node at some level i,
and a node is to be inserted at the level i 1 as either its left child or right child. This
is the node after which the insertion is to be made.
2. Link a new node to the node that becomes its parent node, that is, either the Lchild or
the Rchild.
3.8.Binary Tree Traversals

When we wanted to display a binary tree, we need to follow some order in which all the
nodes of that binary tree must be displayed. In any binary tree displaying order of nodes
depends on the traversal method.

Displaying (or) visiting order of nodes in a binary tree is called as Binary Tree Traversal.

There are three types of binary tree traversals.

1. In - Order Traversal

2. Pre - Order Traversal

3. Post - Order Traversal

Consider the following binary tree...

3.8.1. In - Order Traversal ( leftChild - root - rightChild )

In In-Order traversal, the root node is visited between left child and right child. In this
traversal, the left child node is visited first, then the root node is visited and later we go for
visiting right child node. This in-order traversal is applicable for every root node of all
subtrees in the tree. This is performed recursively for all nodes in the tree.

In the above example of binary tree, first we try to visit left child of root node 'A', but A's left
child is a root node for left subtree. so we try to visit its (B's) left child 'D' and again D is a
root for subtree with nodes D, I and J. So we try to visit its left child 'I' and it is the left most
child. So first we visit 'I' then go for its root node 'D' and later we visit D's right child 'J'. With
this we have completed the left part of node B. Then visit 'B' and next B's right child 'F' is
visited. With this we have completed left part of node A. Then visit root node 'A'. With this
we have completed left and root parts of node A. Then we go for right part of the node A. In
right of A again there is a subtree with root C. So go for left child of C and again it is a
subtree with root G. But G does not have left part so we visit 'G' and then visit G's right child
K. With this we have completed the left part of node C. Then visit root node 'C' and next visit
C's right child 'H' which is the right most child in the tree so we stop the process.
That means here we have visited in the order of I - D - J - B - F - A - G - K - C - H using In-
Order Traversal.

Inorder Traversal:
Algorithm Inorder(tree)
1. Traverse the left subtree, i.e., call Inorder(left-subtree)
2. Visit the root.
3. Traverse the right subtree, i.e., call Inorder(right-subtree)
In-Order Traversal for above example of binary tree is

I-D-J-B-F-A-G-K-C-H

3.8.2. Pre - Order Traversal ( root - leftChild - rightChild )

In Pre-Order traversal, the root node is visited before left child and right child nodes. In this
traversal, the root node is visited first, then its left child and later its right child. This pre-
order traversal is applicable for every root node of all subtrees in the tree.

In the above example of binary tree, first we visit root node 'A' then visit its left
child 'B' which is a root for D and F. So we visit B's left child 'D' and again D is a root for I and
J. So we visit D's left child 'I' which is the left most child. So next we go for visiting D's right
child 'J'. With this we have completed root, left and right parts of node D and root, left parts
of node B. Next visit B's right child 'F'. With this we have completed root and left parts of
node A. So we go for A's right child 'C' which is a root node for G and H. After visiting C, we
go for its left child 'G' which is a root for node K. So next we visit left of G, but it does not
have left child so we go for G's right child 'K'. With this we have completed node C's root
and left parts. Next visit C's right child 'H' which is the right most child in the tree. So we
stop the process.

That means here we have visited in the order of A-B-D-I-J-F-C-G-K-H using Pre-Order
Traversal.

Algorithm Preorder(tree)
1. Visit the root.
2. Traverse the left subtree, i.e., call Preorder(left-subtree)
3. Traverse the right subtree, i.e., call Preorder(right-subtree)
Pre-Order Traversal for above example of binary tree is

A-B-D-I-J-F-C-G-K-H
3.8.3. Post - Order Traversal ( leftChild - rightChild - root )

In Post-Order traversal, the root node is visited after left child and right child. In this
traversal, left child node is visited first, then its right child and then its root node. This is
recursively performed until the right most node is visited.

Here we have visited in the order of I - J - D - F - B - K - G - H - C - A using Post-Order


Traversal.

Postorder Traversal:
Algorithm Postorder(tree)
1. Traverse the left subtree, i.e., call Postorder(left-subtree)
2. Traverse the right subtree, i.e., call Postorder(right-subtree)
3. Visit the root.
Post-Order Traversal for above example of binary tree is

I-J-D-F-B-K-G-H-C-A

3.9. Depth First Traversal

Depth First Search (DFS) algorithm traverses a graph in a depthward motion and uses a
stack to remember to get the next vertex to start a search, when a dead end occurs in any
iteration.

As in the example given above, DFS algorithm traverses from A to B to C to D first then to E,
then to F and lastly to G. It employs the following rules.

 Rule 1 − Visit the adjacent unvisited vertex. Mark it as visited. Display it. Push it in a
stack.

 Rule 2 − If no adjacent vertex is found, pop up a vertex from the stack. (It will pop up
all the vertices from the stack, which do not have adjacent vertices.)
 Rule 3 − Repeat Rule 1 and Rule 2 until the stack is empty.

Step Traversal Description

Initialize the stack.

1.

Mark S as visited and put it onto the


stack. Explore any unvisited adjacent
node from S. We have three nodes
and we can pick any of them. For
this example, we shall take the node
2.
in an alphabetical order.

Mark A as visited and put it onto the


stack. Explore any unvisited adjacent
node from A. Both Sand D are
adjacent to A but we are concerned
for unvisited nodes only.
3.
Visit D and mark it as visited and put
onto the stack. Here, we
have B and C nodes, which are
adjacent to D and both are
unvisited. However, we shall again
4.
choose in an alphabetical order.

We choose B, mark it as visited and


put onto the stack. Here Bdoes not
have any unvisited adjacent node.
So, we pop Bfrom the stack.
5.

We check the stack top for return to


the previous node and check if it has
any unvisited nodes. Here, we
find D to be on the top of the stack.
6.
Only unvisited adjacent node is
from D is C now. So we visit C, mark
it as visited and put it onto the stack.

7.

As C does not have any unvisited adjacent node so we keep popping the stack until we find
a node that has an unvisited adjacent node. In this case, there's none and we keep popping
until the stack is empty.

#include<iostream>
#include<conio.h>
#include<stdlib.h>
using namespace std;
int cost[10][10],i,j,k,n,stk[10],top,v,visit[10],visited[10];

main()
{
int m;
cout <<"enterno of vertices";
cin >> n;
cout <<"ente no of edges";
cin >> m;
cout <<"\nEDGES \n";
for(k=1;k<=m;k++)
{
cin >>i>>j;
cost[i][j]=1;
}

cout <<"enter initial vertex";


cin >>v;
cout <<"ORDER OF VISITED VERTICES";
cout << v <<" ";
visited[v]=1;
k=1;
while(k<n)
{
for(j=n;j>=1;j--)
if(cost[v][j]!=0 && visited[j]!=1 && visit[j]!=1)
{
visit[j]=1;
stk[top]=j;
top++;
}
v=stk[--top];
cout<<v << " ";
k++;
visit[v]=0; visited[v]=1;
}
}

OUTPUT
enterno of vertices9
ente no of edges9
EDGES
12
23
26
15
14
47
57
78
89
enter initial vertex1
ORDER OF VISITED VERTICES1 2 3 6 4 7 8 9 5

3.10. Breadth First Traversal

Breadth First Search (BFS) algorithm traverses a graph in a breadthward motion and uses a
queue to remember to get the next vertex to start a search, when a dead end occurs in any
iteration.
As in the example given above, BFS algorithm traverses from A to B to E to F first then to C
and G lastly to D. It employs the following rules.

 Rule 1 − Visit the adjacent unvisited vertex. Mark it as visited. Display it. Insert it in a
queue.

 Rule 2 − If no adjacent vertex is found, remove the first vertex from the queue.

 Rule 3 − Repeat Rule 1 and Rule 2 until the queue is empty.

Step Traversal Description

Initialize the queue.

1.

We start from visiting S(starting


node), and mark it as visited.

2.

We then see an unvisited adjacent


node from S. In this example, we
have three nodes but alphabetically
we choose A, mark it as visited and
3. enqueue it.
Next, the unvisited adjacent node
from S is B. We mark it as visited
and enqueue it.

4.

Next, the unvisited adjacent node


from S is C. We mark it as visited and
enqueue it.

5.

Now, S is left with no unvisited


adjacent nodes. So, we dequeue and
find A.

6.

From A we have D as unvisited


adjacent node. We mark it as visited
and enqueue it.

7.

At this stage, we are left with no unmarked (unvisited) nodes. But as per the algorithm we
keep on dequeuing in order to get all unvisited nodes. When the queue gets emptied, the
program is over.
#include<iostream>
#include<conio.h>
#include<stdlib.h>
using namespace std;
int cost[10][10],i,j,k,n,qu[10],front,rare,v,visit[10],visited[10];

main()
{
int m;
cout <<"enterno of vertices";
cin >> n;
cout <<"ente no of edges";
cin >> m;
cout <<"\nEDGES \n";
for(k=1;k<=m;k++)
{
cin >>i>>j;
cost[i][j]=1;
}

cout <<"enter initial vertex";


cin >>v;
cout <<"Visitied vertices\n";
cout << v;
visited[v]=1;
k=1;
while(k<n)
{
for(j=1;j<=n;j++)
if(cost[v][j]!=0 && visited[j]!=1 && visit[j]!=1)
{
visit[j]=1;
qu[rare++]=j;
}
v=qu[front++];
cout<<v << " ";
k++;
visit[v]=0; visited[v]=1;
}
}

OUTPUT
enterno of vertices9
ente no of edges9
EDGES
12
23
15
14
47
78
89
26
57
enter initial vertex1
Visited vertices
12 4 5 3 6 7 8 9
3.11. Other Tree Operations
Using traversal as a basic operation, many other operations can be performed on a tree,
such as fi nding the height of the tree, computing the total number of nodes, leaf nodes,
and so on. Let us study a few of such operations.

3.11. 1 counting nodes


CountNode() is the function that returns the total count of nodes in a linked binary
tree.
int BinaryTree :: CountNode(TreeNode *Root)
{
if(Root == Null)
return 0;
else
return(1 + CountNode(Root->Rchild) + CountNode(Root->Lchild));
}

3.11.2 counting leaf nodes


The CountLeaf() operation counts the total number of leaf nodes in a linked binary tree.
Leaf nodes are those with no left or right children.
int BinaryTree :: CountLeaf(TreeNode *Root)
{
if(Root == Null)
return 0;
else if((Root->Rchild == Null) && (Root->Lchild == Null))
return(1);
else
return(CountLeaf(Root->Lchild) + CountLeaf(Root->Rchild));
}

3.11.3 Computing Height of Binary Tree


The TreeHeight() operation computes the height of a linked binary tree. Height of a
tree is the maximum path length in the tree. We can get the path length by traversing the
tree depthwise. Let us consider that an empty tree’s height is 0 and the tree with only one
node has the height 1.
int BinaryTree :: TreeHeight(TreeNode *Root)
{
int heightL, heightR;
if(Root == Null)
return 0;
if(Root->Lchild == Null && Root->Rchild == Null)
return 0;
heightL = TreeHeight(Root->Lchild);
heightR = TreeHeight(Root->Rchild);
if(heightR > heightL)
return(heightR + 1);
return(heightL + 1);
}

3.11. 4 Getting Mirror, Replica, or Tree Interchange of Binary Tree


The Mirror() operation finds the mirror of the tree that will interchange all left and right
subtrees in a linked binary tree.
void BinaryTree :: Mirror(TreeNode *Root)
{
TreeNode *Tmp;
if(Root != Null)
{
Tmp = Root->Lchild;
Root->Lchild = Root->Rchild;
Root->Rchild = Tmp;
Mirror(Root->Lchild);
Mirror(Root->Rchild);
}
}

3.11.5 Copying Binary Tree


The TreeCopy() operation makes a copy of the linked binary tree. The function should
allocate the necessary nodes and copy the respective contents into them.
TreeNode *BinaryTree :: TreeCopy()
{
TreeNode *Tmp;
if(Root == Null)
return Null;
Tmp = new TreeNode;
Tmp Lchild = TreeCopy(Root Lchild);
Tmp Rchild = TreeCopy(Root Rchild);
Tmp Data = Root Data;
return Tmp;
}

3.11.6 Equality Test


The BTree_Equal() operation checks whether two binary trees are equal. Two trees are
said to be equal if they have the same topology, and all the corresponding nodes are equal.
The same topology refers to the fact that each branch in the first tree corresponds to a
branch in the second tree in the same order and vice versa.
int BinaryTree :: BTree_Equal(Binarytree T1 , BinaryTree T2)
{
if(Root == Null && T2.Root == Null)
return 1;
return(Root && T2.Root);
&&(Root->Data == T2.Root->Data);
&&BTree_Equal(Root->Lchild ,T2.Root->Lchild);
&&BTree_Equal(Root->Rchild, T2.Root->Rchild));
}

3.12. Binary Search Tree


A Binary Search Tree (BST) is a tree in which all the nodes follow the below-mentioned
properties −

 The left sub-tree of a node has a key less than or equal to its parent node's key.

 The right sub-tree of a node has a key greater than or equal to its parent node's key.

Thus, BST divides all its sub-trees into two segments; the left sub-tree and the right sub-
tree and can be defined as −

left_subtree (keys) ≤ node (key) ≤ right_subtree (keys)

3.12.1.Representation
BST is a collection of nodes arranged in a way where they maintain BST properties. Each
node has a key and an associated value. While searching, the desired key is compared to
the keys in BST and if found, the associated value is retrieved.

Following is a pictorial representation of BST −

We observe that the root node key (27) has all less-valued keys on the left sub-tree and the
higher valued keys on the right sub-tree.

In a binary search tree, all the nodes in left subtree of any node contains smaller values and
all the nodes in right subtree of that contains larger values as shown in following figure...
Example

The following tree is a Binary Search Tree. In this tree, left subtree of every node contains
nodes with smaller values and right subtree of every node contains larger values.

Every Binary Search Tree is a binary tree but all the Binary Trees need not to be binary
search trees.

3.12.2. Operations on a Binary Search Tree

The following operations are performed on a binary earch tree...

1. Search

2. Insertion

3. Deletion

3.12.2.1.Search Operation in BST

In a binary search tree, the search operation is performed with O(log n) time complexity.
The search operation is performed as follows...

 Step 1: Read the search element from the user


 Step 2: Compare, the search element with the value of root node in the tree.
 Step 3: If both are matching, then display "Given node found!!!" and terminate the
function
 Step 4: If both are not matching, then check whether search element is smaller or
larger than that node value.
 Step 5: If search element is smaller, then continue the search process in left subtree.
 Step 6: If search element is larger, then continue the search process in right subtree.
 Step 7: Repeat the same until we found exact element or we completed with a leaf
node
 Step 8: If we reach to the node with search value, then display "Element is found"
and terminate the function.
 Step 9: If we reach to a leaf node and it is also not matching, then display "Element
not found" and terminate the function.

Algorithm
struct node* search(int data){
struct node *current = root;
printf("Visiting elements: ");
while(current->data != data){
if(current != NULL) {
printf("%d ",current->data);
//go to left tree
if(current->data > data){
current = current->leftChild;
}//else go to right tree
else {
current = current->rightChild;
}
//not found
if(current == NULL){
return NULL;
}
}
}
return current;
}
3.12.2.2. Insertion Operation in BST

In a binary search tree, the insertion operation is performed with O(log n) time complexity.
In binary search tree, new node is always inserted as a leaf node. The insertion operation is
performed as follows...

Step 1: Create a newNode with given value and set its left and right to NULL.
Step 2: Check whether tree is Empty.
Step 3: If the tree is Empty, then set set root to newNode.
Step 4: If the tree is Not Empty, then check whether value of newNode
is smaller or larger than the node (here it is root node).
Step 5: If newNode is smaller than or equal to the node, then move to its left child. If
newNode is larger than the node, then move to its right child.
Step 6: Repeat the above step until we reach to a leaf node (e.i., reach to NULL).
Step 7: After reaching a leaf node, then isert the newNode as left child if newNode
is smaller or equal to that leaf else insert it as right child.
void insert(int data) {
struct node *tempNode = (struct node*) malloc(sizeof(struct node));
struct node *current;
struct node *parent;

tempNode->data = data;
tempNode->leftChild = NULL;
tempNode->rightChild = NULL;

//if tree is empty


if(root == NULL) {
root = tempNode;
} else {
current = root;
parent = NULL;

while(1) {
parent = current;

//go to left of the tree


if(data < parent->data) {
current = current->leftChild;
//insert to the left

if(current == NULL) {
parent->leftChild = tempNode;
return;
}
}//go to right of the tree
else {
current = current->rightChild;
//insert to the right
if(current == NULL) {
parent->rightChild = tempNode;
return;
}
}
}
}
}

3.12.2.3. Deletion Operation in BST

In a binary search tree, the deletion operation is performed with O(log n) time complexity.
Deleting a node from Binary search tree has follwing three cases...

• Case 1: Deleting a Leaf node (A node with no children)


• Case 2: Deleting a node with one child
• Case 3: Deleting a node with two children

Case 1: Deleting a leaf node

We use the following steps to delete a leaf node from BST...

 Step 1: Find the node to be deleted using search operation


 Step 2: Delete the node using free function (If it is a leaf) and terminate the function.

Case 2: Deleting a node with one child

We use the following steps to delete a node with one child from BST...

 Step 1: Find the node to be deleted using search operation


 Step 2: If it has only one child, then create a link between its parent and child nodes.
 Step 3: Delete the node using free function and terminate the function.

Case 3: Deleting a node with two children

We use the following steps to delete a node with two children from BST...

 Step 1: Find the node to be deleted using search operation


 Step 2: If it has two children, then find the largest node in its left subtree (OR)
the smallest node in its right subtree.
 Step 3: Swap both deleting node and node which found in above step.
 Step 4: Then, check whether deleting node came to case 1 or case 2 else goto steps
2
 Step 5: If it comes to case 1, then delete using case 1 logic.
 Step 6: If it comes to case 2, then delete using case 2 logic.
 Step 7: Repeat the same process until node is deleted from the tree.

Example

Construct a Binary Search Tree by inserting the following sequence of numbers...

10,12,5,4,20,8,7,15 and 13

Above elements are inserted into a Binary Search Tree as follows...

/*
* C++ Program To Implement BST
*/
# include <iostream>
# include <cstdlib>
using namespace std;
/*
* Node Declaration
*/
struct node
{
int info;
struct node *left;
struct node *right;
}*root;

/*
* Class Declaration
*/
class BST
{
public:
void find(int, node **, node **);
void insert(int);
void del(int);
void case_a(node *,node *);
void case_b(node *,node *);
void case_c(node *,node *);
void preorder(node *);
void inorder(node *);
void postorder(node *);
void display(node *, int);
BST()
{
root = NULL;
}
};
/*
* Main Contains Menu
*/
int main()
{
int choice, num;
BST bst;
node *temp;
while (1)
{
cout<<"-----------------"<<endl;
cout<<"Operations on BST"<<endl;
cout<<"-----------------"<<endl;
cout<<"1.Insert Element "<<endl;
cout<<"2.Delete Element "<<endl;
cout<<"3.Inorder Traversal"<<endl;
cout<<"4.Preorder Traversal"<<endl;
cout<<"5.Postorder Traversal"<<endl;
cout<<"6.Display"<<endl;
cout<<"7.Quit"<<endl;
cout<<"Enter your choice : ";
cin>>choice;
switch(choice)
{
case 1:
temp = new node;
cout<<"Enter the number to be inserted : ";
cin>>temp->info;
bst.insert(root, temp);
case 2:
if (root == NULL)
{
cout<<"Tree is empty, nothing to delete"<<endl;
continue;
}
cout<<"Enter the number to be deleted : ";
cin>>num;
bst.del(num);
break;
case 3:
cout<<"Inorder Traversal of BST:"<<endl;
bst.inorder(root);
cout<<endl;
break;
case 4:
cout<<"Preorder Traversal of BST:"<<endl;
bst.preorder(root);
cout<<endl;
break;
case 5:
cout<<"Postorder Traversal of BST:"<<endl;
bst.postorder(root);
cout<<endl;
break;
case 6:
cout<<"Display BST:"<<endl;
bst.display(root,1);
cout<<endl;
break;
case 7:
exit(1);
default:
cout<<"Wrong choice"<<endl;
}
}
}

/*
* Find Element in the Tree
*/
void BST::find(int item, node **par, node **loc)
{
node *ptr, *ptrsave;
if (root == NULL)
{
*loc = NULL;
*par = NULL;
return;
}
if (item == root->info)
{
*loc = root;
*par = NULL;
return;
}
if (item < root->info)
ptr = root->left;
else
ptr = root->right;
ptrsave = root;
while (ptr != NULL)
{
if (item == ptr->info)
{
*loc = ptr;
*par = ptrsave;
return;
}
ptrsave = ptr;
if (item < ptr->info)
ptr = ptr->left;
else
ptr = ptr->right;
}
*loc = NULL;
*par = ptrsave;
}

/*
* Inserting Element into the Tree
*/
void BST::insert(node *tree, node *newnode)
{
if (root == NULL)
{
root = new node;
root->info = newnode->info;
root->left = NULL;
root->right = NULL;
cout<<"Root Node is Added"<<endl;
return;
}
if (tree->info == newnode->info)
{
cout<<"Element already in the tree"<<endl;
return;
}
if (tree->info > newnode->info)
{
if (tree->left != NULL)
{
insert(tree->left, newnode);
}
else
{
tree->left = newnode;
(tree->left)->left = NULL;
(tree->left)->right = NULL;
cout<<"Node Added To Left"<<endl;
return;
}
}
else
{
if (tree->right != NULL)
{
insert(tree->right, newnode);
}
else
{
tree->right = newnode;
(tree->right)->left = NULL;
(tree->right)->right = NULL;
cout<<"Node Added To Right"<<endl;
return;
}
}
}

/*
* Delete Element from the tree
*/
void BST::del(int item)
{
node *parent, *location;
if (root == NULL)
{
cout<<"Tree empty"<<endl;
return;
}
find(item, &parent, &location);
if (location == NULL)
{
cout<<"Item not present in tree"<<endl;
return;
}
if (location->left == NULL && location->right == NULL)
case_a(parent, location);
if (location->left != NULL && location->right == NULL)
case_b(parent, location);
if (location->left == NULL && location->right != NULL)
case_b(parent, location);
if (location->left != NULL && location->right != NULL)
case_c(parent, location);
free(location);
}

/*
* Case A
*/
void BST::case_a(node *par, node *loc )
{
if (par == NULL)
{
root = NULL;
}
else
{
if (loc == par->left)
par->left = NULL;
else
par->right = NULL;
}
}
/*
* Case B
*/
void BST::case_b(node *par, node *loc)
{
node *child;
if (loc->left != NULL)
child = loc->left;
else
child = loc->right;
if (par == NULL)
{
root = child;
}
else
{
if (loc == par->left)
par->left = child;
else
par->right = child;
}
}

/*
* Case C
*/
void BST::case_c(node *par, node *loc)
{
node *ptr, *ptrsave, *suc, *parsuc;
ptrsave = loc;
ptr = loc->right;
while (ptr->left != NULL)
{
ptrsave = ptr;
ptr = ptr->left;
}
suc = ptr;
parsuc = ptrsave;
if (suc->left == NULL && suc->right == NULL)
case_a(parsuc, suc);
else
case_b(parsuc, suc);
if (par == NULL)
{
root = suc;
}
else
{
if (loc == par->left)
par->left = suc;
else
par->right = suc;
}
suc->left = loc->left;
suc->right = loc->right;
}

/*
* Pre Order Traversal
*/
void BST::preorder(node *ptr)
{
if (root == NULL)
{
cout<<"Tree is empty"<<endl;
return;
}
if (ptr != NULL)
{
cout<<ptr->info<<" ";
preorder(ptr->left);
preorder(ptr->right);
}
}
/*
* In Order Traversal
*/
void BST::inorder(node *ptr)
{
if (root == NULL)
{
cout<<"Tree is empty"<<endl;
return;
}
if (ptr != NULL)
{
inorder(ptr->left);
cout<<ptr->info<<" ";
inorder(ptr->right);
}
}

/*
* Postorder Traversal
*/
void BST::postorder(node *ptr)
{
if (root == NULL)
{
cout<<"Tree is empty"<<endl;
return;
}
if (ptr != NULL)
{
postorder(ptr->left);
postorder(ptr->right);
cout<<ptr->info<<" ";
}
}

/*
* Display Tree Structure
*/
void BST::display(node *ptr, int level)
{
int i;
if (ptr != NULL)
{
display(ptr->right, level+1);
cout<<endl;
if (ptr == root)
cout<<"Root->: ";
else
{
for (i = 0;i < level;i++)
cout<<" ";
}
cout<<ptr->info;
display(ptr->left, level+1);
}
}
OUTPUT
-----------------
Operations on BST
-----------------
1.Insert Element
2.Delete Element
3.Inorder Traversal
4.Preorder Traversal
5.Postorder Traversal
6.Display
7.Quit
Enter your choice : 1
Enter the number to be inserted : 8
Root Node is Added
-----------------
Operations on BST
-----------------
1.Insert Element
2.Delete Element
3.Inorder Traversal
4.Preorder Traversal
5.Postorder Traversal
6.Display
7.Quit
Enter your choice : 6
Display BST:

Root->: 8
-----------------
Operations on BST
-----------------
1.Insert Element
2.Delete Element
3.Inorder Traversal
4.Preorder Traversal
5.Postorder Traversal
6.Display
7.Quit
Enter your choice : 1
Enter the number to be inserted : 9
Node Added To Right
-----------------
Operations on BST
-----------------
1.Insert Element
2.Delete Element
3.Inorder Traversal
4.Preorder Traversal
5.Postorder Traversal
6.Display
7.Quit
Enter your choice : 6
Display BST:

9
Root->: 8
-----------------
Operations on BST
-----------------
1.Insert Element
2.Delete Element
3.Inorder Traversal
4.Preorder Traversal
5.Postorder Traversal
6.Display
7.Quit
Enter your choice : 1
Enter the number to be inserted : 5
Node Added To Left
-----------------
Operations on BST
-----------------
1.Insert Element
2.Delete Element
3.Inorder Traversal
4.Preorder Traversal
5.Postorder Traversal
6.Display
7.Quit
Enter your choice : 6
Display BST:

9
Root->: 8
5

3.13. Threaded Binary Tree

A binary tree is represented using array representation or linked list representation. When a
binary tree is represented using linked list representation, if any node is not having a child
we use NULL pointer in that position. In any binary tree linked list representation, there are
more number of NULL pointer than actual pointers. Generally, in any binary tree linked list
representation, if there are 2N number of reference fields, then N+1 number of reference
fields are filled with NULL ( N+1 are NULL out of 2N ). This NULL pointer does not play any
role except indicating there is no link (no child).

A. J. Perlis and C. Thornton have proposed new binary tree called "Threaded Binary Tree",
which make use of NULL pointer to improve its traversal processes. In threaded binary tree,
NULL pointers are replaced by references to other nodes in the tree, called threads.

Threaded Binary Tree is also a binary tree in which all left child pointers that are NULL (in
Linked list representation) points to its in-order predecessor, and all right child pointers
that are NULL in Linked list representation) points to its in-order successo
If there is no in-order predecessor or in-order successor, then it point to root node.

Consider the following binary tree...

To convert above binary tree into threaded binary tree, first find the in-order traversal of
that tree...

In-order traversal of above binary tree...

H-D-I-B-E-A-F-J-C-G

When we represent above binary tree using linked list representation, nodes H, I, E, F,
J and G left child pointers are NULL. This NULL is replaced by address of its in-order
predecessor, respectively (I to D, E to B, F to A, J to F and G to C), but here the node H does
not have its in-order predecessor, so it points to the root node A. And nodes H, I, E,
J and G right child pointers are NULL. This NULL ponters are replaced by address of its in-
order successor, respectively (H to D, I to B, E to A, and J to C), but here the node G does not
have its in-order successor, so it points to the root node A.

Above example binary tree become as follows after converting into threaded binary tree.

In above figure threadeds are indicated with dotted links.

/*
* C++ Program to Implement Threaded Binary Tree
*/
#include <iostream>
#include <cstdlib>
#define MAX_VALUE 65536
using namespace std;

/* Class Node */

class Node
{
public:
int key;
Node *left, *right;
bool leftThread, rightThread;
};

/* Class ThreadedBinarySearchTree */

class ThreadedBinarySearchTree
{
private:
Node *root;
public:
/* Constructor */
ThreadedBinarySearchTree()
{
root = new Node();
root->right = root->left = root;
root->leftThread = true;
root->key = MAX_VALUE;
}

/* Function to clear tree */


void makeEmpty()
{
root = new Node();
root->right = root->left = root;
root->leftThread = true;
root->key = MAX_VALUE;
}

/* Function to insert a key */


void insert(int key)
{
Node *p = root;
for (;;)
{
if (p->key < key)
{
if (p->rightThread)
break;
p = p->right;
}
else if (p->key > key)
{
if (p->leftThread)
break;
p = p->left;
}
else
{
/* redundant key */
return;
}
}
Node *tmp = new Node();
tmp->key = key;
tmp->rightThread = tmp->leftThread = true;
if (p->key < key)
{
/* insert to right side */
tmp->right = p->right;
tmp->left = p;
p->right = tmp;
p->rightThread = false;
}
else
{
tmp->right = p;
tmp->left = p->left;
p->left = tmp;
p->leftThread = false;
}
}

/* Function to search for an element */


bool search(int key)
{
Node *tmp = root->left;
for (;;)
{
if (tmp->key < key)
{
if (tmp->rightThread)
return false;
tmp = tmp->right;
}
else if (tmp->key > key)
{
if (tmp->leftThread)
return false;
tmp = tmp->left;
}
else
{
return true;
}
}
}

/* Fuction to delete an element */


void Delete(int key)
{
Node *dest = root->left, *p = root;
for (;;)
{
if (dest->key < key)
{
/* not found */
if (dest->rightThread)
return;
p = dest;
dest = dest->right;
}
else if (dest->key > key)
{
/* not found */
if (dest->leftThread)
return;
p = dest;
dest = dest->left;
}
else
{
/* found */
break;
}
}
Node *target = dest;
if (!dest->rightThread && !dest->leftThread)
{
/* dest has two children*/
p = dest;
/* find largest node at left child */
target = dest->left;
while (!target->rightThread)
{
p = target;
target = target->right;
}
/* using replace mode*/
dest->key = target->key;
}
if (p->key >= target->key)
{
if (target->rightThread && target->leftThread)
{
p->left = target->left;
p->leftThread = true;
}
else if (target->rightThread)
{
Node *largest = target->left;
while (!largest->rightThread)
{
largest = largest->right;
}
largest->right = p;
p->left = target->left;
}
else
{
Node *smallest = target->right;
while (!smallest->leftThread)
{
smallest = smallest->left;
}
smallest->left = target->left;
p->left = target->right;
}
}
else
{
if (target->rightThread && target->leftThread)
{
p->right = target->right;
p->rightThread = true;
}
else if (target->rightThread)
{
Node *largest = target->left;
while (!largest->rightThread)
{
largest = largest->right;
}
largest->right = target->right;
p->right = target->left;
}
else
{
Node *smallest = target->right;
while (!smallest->leftThread)
{
smallest = smallest->left;
}
smallest->left = p;
p->right = target->right;
}
}
}

/* Function to print tree */


void printTree()
{
Node *tmp = root, *p;
for (;;)
{
p = tmp;
tmp = tmp->right;
if (!p->rightThread)
{
while (!tmp->leftThread)
{
tmp = tmp->left;
}
}
if (tmp == root)
break;
cout<<tmp->key<<" ";
}
cout<<endl;
}
};

/* Main Contains Menu */


int main()
{
ThreadedBinarySearchTree tbst;
cout<<"ThreadedBinarySearchTree Test\n";
char ch;
int choice, val;
/* Perform tree operations */
do
{
cout<<"\nThreadedBinarySearchTree Operations\n";
cout<<"1. Insert "<<endl;
cout<<"2. Delete"<<endl;
cout<<"3. Search"<<endl;
cout<<"4. Clear"<<endl;
cout<<"Enter Your Choice: ";
cin>>choice;
switch (choice)
{
case 1 :
cout<<"Enter integer element to insert: ";
cin>>val;
tbst.insert(val);
break;
case 2 :
cout<<"Enter integer element to delete: ";
cin>>val;
tbst.Delete(val);
break;
case 3 :
cout<<"Enter integer element to search: ";
cin>>val;
if (tbst.search(val) == true)
cout<<"Element "<<val<<" found in the tree"<<endl;
else
cout<<"Element "<<val<<" not found in the tree"<<endl;
break;
case 4 :
cout<<"\nTree Cleared\n";
tbst.makeEmpty();
break;
default :
cout<<"Wrong Entry \n ";
break;
}
/* Display tree */
cout<<"\nTree = ";
tbst.printTree();
cout<<"\nDo you want to continue (Type y or n): ";
cin>>ch;
}
while (ch == 'Y'|| ch == 'y');
return 0;
}

OUTPUT
ThreadedBinarySearchTree Test

ThreadedBinarySearchTree Operations
1. Insert
2. Delete
3. Search
4. Clear
Enter Your Choice: 1
Enter integer element to insert: 28

Tree = 28

Do you want to continue (Type y or n): y

ThreadedBinarySearchTree Operations
1. Insert
2. Delete
3. Search
4. Clear
Enter Your Choice: 1
Enter integer element to insert: 5

Tree = 5 28

Do you want to continue (Type y or n):


y

ThreadedBinarySearchTree Operations
1. Insert
2. Delete
3. Search
4. Clear
Enter Your Choice: 1
Enter integer element to insert: 19

Tree = 5 19 28

Do you want to continue (Type y or n): y


3.14. Applications of binary trees
3.14.1. Expression Tree
Expression tree is a binary tree in which each internal node corresponds to operator and
each leaf node corresponds to operand so for example expression tree for 3 + ((5+9)*2)
would be:

Inorder traversal of expression tree produces infix version of given postfix expression (same
with preorder traversal it gives prefix expression)
Evaluating the expression represented by expression tree:
Let t be the expression tree
If t is not null then
If t.value is operand then
Return t.value
A = solve(t.left)
B = solve(t.right)
// calculate applies operator 't.value'
// on A and B, and returns value
Return calculate(A, B, t.value)
Construction of Expression Tree:
Now For constructing expression tree we use a stack. We loop through input expression and
do following for every character.
1) If character is operand push that into stack
2) If character is operator pop two values from stack make them its child and push current
node again.
At the end only element of stack will be root of expression tree.

3.14.2.Decision tree
A decision tree is a flowchart-like structure in which each internal node represents a "test"
on an attribute (e.g. whether a coin flip comes up heads or tails), each branch represents
the outcome of the test and each leaf node represents a class label (decision taken after
computing all attributes). The paths from root to leaf represents classification rules.
In decision analysis a decision tree and the closely related influence diagram are used as a
visual and analytical decision support tool, where the expected values (or expected utility)
of competing alternatives are calculated.
A decision tree consists of 3 types of nodes:

1. Decision nodes - commonly represented by squares


2. Chance nodes - represented by circles
3. End nodes - represented by triangles
Decision trees are commonly used in operations research and operations management. If in
practice decisions have to be taken online with no recall under incomplete knowledge, a
decision tree should be paralleled by a probability model as a best choice model or online
selection model algorithm. Another use of decision trees is as a descriptive means for
calculating conditional probabilities.
Decision trees, influence diagrams, utility functions, and other decision analysis tools and
methods are taught to undergraduate students in schools of business, health economics,
and public health, and are examples of operations research or management
science methods.

3.15. Sorting and Searching

3.15.0. What is Search?

Search is a process of finding a value in a list of values. In other words, searching is the
process of locating given value position in a list of values.

Searching can be done on internal data structures or on external data structures.


Information retrieval in the required format is the central activity in all computer
applications. This involves searching. This block deals with searching techniques. Searching
methods are designed to take advantage of the file organisation and optimize the search for
a particular record or to establish its absence. The file organisation and searching method
chosen can make a substantial difference to an application’s performance.

3.15.1. Basics Searching Techniques

Consider a list of n elements or can represent a file of n records, where each element is a
key / number. The task is to find a particular key in the list in the shortest possible time. If
you know you are going to search for an item in a set, you will need to think carefully about
what type of data structure you will use for that set. At low level, the only searches that get
mentioned are for sorted and unsorted arrays. However, these are not the only data types
that are useful for searching.

searching.

1. Linear search
Start at the beginning of the list and check every element of the list. Very slow [order O(n) ]
but works on an unsorted list.

2. Binary Search

This is used for searching in a sorted array. Test the middle element of the array. If it is too
big. Repeat the process in the left half of the array, and the right half if it’s too small. In this
way, the amount of space that needs to be searched is halved every time, so the time is
O(log n). .

3. Hash Search

Searching a hash table is easy and extremely fast, just find the hash value for the item
you’re looking for then go to that index and start searching the array until you find what you
are looking for or you hit a blank spot. The order is pretty close to o(1), depending on how
full your hash table is.

4. Binary Tree search

Search a binary tree is just as easy as searching a hash table, but it is usually slower
(especially if the tree is badly unbalanced). Just start at the root. Then go down the left
subtree if the root is too big and the right subtree if is too small. Repeat until you find what
you want or the sub-tree you want isn’t there. The running time is O(log n) on average and
O(n) in the worst case.

searching.

1. Sequential/ Linear search

2. Binary search

3. Fibonacci search

4. Index sequential search

5. Hashed search

3.15.1.1.Linear Search

Linear search algorithm finds given element in a list of elements with O(n) time complexity
where n is total number of elements in the list. This search process starts comparing of
search element with the first element in the list. If both are matching then results with
element found otherwise search element is compared with next element in the list. If both
are matched, then the result is "element found". Otherwise, repeat the same with the next
element in the list until search element is compared with last element in the list, if that last
element also doesn't match, then the result is "Element not found in the list". That means,
the search element is compared with element by element in the list.
Linear search is implemented using following steps...

 Step 1: Read the search element from the user


 Step 2: Compare, the search element with the first element in the list.
 Step 3: If both are matching, then display "Given element found!!!" and terminate
the function
 Step 4: If both are not matching, then compare search element with the next
element in the list.
 Step 5: Repeat steps 3 and 4 until the search element is compared with the last
element in the list.
 Step 6: If the last element in the list is also doesn't match, then display "Element not
found!!!" and terminate the function.

Example

Consider the following list of element and search element...


Implementation of Linear search
#include<iostream>

using namespace std;

int main()
{
int a[20],n,x,i,flag=0;
cout<<"How many elements?";
cin>>n;
cout<<"\nEnter elements of the array\n";

for(i=0;i<n;++i)
cin>>a[i];

cout<<"\nEnter element to search:";


cin>>x;

for(i=0;i<n;++i)
{
if(a[i]==x)
{
flag=1;
break;
}
}

if(flag)
cout<<"\nElement is found at position "<<i+1;
else
cout<<"\nElement not found";

return 0;
}

Output
How many elements?4
Enter elements of the array
5 9 12 4
Enter element to search:9
Element is found at position 2
3.15.1.2. Binary Search Algorithm

Binary search algorithm finds given element in a list of elements with O(log n) time
complexity where n is total number of elements in the list. The binary search algorithm can
be used with only sorted list of element. That means, binary search can be used only with
lkist of element which are already arraged in a order. The binary search can not be used for
list of element which are in random order. This search process starts comparing of the
search element with the middle element in the list. If both are matched, then the result is
"element found". Otherwise, we check whether the search element is smaller or larger than
the middle element in the list. If the search element is smaller, then we repeat the same
process for left sublist of the middle element. If the search element is larger, then we repeat
the same process for right sublist of the middle element. We repeat this process until we
find the search element in the list or until we left with a sublist of only one element. And if
that element also doesn't match with the search element, then the result is "Element not
found in the list".

Binary search is implemented using following steps...

 Step 1: Read the search element from the user


 Step 2: Find the middle element in the sorted list
 Step 3: Compare, the search element with the middle element in the sorted list.
 Step 4: If both are matching, then display "Given element found!!!" and terminate
the function
 Step 5: If both are not matching, then check whether the search element is smaller
or larger than middle element.
 Step 6: If the search element is smaller than middle element, then repeat steps 2, 3,
4 and 5 for the left sublist of the middle element.
 Step 7: If the search element is larger than middle element, then repeat steps 2, 3, 4
and 5 for the right sublist of the middle element.
 Step 8: Repeat the same process until we find the search element in the list or until
sublist contains only one element.
 Step 9: If that element also doesn't match with the search element, then display
"Element not found in the list!!!" and terminate the function.

Example

Consider the following list of element and search element...


Binary Search implementation
#include<iostream>

using namespace std;

int main()
{
int search(int [],int,int);
int n,i,a[100],e,res;
cout<<"How Many Elements:";
cin>>n;
cout<<"\nEnter Elements of Array in Ascending order\n";

for(i=0;i<n;++i)
{
cin>>a[i];
}

cout<<"\nEnter element to search:";


cin>>e;

res=search(a,n,e);

if(res!=0)
cout<<"\nElement found at position "<<res+1;
else
cout<<"\nElement is not found....!!!";

return 0;
}

int search(int a[],int n,int e)


{
int f,l,m;
f=0;
l=n-1;

while(f<=l)
{
m=(f+l)/2;
if(e==a[m])
return(m);
else
if(e>a[m])
f=m+1;
else
l=m-1;
}

return 0;
}

Output
How Many Elements:5
Enter Elements of Array in Ascending order
12 39 40 68 77
Enter element to search:40
Element found at position 3

3.15.1.3. Fibonacci Searching :

The Fibonacci search technique is a method of searching a sorted array using a divide and
conquer algorithm that narrows down possible locations with the aid of Fibonacci numbers.
Compared to binary search where the sorted array is divided into two arrays which are both
examined recursively and recombined, Fibonacci search only examines locations whose
addresses have lower dispersion. Therefore, when the elements being searched have non-
uniform access memory storage (i.e., the time needed to access a storage location varies
depending on the location accessed), the Fibonacci search has an advantage over binary
search in slightly reducing the average time needed to access a storage location. Fibonacci
search has a complexity of O(log(n)) (see Big O notation).

Algorithm
Let k be defined as an element in F, the array of Fibonacci numbers. n = Fm is the array size.
If n is not a Fibonacci number, let Fm be the smallest number in F that is greater than n.
The array of Fibonacci numbers is defined where Fk+2 = Fk+1 + Fk, when k ≥ 0, F1 = 1,
and F0 = 0.
To test whether an item is in the list of ordered numbers, follow these steps:

1. Set k = m.
2. If k = 0, stop. There is no match; the item is not in the array.
3. Compare the item against element in Fk−1.
4. If the item matches, stop.
5. If the item is less than entry Fk−1, discard the elements from positions Fk−1 + 1 to n.
Set k = k − 1 and return to step 2.
6. If the item is greater than entry Fk−1, discard the elements from positions 1 to Fk−1.
Renumber the remaining elements from 1 to Fk−2, set k = k − 2, and return to step 2.
Alternative implementation (from "Sorting and Searching" by Knuth):
Given a table of records R1, R2, ..., RN whose keys are in increasing order K1 < K2 < ... < KN, the
algorithm searches for a given argument K. Assume N+1 = Fk+1
Step 1. [Initialize] i ← Fk, p ← Fk-1, q ← Fk-2 (throughout the algorithm, p and q will be
consecutive Fibonacci numbers)
Step 2. [Compare] If K < Ki, go to Step 3; if K > Ki go to Step 4; and if K = Ki, the algorithm
terminates successfully.
Step 3. [Decrease i] If q=0, the algorithm terminates unsuccessfully. Otherwise set (i, p, q) ←
(p, q, p - q) (which moves p and q one position back in the Fibonacci sequence); then return
to Step 2
Step 4. [Increase i] If p=1, the algorithm terminates unsuccessfully. Otherwise set (i,p,q) ← (i
+ q, p - q, 2q - p) (which moves p and q two positions back in the Fibonacci sequence); and
return to Step 2
3.15.1.4. Indexed Sequential Search:

In this searching technique, first of all an index file is created that contains references to a
group of records, once an index is obtained, the partial searching takes less time since it is to
be located in the group/bucket specified by the index. The program given below creates an
index file for the employee records by grouping the records and then locates the required
key by searching the index first and then returns the required record.

3.15.2.Sorting

Sorting is a technique to rearrange the elements of a list in ascending or descending order,


which can be numerical, lexicographical, or any user-defined order. Sorting is a process
through which the data is arranged in ascending or descending order. Sorting can be
classified in two types;

Internal Sorts:- This method uses only the primary memory during sorting process. All data
items are held in main memory and no secondary memory is required this sorting process. If
all the data that is to be sorted can be accommodated at a time in memory is called internal
sorting. There is a limitation for internal sorts; they can only process relatively small lists due
to memory constraints. There are 3 types of internal sorts.

• Selection sort
• Heap Sort
• Insertion sort
• Shell Sort algorithm
• Bubble Sort
• Quick sort algorithm

External Sorts:- Sorting large amount of data requires external or secondary memory. This
process uses external memory such as HDD, to store the data which is not fit into the main
memory. So, primary memory holds the currently being sorted data only. All external sorts
are based on process of merging. Different parts of data are sorted separately and merged
together. Ex:- Merge Sort

3.15.2.1.Selection sort
Selection sorting is conceptually the most simplest sorting algorithm. This algorithm first
finds the smallest element in the array and exchanges it with the element in the first
position, then find the second smallest element and exchange it with the element in the
second position, and continues in this way until the entire array is sorted.
How Selection Sorting Works

In the first pass, the smallest element found is 1, so it is placed at the first position, then
leaving first element, smallest element is searched from the rest of the elements, 3 is the
smallest, so it is then placed at the second position. Then we leave 1 nad 3, from the rest of
the elements, we search for the smallest and put it at third position and keep doing this,
until array is sorted.
Sorting using Selection Sort Algorithm
void selectionSort(int a[], int size)
{
int i, j, min, temp;
for(i=0; i < size-1; i++ )
{
min = i; //setting min as i
for(j=i+1; j < size; j++)
{
if(a[j] < a[min]) //if element at j is less than element at min position
{
min = j; //then set min as j
}
}
temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
Complexity Analysis of Selection Sorting
Worst Case Time Complexity : O(n2)
Best Case Time Complexity : O(n2)
Average Time Complexity : O(n2)
Space Complexity : O(1)

Implementation of selection sort


#include<iostream>

using namespace std;

int main()
{
int i,j,n,loc,temp,min,a[30];
cout<<"Enter the number of elements:";
cin>>n;
cout<<"\nEnter the elements\n";

for(i=0;i<n;i++)
{
cin>>a[i];
}

for(i=0;i<n-1;i++)
{
min=a[i];
loc=i;
for(j=i+1;j<n;j++)
{
if(min>a[j])
{
min=a[j];
loc=j;
}
}

temp=a[i];
a[i]=a[loc];
a[loc]=temp;
}

cout<<"\nSorted list is as follows\n";


for(i=0;i<n;i++)
{
cout<<a[i]<<" ";
}

return 0;

3.15.2.3. Heap Sort Algorithm


Heap Sort is one of the best sorting methods being in-place and with no quadratic worst-
case scenarios. Heap sort algorithm is divided into two basic parts :

 Creating a Heap of the unsorted list.


 Then a sorted array is created by repeatedly removing the largest/smallest element
from the heap, and inserting it into the array. The heap is reconstructed after each
removal.

What is a Heap ?
Heap is a special tree-based data structure, that satisfies the following special heap
properties :

1. Shape Property : Heap data structure is always a Complete Binary Tree, which means all
levels of the tree are fully filled.

2. Heap Property : All nodes are either [greater than or equal to] or [less than or equal
to] each of its children. If the parent nodes are greater than their children, heap is called
a Max-Heap, and if the parent nodes are smalled than their child nodes, heap is
called Min-Heap.
How Heap Sort Works
Initially on receiving an unsorted list, the first step in heap sort is to create a Heap data
structure(Max-Heap or Min-Heap). Once heap is built, the first element of the Heap is either
largest or smallest(depending upon Max-Heap or Min-Heap), so we put the first element of
the heap in our array. Then we again make heap using the remaining elements, to again pick
the first element of the heap and put it into the array. We keep on doing the same
repeatedly untill we have the complete sorted list in our array.
In the below algorithm, initially heapsort() function is called, which calls buildheap() to build
heap, which inturn uses satisfyheap() to build the heap.
Sorting using Heap Sort Algorithm
/* Below program is written in C++ language */
void heapsort(int[], int);
void buildheap(int [], int);
void satisfyheap(int [], int, int);
void main()
{
int a[10], i, size;
cout << "Enter size of list"; // less than 10, because max size of array is 10
cin >> size;
cout << "Enter" << size << "elements";
for( i=0; i < size; i++)
{
cin >> a[i];
}
heapsort(a, size);
getch();
}

void heapsort(int a[], int length)


{
buildheap(a, length);
int heapsize, i, temp;
heapsize = length - 1;
for( i=heapsize; i >= 0; i--)
{
temp = a[0];
a[0] = a[heapsize];
a[heapsize] = temp;
heapsize--;
satisfyheap(a, 0, heapsize);
}
for( i=0; i < length; i++)
{
cout << "\t" << a[i];
}
}

void buildheap(int a[], int length)


{
int i, heapsize;
heapsize = length - 1;
for( i=(length/2); i >= 0; i--)
{
satisfyheap(a, i, heapsize);
}
}

void satisfyheap(int a[], int i, int heapsize)


{
int l, r, largest, temp;
l = 2*i;
r = 2*i + 1;
if(l <= heapsize && a[l] > a[i])
{
largest = l;
}
else
{
largest = i;
}
if( r <= heapsize && a[r] > a[largest])
{
largest = r;
}
if(largest != i)
{
temp = a[i];
a[i] = a[largest];
a[largest] = temp;
satisfyheap(a, largest, heapsize);
}
}
Complexity Analysis of Heap Sort
Worst Case Time Complexity : O(n log n)
Best Case Time Complexity : O(n log n)
Average Time Complexity : O(n log n)
Space Complexity : O(n)

 Heap sort is not a Stable sort, and requires a constant space for sorting a list.
 Heap Sort is very fast and is widely used for sorting.

3.15.2.4. Insertion Sorting


It is a simple Sorting algorithm which sorts the array by shifting elements one by one.
Following are some of the important characteristics of Insertion Sort.

1. It has one of the simplest implementation


2. It is efficient for smaller data sets, but very inefficient for larger lists.
3. Insertion Sort is adaptive, that means it reduces its total number of steps if given a
partially sorted list, hence it increases its efficiency.
4. It is better than Selection Sort and Bubble Sort algorithms.
5. Its space complexity is less, like Bubble Sorting, inerstion sort also requires a single
additional memory space.
6. It is Stable, as it does not change the relative order of elements with equal keys

How Insertion Sorting Works


Sorting using Insertion Sort Algorithm
int a[6] = {5, 1, 6, 2, 4, 3};
int i, j, key;
for(i=1; i<6; i++)
{
key = a[i];
j = i-1;
while(j>=0 && key < a[j])
{
a[j+1] = a[j];
j--;
}
a[j+1] = key;
}
Now lets, understand the above simple insertion sort algorithm. We took an array with 6
integers. We took a variable key, in which we put each element of the array, in each pass,
starting from the second element, that is a[1].
Then using the while loop, we iterate, until j becomes equal to zero or we find an element
which is greater than key, and then we insert the key at that position.
In the above array, first we pick 1 as key, we compare it with 5(element before 1), 1 is
smaller than 5, we shift 1 before 5. Then we pick 6, and compare it with 5 and 1, no shifting
this time. Then 2 becomes the key and is compared with, 6 and 5, and then 2 is placed after
1. And this goes on, until complete array gets sorted.
Insertion Sorting in C++
#include <stdlib.h>
#include <iostream.h>
using namespace std;
//member functions declaration
void insertionSort(int arr[], int length);
void printArray(int array[],int size);

int main() {
int array[5]= {5,4,3,2,1};
insertionSort(array,5);
return 0;
}

void insertionSort(int arr[], int length) {


int i, j ,tmp;
for (i = 1; i < length; i++) {
j = i;
while (j > 0 && arr[j - 1] > arr[j]) {
tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
j--;
}
printArray(arr,5);
}
}

void printArray(int array[], int size){


cout<< "Sorting tha array using Insertion sort... ";
int j;
for (j=0; j < size;j++)
for (j=0; j < size;j++)
cout <<" "<< array[j];
cout << endl;
}
Complexity Analysis of Insertion Sorting
Worst Case Time Complexity : O(n2)
Best Case Time Complexity : O(n)
Average Time Complexity : O(n2)
Space Complexity : O(1)

3.15.2.5. Bubble Sorting


Bubble Sort is an algorithm which is used to sort N elements that are given in a memory for
eg: an Array with N number of elements. Bubble Sort compares all the element one by one
and sort them based on their values.
It is called Bubble sort, because with each iteration the smaller element in the list bubbles
up towards the first place, just like a water bubble rises up to the water surface.
Sorting takes place by stepping through all the data items one-by-one in pairs and
comparing adjacent data items and swapping each pair that is out of order.

Sorting using Bubble Sort Algorithm


Let's consider an array with values {5, 1, 6, 2, 4, 3}
int a[6] = {5, 1, 6, 2, 4, 3};
int i, j, temp;
for(i=0; i<6; i++)
{
for(j=0; j<6-i-1; j++)
{
if( a[j] > a[j+1])
{
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
//now you can print the sorted array after this
Above is the algorithm, to sort an array using Bubble Sort. Although the above logic will sort
and unsorted array, still the above algorithm isn't efficient and can be enhanced further.
Because as per the above logic, the for loop will keep going for six iterations even if the
array gets sorted after the second iteration.
Hence we can insert a flag and can keep checking whether swapping of elements is taking
place or not. If no swapping is taking place that means the array is sorted and wew can jump
out of the for loop.
int a[6] = {5, 1, 6, 2, 4, 3};
int i, j, temp;
for(i=0; i<6; i++)
{
int flag = 0; //taking a flag variable
for(j=0; j<6-i-1; j++)
{
if( a[j] > a[j+1])
{
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
flag = 1; //setting flag as 1, if swapping occurs
}
}
if(!flag) //breaking out of for loop if no swapping takes place
{
break;
}
}
In the above code, if in a complete single cycle of j iteration(inner for loop), no swapping
takes place, and flag remains 0, then we will break out of the for loops, because the array
has already been sorted.
Complexity Analysis of Bubble Sorting
In Bubble Sort, n-1 comparisons will be done in 1st pass, n-2 in 2nd pass, n-3 in 3rd pass and
so on. So the total number of comparisons will be
(n-1)+(n-2)+(n-3)+.....+3+2+1
Sum = n(n-1)/2
i.e O(n2)
Hence the complexity of Bubble Sort is O(n2).
The main advantage of Bubble Sort is the simplicity of the algorithm.Space complexity for
Bubble Sort is O(1), because only single additional memory space is required
for temp variable
Best-case Time Complexity will be O(n), it is when the list is already sorted.
3.15.2.6. Quick Sort Algorithm
Quick Sort, as the name suggests, sorts any list very quickly. Quick sort is not stable search,
but it is very fast and requires very less aditional space. It is based on the rule of Divide and
Conquer(also called partition-exchange sort). This algorithm divides the list into three main
parts :

1. Elements less than the Pivot element


2. Pivot element
3. Elements greater than the pivot element

In the list of elements, mentioned in below example, we have taken 25 as pivot. So after the
first pass, the list will be changed like this.
6 8 17 14 25 63 37 52
Hnece after the first pass, pivot will be set at its position, with all the elements smaller to it
on its left and all the elements larger than it on the right. Now 6 8 17 14 and 63 37 52 are
considered as two separate lists, and same logic is applied on them, and we keep doing this
until the complete list is sorted.
How Quick Sorting Works
Sorting using Quick Sort Algorithm
/* a[] is the array, p is starting index, that is 0,
and r is the last index of array. */
void quicksort(int a[], int p, int r)
{
if(p < r)
{
int q;
q = partition(a, p, r);
quicksort(a, p, q);
quicksort(a, q+1, r);
}
}
int partition(int a[], int p, int r)
{
int i, j, pivot, temp;
pivot = a[p];
i = p;
j = r;
while(1)
{
while(a[i] < pivot && a[i] != pivot)
i++;
while(a[j] > pivot && a[j] != pivot)
j--;
if(i < j)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
else
{
return j;
}
}
}
Complexity Analysis of Quick Sort
Worst Case Time Complexity : O(n2)
Best Case Time Complexity : O(n log n)
Average Time Complexity : O(n log n)
Space Complexity : O(n log n)

 Space required by quick sort is very less, only O(n log n) additional space is required.
 Quick sort is not a stable sorting technique, so it might change the occurence of two
similar elements in the list while sorting.

3.15.2.6. Merge Sort Algorithm


Merge Sort follows the rule of Divide and Conquer. But it doesn't divides the list into two
halves. In merge sort the unsorted list is divided into N sublists, each having one element,
because a list of one element is considered sorted. Then, it repeatedly merge these sublists,
to produce new sorted sublists, and at lasts one sorted list is produced.
Merge Sort is quite fast, and has a time complexity of O(n log n). It is also a stable sort,
which means the "equal" elements are ordered in the same order in the sorted list.

How Merge Sort Works


Like we can see in the above example, merge sort first breaks the unsorted list into sorted
sublists, and then keep merging these sublists, to finlly get the complete sorted list.

Sorting using Merge Sort Algorithm


/* a[] is the array, p is starting index, that is 0, and r is the last index of array. */
Lets take a[5] = {32, 45, 67, 2, 7} as the array to be sorted.
void mergesort(int a[], int p, int r)
{
int q;
if(p < r)
{
q = floor( (p+r) / 2);
mergesort(a, p, q);
mergesort(a, q+1, r);
merge(a, p, q, r);
}
}
void merge(int a[], int p, int q, int r)
{
int b[5]; //same size of a[]
int i, j, k;
k = 0;
i = p;
j = q+1;
while(i <= q && j <= r)
{
if(a[i] < a[j])
{
b[k++] = a[i++]; // same as b[k]=a[i]; k++; i++;
}
else
{
b[k++] = a[j++];
}
}

while(i <= q)
{
b[k++] = a[i++];
}

while(j <= r)
{
b[k++] = a[j++];
}

for(i=r; i >= p; i--)


{
a[i] = b[--k]; // copying back the sorted list to a[]
}
}
Complexity Analysis of Merge Sort
Worst Case Time Complexity : O(n log n)
Best Case Time Complexity : O(n log n)
Average Time Complexity : O(n log n)
Space Complexity : O(n)

 Time complexity of Merge Sort is O(n Log n) in all 3 cases (worst, average and best) as
merge sort always divides the array in two halves and take linear time to merge two
halves.
 It requires equal amount of additional space as the unsorted list. Hence its not at all
recommended for searching large unsorted lists.
 It is the best Sorting technique for sorting Linked Lists.

3.15.3 Multi-way merge

The basic algorithm is 2-way merge - we use 2 output tapes.

Assume that we have k tapes - then the number of passes will be reduced - logk(N/M)
At a given merge step we merge the first k runs, then the second k runs, etc.

The task here: to find the smallest element out of k elements

Solution: priority queues

Idea: Take the smallest elements from the first k runs,


store them into main memory in a heap tree.

Then repeatedly output the smallest element from the heap.


The smallest element is replaced with the next element from the run from which it came.

When finished with the first set of runs, do the same with the next set of runs.

External Sorting: Example of multiway external sorting

Ta1: 17, 3, 29, 56, 24, 18, 4, 9, 10, 6, 45, 36, 11, 43

Assume that we have three tapes (k = 3) and the memory can hold three records.

A. Main memory sort

The first three records are read into memory, sorted and written on Tb1,
the second three records are read into memory, sorted and stored on Tb2,
finally the third three records are read into memory, sorted and stored on
Tb3.

Now we have one run on each of the three tapes:

Tb1: 3, 17, 29
Tb2: 18, 24, 56

Tb3: 4, 9, 10

The next portion of three records is sorted into main memory


and stored as the second run on Tb1:

Tb1: 3, 17, 29, 6, 36, 45

The next portion, which is also the last one, is sorted and stored onto Tb2:

Tb2: 18, 24, 56, 11, 43

Nothing is stored on Tb3.

Thus, after the main memory sort, our tapes look like this:

Tb1: 3, 17, 29, | 6, 36, 45,

Tb2: 18, 24, 56, | 11, 43

Tb3: 4, 9, 10

B. Merging

B.1. Merging runs of length M to obtain runs of length k*M

In our example we merge runs of length 3


and the resulting runs would be of length 9.

a. We build a heap tree in main memory out of the first records in each
tape.
These records are: 3, 18, and 4.
b. We take the smallest of them - 3, using the deleteMin operation,
and store it on tape Ta1.

The record '3' belonged to Tb1, so we read the next record from Tb1 -
17,
and insert it into the heap. Now the heap contains 18, 4, and 17.

c. The next deleteMin operation will output 4, and it will be stored on


Ta1.

The record '4' comes from Tb3, so we read the next record '9' from
Tb3
and insert it into the heap.
Now the heap contains 18, 17 and 9.
d. Proceeding in this way, the first three runs will be stored in sorted
order on Ta1.

Ta1: 3, 4, 9, 10, 17, 18, 24, 29, 56

Now it is time to build a heap of the second three runs.


(In fact they are only two, and the run on Tb2 is not complete.)

The resulting sorted run on Ta2 will be:

Ta2: 6, 11, 36, 43, 45

This finishes the first pass.

B.2. Building runs of length k*k*M

We have now only two tapes: Ta1 and Ta2.

o We build a heap of the first elements of the two tapes - 3 and 6,


and output the smallest element '3' to tape Tb1.
o Then we read the next record from the tape where the record '3'
belonged - Ta1,
and insert it into the heap.
o Now the heap contains 6 and 4, and using the deleteMin operation
the smallest record - 4 is output to tape Tb1.

Proceeding in this way, the entire file will be sorted on tape Tb1.

Tb1: 3, 4, 6, 9, 10, 11, 17, 18, 24, 29, 36, 43, 45, 56

The number of passes for the multiway merging is logk(N/M).


In the example this is [log3(14/3)] + 1 = 2.

3.15.3.1.polyphase merge sort

polyphase merge sort is an algorithm which decreases the number of "runs" at every
iteration of the main loop by merging "runs" in pairs. Typically, a merge sort splits items into
groups then recursively sorts each group. Once the groups are sorted, they are merged into
a final, sorted sequence.

Polyphase merge sorts are ideal for sorting and merging large files. Two pairs of input and
output files are opened as file streams. At the end of each iteration, input files are deleted,
output files are closed and reopened as input files. The use of file streams makes it possible
to sort and merge files which can not be loaded into the computer's main memory.

Perfect 3 file polyphase merge sort


It is easiest to look at the polyphase merge starting from its ending conditions and working
backwards. At the start of each iteration, there will be two input files and one output file. At
the end of the iteration, one input file will have been completely consumed and will become
the output file for the next iteration. The current output file will become an input file for the
next iteration. The remaining files (just one in the 3 file case) have only been partially
consumed and their remaining runs will be input for the next iteration.

File 1 just emptied and became the new output file. One run is left on each input tape, and
merging those runs together will make the sorted file.

File 1 (out): <1 run> * (the sorted file)


File 2 (in ): ... | <1 run> * --> ... <1 run> | * (consumed)
File 3 (in ): | <1 run> * <1 run> | * (consumed)

... possible runs that have already been read


| marks the read pointer of the file
* marks end of file

Stepping back to the previous iteration, we were reading from 1 and 2. One run is merged
from 1 and 2 before file 1 goes empty. Notice that file 2 is not completely consumed -- it has
one run left to match the final merge (above).

File 1 (in ): ... | <1 run> * ... <1 run> | *


File 2 (in ): | <2 run> * --> <1 run> | <1 run> *
File 3 (out): <1 run> *

Stepping back another iteration, 2 runs are merged from 1 and 3 before file 3 goes empty.

File 1 (in ): | <3 run> ... <2 run> | <1 run> *


File 2 (out): --> <2 run> *
File 3 (in ): ... | <2 run> * <2 run> | *

Stepping back another iteration, 3 runs are merged from 2 and 3 before file 2 goes empty.

File 1 (out): <3 run> *


File 2 (in ): ... | <3 run> * --> ... <3 run> | *
File 3 (in ): | <5 run> * <3 run> | <2 run> *

Stepping back another iteration, 5 runs are merged from 1 and 2 before file 1 goes empty.

File 1 (in ): ... | <5 run> * ... <5 run> | *


File 2 (in ): | <8 run> * --> <5 run> | <3 run> *
File 3 (out): <5 run> *

Looking at the number of runs merged working backwards: 1, 1, 2, 3, 5, ... reveals a


Fibonacci sequence.
For everything to work out right, the initial file to be sorted must be distributed to the
proper input files and each input file must have the correct number of runs on it. In the
example, that would mean an input file with 13 runs would write 5 runs to file 1 and 8 runs
to file 2.

In practice, the input file won't happen to have a Fibonacci number of runs it (and the
number of runs won't be known until after the file has been read). The fix is to pad the input
files with dummy runs to make the required Fibonacci sequence.

For comparison, the ordinary merge sort will combine 16 runs in 4 passes using 4 files. The
polyphase merge will combine 13 runs in 5 passes using only 3 files. Alternatively, a
polyphase merge will combine 17 runs in 4 passes using 4 files. (Sequence: 1, 1, 1, 3, 5, 9,
17, 31, 57, ...)

An iteration (or pass) in ordinary merge sort involves reading and writing the entire file. An
iteration in a polyphase sort does not read or write the entire file[4], so a typical polyphase
iteration will take less time than a merge sort iteration.

3.16. Comparision of all sorting algorithms

Selection sort
Selection sort is an exception in our list. This is considered an academic sorting algorithm.
Why? Because the time efficiency is always O(n2) which is not acceptable. There is no real
world usage for selection sort except passing the data structure course exam.
pros

 Nothing

cons

 Always run at O(n2) even at best case scenario

Practical usage

 Help students to get some credits towards their degree, nothing to be precise

Bubble sort
This is the other exception in the list because bubble sort is too slow to be practical. Unless
the sequence is almost sorted feasibility of bubble sort is zero and the running time is O(n 2).
This is one of the three simple sorting algorithms alongside selection sort and insertion sort
but like selection sort falls short of insertions sort in terms of efficiency even for small
sequences.
pros

 Again nothing, maybe just “catchy name1”


cons

 With polynomial O(n2) it is too slow

Practical usage

 Implementing it makes for an interesting programming exercise

Insertion sort
Insertion sort is definitely not the most efficient algorithm out there but its power lies in its
simplicity. Since it is very easy to implement and adequately efficient for small number of
elements, it is useful for small applications or trivial ones. The definition of small is vague
and depends on a lot of things but a safe bet is if under 50, insertion sort is fast enough.
Another situation that insertion sort is useful is when the sequence is almost sorted. Such
sequences may seem like exceptions but in real world applications often you encounter
almost sorted elements. The run time of insertions sort is O(n 2) at worst case scenario. So
far we have another useless alternative for selection sort. But if implemented well the run
time can be reduced to O(n+k). n is the number of elements and k is the number of
inversions (the number of pair of elements out of order). With this new run time in mind
you can see if the sequence is almost sorted (k is small) the run time can be almost linear
which is a huge improvement over the polynomial n2.
pros

 Easy to implement
 The more the sequence is ordered the closer is run time to linear time O(n)

cons

 Not suitable for large data sets


 Still polynomial at worst case

Practical usage

 For small applications when the sequence is small (less than 50 elements)
 When the sequence is going to be almost sorted

Heap sort
This is the first general purpose sorting algorithm we are introducing here. Heap sort runs at
O(nlogn) which is optimal for comparison based sorting algorithms. Though heap sort has
the same run time as quick sort and merge sort but it is usually outperformed in real world
scenarios. If you are asking then why should anyone use it, the answer lies in space
efficiency. Nowadays computers come with huge amount of memory, enough for many
applications. Does this mean heap sort is losing its shine? No, still when writing programs for
environments with limited memory, such as embedded systems or space efficiency is much
more important than time efficiency. A rule of thumb is if the sequence is small enough to
easily fit in main memory then heap sort is good choice.
pros

 Runs at O(nlogn)
 Can be easily implemented to be executed in place

cons

 Not as fast as other comparison based algorithms in large data sets


 It doesn’t provide stable sorting

Practical usage

 The natural choice for small and medium sized sequences


 If the main memory size is concerned heap sort is the best option

Quick sort
One of the most widely used sorting algorithms in computer industry. Surprisingly quick sort
has a running time of O(n2) that makes it susceptible in real-time applications. Having a
polynomial worst case scenario still quick sort usually outperforms both quick sort and
merge sort (coming next). The reason behind the popularity of quick sort despite the short
comings is both being fast in real world scenarios (not necessarily worst case) and the ability
to be implemented as an in place algorithm.
pros

 Most often than not runs at O(nlogn)


 Quick sort is tried and true, has been used for many years in industry so you can be
assured it is not going to fail you
 High space efficiency by executing in place

cons

 Polynomial worst case scenario makes it susceptible for time critical applications
 Provides non stable sort due to swapping of elements in partitioning step

Practical usage

 Best choice for general purpose and in memory sorting


 Used to be the standard algorithm for sorting of arrays of primitive types in Java
 qsort utility in C programming language is powered by quick sort

Merge sort
Having a O(nlogn) worst case scenario run time makes merge sort a powerful sorting
algorithm. The main drawback of this algorithm is its space inefficiency. That is in the
process of sorting lots of temporary arrays have to be created and many copying of
elements is involved. This doesn’t mean merge sort is not useful. When the data to be
sorted is distributed across different locations like cache, main memory etc then copying
data is inevitable. Merge sort mainly owes its popularity to Tim Peters who designed a
variant of it which is in essence a bottom-up merge sort and is known as Tim sort.
pros

 Excellent choice when data is fetched from resources other than main memory
 Having a worst case scenario run time of O(nlogn) which is optimal
 Tim sort variant is really powerful

cons

 Lots of overhead in copying data between arrays and making new arrays
 Extremely difficult to implement it in place for arrays
 Space inefficiency

Practical usage

 When data is in different locations like cache, main memory, external memory etc.
 A multi-way merge sort variant is used in GNU sorting utility
 Tim sort variant is standard sorting algorithm in Python programming language since
2003
 Default sorting algorithm of arrays of object type in Java since version 7 onward

Special purpose sorting algorithms


Though currently O(nlogn) seems like an unbreakable cap for sorting algorithms, this just
holds true for general purpose sorts. If the entities to be sorted are integers, strings or d-
tuples then you are not limited by the sorting algorithms above. Radix sort and Bucket sort
are two of most famous special purpose sorting algorithms. their worst case scenario run
time is O(f(n+r)). [0, r-1] is the range of integers and f=1 for bucket sort. All in all this means
if f(n+r) is significantly below nlogn function then these methods are faster than three
powerful general purpose sorting algorithms, merge sort, quick sort and heap sort.
pros

 They can run faster than nlogn

cons

 Cannot be used for every type of data


 Not necessarily always run faster than general purpose algorithms

Practical usage

 When the prerequisites of data types is met then they are the definitive choice

Comparison table2:
worst case time average case time best case time worst case space

Selection sort O(n2) O(n2) O(n2) O(n)

Bubble sort O(n2) O(n2) O(n) O(1)

Insertion sort O(n2) O(n2) O(n) O(n)

Heap sort O(nlogn) O(nlogn) O(nlogn) O(1)

Quick sort O(n2) O(nlogn) O(nlogn) O(logn)

Merge sort O(nlogn) O(nlogn) O(nlogn) O(n)

3.17. Search Trees

The binary search tree (BST) is one of the fundamental data structures extensively used for
searching the target in a set of ordered data. These two cases lead to the following two
kinds of search trees:
1. Static BST—is one that is not allowed to update its structure once it is constructed. In
other words, the static BST is an offl ine algorithm, which is presumably aware of the
access sequence beforehand.
2. Dynamic BST—is one that changes during the access sequence. We assume that the
dynamic BST is an online algorithm, which does not have prior information about the
sequence.

Symbol Table
The symbol table is a kind of a ‘keyed table’ which stores <key, information> pairs with
no additional logical structure.
The operations performed on symbol tables are the following:
1. Inserting the <key, information> pairs into the collection.
2. Removing the <key, information> pairs by specifying the key.
3. Searching for a particular key.
4. Retrieving the information associated with a key.

3.17.1. Representation of Symbol Table


There are two different techniques for implementing a keyed table, namely, the symbol
table and the tree table.

Static Tree Tables


When symbols are known in advance and no insertion and deletion is allowed, such a
structure is called a static tree table. An example of this type of table is a reserved word
table in a compiler.
There are four options for searching:
1. Static tree table can be stored as a sorted sequential list and binary search (O(log2n))
can be used to search a symbol.
2. Balanced BST can be used to find symbols having equal probabilities.
3. Hash tables, having the search time O(1), can be used to store a symbol table.
4. OBST is used when different symbols are searched with different probabilities.

Dynamic Tree Tables


A dynamic tree table is used when symbols are not known in advance but are inserted as
they come and deleted if not required. Dynamic keyed tables are those that are built onthe-
fly. The keys have no history associated with their use. The dynamically built tree that
is a balanced BST is the best choice.

3.18. Optimal Binary Search Tree(OBST)

An optimal binary search tree is a binary search tree for which the nodes are arranged on
levels such that the tree cost is minimum.
For the purpose of a better presentation of optimal binary search trees, we will consider
“extended binary search trees”, which have the keys stored at their internal nodes. Suppose
“n” keys k1, k2, … , k n are stored at the internal nodes of a binary search tree. It is assumed
that the keys are given in sorted order, so that k1< k2 < … < kn.

Algorithm
OBST(i, j) denotes the optimal binary search tree containing the keys ki,
ki+1, …, kj;
Wi, j denotes the weight matrix for OBST(i, j)
Wi, j can be defined using the following formula:

Ci, j, 0 ≤ i ≤ j ≤ n denotes the cost matrix for OBST(i, j)


Ci, j can be defined recursively, in the following manner:
Ci, i = Wi, j
Ci, j = Wi, j + mini<k≤j(Ci, k - 1 + Ck, j)
Ri, j, 0 ≤ i ≤ j ≤ n denotes the root matrix for OBST(i, j)
Assigning the notation Ri, j to the value of k for which we obtain a minimum
in the above relations, the optimal binary search tree is OBST(0, n) and each
subtree OBST(i, j) has the root kRij and as subtrees the trees denoted by
OBST(i, k-1) and OBST(k, j).
*OBST(i, j) will involve the weights qi-1, pi, qi, …, pj, qj.

All possible optimal subtrees are not required. Those that are consist of sequences of keys
that are immediate successors of the smallest key in the subtree, successors in the sorted
order for the keys.

The bottom-up approach generates all the smallest required optimal subtrees first, then all
next smallest, and so on until the final solution involving all the weights is found. Since the
algorithm requires access to each subtree’s weighted path length, these weighted path
lengths must also be retained to avoid their recalculation. They will be stored in the weight
matrix ‘W’. Finally, the root of each subtree must also be stored for reference in the root
matrix ‘R’.

ALGORITHMS IN PSEUDOCODE

We have the following procedure for determining R(i, j) and C(i, j) with
0 <= i <= j <= n:
PROCEDURE COMPUTE_ROOT(n, p, q; R, C)
begin
for i = 0 to n do
C (i, i) _ 0
W (i, i) _ q(i)
for m = 0 to n do
for i = 0 to (n – m) do
j_i+m
W (i, j) _ W (i, j – 1) + p (j) + q (j)
*find C (i, j) and R (i, j) which minimize the tree cost
end

The following function builds an optimal binary search tree

FUNCTION CONSTRUCT(R, i, j)
begin
*build a new internal node N labeled (i, j)
k _ R (i, j)
if i = k then
*build a new leaf node N’ labeled (i, i)
else
*N’ _ CONSTRUCT(R, i, k)
*N’ is the left child of node N
if k = (j – 1) then
*build a new leaf node N’’ labeled (j, j)
else
*N’’ _ CONSTRUCT(R, k + 1, j)
*N’’ is the right child of node N
return N
end

EXAMPLE OF RUNNING THE ALGORITHM

Find the optimal binary search tree for N = 6, having keys k1 … k6 and
weights p1 = 10, p2 = 3, p3 = 9, p4 = 2, p5 = 0, p6 = 10; q0 = 5, q1 = 6, q2 = 4, q3
= 4, q4 = 3, q5 = 8, q6 = 0. The following figure shows the arrays as they
would appear after the initialization and their final disposition.
Thus the OBST will have the final structure

3.19. AVL Tree

AVL tree is a self balanced binary search tree. That means, an AVL tree is also a binary
search tree but it is a balanced tree. A binary tree is said to be balanced, if the difference
between the hieghts of left and right subtrees of every node in the tree is either -1, 0 or +1.
In other words, a binary tree is said to be balanced if for every node, height of its children
differ by at most one. In an AVL tree, every node maintains a extra information known
as balance factor. The AVL tree was introduced in the year of 1962 by G.M. Adelson-Velsky
and E.M. Landis.

An AVL tree is defined as follows...

An AVL tree is a balanced binary search tree. In an AVL tree, balance factor of every node
is either -1, 0 or +1.

Balance factor of a node is the difference between the heights of left and right subtrees of
that node. The balance factor of a node is calculated either height of left subtree - height of
right subtree (OR) height of right subtree - height of left subtree. In the following
explanation, we are calculating as follows...

Balance factor = heightOfLeftSubtree - heightOfRightSubtree

Example

The above tree is a binary search tree and every node is satisfying balance factor condition.
So this tree is said to be an AVL tree.

Every AVL Tree is a binary search tree but all the Binary Search Trees need not to be AVL
trees.

3.19.1. AVL Tree Rotations

In AVL tree, after performing every operation like insertion and deletion we need to check
the balance factor of every node in the tree. If every node satisfies the balance factor
condition then we conclude the operation otherwise we must make it balanced. We
use rotationoperations to make the tree balanced whenever the tree is becoming
imbalanced due to any operation.

Rotation operations are used to make a tree balanced.

Rotation is the process of moving the nodes to either left or right to make tree balanced.
There are four rotations and they are classified into two types.

3.19.1.1. Single Left Rotation (LL Rotation)

In LL Rotation every node moves one position to left from the current position. To
understand LL Rotation, let us consider following insertion operations into an AVL Tree...

3.19.1.2. Single Right Rotation (RR Rotation)

In RR Rotation every node moves one position to right from the current position. To
understand RR Rotation, let us consider following insertion operations into an AVL Tree...

3.19.1.3. Left Right Rotation (LR Rotation)

The LR Rotation is combination of single left rotation followed by single right rotation. In LR
Roration, first every node moves one position to left then one position to right from the
current position. To understand LR Rotation, let us consider following insertion operations
into an AVL Tree...
3.19.1.4. Right Left Rotation (RL Rotation)

The RL Rotation is combination of single right rotation followed by single left rotation. In RL
Roration, first every node moves one position to right then one position to left from the
current position. To understand RL Rotation, let us consider following insertion operations
into an AVL Tree...

3.19.2. Operations on an AVL Tree

The following operations are performed on an AVL tree...

1. Search

2. Insertion

3. Deletion

3.19.2.1. Search Operation in AVL Tree

In an AVL tree, the search operation is performed with O(log n) time complexity. The search
operation is performed similar to Binary search tree search operation. We use the following
steps to search an element in AVL tree...

 Step 1: Read the search element from the user


 Step 2: Compare, the search element with the value of root node in the tree.
 Step 3: If both are matching, then display "Given node found!!!" and terminate the
function
 Step 4: If both are not matching, then check whether search element is smaller or
larger than that node value.
 Step 5: If search element is smaller, then continue the search process in left subtree.
 Step 6: If search element is larger, then continue the search process in right subtree.
 Step 7: Repeat the same until we found exact element or we completed with a leaf
node
 Step 8: If we reach to the node with search value, then display "Element is found"
and terminate the function.
 Step 9: If we reach to a leaf node and it is also not matching, then display "Element
not found" and terminate the function.

3.19.2.2. Insertion Operation in AVL Tree

In an AVL tree, the insertion operation is performed with O(log n) time complexity. In AVL
Tree, new node is always inserted as a leaf node. The insertion operation is performed as
follows...

 Step 1: Insert the new element into the tree using Binary Search Tree insertion logic.
 Step 2: After insertion, check the Balance Factor of every node.
 Step 3: If the Balance Factor of every node is 0 or 1 or -1 then go for next operation.
 Step 4: If the Balance Factor of any node is other than 0 or 1 or -1 then tree is said to
be imbalanced. Then perform the suitable Rotation to make it balanced. And go for
next operation.
Example: Construct an AVL Tree by inserting numbers from 1 to 8.
3.19.2.3 Deletion Operation in AVL Tree

In an AVL Tree, the deletion operation is similar to deletion operation in BST. But after every
deletion operation we need to check with the Balance Factor condition. If the tree is
balanced after deletion then go for next operation otherwise perform the suitable rotation
to make the tree Balanced.

#include <iostream.h>
#include <stdlib.h>
#include<constream.h>
#define FALSE 0
#define TRUE 1
struct AVLNode
{
int data ;
int balfact ;
AVLNode *left ;
AVLNode *right ;
};

class avltree
{
private :
AVLNode *root ;
public :
avltree( ) ;
AVLNode* insert ( int data, int *h ) ;
static AVLNode* buildtree ( AVLNode *root, int data, int *h ) ;
void display( AVLNode *root ) ;
AVLNode* deldata ( AVLNode* root, int data, int *h ) ;
static AVLNode* del ( AVLNode *node, AVLNode* root, int *h ) ;
static AVLNode* balright ( AVLNode *root, int *h ) ;
static AVLNode* balleft ( AVLNode* root, int *h ) ;
void setroot ( AVLNode *avl ) ;
~avltree( ) ;
static void deltree ( AVLNode *root ) ;
};
avltree :: avltree( )
{
root = NULL ;
}
AVLNode* avltree :: insert ( int data, int *h )
{
root = buildtree ( root, data, h ) ;
return root ;
}
AVLNode* avltree :: buildtree ( AVLNode *root, int data, int *h )
{
AVLNode *node1, *node2 ;

if ( root == NULL )
{
root = new AVLNode ;
root -> data = data ;
root -> left = NULL ;
root -> right = NULL ;
root -> balfact = 0 ;
*h = TRUE ;
return ( root ) ;
}
if ( data < root -> data )
{
root -> left = buildtree ( root -> left, data, h ) ;

// If left subtree is higher


if ( *h )
{
switch ( root -> balfact )
{
case 1 :
node1 = root -> left ;
if ( node1 -> balfact == 1 )
{
cout << "\nRight rotation." ;
root -> left = node1 -> right ;
node1 -> right = root ;
root -> balfact = 0 ;
root = node1 ;
}
else
{
cout << "\nDouble rotation, left then right." ;
node2 = node1 -> right ;
node1 -> right = node2 -> left ;
node2 -> left = node1 ;
root -> left = node2 -> right ;
node2 -> right = root ;
if ( node2 -> balfact == 1 )
root -> balfact = -1 ;
else
root -> balfact = 0 ;
if ( node2 -> balfact == -1 )
node1 -> balfact = 1 ;
else
node1 -> balfact = 0 ;
root = node2 ;
}
root -> balfact = 0 ;
*h = FALSE ;
break ;

case 0 :
root -> balfact = 1 ;
break ;
case -1 :
root -> balfact = 0 ;
*h = FALSE ;
}
}
}

if ( data > root -> data )


{
root -> right = buildtree ( root -> right, data, h ) ;

if ( *h )
{
switch ( root -> balfact )
{
case 1 :
root -> balfact = 0 ;
*h = FALSE ;
break ;
case 0 :
root -> balfact = -1 ;
break ;
case -1 :
node1 = root -> right ;
if ( node1 -> balfact == -1 )
{
cout << "\nLeft rotation." ;
root -> right = node1 -> left ;
node1 -> left = root ;
root -> balfact = 0 ;
root = node1 ;
}
else
{

cout << "\nDouble rotation, right then left." ;


node2 = node1 -> left ;
node1 -> left = node2 -> right ;
node2 -> right = node1 ;
root -> right = node2 -> left ;
node2 -> left = root ;
if ( node2 -> balfact == -1 )
root -> balfact = 1 ;
else
root -> balfact = 0 ;
if ( node2 -> balfact == 1 )
node1 -> balfact = -1 ;
else
node1 -> balfact = 0 ;
root = node2 ;
}
root -> balfact = 0 ;
*h = FALSE ;
}
}
}
return ( root ) ;
}
void avltree :: display ( AVLNode* root )
{
if ( root != NULL )
{
display ( root -> left ) ;
cout << root -> data << "\t" ;
display ( root -> right ) ;
}
}
AVLNode* avltree :: deldata ( AVLNode *root, int data, int *h )
{
AVLNode *node ;
if ( root -> data == 13 )
cout << root -> data ;
if ( root == NULL )
{
cout << "\nNo such data." ;
return ( root ) ;
}
else
{
if ( data < root -> data )
{
root -> left = deldata ( root -> left, data, h ) ;
if ( *h )
root = balright ( root, h ) ;
}
else
{
if ( data > root -> data )
{
root -> right = deldata ( root -> right, data, h ) ;
if ( *h )
root = balleft ( root, h ) ;
}
else
{
node = root ;
if ( node -> right == NULL )
{
root = node -> left ;
*h = TRUE ;
delete ( node ) ;
}
else
{
if ( node -> left == NULL )
{
root = node -> right ;
*h = TRUE ;
delete ( node ) ;
}
else
{
node -> right = del ( node -> right,
node, h ) ;
if ( *h )
root = balleft ( root, h ) ;
}
}
}
}
}
return ( root ) ;
}
AVLNode* avltree :: del ( AVLNode *succ, AVLNode *node, int *h )
{
AVLNode *temp = succ ;

if ( succ -> left != NULL )


{
succ -> left = del ( succ -> left, node, h ) ;
if ( *h )
succ = balright ( succ, h ) ;
}
else
{
temp = succ ;
node -> data = succ -> data ;
succ = succ -> right ;
delete ( temp ) ;
*h = TRUE ;
}
return ( succ ) ;
}
AVLNode* avltree :: balright ( AVLNode *root, int *h )
{
AVLNode *temp1, *temp2 ;
switch ( root -> balfact )
{
case 1 :
root -> balfact = 0 ;
break ;
case 0 :
root -> balfact = -1 ;
*h = FALSE ;
break ;
case -1 :
temp1 = root -> right ;
if ( temp1 -> balfact <= 0 )
{
cout << "\nLeft rotation." ;
root -> right = temp1 -> left ;
temp1 -> left = root ;
if ( temp1 -> balfact == 0 )
{
root -> balfact = -1 ;
temp1 -> balfact = 1 ;
*h = FALSE ;
}
else
{
root -> balfact = temp1 -> balfact = 0 ;
}
root = temp1 ;
}
else
{
cout << "\nDouble rotation, right then left." ;
temp2 = temp1 -> left ;
temp1 -> left = temp2 -> right ;
temp2 -> right = temp1 ;
root -> right = temp2 -> left ;
temp2 -> left = root ;
if ( temp2 -> balfact == -1 )
root -> balfact = 1 ;
else
root -> balfact = 0 ;
if ( temp2 -> balfact == 1 )
temp1 -> balfact = -1 ;
else
temp1 -> balfact = 0 ;
root = temp2 ;
temp2 -> balfact = 0 ;
}
}
return ( root ) ;
}
AVLNode* avltree :: balleft ( AVLNode *root, int *h )
{
AVLNode *temp1, *temp2 ;
switch ( root -> balfact )
{
case -1 :
root -> balfact = 0 ;
break ;

case 0 :
root -> balfact = 1 ;
*h = FALSE ;
break ;

case 1 :
temp1 = root -> left ;
if ( temp1 -> balfact >= 0 )
{
cout << "\nRight rotation." ;
root -> left = temp1 -> right ;
temp1 -> right = root ;

if ( temp1 -> balfact == 0 )


{
root -> balfact = 1 ;
temp1 -> balfact = -1 ;
*h = FALSE ;
}
else
{
root -> balfact = temp1 -> balfact = 0 ;
}
root = temp1 ;
}
else
{
cout << "\nDouble rotation, left then right." ;
temp2 = temp1 -> right ;
temp1 -> right = temp2 -> left ;
temp2 -> left = temp1 ;
root -> left = temp2 -> right ;
temp2 -> right = root ;
if ( temp2 -> balfact == 1 )
root -> balfact = -1 ;
else
root -> balfact = 0 ;
if ( temp2-> balfact == -1 )
temp1 -> balfact = 1 ;
else
temp1 -> balfact = 0 ;
root = temp2 ;
temp2 -> balfact = 0 ;
}
}
return ( root ) ;
}
void avltree :: setroot ( AVLNode *avl )
{
root = avl ;
}
avltree :: ~avltree( )
{
deltree ( root ) ;
}

void avltree :: deltree ( AVLNode *root )


{
if ( root != NULL )
{
deltree ( root -> left ) ;
deltree ( root -> right ) ;
}
delete ( root ) ;
}
void main( )
{
avltree at ;
AVLNode *avl = NULL ;
int h ;
clrscr();
avl = at.insert ( 20, &h ) ;
at.setroot ( avl ) ;
avl = at.insert ( 6, &h ) ;
at.setroot ( avl ) ;
avl = at.insert ( 29, &h ) ;
at.setroot ( avl ) ;
avl = at.insert ( 5, &h ) ;
at.setroot ( avl ) ;
avl = at.insert ( 12, &h ) ;
at.setroot ( avl ) ;
avl = at.insert ( 25, &h ) ;
at.setroot ( avl ) ;
avl = at.insert ( 32, &h ) ;
at.setroot ( avl ) ;
avl = at.insert ( 10, &h ) ;
at.setroot ( avl ) ;
avl = at.insert ( 15, &h ) ;
at.setroot ( avl ) ;
avl = at.insert ( 27, &h ) ;
at.setroot ( avl ) ;
avl = at.insert ( 13, &h ) ;
at.setroot ( avl ) ;
cout << endl << "AVL tree:\n" ;
at.display ( avl ) ;
avl = at.deldata ( avl, 20, &h ) ;
at.setroot ( avl ) ;
avl = at.deldata ( avl, 12, &h ) ;
at.setroot ( avl ) ;
cout << endl << "AVL tree after deletion of a node:\n" ;
at.display ( avl ) ;
getch();
}

UNIT IV

4.0. Hashing
Hashing is a technique to convert a range of key values into a range of indexes of an array.
We're going to use modulo operator to get a range of key values. Consider an example of
hash table of size 20, and the following items are to be stored. Item are in the (key,value)
format.

(1,20) , (2,70) , (42,80) , (4,25) , (12,44) , (14,32) , (17,11) , (13,78) , (37,98)

Sr. No. Key Hash Array Index

1 1 1 % 20 = 1 1

2 2 2 % 20 = 2 2

3 42 42 % 20 = 2 2

4 4 4 % 20 = 4 4

5 12 12 % 20 = 12 12

6 14 14 % 20 = 14 14

7 17 17 % 20 = 17 17

8 13 13 % 20 = 13 13

9 37 37 % 20 = 17 17

4.1. KEY TERMS AND ISSUES


• Hash table Hash table is an array [0 to Max − 1] of size Max.
• Bucket A bucket is an index position in a hash table that can store more than one
record. When the same index is mapped with two keys, both the records are stored
in the same bucket. The assumption is that the buckets are equal in size.

• Probe Each action of address calculation and check for success is called as a probe.

• Collision The result of two keys hashing into the same address is called collision.

• Synonym Keys that hash to the same address are called synonyms.

• Overflow The result of many keys hashing to a single address and lack of room in the
bucket is known as an overflow. Collision and overflow are synonymous when the
bucket is of size 1.

• Open or external hashing When we allow records to be stored in potentially


unlimited space, it is called as open or external hashing.

• Closed or internal hashing When we use fixed space for storage eventually limiting
the number of records to be stored, it is called as closed or internal hashing.

• Hash function Hash function is an arithmetic function that transforms a key into an
• address which is used for storing and retrieving a record.

• Perfect hash function The hash function that transforms different keys into different
• addresses is called a perfect hash function. The worth of a hash function depends on
how
• well it avoids collision.

• Load density The maximum storage capacity, that is, the maximum number of
records
• that can be accommodated, is called as loading density.

• Full table A full table is one in which all locations are occupied. Owing to the
• characteristics of hash functions, there are always empty locations, rather a hash
function should not allow the table to get filled in more than 75%.

• Load factor Load factor is the number of records stored in a table divided by the
• maximum capacity of the table, expressed in terms of percentage.

• Rehashing Rehashing is with respect to closed hashing. When we try to store the
record with Key1 at the bucket position Hash(Key1) and find that it already holds a
record, it is collision situation. To handle collision, we use a strategy to choose a
sequence of alternative locations Hash1(Key1), Hash2(Key1), and so on within the
bucket table so as to place the record with Key1. This is known as rehashing.

Issues in hashing In case of collision, there are two main issues to be considered:
1. We need a good hashing function that minimizes the number of collisions.
2. We want an efficient collision resolution strategy so as to store or locate synonyms.

4.2. Hash Table


 A hash table is a data structure that stores elements and allows insertions, lookups,
and deletions to be performed in O(1) time.
 A hash table is an alternative method for representing a dictionary
 In a hash table, a hash function is used to map keys into positions in a table. This act
is called hashing
 Hash Table Operations
– Search: compute f(k) and see if a pair exists
– Insert: compute f(k) and place it in that position
– Delete: compute f(k) and delete the pair in that position
 In ideal situation, hash table search, insert or delete takes (1)
 Internet routers is a good example of why hash tables are required.
 A router table (especially in those routers in the
backbone networks of internet operators) may contain hundreds of thousands or
millions of entries.
When a packet has to be routed to a specific IP address, the router has to determine the
best route by querying the router table in an efficient manner. Hash Tables are used as an
efficient lookup structure having as key the IP address and as value the path that should be
follow for that address.

How Does it Work


 The table part is just an ordinary array, it is the Hash that we are interested in.
 The Hash is a function that transforms a key into address or index of array(table)
where the record will be stored. If the size of the table is N, then the integer will
be in the range 0 to N-1. The integer is used as an index into the array. Thus, in
essence, the key itself indexes the array.
 If h is a hash function and k is key then h(k) is called the hash of the key and is the
index at which a record with the key k should be placed.
The hash function generates this address by performing some simple arithmetic or logical
operations on the key.
Ideal Hashing Example
 Pairs are: (22,a),(33,c),(3,d),(72,e),(85,f)- -(key, value) pairs
 Hash table is ht[0:7], m = 8 (where m is the number of positions in the hash table)
 Hash function h is k % m = k % 8
 Where are the pairs stored?

4.3. Hash functions


Hash function Hash function is one that maps a key in the range [0 to Max − 1], the result of
which is used as an index (or address) in the hash table for storing and retrieving records.
One more way to define a hash function is as the function that transforms a key into an
address. The address generated by a hashing function is called the home address. All home
addresses refer to a particular area of the memory called the prime area.

4.3.1. Division Hash Method

 The key K is divided by some number m and the remainder is used as the
hash address of K.

 h(k)=k mod m

 This gives the indexes in the range 0 to m-1 so the hash table should be of
size m

 This is an example of uniform hash function if value of m will be chosen


carefully.

 Generally a prime number is a best choice which will spread keys evenly.

A uniform hash function is designed to distribute the keys roughly evenly into
the available positions within the array (or hash table).

For example:- Let us say apply division approach to find hash value for some values
considering number of buckets be 10 as shown below.
4.3.2. The Multiplication Method

The multiplication method for creating a hash function operates in two steps.
ä Step 1. Multiply the key k by a constant A in the range 0 < A < 1,
and extract the fractional part of kA.
ä Step 2. Multiply this value by m and take the floor of the result. Why?
In short, the hash function is
h(k) = bm·(kA mod 1)c,
where (kA mod 1) denotes the fractional part of kA, that is, kA−bkAc.

E.g., m = 10000, k = 123456, and A =


p5−1
2 = 0.618033,
then
h(k) = b10000 ·(123456 ·0.61803. . .mod 1)c
= b10000 ·(76300.0041151. . . mod 1)c
= b10000 ·0.0041151. . . c
= b41.151. . . c
= 41.
The advantage of this method is that the value choice of m is not critical.

4.3.3. The Folding Method

 The key K is partitioned into a number of parts ,each

of which has the same length as the required address with the possible
exception of the last part .

 The parts are then added together , ignoring the final carry, to form an
address.

 Example: If key=356942781 is to be transformed into a three digit address.

P1=356, P2=942, P3=781 are added to yield 079.

4.3.4. The Mid- Square Method


 The key K is multiplied by itself and the address is obtained by selecting an
appropriate number of digits from the middle of the square.

 The number of digits selected depends on the size of the table.

 Example: If key=123456 is to be transformed.

 (123456)2=15241383936

If a three-digit address is required, positions 5 to 7 could be chosen giving address


138.

4.3.5. Universal hashing

If a malicious adversary chooses keys to be hashed, then he can choose n keys that all hash
to the same slot, yielding an average retrieval time of Q(n). Any fixed hash function is
vulnerable to this sort of worst case behavior. The only effective way to improve the
situation is to choose a hash function randomly in a way that is independent of the keys that
are actually going to be stored.
This approach is referred to as universal hashing. In other words, universal hashing is to
select the hash function at random and at run time from a carefully designed collection of
hash functions.
Let H = {h1,h2, . . . ,hl} be a finite collection of hash functions that map a given
universe U of keys into a range {0,1, . . . ,m−1}.
Such a collection is said to be universal if for each pair of distinct keys x, y 2U, the
number of hash functions h 2 H for which h(x) = h(y) is at most

4.4. Characteristics of a Good Hash Function

 The hash value is fully determined by the data being hashed.

 The hash function uses all the input data.

 The hash function "uniformly" distributes the data across the entire set of possible
hash values.

The hash function generates very different hash values for similar strings.

4.5 HASH COLLISION:


A situation when the resultant hashes for two or more data elements in the data
set U, maps to the same location in the has table, is called a hash collision. In such a
situation two or more data elements would qualify to be stored/mapped to the same
location in the hash table.
4.5.1. Collision Resolution Techniques

•There are two broad ways of collision resolution:

1.Separate Chaining:: An array of linked list implementation.


2.Open Addressing: Array-based
implementation.
(i) Linear probing
(linear search)
(ii) Quadratic probing
(nonlinear search)
(iii) Double hashing (uses two hash functions)

4.5.1.1. Separate Chaining

The hash table is implemented as an array of linked lists.•Inserting an item,r, that


hashes at indexiis simply insertion into the linked list at positioni. •Synonyms are chained in
the same linked list.

•Retrieval of an item,r, with hash address,i, is simply retrieval from the linked list at
positioni.
•Deletion of an item,r, with hash address,i, is simply deletingrfrom the linked list at
positioni.
•Example:Load the keys 23, 13, 21, 14, 7, 8, and 15, in this order, in a hash table of size 7
using separate chaining with the hash function: h(key) = key % 7
4.5.1.2.Open addressing
Open addressing hash tables store the records directly within the array.
A hash collision is resolved by probing, or searching through alternate locations in the array.

• Linear probing
• Quadratic probing
• Random probing
• Double hashing

#define HASHTABLESIZE 51
typedefstruct
{
intkey[HASHTABLESIZE];
intstate[HASHTABLESIZE];
/* -1=lazy delete, 0=empty, 1=occupied */
} hashtable;
/* The hash function */
inthash(intinput)
{
return input % HASHTABLESIZE;
}

In Open addressing, if collision occurs, alternative cells are tried. h0(X), h1(X), h2(X), ...

hi(X) = (Hash(X) + F(i)) mod TableSize


Linear probing:F(i)= i
Quadratic probing:F(i)= i2
􀁻Double hashing:F(i)= i * Hash2(X)

4.5.1.2.Hashing with Linear Probe


When using a linear probe, the item will be stored in the next available slot in the table,
assuming that the table is not already full.

This is implemented via a linear search for an empty slot, from the point of collision. If the
physical end of table is reached during the linear search, the search will wrap around to the
beginning of the table and continue from there.

If an empty slot is not found before reaching the point of collision, the table is full.

A problem with the linear probe method is that it is possible for blocks of data to form when
collisions are resolved. This is known as primary clustering.

This means that any key that hashes into the cluster will require several attempts to resolve
the collision.

For example, insert the nodes 89, 18, 49, 58, and 69 into a hash table that holds 10 items
using the division method:

4.5.1.3. Hashing with Quadratic Probe

To resolve the primary clustering problem, quadratic probing can be used. With quadratic
probing, rather than always moving one spot, move i2 spots from the point of collision,
where i is the number of attempts to resolve the collision.
Limitation: at most half of the table can be used as alternative locations to resolve collisions.

This means that once the table is more than half full, it's difficult to find an empty spot. This
new problem is known as secondary clustering because elements that hash to the same
hash key will always probe the same alternative cells.

4.5.1.4. Hashing with Double Hashing

Double hashing uses the idea of applying a second hash function to the key when a collision
occurs. The result of the second hash function will be the number of positions form the
point of collision to insert.

There are a couple of requirements for the second function:

 it must never evaluate to 0


 must make sure that all cells can be probed

A popular second hash function is: Hash2(key) = R - ( key % R ) where R is a prime number
that is smaller than the size of the table.
4.5.1.5. Hashing with Rehashing

Once the hash table gets too full, the running time for operations will start to take too long
and may fail. To solve this problem, a table at least twice the size of the original will be built
and the elements will be transferred to the new table.

The new size of the hash table:

 should also be prime


 will be used to calculate the new insertion spot (hence the name rehashing)

This is a very expensive operation! O(N) since there are N elements to rehash and the table
size is roughly 2N. This is ok though since it doesn't happen that often.

The question becomes when should the rehashing be applied?

Some possible answers:

 once the table becomes half full


 once an insertion fails
 once a specific load factor has been reached, where load factor is the ratio of the
number of elements in the hash table to the table size.

4.6. Overflow Handling

 An overflow occurs when the home bucket for a new pair (key, element) is full.
 We may handle overflows by:
o Search the hash table in some systematic fashion for a bucket that is not full.
 Linear probing (linear open addressing).
 Quadratic probing.
 Random probing.
o Eliminate overflows by permitting each bucket to keep a list of all pairs for
which it is the home bucket.
 Array linear list.
 Chain

Open addressing ensures that all elements are stored directly into the hash table, thus it
attempts to resolve collisions using various methods.

 Linear Probing resolves collisions by placing the data into the next open slot in the
table.

Linear Probing – Get And Insert

 divisor = b (number of buckets) = 17.

Home bucket = key % 17.


Insert pairs whose keys are 6, 12, 34, 29, 28, 11, 23, 7, 0, 33, 30, 45

Performance Of Linear Probing

Worst-case find/insert/erase time is Q(n), where n is the number of pairs in the table.
This happens when all pairs are in the same cluster.

Expected Performance
 a = loading density = (number of pairs)/b.
o a = 12/17.
 Sn = expected number of buckets examined in a successful search when n is large
 Un = expected number of buckets examined in a unsuccessful search when n is large
 Time to put and remove is governed by Un.

 Quadratic Probing
 Linear probing searches buckets (H(x)+i2)%b
 Quadratic probing uses a quadratic function of i as the increment
 Examine buckets H(x), (H(x)+i2)%b, (H(x)-i2)%b, for 1<=i<=(b-1)/2
 b is a prime number of the form 4j+3, j is an integer

 Random Probing
 Random Probing works incorporating with random numbers.
o H(x):= (H’(x) + S[i]) % b
o S[i] is a table with size b-1
 S[i] is a random permuation of integers [1,b-1].

4.7. What is extendible hashing ?


 It is an approach that tries to make hashing dynamic,i.e. to allow insertions and
deletions to occur without resulting in poor performance after many of these
operations.
 Extendible hashing combines two ingredients: Hashing and tries.
 tries are digital trees like the one used in Lempel-¡iv
 Keys are placed into buckets,which are independent parts of a file in disk.
 Keys having a hashing address with the same prefix share the same bucket.
 Atrie is used for fast access to the buckets. It uses a prefix of the
 hashing address in order to locate the desired bucket.

Example

Assume that the hash function returns a string of bits. The first i bits of each string will
be used as indices to figure out where they will go in the "directory" (hash table).
Additionally, i is the smallest number such that the index of every item in the table is
unique.

Keys to be used:
Let's assume that for this particular example, the bucket size is 1. The first two keys to
be inserted, k1 and k2, can be distinguished by the most significant bit, and would be
inserted into the table as follows:
Now, if k3 were to be hashed to the table, it wouldn't be enough to distinguish all three keys
by one bit (because both k3 and k1 have 1 as their leftmost bit). Also, because the bucket
size is one, the table would overflow. Because comparing the first two most significant bits
would give each key a unique location, the directory size is doubled as follows:

And so now k1 and k3 have a unique location, being distinguished by the first two leftmost
bits. Because k2 is in the top half of the table, both 00 and 01 point to it because there is no
other key to compare to that begins with a 0.
Now, k4 needs to be inserted, and it has the first two bits as 01..(1110), and using a 2 bit depth in the
directory, this maps from 01 to Bucket A. Bucket A is full (max size 1), so it must be split; because
there is more than one pointer to Bucket A, there is no need to increase the directory size.

What is needed is information about:

1. The key size that maps the directory (the global depth), and
2. The key size that has previously mapped the bucket (the local depth)
In order to distinguish the two action cases:

1. Doubling the directory when a bucket becomes full


2. Creating a new bucket, and re-distributing the entries between the old and the
new bucket
Examining the initial case of an extendible hash structure, if each directory entry points
to one bucket, then the local depth should be equal to the global depth.
The number of directory entries is equal to 2global depth, and the initial number of buckets
is equal to 2local depth.
Thus if global depth = local depth = 0, then 20 = 1, so an initial directory of one pointer to
one bucket.
Back to the two action cases:
If the local depth is equal to the global depth, then there is only one pointer to the
bucket, and there is no other directory pointers that can map to the bucket, so the
directory must be doubled (case1).
If the bucket is full, if the local depth is less than the global depth, then there exists
more than one pointer from the directory to the bucket, and the bucket can be split
(case 2).
Key 01 points to Bucket A, and Bucket A's local depth of 1 is less than the directory's global
depth of 2, which means keys hashed to Bucket A have only used a 1 bit prefix (i.e. 0), and
the bucket needs to have its contents split using keys 1 + 1 = 2 bits in length; in general, for
any local depth d where d is less than D, the global depth, then d must be incremented after
a bucket split, and the new d used as the number of bits of each entry's key to redistribute
the entries of the former bucket into the new buckets.

Now,
is tried again, with 2 bits 01.., and now key 01 points to a new bucket but there is still B K1 in
it (B K1 and also begins with 01). If ADBC had been 000110, with key 00, there would have
been no problem, because 00 would have remained in the new bucket A' and bucket D
would have been empty.
So Bucket D needs to be split, but a check of its local depth, which is 2, is the same as the
global depth, which is 2, so the directory must be split again, in order to hold keys of
sufficient detail, e.g. 3 bits.

1. Bucket D needs to split due to being full.


2. As D's local depth = the global depth, the directory must double to increase bit
detail of keys.
3. Global depth has incremented after directory split to 3.
4. The new entry is rekeyed with global depth 3 bits and ends up in D which has
local depth 2, which can now be incremented to 3 and D can be split to D' and E.

5. The contents of the split bucket D, , has been re-keyed with 3 bits, and it ends
up in D.
6. K4 is retried and it ends up in E which has a spare slot.

Now, k2 is in D and E is tried again, with 3 bits 011.., and it points to bucket D which already
contains k2 so is full; D's local depth is 2 but now the global depth is 3 after the directory
doubling, so now D can be split into bucket's D' and E, the contents of D, k2 has its retried
with a new global depth bitmask of 3 and ends up in D', then the new entry is retried
with bitmasked using the new global depth bit count of 3 and this gives 011 which now
points to a new bucket E which is empty. So k1 goes in Bucket E.

4.8. Dictionary
a dictionary (table) is an abstract model of a database like a priority queue, a dictionary
stores key-element pairs. the main operation supported by a dictionary is searching by key

4.8.1.Dictionary ADT

The dictionary ADT models a searchable collection of keyelement items. The main
operations of a dictionary are searching, inserting, and deleting items.
Multiple items with the same key are allowed.

4.8.2. Applications:
 address book
 credit card authorization
 mapping host names (e.g., cs16.net) to internet addresses (e.g., 128.148.34.101)

4.8.3. Dictionary ADT methods:

 find(k): if the dictionary has an item with key k, returns the position of this element,
 else, returns a null position.
 insertItem(k, o): inserts item (k, o) into the dictionary
 removeElement(k): if the dictionary has an item with key k, removes it from the
dictionary and returns its element. An error occurs if there is no such element.
 size(), isEmpty()
 keys(), Elements()

4.8.4. Log File

A log file is a dictionary implemented by means of an unsorted sequence


We store the items of the dictionary in a sequence (based on a doubly-linked lists or a
circular array), in arbitrary order
Performance: insertItem takes O(1) time since we can insert the new item at the
beginning or at the end of the sequence. find and removeElement take O(n) time since in
the worst case (the item is not found) we traverse the entire sequence to look for
an item with the given key The log file is effective only for dictionaries of small size or for
dictionaries on which insertions are the most common operations, while searches and
removals are rarely performed .

4.9. Skip Lists

 Skip lists improve the performance of insert and delete operations


 Employ a randomization technique to determine where and how many to put
additional forward pointers
 The expected performance of search and delete operations on skip lists is O(logn)
However, the worst-case performance is (n)

Skip List – pointers, search, insert

Skip List – Insertions & Deletions


 When insertions or deletions occur, we require O(n) work to maintain the structure
of skip lists

 When an insertion is made, the pair level is i with probability 1/2i

 We can assign the newly inserted pair at level i with probability pi

 For general p, the number of chain levels is log1/pn + 1

 See above Figure for inserting 77

We have no control over the structure that is left following a deletion

Skip List – Assigning Levels

 The level assignment of newly inserted pair is done using a random number
generator (0 to RAND_MAX)

 The probability that the next random number is  Cutoff = p * RAND_MAX is p

 The following is used to assign a level number

int lev = 0

while (rand() <= CutOff) lev++;

In a regular skip list structure with N pairs, the maximum level is log1/pN - 1

4.10. Heaps

Heap is a special case of balanced binary tree data structure where the root-node key is
compared with its children and arranged accordingly. If α has child node β then −

key(α) ≥ key(β)

As the value of parent is greater than that of child, this property generates Max Heap.
Based on this criteria, a heap can be of two types −

For Input → 35 33 42 10 14 19 27 44 26 31
Min-Heap − Where the value of the root node is less than or equal to either of its children.
Max-Heap − Where the value of the root node is greater than or equal to either of its
children.

Both trees are constructed using the same input and order of arrival.

Applications: A heap has many applications, including the most efficient implementation of
priority queues, which are useful in many applications. In particular, heaps are crucial in
several efficient graph algorithms.

Variants:

 2-3 heap
 Binary heap
 Many many others

Binary heap storage rules -- A heap implemented with a binary tree in which the following
two rules are followed:

 The element contained by each node is greater than or equal to the elements of that
node's children.
 The tree is a complete binary tree.

Example: which one is a heap?

Adding an Element to a Heap

Example: We want to insert a node with value 42 to the heap on the left.
The above process is called reheapification upward.

Pseudocode for Adding an Element:

1. Place the new element in the heap in the first available location. This keeps the
structure as a complete binary tree, but it might no longer be a heap since the new
element might have a greater value than its parent.
2. while (the new element has a greater value than its parent) swap the new element
with its parent.
3. Notice that Step 2 will stop when the new element reaches the root or when the
new element's parent has a value greater than or equal to the new element's
value.

Removing the Root of a Heap

The procedure for deleting the root from the heap -- effectively extracting the maximum
element in a max-heap or the minimum element in a min-heap.

The above process is called reheapification downward.

Psuedocode for Removing the Root:

1. Copy the element at the root of the heap to the variable used to return a value.
2. Copy the last element in the deepest level to the root and then take this last node
out of the tree. This element is called the "out-of-place" element.
3. while (the out-of-place element has a value that is lower than one of its children)
swap the out-of-place element with its greatest-value child.
4. Return the answer that was saved in Step 1.
5. Notice that Step 3 will stop when the out-of-place element reaches a leaf or it has
a value that is greater or equal to all its children.

Now, think about how to build a heap. Check out the example of inserting 27, 35, 23, 22,
4, 45, 21, 5, 42 and 19 to an empty heap.

4.10. Heap Implementation

A more common approach is to store the heap in an array. Since heap is always a complete
binary tree, it can be stored compactly. No space is required for pointers; instead, the
parent and children of each node can be found by simple arithmetic on array indices.

The rules (assume the root is stored in arr[0]):

 For each index i, element arr[i] has children at arr[2i + 1] and arr[2i + 2], and the
parent at arr[floor( ( i - 1 )/2 )].

This implementation is particularly useful in the heapsort algorithm, where it allows the
space in the input array to be reused to store the heap (i.e., the algorithm is in-place).
However it requires allocating the array before filling it, which makes this method not that
useful in priority queues implementation, where the number of elements is unknown.

It is perfectly acceptable to use a traditional binary tree data structure to implement a


binary heap. There is an issue with finding the adjacent element on the last level on the
binary heap when adding an element. Here's a method we can follow.

4.11. Building a Heap

A heap could be built by successive insertions. This approach requires O(n log n) time
for n elements. Why?

The optimal method:

 Starts by arbitrarily putting the elements on a binary tree.


 Starting from the lowest level and moving upwards until the heap property is
restored by shifting the root of the subtree downward as in the removal algorithm.
 If all the subtrees at some height h (measured from the bottom) have already been
"heapified", the trees at hight h+1 can be heapified by sending their root down. This
process takes O(h) swaps.

As an example, let's build a heap with the following values: 20, 35, 23, 22, 4, 45, 21, 5, 42
and 19. Click here to see the process.

As has been proved here, this optimal method requires O(n) time for n elements.

4.12 Applications of Heaps:

1) Heap Sort: Heap Sort uses Binary Heap to sort an array in O(nLogn) time.
2) Priority Queue: Priority queues can be efficiently implemented using Binary Heap because
it supports insert(), delete() and extractmax(), decreaseKey() operations in O(logn) time.
Binomoial Heap and Fibonacci Heap are variations of Binary Heap. These variations perform
union also efficiently.
3) Graph Algorithms: The priority queues are especially used in Graph Algorithms
like Dijkstra’s Shortest Pathand Prim’s Minimum Spanning Tree.
4) Many problems can be efficiently solved using Heaps. See following for example.
a) K’th Largest Element in an array.
b) Sort an almost sorted array/
c) Merge K Sorted Arrays.

4.13. Indexing
We know that data is stored in the form of records. Every record has a key field, which
helps it to be recognized uniquely.

Indexing is a data structure technique to efficiently retrieve records from the database files
based on some attributes on which the indexing has been done. Indexing in database
systems is similar to what we see in books.

Indexing is defined based on its indexing attributes. Indexing can be of the following types

 Primary Index − Primary index is defined on an ordered data file. The data file is
ordered on a key field. The key field is generally the primary key of the relation.

 Secondary Index − Secondary index may be generated from a field which is a


candidate key and has a unique value in every record, or a non-key with duplicate
values.

 Clustering Index − Clustering index is defined on an ordered data file. The data file is
ordered on a non-key field.
Ordered Indexing is of two types −

 Dense Index
 Sparse Index
Dense Index
In dense index, there is an index record for every search key value in the database. This
makes searching faster but requires more space to store index records itself. Index records
contain search key value and a pointer to the actual record on the disk.

Sparse Index
In sparse index, index records are not created for every search key. An index record here
contains a search key and an actual pointer to the data on the disk. To search a record, we
first proceed by index record and reach at the actual location of the data. If the data we are
looking for is not where we directly reach by following the index, then the system starts
sequential search until the desired data is found.

Multilevel Index
Index records comprise search-key values and data pointers. Multilevel index is stored on
the disk along with the actual database files. As the size of the database grows, so does the
size of the indices. There is an immense need to keep the index records in the main
memory so as to speed up the search operations. If single-level index is used, then a large
size index cannot be kept in memory which leads to multiple disk accesses.
Multi-level Index helps in breaking down the index into several smaller indices in order to
make the outermost level so small that it can be saved in a single disk block, which can
easily be accommodated anywhere in the main memory.

4.14. Multiway Search tree

 A multiway tree is a tree that can have more than two children
 A multiway tree of order m88 (or an **m-way tree) is one in which a tree can have
m children.
 An m-way search tree is a m-way tree in which:
a) Each node has m children and m-1 key fields
b) The keys in each node are in ascending order.
c) The keys in the first i children are smaller than the ithith key
d) The keys in the last m-i children are larger than the ithith key

 In a binary search tree, m=2. So it has one value and two sub trees.
 The figure above is a m-way search tree of order 3.
 M-way search trees give the same advantages to m-way trees that binary search
trees gave to binary trees - they provide fast information retrieval and update.
 However, they also have the same problems that binary search trees had - they can
become unbalanced, which means that the construction of the tree becomes of vital
importance.
 In m-way search tree, each sub-tree is also a m-way search tree and follows the
same rules.
 An extension of a multiway search tree of order m is a B-tree of order m.
 This type of tree will be used when the data to be accessed/stored is located on
secondary storage devices because they allow for large amounts of data to be stored
in a node.

4.15. B - Trees

In a binary search tree, AVL Tree, Red-Black tree etc., every node can have only one value
(key) and maximum of two children but there is another type of search tree called B-Tree in
which a node can store more than one value (key) and it can have more than two children.
B-Tree was developed in the year of 1972 by Bayer and McCreight with the name Height
Balanced m-way Search Tree. Later it was named as B-Tree.

B-Tree can be defined as follows...

B-Tree is a self-balanced search tree with multiple keys in every node and more than two
children for every node.

Here, number of keys in a node and number of children for a node is depend on the order of
the B-Tree. Every B-Tree has order.

B-Tree of Order m has the following properties...

 Property #1 - All the leaf nodes must be at same level.


 Property #2 - All nodes except root must have at least [m/2]-1 keys and maximum
of m-1 keys.
 Property #3 - All non leaf nodes except root (i.e. all internal nodes) must have at
least m/2 children.
 Property #4 - If the root node is a non leaf node, then it must have at least
2 children.
 Property #5 - A non leaf node with n-1 keys must have n number of children.
 Property #6 - All the key values within a node must be in Ascending Order.

For example, B-Tree of Order 4 contains maximum 3 key values in a node and maximum 4
children for a node.
Example

Operations on a B-Tree

The following operations are performed on a B-Tree...

1. Search

2. Insertion

3. deletion

Search Operation in B-Tree

In a B-Ttree, the search operation is similar to that of Binary Search Tree. In a Binary search
tree, the search process starts from the root node and every time we make a 2-way decision
(we go to either left subtree or right subtree). In B-Tree also search process starts from the
root node but every time we make n-way decision where n is the total number of children
that node has. In a B-Ttree, the search operation is performed with O(log n) time
complexity. The search operation is performed as follows...

 Step 1: Read the search element from the user


 Step 2: Compare, the search element with first key value of root node in the tree.
 Step 3: If both are matching, then display "Given node found!!!" and terminate the
function
 Step 4: If both are not matching, then check whether search element is smaller or
larger than that key value.
 Step 5: If search element is smaller, then continue the search process in left subtree.
 Step 6: If search element is larger, then compare with next key value in the same
node and repeate step 3, 4, 5 and 6 until we found exact match or comparision
completed with last key value in a leaf node.
 Step 7: If we completed with last key value in a leaf node, then display "Element is
not found" and terminate the function.

Insertion Operation in B-Tree

In a B-Tree, the new element must be added only at leaf node. That means, always the new
keyValue is attached to leaf node only. The insertion operation is performed as follows...

 Step 1: Check whether tree is Empty.


 Step 2: If tree is Empty, then create a new node with new key value and insert into
the tree as a root node.
 Step 3: If tree is Not Empty, then find a leaf node to which the new key value cab be
added using Binary Search Tree logic.
 Step 4: If that leaf node has an empty position, then add the new key value to that
leaf node by maintaining ascending order of key value within the node.
 Step 5: If that leaf node is already full, then split that leaf node by sending middle
value to its parent node. Repeat tha same until sending value is fixed into a node.
 Step 6: If the spilting is occuring to the root node, then the middle value becomes
new root node for the tree and the height of the tree is increased by one.

Example

Construct a B-Tree of Order 3 by inserting numbers from 1 to 10.


4.16. What is a B+-tree?

Most queries can be executed more quickly if the values are stored in order. But it's not
practical to hope to store all the rows in the table one after another, in sorted order,
because this requires rewriting the entire table with each insertion or deletion of a row.

This leads us to instead imagine storing our rows in a tree structure. Our first instinct would
be a balanced binary search tree like a red-black tree, but this really doesn't make much
sense for a database since it is stored on disk. You see, disks work by reading and writing
whole blocks of data at once — typically 512 bytes or four kilobytes. A node of a binary
search tree uses a small fraction of that, so it makes sense to look for a structure that fits
more neatly into a disk block.

Hence the B+-tree, in which each node stores up to d references to children and up to d − 1
keys. Each reference is considered “between” two of the node's keys; it references the root
of a subtree for which all values are between these two keys.

Here is a fairly small tree using 4 as our value for d.

A B+-tree requires that each leaf be the same distance from the root, as in this picture,
where searching for any of the 11 values (all listed on the bottom level) will involve loading
three nodes from the disk (the root block, a second-level block, and a leaf).

In practice, d will be larger — as large, in fact, as it takes to fill a disk block. Suppose a block
is 4KB, our keys are 4-byte integers, and each reference is a 6-byte file offset. Then we'd
choose d to be the largest value so that 4 (d − 1) + 6 d ≤ 4096; solving this inequality for d,
we end up with d ≤ 410, so we'd use 410 for d. As you can see, d can be large.

A B+-tree maintains the following invariants:

 Every node has one more references than it has keys.


 All leaves are at the same distance from the root.
 For every non-leaf node N with k being the number of keys in N: all keys in the first
child's subtree are less than N's first key; and all keys in the ith child's subtree
(2 ≤ i ≤ k) are between the (i − 1)th key of n and the ith key of n.
 The root has at least two children.
 Every non-leaf, non-root node has at least floor(d / 2) children.
 Each leaf contains at least floor(d / 2) keys.
 Every key from the table appears in a leaf, in left-to-right sorted order.

In our examples, we'll continue to use 4 for d. Looking at our invariants, this requires that
each leaf have at least two keys, and each internal node to have at least two children (and
thus at least one key).

2. Insertion algorithm

Descend to the leaf where the key fits.

1. If the node has an empty space, insert the key/reference pair into the node.
2. If the node is already full, split it into two nodes, distributing the keys evenly
between the two nodes. If the node is a leaf, take a copy of the minimum value in
the second of these two nodes and repeat this insertion algorithm to insert it into
the parent node. If the node is a non-leaf, exclude the middle value during the split
and repeat this insertion algorithm to insert this excluded value into the parent
node.

Initial:

Insert 20:

Insert 13:

Insert 15:
Insert 10:

Insert 11:

Insert 12:

3. Deletion algorithm

Descend to the leaf where the key exists.

1. Remove the required key and associated reference from the node.
2. If the node still has enough keys and references to satisfy the invariants, stop.
3. If the node has too few keys to satisfy the invariants, but its next oldest or next
youngest sibling at the same level has more than necessary, distribute the keys
between this node and the neighbor. Repair the keys in the level above to represent
that these nodes now have a different “split point” between them; this involves
simply changing a key in the levels above, without deletion or insertion.
4. If the node has too few keys to satisfy the invariant, and the next oldest or next
youngest sibling is at the minimum for the invariant, then merge the node with its
sibling; if the node is a non-leaf, we will need to incorporate the “split key” from the
parent into our merging. In either case, we will need to repeat the removal algorithm
on the parent node to remove the “split key” that previously separated these
merged nodes — unless the parent is the root and we are removing the final key
from the root, in which case the merged node becomes the new root (and the tree
has become one level shorter than before).

Initial:
Delete 13:

Delete 15:

Delete 1:

4.17. File structure


A file is a collection of records which are related to each other. The size of file is limited by
the size of memory and storage medium.

Two characteristics determine how the file is organised:

I. File Activity:
It specifies that percent of actual records proceeds in single run. If a small percent of record
is accessed at any given time, the file should be organized on disk for the direct access in
contrast. If a fare percentage of records affected regularly than storing the file on tape
would be more efficient & less costly.

II. File Volatility:


It addresses the properties of record changes. File records with many changes are highly
volatile means the disk design will be more efficient than tape.

4.18. External storage


Classification of Physical Storage Media
Speed with which data can be accessed
Cost per unit of data
Reliability
data loss on power failure or system crash
physical failure of the storage device
Can differentiate storage into:
volatile storage: loses contents when power is switched off
non-volatile storage:
 Contents persist even when power is switched off.
Includes secondary and tertiary storage, as well as batter- backed up main-memory
Cache – fastest and most costly form of storage; volatile; managed by the computer
system hardware.
Main memory:
fast access (10s to 100s of nanoseconds; 1 nanosecond = 10–9 seconds)
generally too small (or too expensive) to store the entire database
 capacities of up to a few Gigabytes widely used currently
 Capacities have gone up and per-byte costs have decreased steadily
and rapidly (roughly factor of 2 every 2 to 3 years)
Volatile — contents of main memory are usually lost if a power failure or system crash
occurs.
Flash memory
Data survives power failure
Data can be written at a location only once, but location can be erased and
written to again
 Can support only a limited number (10K – 1M) of write/erase cycles.
 Erasing of memory has to be done to an entire bank of memory
Reads are roughly as fast as main memory
But writes are slow (few microseconds), erase is slower
Widely used in embedded devices such as digital cameras, phones, and USB keys
Magnetic-disk
Data is stored on spinning disk, and read/written magnetically
Primary medium for the long-term storage of data; typically stores entire
database.
Data must be moved from disk to main memory for access, and written back
for storage
 Much slower access than main memory (more on this later)
direct-access – possible to read data on disk in any order, unlike magnetic
tape
Capacities range up to roughly 1.5 TB as of 2009
 Much larger capacity and cost/byte than main memory/flash memory
 Growing constantly and rapidly with technology improvements (factor
of 2 to 3 every 2 years)
Survives power failures and system crashes
disk failure can destroy data, but is rare
Optical storage
non-volatile, data is read optically from a spinning disk using a laser
CD-ROM (640 MB) and DVD (4.7 to 17 GB) most popular forms
Blu-ray disks: 27 GB to 54 GB
Write-one, read-many (WORM) optical disks used for archival storage (CD-R,
DVD-R, DVD+R)
Multiple write versions also available (CD-RW, DVD-RW, DVD+RW, and DVD-
RAM)
Reads and writes are slower than with magnetic disk
Juke-box systems, with large numbers of removable disks, a few drives, and a mechanism
for automatic loading/unloading of disks available for storing large volumes of data
Tape storage
non-volatile, used primarily for backup (to recover from disk failure), and for
archival data
sequential-access – much slower than disk
very high capacity (40 to 300 GB tapes available)
tape can be removed from drive  storage costs much cheaper than disk,
but drives are expensive
Tape jukeboxes available for storing massive amounts of data
 hundreds of terabytes (1 terabyte = 109 bytes) to even multiple
petabytes (1 petabyte = 1012 bytes)

4.18. Storage Hierarchy

4.19.File organisation
A file is organised to ensure that records are available for processing. It should be designed
with the activity and volatility information and the nature of storage media, Other
consideration are cost of file media, enquiry, requirements of users and file’s privacy,
integrity, security and confidentiality.
The various file organization methods are:

 Sequential access.
 Direct or random access.
 Index sequential access.

The selection of a particular method depends on:

 Type of application.
 Method of processing.
 Size of the file.
 File inquiry capabilities.
 File volatility.
 The response time.
4.19.1. Sequential access method
Sequential access method: Here the records are arranged in the ascending or descending
order or chronological order of a key field which may be numeric or both. Since the records
are ordered by a key field, there is no storage location identification. It is used in
applications like payroll management where the file is to be processed in entirety, i.e. each
record is processed. Here, to have an access to a particular record, each record must be
examined until we get the desired record. Sequential files are normally created and stored
on magnetic tape using batch processing method.

Advantages:

 Simple to understand.
 Easy to maintain and organize
 Loading a record requires only the record key.
 Relatively inexpensive I/O media and devices can be used.
 Easy to reconstruct the files.
 The proportion of file records to be processed is high.

Disadvantages:

 Entire file must be processed, to get specific information.


 Very low activity rate stored.
 Transactions must be stored and placed in sequence prior to processing.
 Data redundancy is high, as same data can be stored at different places with
different keys.
 Impossible to handle random enquiries.

Direct access files organization: (Random or relative organization). Files in his type are
stored in direct access storage devices such as magnetic disk, using an identifying key. The
identifying key relates to its actual storage position in the file. The computer can directly
locate the key to find the desired record without having to search through any other record
first. Here the records are stored randomly, hence the name random file. It uses online
system where the response and updation are fast.

Advantages:

 Records can be immediately accessed for updation.


 Several files can be simultaneously updated during transaction processing.
 Transaction need not be sorted.
 Existing records can be amended or modified.
 Very easy to handle random enquiries.
 Most suitable for interactive online applications.

Disadvantages:

 Data may be accidentally erased or over written unless special precautions are
taken.
 Risk of loss of accuracy and breach of security. Special backup and reconstruction
procedures must be established.
 Less efficient use of storage space.
 Expensive hardware and software are required.
 High complexity in programming.
 File updation is more difficult when compared to that of sequential method.

4.19.2.Indexed sequential access organization


Indexed sequential access organization: Here the records are stored sequentially on a
direct access device i.e. magnetic disk and the data is accessible randomly and sequentially.
It covers the positive aspects of both sequential and direct access files.
The type of file organization is suitable for both batch processing and online processing.
Here, the records are organized in sequence for efficient processing of large batch jobs but
an index is also used to speed up access to the records.
Indexing permit access to selected records without searching the entire file.

Advantages:

 Permits efficient and economic use of sequential processing technique when the
activity rate is high.
 Permits quick access to records, in a relatively efficient way when this activity is a
fraction of the work load.

Disadvantages:

 Slow retrieval, when compared to other methods.


 Does not use the storage space efficiently.
 Hardware and software used are relatively expensive.

4.19.3. Linked Organzation


 In linked organization, the physical sequence of records is different from the logical
sequence
 of records. The next logical record is obtained by following a link value from
 the present record. Records are linked according to increasing primary key, so
insertion
 and deletion is easy. If index is not maintained, then direct searching is diffi cul t and
only
 sequential search is possible.

You might also like