Thanks to visit codestin.com
Credit goes to www.scribd.com

0% found this document useful (0 votes)
5 views100 pages

DS Lecture Notes

This document provides an overview of abstract data types (ADTs) and abstract data structures, emphasizing their importance in programming and algorithm design. It details various types of linked lists, including singly, doubly, and circularly linked lists, along with their operations and implementations. Additionally, the document introduces the stack ADT, its operations, and its applications in programming.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views100 pages

DS Lecture Notes

This document provides an overview of abstract data types (ADTs) and abstract data structures, emphasizing their importance in programming and algorithm design. It details various types of linked lists, including singly, doubly, and circularly linked lists, along with their operations and implementations. Additionally, the document introduces the stack ADT, its operations, and its applications in programming.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 100

UNIT I Linear Structures

Abstract data type

An abstract data type (ADT) is a specification of a set of data and the set of
operations that can be performed on the data. Such a data type is abstract in the sense that it is
independent of various concrete implementations. The definition can be mathematical, or it can
be programmed as an interface. A first class ADT supports the creation of multiple instances of
the ADT, and the interface normally provides a constructor, which returns an abstract handle
to new data, and several operations, which are functions accepting the abstract handle as an
argument.

Abstract data types (ADT) typically seen in textbooks and implemented in programming
languages (or their libraries) include:

Complex number, Container, Deque, List, Map, Multimap, Multiset, Priority queue
Queue, Set, Stack, String, and Tree.

Abstract data structure

An abstract data structure is an abstract storage for data defined in terms of the set
of operations to be performed on data and computational complexity for performing these
operations, regardless of the implementation in a concrete data structure.

Selection of an abstract data structure is crucial in the design of efficient algorithms and in
estimating their computational complexity, while selection of concrete data structures is
important for efficient implementation of algorithms.

This notion is very close to that of an abstract data type, used in the theory of
programming languages. The names of many abstract data structures (and abstract data types)
match the names of concrete data structures.

Built-in abstract data types

Because some ADTs are so common and useful in computer programs, some
programming languages build implementations of ADTs into the language as native types or
add them into their standard libraries. For instance, Perl arrays can be thought of as an
implementation of the List or Deque ADTs and Perl hashes can be thought of in terms of Map
or Table ADTs. The C++ Standard Library and Java libraries provide classes that implement
the List, Stack, Queue, Map, Priority Queue, and String ADTs.

Linked list ADT

A linked list is one of the fundamental data structures, and can be used to implement
other data structures. It consists of a sequence of nodes, each containing arbitrary data fields
and one or two references ("links") pointing to the next and/or previous nodes. The principal
benefit of a linked list over a conventional array is that the order of the linked items may be
different from the order that the data items are stored in memory or on disk, allowing the list of
items to be traversed in a different order. A linked list is a self-referential datatype because it

1
contains a pointer or link to another datum of the same type. Linked lists permit insertion and
removal of nodes at any point in the list in constant time, but do not allow random access.
Several different types of linked list exist: singly-linked lists, doubly-linked lists, and
circularly-linked lists.

Representation of linked lists


Array representation
Linked list representation

Types of linked lists - 1. Linearly linked list

1. (a)Singly-linked list

The simplest kind of linked list is a singly-linked list (or slist for short), which has one link per
node. This link points to the next node in the list, or to a null value or empty list if it is the final
node.

A singly linked list's node is divided into two parts. The first part holds or points to information
about the node, and second part holds the address of next node. A singly linked list travels one
way.

1. (b)Doubly-linked list

A more sophisticated kind of linked list is a doubly-linked list or two-way linked list. Each
node has two links: one points to the previous node, or points to a null value or empty list if it
is the first node; and one points to the next, or points to a null value or empty list if it is the
final node.

2.Circularly-linked list

In a circularly-linked list, the first and final nodes are linked together. This can be done for
both singly and doubly linked lists. To traverse a circular linked list, you begin at any node and
follow the list in either direction until you return to the original node. Viewed another way,
circularly-linked lists can be seen as having no beginning or end. This type of list is most
useful for managing buffers for data ingest, and in cases where you have one object in a list
and wish to see all other objects in the list.

The pointer pointing to the whole list may be called the access pointer.

Linked list operations

When manipulating linked lists in-place, care must be taken to not use values that you have
invalidated in previous assignments. This makes algorithms for inserting or deleting linked list
nodes somewhat subtle. This section gives pseudo code for adding or removing nodes from
singly, doubly, and circularly linked lists in-place. Throughout we will use null to refer to an
end-of-list marker or sentinel, which may be implemented in a number of ways.

2
Linearly-linked lists

1.Singly-linked lists

Our node data structure will have two fields. We also keep a variable firstNode which always
points to the first node in the list, or is null for an empty list.

record Node {
data // The data being stored in the node
next // A reference to the next node, null for last node
}
record List {
Node firstNode // points to first node of list; null for
empty list
}

Traversal of a singly-linked list is simple, beginning at the first node and following each next
link until we come to the end:

node := list.firstNode
while node not null {
(do something with node.data)
node := node.next
}

The following code inserts a node after an existing node in a singly linked list. The diagram
shows how it works. Inserting a node before an existing one cannot be done; instead, you have
to locate it while keeping track of the previous node.

Insert Algorithm

function insertAfter(Node node, Node newNode) { // insert


newNode after node
newNode.next := node.next
node.next := newNode
}

Inserting at the beginning of the list requires a separate function. This requires updating
firstNode.

function insertBeginning(List list, Node newNode) { // insert


node before current first node
newNode.next := list.firstNode
list.firstNode := newNode
}

Similarly, we have functions for removing the node after a given node, and for removing a
node from the beginning of the list. The diagram demonstrates the former. To find and remove
a particular node, one must again keep track of the previous element.

3
Delete Algorithm

function removeAfter(Node node) { // remove node past this one


obsoleteNode := node.next
node.next := node.next.next
destroy obsoleteNode
}
function removeBeginning(List list) { // remove first node
obsoleteNode := list.firstNode
list.firstNode := list.firstNode.next // point
past deleted node
destroy obsoleteNode
}

Notice that removeBeginning() sets list.firstNode to null when removing the last node in the
list.

Since we can't iterate backwards, efficient "insertBefore" or "removeBefore" operations are not
possible.

Appending one linked list to another can be inefficient unless a reference to the tail is kept as
part of the List structure, because we must traverse the entire first list in order to find the tail,
and then append the second list to this. Thus, if two linearly-linked lists are each of length n,
list appending has asymptotic time complexity of O(n). In the Lisp family of languages, list
appending is provided by the append procedure.

Many of the special cases of linked list operations can be eliminated by including a dummy
element at the front of the list. This ensures that there are no special cases for the beginning of
the list and renders both insertBeginning() and removeBeginning() unnecessary. In this case,
the first useful data in the list will be found at list.firstNode.next.

2.Doubly-linked lists

With doubly-linked lists there are even more pointers to update, but also less information is
needed, since we can use backwards pointers to observe preceding elements in the list. This
enables new operations, and eliminates special-case functions. We will add a prev field to our
nodes, pointing to the previous element, and a lastNode field to our list structure which always
points to the last node in the list. Both list.firstNode and list.lastNode are null for an empty list.

record Node {
data // The data being stored in the node
next // A reference to the next node; null for last node
prev // A reference to the previous node; null for first
node
}
record List {
Node firstNode // points to first node of list; null for
empty list

4
Node lastNode // points to last node of list; null for
empty list}

Iterating through a doubly linked list can be done in either direction. In fact, direction can
change many times, if desired.

Forwards

node := list.firstNode
while node ≠ null
<do something with node.data>
node := node.next

Backwards

node := list.lastNode
while node ≠ null
<do something with node.data>
node := node.prev

These symmetric functions add a node either after or before a given node, with the diagram
demonstrating after:

Insert algorithm for Double Linked List

function insertAfter(List list, Node node, Node newNode)


newNode.prev := node
newNode.next := node.next
if node.next = null
list.lastNode := newNode
else
node.next.prev := newNode
node.next := newNode

function insertBefore(List list, Node node, Node newNode)


newNode.prev := node.prev
newNode.next := node
if node.prev is null
list.firstNode := newNode
else
node.prev.next := newNode
node.prev := newNode

We also need a function to insert a node at the beginning of a possibly-empty list:

function insertBeginning(List list, Node newNode)


if list.firstNode = null
list.firstNode := newNode
list.lastNode := newNode
newNode.prev := null
5
newNode.next := null
else
insertBefore(list, list.firstNode, newNode)

A symmetric function inserts at the end:

function insertEnd(List list, Node newNode)


if list.lastNode = null
insertBeginning(list, newNode)
else
insertAfter(list, list.lastNode, newNode)

Delete algorithm for Double Linked List

Removing a node is easier, only requiring care with the firstNode and lastNode:

function remove(List list, Node node)


if node.prev = null
list.firstNode := node.next
else
node.prev.next := node.next
if node.next = null
list.lastNode := node.prev
else
node.next.prev := node.prev
destroy node
One subtle consequence of this procedure is that deleting the last element of a list sets
both firstNode and lastNode to null, and so it handles removing the last node from a one-
element list correctly. Notice that we also don't need separate "removeBefore" or
"removeAfter" methods, because in a doubly-linked list we can just use "remove(node.prev)"
or "remove(node.next)" where these are valid.

3.Circularly-linked list

Circularly-linked lists can be either singly or doubly linked. In a circularly linked list,
all nodes are linked in a continuous circle, without using null. For lists with a front and a back
(such as a queue), one stores a reference to the last node in the list. The next node after the last
node is the first node. Elements can be added to the back of the list and removed from the front
in constant time.

Both types of circularly-linked lists benefit from the ability to traverse the full list
beginning at any given node. This often allows us to avoid storing firstNode and lastNode,
although if the list may be empty we need a special representation for the empty list, such as a
lastNode variable which points to some node in the list or is null if it's empty; we use such a
lastNode here. This representation significantly simplifies adding and removing nodes with a
non-empty list, but empty lists are then a special case.

6
Doubly-circularly-linked lists

Assuming that someNode is some node in a non-empty list, this code iterates through that list
starting with someNode (any node will do):

Forwards

node := someNode
do
do something with node.value
node := node.next
while node ≠ someNode

Backwards

node := someNode
do
do something with node.value
node := node.prev
while node ≠ someNode

Notice the postponing of the test to the end of the loop. This is important for the case where the
list contains only the single node someNode.

Insert Algorithm for Circular List

This simple function inserts a node into a doubly-linked circularly-linked list after a given
element:

function insertAfter(Node node, Node newNode)


newNode.next := node.next
newNode.prev := node
node.next.prev := newNode
node.next := newNode

To do an "insertBefore", we can simply "insertAfter(node.prev, newNode)". Inserting an


element in a possibly empty list requires a special function:

function insertEnd(List list, Node node)


if list.lastNode = null
node.prev := node
node.next := node
else
insertAfter(list.lastNode, node)
list.lastNode := node

To insert at the beginning we simply "insertAfter(list.lastNode, node)". Finally, removing a


node must deal with the case where the list empties:

Delete Algorithm for Circular List


7
function remove(List list, Node node)
if node.next = node
list.lastNode := null
else
node.next.prev := node.prev
node.prev.next := node.next
if node = list.lastNode
list.lastNode := node.prev;
destroy node

As in doubly-linked lists, "removeAfter" and "removeBefore" can be implemented with


"remove(list, node.prev)" and "remove(list, node.next)".

Stack ADT - Introduction

STACK is a special type of data structure where insertion is done from one end called
„top‟ of stack and deletion will be done from the same end. A stack is a limited version of an
array. New elements, or nodes as they are often called, can be added to a stack and removed
from a stack only from one end. For this reason, a stack is referred to as a LIFO structure (Last-
In First-Out).

Stacks have many applications. For example, as processor executes a program, when a
function call is made, the called function must know how to return back to the program, so the
current address of program execution is pushed onto a stack. Once the function is finished, the
address that was saved is removed from the stack, and execution of the program resumes. If a
series of function calls occur, the successive return values are pushed onto the stack in LIFO
order so that each function can return back to calling program. Stacks support recursive
function calls in the same manner as conventional nonrecursive calls.

Stacks are also used by compilers in the process of evaluating expressions and generating
machine language code. They are also used to store return addresses in a chain of method calls
during execution of a program.

Operations
An abstract data type (ADT) consists of a data structure and a set of primitive
operations. The main primitives of a stack are known as:

Push adds a new node


Pop removes a node
Additional primitives can be defined:
IsEmpty reports whether the stack is empty
IsFull reports whether the stack is full
Initialise creates/initialises the stack
Destroy deletes the contents of the stack (may be implemented by re-initialising
the stack)
Initialise
Creates the structure – i.e. ensures that the structure exists but contains no elements
e.g. Initialise(S) creates a new empty stack named S
Push
e.g. Push(X,S) adds the value X to the TOP of stack S
8
Pop
e.g. Pop(S) removes the TOP node and returns its value
There are several applications where a stack can be used. For example, recursion, keeping tack
of function calls, evaluation of expression etc.

Implementation of Stacks using Arrays

Stacks are one of the most important linear data structures of variable size. This is an
important subclass of the lists(arrays) that permit the insertion and deletion from only one end
TOP. But to allow flexibility sometimes we can say that the stack allows us to access(read)
elements in the middle and also change them as we will see in two functions to be explained
later. But this is a very rare phenomenon.

Some terminologies

Insertion : the operation is also called push


Deletion : the operation is also called pop
Top : A pointer, which keeps track of the top element in
the Stack. If an array of size N is declared to be
a stack, then TOP will be –1 when the stack is empty
and is N when the stack is full.
Pictorial representation of stack
Insertion Deletion

Current Maximum
TOP Size
Allocated Fixed
Bottom Bottom

Fig. Memory representation of Stacks

Function To Insert an element into the stack

Before inserting any element into the stack we must check whether the stack is full. In
such case we cannot enter the element into the stack. If the stack is not full, we must increment
the position of the TOP and insert the element. So, first we will write a function that checks
whether the stack is full.

/* function to check whether stack is full */

int stack_full(int top)


{
if(top == SIZE -1)
return (1);

9
else
return (0);
}

The function returns 1, if, the stack is full. Since we place TOP at –1 to denote stack
empty condition, the top varies from 0 to SIZE –1 when it stores elements. Therefore TOP at
SIZE-1 denotes that the stack is full.
/* function to insert an element into the stack */

push ( float s[], int *t, float val)


{
if( ! stack_full(*t))
{
*t = *t + 1;
s[*t] = val;
}
else
{
printf(“\n STACK FULL”);
}
}

Note that we are accepting the TOP as a pointer(*t). This is because the changes must
be reflected to the calling function. If we simply pass it by value the changes will not be
reflected. Otherwise we have to explicitly return it back to the calling function.
Function to delete an element from the stack
Before deleting any element from the stack we must check whether the stack is empty.
In such case we cannot delete the element from the stack. If the stack is not empty, we must
delete the element by decrementing the position of the TOP. So, first we will write a function
that checks whether the stack is empty.

/* function to check whether stack is empty */

int stack_empty(int top)


{
if(top == -1)
return (1);
else
return (0);
}

This function returns 1 if the stack is empty. Since the elements are stored from
positions 0 to SIZE-1, the empty condition is considered when the TOP has –1 in it.

/* function to delete an element from the stack */

float pop ( float s[], int *t)


{
float val;

10
if( ! stack_empty(*t))
{
val = s[*t];
*t = *t - 1;
return val;
}
else
{
printf(“\n STACK EMPTY”);
return 0;
}
}
Since the TOP points to the current top item, first we store this value in a temporary
variable and then decrements the TOP. Now return temporary variable to the calling function.

We can also see functions to display the stack, either in the same way as they arrived or
the reverse( the way in which they are waiting to be processed).

/* displays from top to bottom */

void display_TB(float s[], int top)


{
while( top >= 0)
{
printf(“%f\n”, s[top]);
top--;
}
}
/* displays from bottom to top */

void display_BT(float s[], int top)


{
int i;
for( i=0; i<=top ;i++)
{
printf(“%f\n”, s[i]);
top--;
}
}
Function to peep an element from the stack

float peep ( float s[], int *t, int i)


{
float val;
if( (*t-i+1) > 0)
{
val = s[*t-i+1];
return val;
}

11
else
{
printf(“\n STACK UNDERFLOW ON PEEP”);
return 0;
}}

In this function we return „ith‟ element from the top of the stack. Thee element is not
deleted by the function.

Function to change an element in the stack

Even though this is not allowed in a strict stack , sometimes it becomes necessary to
change an element than popping all the elements up to it and then inserting all of them with
one value changed.

void change ( float s[], int *t, int i, float cval)


{
if( (*t-i+1) > 0)
{
s[*t-i+1]=cval;
}
else
{
printf(“\n STACK UNDERFLOW ON PEEP”);
return 0;
}
}
This function changes the value of the „ith‟ element from the top of the stack to the
value contained in cval.
Implementing Stacks using Linked Lists

In previous section we have seen how the stacks can be implemented using ARRAYS.
We have also stated the limitations of arrays for implementation. Another way by which we
can implement the data structure using the dynamic allocation of memory i.e. better use of
memory.
In following sections we will consider all the functions for data structure stack in detail,
using linked lists.

The structure for the LIFO data structure stack is as follows:

typedef struct stack_link


{
int val;
struct stack_link *next;
}*STAK;

The linked list will have header node, which will always point to the top of the stack.
Another important thing for this implementation is that the new element will be always added
at the beginning of the linked list and while deleting the element it will be deleted from the

12
same end i.e. the beginning of the linked list. Let us have a function for getting the address of
every new node say new_node.

STAK new_node()
{
return (malloc(sizeof(struct stack_link));
}

The header node will be declared as :

STAK hlist;

Observe there is a small change in this declaration when compared to the one we used
in the linked list in the previous session. We used to declare a variable as NODE *list. But here
a pointer is omitted while declaring because we have defined the pointer to the stack as *STAK
in the type definition itself. If we type defined it as STAK instead of *STAK we need to define
variables as STAK *hlist.

Function to check whether the stack is empty

To check whether the stack is empty we, we will pass the header node, it will always
point to the top of the stack. Thus when the header points to NULL, the stack will be empty.

int stack_empty(STAK h)
{
if(h->next == NULL)
return (1);
else
return (0);
}
Function to check whether the stack is full
There is no condition base on any existing pointer, but if the new_node function returns
NULL, i.e. no more memory is available for allocation.

/* function to check whether stack is full */

STAK stack_full()
{
return(new_node));
}

The function should be used very carefully, we should not generate a new node when
the stack is not full. The stack_full function must have returned the address of the new allotted
node.
Function To push an element into the stack

push ( STAK h, int data)


{
STAK n;

13
n =stack_full();
if( n != NULL)
{
n->val = data;
n -> next = h->next;
h->next = n;
}
else
{
printf(“\n NO free Memory”);
}
}
Function To pop an element from the stack

int pop ( STAK h)


{
int data;
STAK d;

if( ! stack_empty(h))
{
data = h->next->val;
d = h ->next;
h->next = h->next->next;
free(d);

return data;
}
else
{
printf(“\n STACK EMPTY”);
return 0;
}}
Queues

The data structure queue can be considered as the processing by FIRST IN FIRST OUT
technique, commonly known as FIFO.
As we find there must be facility to put the items at the rear end of the queue, or to
remove them from the front end. Also we should have a way by which we will be able to know
about the status of the queues as whether it is full or empty.
Queues like stacks, also arise quite naturally in the computer solution of many
problems. Perhaps the most common occurrence of a queue in computer applications is for the
scheduling of the jobs. In batch processing the jobs are “queued-up” as they are read-in and
executed, one after another in the order they were received.
Implementing Queues using Arrays
As mentioned earlier, when we talk of queues we talk about two distinct ends; the front
and the rear. Additions to the queue take place at the rear. Deletions are made from the front.

14
So, if the job is submitted for execution, it joins at the rear of the job queue. The job at the
front of the queue is the next one to be executed.

front rear

12 34 45 60 11 33 49

deletion from insertion from


this end. this end.

Fig 3. Queue

- The element, which will be deleted first is 12, addition will take place after 49.

We will see some important functions with respect to queues. As we will be using array to
represent the queue, the SIZE will be the limitation on the functions. The front and the rear are
the two positions of the array, both filled, indicating that the queue exists from front position to
rear position. Initially front is 0 and rear is –1.
Function to insert an element into the Queue

Before inserting any element into the queue we must check whether the queue is full.
/* function to check whether queue is full */
int q_full(int rear)
{
if(rear == SIZE -1)
return (1);
else
return (0);
}
This function returns 1, if the queue is full. Since we place rear at –1 to denote queue
empty condition, the rear varies from 0 to SIZE –1 when it stores elements. Therefore rear at
SIZE-1 denotes that the queue is full.
/* function to insert an element into the queue */
add_q ( int a[], int *r, int val)
{
if( ! q_full(*r))
{
*r = *r + 1;
a[*r] = val;
}
else
{
printf(“\n STACK FULL”);
}
}
The call for the function will be

15
add_q(a, &rear, value);
Function to delete an element from the queue
Before deleting any element from the queue we must check whether the queue is
empty. In such case we cannot delete the element from the queue.
/* function to check whether queue is empty */
int q_empty(int front, int rear)
{
if( front > rear)
return (1);
else
return (0);
}
This function returns 1 if the queue is empty.
/* function to delete an element from the queue */
int delete_q ( int a[], int *f, int *r)
{
int val;
if( ! q_empty(*f,*r))
{
*f = *f +1; /* the new front position */
val = a[*f-1];
if( *f > *r)
{
*f = 0;
*r = -1;
}
return (val);
}
else
{
printf(“\n QUEUE EMPTY”);
return 0;
}
}
The major problem in the above implementation is that whenever we remove an
element from the queue, front will increment and the location used by the element cannot be
used again. This problem can be solved if we shift all the elements to the left by one location
on every delete operation. This will be very time consuming and is not the effective way of
solving the problem.
Queues using linked lists
Just like stacks, even queues can be implemented using linked lists. The structure for
the linked list for a queue will be given below:
typedef struct q_link
{
int val;
struct q_link *next;
}*QT;

16
The linked queue will have a header node, which will always point to the front position
of the queue. Another pointer will be required which will always point to the last node of the
queue. Remember that the new element will be always added at the rear end of the list and
while deleting the element it will be deleted from the front end. Let us have a function for
getting the address of every new node say new_node.

QT new_node()
{
return (malloc(sizeof(struct q_link));
}
The header node will be declared as : QT qlist;
Function to check whether the queue is empty
To check whether the queue is empty we, we will pass the header node, it will always
point to the front of the queues. Thus when the header points to NULL, the queue will be
empty.
int q_empty(QT h)
{
if(h->next == NULL)
return (1);
else
return (0);
}
Function to check whether the queue is full
There is no condition that only a predetermines number of nodes should exist. But if the
new_node function returns NULL, i.e. no more memory is available for allocation.
QT q_full()
{
return(new_node));
}
The function should be used very carefully, we should not generate a new node when
the queue is not full. The q_full function must have returned the address of the new allotted
node.
Function To insert an element into the queue
insert ( QT *rear, int data)
{
QT n;

n =q_full();

if( n != NULL)
{
n->val = data;
n -> next = NULL;
*rear->next = n;
*rear = n;
}
else
{
printf(“\n NO free Memory”);

17
} }
Function To delete an element from the queue
int delete ( QT h)
{
int data;
QT d;

if( ! q_empty(h))
{
data = h->next->val;
d = h ->next;
h->next = h->next->next;
free(d);

return data;
}
else
{
printf(“\n QUEUE EMPTY”);
return 0;
}
}

Circular Queue
The above problem can be solved only when the first position in the array will be
logically the next position of the last position of the array. By this way we can say that the
array is circular in nature because every position in the array will have logical next position in
the array. The queue, which we are going to handle, using this approach is called the circular
queue. Remember that it is not the infinite queue but we reuse the empty locations effectively.
Now all the functions, which we have written previously will change. We will have a very
fundamental function for such case, which will find the logical next position for any given
position in the array.
Function to find the next position in the circular queue
int next_position(int p)
{
if( p == (SIZE-1))
return 0;
else
return(p+1);
}
Function to find whether the circular queue is empty
The circular queue will be empty when rear is –1.
cq_empty(int r)
{
if( r == -1)
return 1;
else
return 0;
}

18
Function to find whether the circular queue is full
The circular queue will be full when, the front is logically the next position in the
queue.
cq_empty(int r)
{
if( f == next_position(r))
return 1;
else
return 0;
}

The circular queue will be full, when the front is 0 and the rear is SIZE–1. Also in all
cases when the immediate next position of the rear is front.
Function to add an element to the circular queue

add_cq(int a[], int *r, int val)


{
if(!cq_empty(*r))
{
*r= next_position(*r);
a[*r]=val;
}
else
{
printf(“\n Cannot add an element”);
}
}
Function to delete an element to the circular queue

delete_cq(int a[], int *f, int *r)


{
int data;

if(!cq_full(*f,*r))
{
data = a[*f];

if( *f == *r)
{
*f = 0;
*r = -1;
}
else
{
*f = next_position(*f);
}
return data;
}
else

19
{
printf(“\n No elements in the queue ”);
}
}

Circular Queue using linked list[ Circular Lists]


The structure for the linked list for a circular list will be given below:
typedef struct cq_link
{
int val;
struct cq_link *next;
}*CQT;
In case of circular lists, there will be a node after the last element of the list and it will
be the front of the list. We can say that the last node will point to the first. The linked circular
list will have a header node, which will always point to the front position of the circular list.
Another pointer will be required which will always point to the last node of the circular list.
Remember that the new element will be always added at the rear end of the list and while
deleting the element it will be deleted from the front end. Thus header as well as the rear will
point to the first node. Let us have a function for getting the address of every new node say
new_node.
CQT new_node()
{
return (malloc(sizeof(struct cq_link));
}
The header node will be declared as :

CQT qlist;
Function to check whether the circular list is empty
To check whether the circular list is empty we, we will pass the header node, it will
always point to the front of the circular lists. Thus when the header points to NULL, the
circular list will be empty.
int cq_empty(CQT h)
{
if(h->next == NULL)
return (1);
else
return (0);
}

Function to check whether the circular list is full


There is no condition base on any existing pointer, but if the new_node function returns
NULL, i.e. no more memory is available for allocation.
CQT cq_full()
{
return(new_node));
}

20
As already stated earlier, the function should be used very carefully, we should not
generate a new node when the circular list is not full. The cq_full function must have returned
the address of the new allotted node.
Function To insert an element into the circular list
Here we are required to pass the address of the pointer rear so that when the new node
is added at the end, the rear will be modified. The added node will now be the new rear of the
list. If we want to have its value back in the function, which is calling it, we should pass the
address of the same. This will actually be pointer-to-pointer concept.
insert (CQT *rear, int data)
{
CQT n;

n =cq_full();

if( n != NULL)
{
n->val = data;
n -> next = rear->next;
*rear->next = n;
*rear = n;
}
else
{
printf(“\n NO free Memory”);
}
}

Function To delete an element from the circular list


int delete ( CQT h, CTQ rear)
{
int data;
CQT d;
if( ! q_empty(h))
{
data = h->next->val;
d = h ->next;
h->next = h->next->next;
rear->next = h->next;
free(d);
return data;
}
else
{
printf(“\n circular list EMPTY”);
return 0;
}
}

Applications of Stacks

21
Recursion & stacks
There are many instances in programming where the solution can t be described using
recursion.
Recursion is a process of expressing a function in terms of itself. A function, which contains a
call to the same function or a call to another function(direct recursion), which eventually call
the first function(indirect recursion) is also termed as recursion.
In recursion, every time a function is called, all the local variables , formal variables
and return address will be pushed on the stack. So, it occupies more stack and most of the time
is spent in pushing and popping. On the other hand, the non-recursive functions execute much
faster and are easy to design.There are many situations where recursion is best suited for
solving problems.If we try to write such functions using iterations we will have to use stacks
explicitly.

Evaluation of expressions using stacks

All the arithmetic expressions contain variables or constants, operators and parenthesis.
These expressions are normally in the infix form, where the operators separate the operands.
Also, there will be rules for the evaluation of the expressions and for assigning the priorities to
the operators. The expression after evaluation will result in a single value.
We can evaluate an expression using the stacks.

5.11.1. Evaluation of Expressions


An expression consists of operators and operands. In an expression if the
operator, which performs an operation , is written in between the operands it is called an infix
expression. If the operator is written before the operands , it is called prefix expression. If the
operator is written after the operands, it is called postfix expression.
Consider an infix expression:
2 + 3 * ( 4 – 6 / 2 + 7 ) / (2 + 3) – (4 –1) * (2 – 10 / 2))

When it comes to the evaluation of the expression, following rules are used.

1. brackets should be evaluated first.


2. * and / have equal priority, which is higher than + and -.
3. All operators are left associative, or when it comes to equal operators, the evaluation
is from left to right.

In the above case the bracket (4-1) is evaluated first. Then (2-10/2) will be evaluated in
which /, being higher priority, 10/2 will be evaluated first. The above sentence is questionable,
because as we move from left to right, the first bracket, which will be evaluated, will be (4-
6/2+7).

The evaluation is as follows:-

Step 1: Division has higher priority. Therefore 6/2 will


result in 3. The expression now will be (4-3+7).
Step 2: As – and + have same priority, (4-3) will b

22
evaluated first.
Step 3: 1+7 will result in 8.

The total evaluation is as follows.

2 + 3 * (4 – 6 / 2 + 7) / (2 + 3)-(( 4 – 1) * (2 – 10 /2 ))
=2 + 3 * 8 / (2 + 3) - ((4 - 1) * (2 – 10 / 2))
=2 + 3 * 8 / 5 -((4 - 1) * (2 – 10 / 2))
=2 + 3 * 8 /5 -(3 * (2 – 10 / 2))
=2 + 3 * 8 /5 -(3 * (2 - 5))
=2 + 3 * 8 / 5 - (3 * (-3))
=2 + 3 * 8 / 5 + 9
=2 + 24 / 5 + 9
=2 + 4.8 + 9
=6.8 + 9
=15.8

Postfix Expressions
In the postfix expression, every operator is preceded by two operands on which it
operates. The postfix expression is the postorder traversal of the tree. The postorder traversal is
Left-Right-Root. In the expression tree, Root is always an operator. Left and Right sub-trees
are expressions by themselves, hence they can be treated as operands.

If we want to operate the postfix expression from the given infix, then consider the
evaluation sequence and apply it from bottom to top, every time converting infix to postfix.

e7 e7
= e6 + a = e6 a +
But e6 = e5 - e4 = e5 e4 – a +
But e5 = a - b = ab – e4 – a +
But e4 = e3 * b = ab – e3 b * - a +
But e3 = e2 + a = ab – e2 a + b * -a +
But e2 = c * e1 = ab – ce1 * a + b * -a +
But e1 = d / a = ab – cda / * a + b * -a +

The postfix expression does not require brackets. The above method will not be useful
for programming. For programming, we use a stack, which will contain operators and opening
brackets. The priorities are assigned using numerical values. Priority of + and – is equal to 1.
Priority of * and / is 2. The incoming priority of the opening bracket is highest and the
outgoing priority of the closing bracket is lowest. An operator will be pushed into the stack,
provided the priority of the stack top operator is less then the current operator. Opening
bracket will always be pushed into the stack. Any operator can be pushed on the opening
bracket. Whenever operand is received, it will directly be printed. Whenever closing bracket is
received, elements will be popped till opening bracket and printed, the execution is as shown.

e.g.

b -( a + b ) * (( c - d) / a + a )

23
1. Symbol is b, hence print b.
2. On -, Stack being empty, push. Therefore

Top  -
3. On (, push. Therefore
Top (
-
4. On a, operand, Hence Print a.

5. On +, Stack being (, push. Therefore

Top +
(
-
6. On b, operand, Hence Print b.

7. On ) , pop till (, and then print.


Therefore pop +, print +. Top  -
Pop (.

8. On *, Stack being -, push, Therefore


Top  *
-

9. On (, push. Therefore
Top  (
*
-

Note : For Convenience, we will draw horizontal stack.

10. On (, push. Therefore -* ((


 Top

11. On C, operand, Hence print C. -*((-


12. On-, push. Therefore  Top
13. On d, operand, Hence print d.

14. On ) , pop till (, and then print.


Therefore pop -, print -. -*(
Pop(  Top

15. On /, push. Therefore -*(/


 Top
16. On a, operand, Hence print a.

17. On +, Stack top is /, pop till (, and then print.


Therefore pop/, print/. -*+

24
Pop(.  Top
Push+.

18. On a, operand, Hence print a.

19. On ), pop till (, and then print +


Therefore pop +, print_+. -*
Pop(.  Top

20. End of the Infix expression. -*


 Top
pop all and print, Hence print +.
print -.

Therefore the generated postfix expression is

bab+cd–a/a+*-

Algorithm to convert an infix expression to postfix expression

Step1: Accept infix expression in a sting S.


Step2: i being the position, let it be equal to 0.
Step3: Initially top = -1, indicating stack is empty.
Step4: If S[i] is equal to operand, print it, go to step 8.
Step5: If S[i] is equal to opening bracket, push it, go to
step 8.
Step6: If S[i] is equal to operator, then
Step6a: Pop the operator from the stack, say, p,
If priority of P is less than priority of s[i], then push
S[i], push p, go to step 8.
Else print p, goto step 6a.
Step7: If S[i] is equal to operator, then
Step7a: pop the operator from the stack, say, p,
If p is not equal to opening bracket, then print
p, step 7a.
Else go to step 8.
Step8: Increment i.
Step9: If s[i] is equal to „\0‟, then go to step 4.
Step10: pop the operator from the stack, say, p.
Step11: Print p.
Step12: If stack is not empty, then go to step 10.
Step13: Stop.

Prefix Expression

To convert Infix expression to prefix expression, we can again have steps, similar to the
above algorithm, but a single stack may not be sufficient. We will require two stacks for the
following.

25
1. Containing operators for assigning priorities.
2. Containing operands or operand expression.

Every time we get the operand, it will be pushed in stack2 and operator will be
pushed in stack1. Pushing the operator in stack1 is unconditional, whereas when operators are
pushed, all the rules of previous methods are applicable as they are. When an operator is
popped from stack1, the corresponding two operands are popped from stack2, say O1 and O2
respectively. We form the prefix expression as operator, O1, O2 and this prefix expression will
be treated as a single entity and pushed on stack2. Whenever closing bracket is received, we
pop the operators from stack1, till opening bracket is received. At the end stack1 will be empty
and stack2 will contain single operand in the form of prefix expression.
e.g.

(a+b)*(c–d)+a

Stack1 Stack2

1. (

2. ( a

3. + a
(

4. + b
( a

5. ( +ab O1=a
O2=b

6. ( +ab

7. +ab

8. * +ab

9. ( +ab
*

26
10. ( c+ab
*

11. - c+ab
(
*

12. - d
( c
* +ab

13. ( O1=c
* +ab O2=d
-cd

14. ( -cd
* +ab

15. * -cd
+ab

16. O1=-cd
O2=+ab

*+ab-cd
17. + *+ab-
cd

18. + a
*+ab-cd

19. O1=*+ab-
cd
O2=a
The expression is + * + ab -cda
The trace of the stack contents and the actions on receiving each symbol is shown.
Stack2 can be stack of header nodes for different lists, containing prefix expressions or could
be a multidimensional array or could be an array of strings.
The recursive functions can be written for both the conversions. Following are the steps
for the non-recursive algorithm for converting Infix to Prefix.
Step1 : Get the prefix expression, say S.
27
Step2 : Set the position counter, i to 0.
Step3 : Initially top1 and top2 are –1, indicating that the
stacks are empty.
Step4 : If S[i] is equal to opening bracket, push it in
stack1, go to step8.
Step5 : If S[i] is equal to operand it in stack2, go to step8.
Step6 : If S[i] is equal to operator, stack1 is empty or stack
top elements has less priority as compared to S[i] ,
go to step 8.
Else p= pop the operator from stack1.
O1= pop the operator from stack1.
O2= pop the operand from stack2.
From the Prefix expression p, O1, O2,
Push in stack2 and go to step6.
Step7 : If S[i]= opening bracket, then
Step7a: p= pop the operator from stack1.
If p is not equal to closing bracket, then
O1= pop the operand from stack2.
O2= pop the operand from stack2.
From the prefix expression p, O1, O2,
Push in stack2 and go to step 7a.
Else go to step 8.
Step8 : Increment i.
Step9 : If s[i] is not equal to “/0”, then go to step4.
Step10: Every time pop one operator from stack1, pop2 operands
from stack2, from the prefix expression p, O1, O2,
push in stack2 and repeat till stack1 becomes empty.
Step11: Pop operand from stack2 and print it as prefix
expression.
Step12: Stop.

28
Unit-2 Trees

A tree is a non-empty set, one element of which is designated the root of the tree while
the remaining elements are partitioned into non-empty sets each of which is a subtree of the
root.

Tree nodes have many useful properties. The depth of a node is the length of the path (or
the number of edges) from the root to that node. The height of a node is the longest path from
that node to its leaves. The height of a tree is the height of the root. A leaf node has no children
-- its only path is up to its parent.

Types of trees:

Binary: Each node has zero, one, or two children. This assertion makes many tree operations
simple and efficient.

Binary Search: A binary tree where any left child node has a value less than its parent node
and any right child node has a value greater than or equal to that of its parent node.

AVL: A balanced binary search tree according to the following specification: The height
difference between any two sibling nodes must not exceed one.

Traversal

Many problems require we visit* the nodes of a tree in a systematic way: tasks such as
counting how many nodes exist or finding the maximum element. Three different methods are
possible for binary trees: preorder, postorder, and in-order, which all do the same three things:
recursively traverse both the left and rights subtrees and visit the current node. The difference
is when the algorithm visits the current node:

preorder: Current node, left subtree, right subtree

postorder: Left subtree, right subtree, current node

in-order: Left subtree, current node, right subtree.

levelorder: Level by level, from left to right, starting from the root node.

Visit means performing some operation involving the current node of a tree, like
incrementing a counter or checking if the value of the current node is greater than any
other recorded.

Sample implementations for Tree Traversal


preorder(node)
print node.value
if node.left ≠ null then preorder(node.left)
if node.right ≠ null then preorder(node.right)
inorder(node)
if node.left ≠ null then inorder(node.left)
29
print node.value
if node.right ≠ null then inorder(node.right)
postorder(node)
if node.left ≠ null then postorder(node.left)
if node.right ≠ null then postorder(node.right)
print node.value
levelorder(root)
queue<node> q
q.push(root)
while not q.empty do
node = q.pop
visit(node)
if node.left ≠ null then q.push(node.left)
if node.right ≠ null then q.push(node.right)

For an algorithm that is less taxing on the stack, see Threaded Trees.

Examples of Tree Traversals

preorder: 50, 30, 20, 40, 90, 100


inorder: 20, 30, 40, 50, 90, 100
postorder: 20, 40, 30, 100, 90, 50
levelorder: 50, 30, 90, 20, 40, 100

Balancing

When entries that are already sorted are stored in a tree, all new records will go the same route,
and the tree will look more like a list (such a tree is called a degenerate tree). Therefore the tree
needs balancing routines, making sure that under all branches are an equal number of records.
This will keep searching in the tree at optimal speed. Specifically, if a tree with n nodes is a
degenerate tree, the longest path through the tree will be n nodes; if it is a balanced tree, the
longest path will be log n nodes.

Binary Search Trees

A typical binary search tree looks like this:

30
Terms
Node Any item that is stored in the tree.

Root The top item in the tree. (50 in the tree above)

Child Node(s) under the current node. (20 and 40 are children of 30 in the tree above)

Parent The node directly above the current node. (90 is the parent of 100 in the tree above)

Leaf A node which has no children. (20 is a leaf in the tree above)

Searching through a binary search tree


To search for an item in a binary tree:

1. Start at the root node


2. If the item that you are searching for is less than the root node, move to the left child of
the root node, if the item that you are searching for is more than the root node, move to
the right child of the root node and if it is equal to the root node, then you have found
the item that you are looking for :)
3. Now check to see if the item that you are searching for is equal to, less than or more
than the new node that you are on. Again if the item that you are searching for is less
than the current node, move to the left child, and if the item that you are searching for is
greater than the current node, move to the right child.
4. Repeat this process until you find the item that you are looking for or until the node
doesn't have a child on the correct branch, in which case the tree doesn't contain the
item which you are looking for.

Example

31
For example, to find the node 40...

1. The root node is 50, which is greater than 40, so you go to 50's left child.
2. 50's left child is 30, which is less than 40, so you next go to 30's right child.
3. 30's right child is 40, so you have found the item that you are looking for :)

Adding an item to a binary search tree


1. To add an item, you first must search through the tree to find the position that you
should put it in. You do this following the steps above.
2. When you reach a node which doesn't contain a child on the correct branch, add the
new node there.

Example For example, to add the node 25...

1. The root node is 50, which is greater than 25, so you go to 50's left child.
2. 50's left child is 30, which is greater than 25, so you go to 30's left child.
3. 30's left child is 20, which is less than 25, so you go to 20's right child.
4. 20's right child doesn't exist, so you add 25 there :)

Deleting an item from a binary search tree


It is assumed that you have already found the node that you want to delete, using the search
technique described above.

Case 1: The node you want to delete is a leaf

For example, do delete 40...

Simply delete the node!

Case 2: The node you want to delete has one child

1. Directly connect the child of the node that you want to delete, to the parent of the node
that you want to delete.

32
For example, to delete 90...

Delete 90, then make 100 the child node of 50.

Case 3: The node you want to delete has two children

1. Find the left-most node in the right subtree of the node being deleted. (After you have
found the node you want to delete, go to its right node, then for every node under that,
go to its left node until the node has no left node) From now on, this node will be
known as the successor.

For example, to delete 30

1. The right node of the node which is being deleted is 40.


2. (From now on, we continually go to the left node until there isn't another one...) The
first left node of 40, is 35.
3. 35 has no left node, therefore 35 is the successor!

Case 1: The successor is the right child of the node being deleted

1. Directly move the child to the right of the node being deleted into the position of the
node being deleted.

33
2. As the new node has no left children, you can connect the deleted node's left subtree's
root as it's left child.

For example, to delete 30

1. Move 40 up to where 30 was.


2. 20 now becomes 40's left child.

Case 2: The successor isn't the right child of the node being deleted

This is best shown with an example

To delete 30...

1. Move the successor into the place where the deleted node was and make it inherit both
of it's children. So 35 moves to where 30 was and 20 and 40 become it's children.
2. Move the successor's (35s) right subtree to where the successor was. So 37 becomes a
child of 40.

Applications of Trees

34
Trees are often used in and of themselves to store data directly, however they are also often
used as the underlying implementation for other types of data structures such as [Hash Tables],
[Sets and Maps]
and other associative containers. Specifically, the C++ Standard Template Library uses special
red/black trees as the underlying implementation for sets and maps, as well as multisets and
multimaps.

Binary Trees

Definitions for rooted trees

A directed edge refers to the link from the parent to the child (the arrows in the picture
of the tree).
The root node of a tree is the node with no parents. There is at most one root node in a
rooted tree.
A leaf node has no children.
The depth of a node n is the length of the path from the root to the node. The set of all
nodes at a given depth is sometimes called a level of the tree. The root node is at depth
zero.
The height of a tree is the depth of its furthest leaf. A tree with only a root node has a
height of zero.
Siblings are nodes that share the same parent node.
If a path exists from node p to node q, where node p is closer to the root node than q,
then p is an ancestor of q and q is a descendant of p.
The size of a node is the number of descendants it has including itself.
In-degree of a Vertex (graph theory) is the number of edges arriving at that vertex.
Out-degree of a vertex is the number of edges leaving that vertex.
Root is the only node in the tree with in-Degree=0.

Types of binary trees

A rooted binary tree is a rooted tree in which every node has at most two children.
A full binary tree (sometimes proper binary tree or 2-tree) is a tree in which every
node has zero or two children.
A perfect binary tree (sometimes complete binary tree) is a full binary tree in which
all leaves are at the same depth.
A balanced binary tree is where the depth of all the leaves differs by at most 1.
Balanced trees have a predictable depth (how many nodes are traversed from the root to
a leaf, root counting as node 0 and subsequent as 1, 2, ..., depth). This depth is equal to
the integer part of log2(n) where n is the number of nodes on the balanced tree.
Example 1: balanced tree with 1 node, log2(1) = 0 (depth = 0). Example 2: balanced
tree with 3 nodes, log2(3) = 1.59 (depth=1). Example 3: balanced tree with 5 nodes,
log2(5) = 2.32 (depth of tree is 2 nodes).
A rooted complete binary tree can be identified with a free magma.
An almost complete binary tree is a tree in which each node that has a right child also
has a left child. Having a left child does not require a node to have a right child. Stated
alternately, an almost complete binary tree is a tree where for a right child, there is
always a left child, but for a left child there may not be a right child.

35
A degenerate tree is a tree where for each parent node, there is only one associated
child node. This means that in a performance measurement, the tree will behave like a
linked list data structure.
The number of nodes n in a perfect binary tree can be found using this formula: n = 2h +
1
− 1 where h is the height of the tree.
The number of leaf nodes n in a perfect binary tree can be found using this formula: n =
2h where h is the height of the tree.
The number of nodes n in a complete binary tree is minimum: n = 2h and maximum: n
= 2h + 1 − 1 where h is the height of the tree.
The number of NULL links in a Complete Binary Tree of n-node is (n+1).
The number of leaf node in a Complete Binary Tree of n-node is UpperBound(n / 2).

- Note that this terminology often varies in the literature, especially with respect to the meaning
"complete" and "full".

Methods for storing binary trees

Binary trees can be constructed from programming language primitives in several ways. In a
language with records and references, binary trees are typically constructed by having a tree
node structure which contains some data and references to its left child and its right child.
Sometimes it also contains a reference to its unique parent. If a node has fewer than two
children, some of the child pointers may be set to a special null value, or to a special sentinel
node.

Binary trees can also be stored as an implicit data structure in arrays, and if the tree is a
complete binary tree, this method wastes no space. In this compact arrangement, if a node has
an index i, its children are found at indices 2i + 1 and 2i + 2, while its parent (if any) is found
at index (assuming the root has index zero). This method benefits from more compact storage
and better locality of reference, particularly during a preorder traversal.

However, it is expensive to grow and wastes space proportional to 2h - n for a tree of height h
with n nodes.

In languages with tagged unions such as ML, a tree node is often a tagged union of two types
of nodes, one of which is a 3-tuple, left child, and right child, and the other of which is a "leaf"
node, which contains no data and functions much like the null value in a language with
pointers.

Methods of iterating over binary trees

Often, one wishes to visit each of the nodes in a tree and examine the value there. There are
several common orders in which the nodes can be visited, and each has useful properties that
are exploited in algorithms based on binary trees.

Pre-order, in-order, and post-order traversal.

Pre-order, in-order, and post-order traversal visit each node in a tree by recursively visiting
each node in the left and right subtrees of the root. If the root node is visited before its subtrees,

36
this is preorder; if after, postorder; if between, in-order. In-order traversal is useful in binary
search trees, where this traversal visits the nodes in increasing order.

Depth-first order

In depth-first order, we always attempt to visit the node farthest from the root that we can, but
with the caveat that it must be a child of a node we have already visited. Unlike a depth-first
search on graphs, there is no need to remember all the nodes we have visited, because a tree
cannot contain cycles. Pre-order is a special case of this.

Breadth-first order

Contrasting with depth-first order is breadth-first order, which always attempts to visit the node
closest to the root that it has not already visited.

Level-order traversal

Traversal where levels are visited successively, starting with level 0 (the root node), and nodes
are visited from left to right on each level.

Depth First Search

Depth-first search (DFS) is an algorithm for traversing or searching a tree, tree structure, or
graph. One starts at the root (selecting some node as the root in the graph case) and explores as
far as possible along each branch before backtracking.

Formally, DFS is an uninformed search that progresses by expanding the first child node of the
search tree that appears and thus going deeper and deeper until a goal node is found, or until it
hits a node that has no children. Then the search backtracks, returning to the most recent node
it hadn't finished exploring. In a non-recursive implementation, all freshly expanded nodes are
added to a LIFO stack for exploration.

Space complexity of DFS is much lower than BFS (breadth-first search). It also lends itself
much better to heuristic methods of choosing a likely-looking branch. Time complexity of both
algorithms are proportional to the number of vertices plus the number of edges in the graphs
they traverse (O(|V| + |E|)).

AVL tree

In computer science, an AVL tree is a self-balancing binary search tree, and it is the first such
data structure to be invented. In an AVL tree, the heights of the two child subtrees of any node
differ by at most one; therefore, it is also said to be height-balanced. Lookup, insertion, and
deletion all take O(log n) time in both the average and worst cases, where n is the number of
nodes in the tree prior to the operation. Insertions and deletions may require the tree to be
rebalanced by one or more tree rotations.

The AVL tree is named after its two inventors, G.M. Adelson-Velsky and E.M. Landis, who
published it in their 1962 paper "An algorithm for the organization of information."

37
The balance factor of a node is the height of its right subtree minus the height of its left
subtree. A node with balance factor 1, 0, or -1 is considered balanced. A node with any other
balance factor is considered unbalanced and requires rebalancing the tree. The balance factor is
either stored directly at each node or computed from the heights of the subtrees.

AVL trees are often compared with red-black trees because they support the same set of
operations and because red-black trees also take O(log n) time for the basic operations. AVL
trees perform better than red-black trees for lookup-intensive applications. The AVL tree
balancing algorithm appears in many computer science curricula.

Operations

The basic operations of an AVL tree generally involve carrying out the same actions as would
be carried out on an unbalanced binary search tree, but preceded or followed by one or more
operations called tree rotations, which help to restore the height balance of the subtrees.

Insertion

Insertion into an AVL tree may be carried out by inserting the given value into the tree as if it
were an unbalanced binary search tree, and then retracing one's steps toward the root updating
the balance factor of the nodes.

If the balance factor becomes -1, 0, or 1 then the tree is still in AVL form, and no rotations are
necessary.

If the balance factor becomes 2 or -2 then the tree rooted at this node is unbalanced, and a tree
rotation is needed. At most a single or double rotation will be needed to balance the tree.

38
Pictorial description of how rotations cause rebalancing in an AVL tree.

Deletion

If the node is a leaf, remove it. If the node is not a leaf, replace it with either the largest in its
left subtree (inorder predecessor) or the smallest in its right subtree (inorder successor), and
remove that node. The node that was found as replacement has at most one subtree. After
deletion retrace the path back up the tree (parent of the replacement) to the root, adjusting the
balance factors as needed.

The retracing can stop if the balance factor becomes -1 or 1 indicating that the height of that
subtree has remained unchanged. If the balance factor becomes 0 then the height of the subtree
has decreased by one and the retracing needs to continue. If the balance factor becomes -2 or 2
then the subtree is unbalanced and needs to be rotated to fix it. If the rotation leaves the
subtree's balance factor at 0 then the retracing towards the root must continue since the height
of this subtree has decreased by one. This is in contrast to an insertion where a rotation
resulting in a balance factor of 0 indicated that the subtree's height has remained unchanged.

The time required is O(log(n)) for lookup plus maximum O(log(n)) rotations on the way back
to the root; so the operation can be completed in O(log n) time.

Threaded trees

A Threaded Binary Tree is a binary tree in which every node that does not have a right child
has a THREAD (in actual sense, a link) to its INORDER successor. By doing this threading we

39
avoid the recursive method of traversing a Tree, which makes use of stacks and consumes a lot
of memory and time.

The node structure for a threaded binary tree varies a bit and its like this --
struct NODE
{
struct NODE *leftchild;
int node_value;
struct NODE *rightchild;
struct NODE *thread;
}

Let's make the Threaded Binary tree out of a normal binary tree...

The INORDER traversal for the above tree is -- D B A E C. So, the respective Threaded
Binary tree will be --

B has no right child and its inorder successor is A and so a thread has been made in between
them. Similarly, for D and E. C has no right child but it has no inorder successor even, so it has
a hanging thread.

A threaded binary tree makes it possible to traverse the values in the binary tree via a
linear traversal that is more rapid than a recursive in-order traversal.

It is also possible to discover the parent of a node from a threaded binary tree, without explicit
use of parent pointers or a stack, albeit slowly. This can be useful where stack space is limited,
or where a stack of parent pointers is unavailable (for finding the parent pointer via DFS).

This is possible, because if a node (k) has a right child (m) then m's left pointer must be either a
child, or a thread back to k. In the case of a left child, that left child must also have a left child
or a thread back to k, and so we can follow m's left children until we find a thread, pointing
back to k. The situation is similar for when m is the left child of k

def parent(node):
if node is node.tree.root:
40
return None
else:
x = node
y = node
while True:

if is_thread(y.right):
p = y.right
if p is None or p.left is not node:
p=x
while not is_thread(p.left):
p = p.left
p = p.left
return p
elif is_thread(x.left):
p = x.left
if p is None or p.left is not node:
p=y
while not is_thread(p.right):
p = p.right
p = p.right
return p
x = x.left
y = y.right

Splay tree

A splay tree is a self-balancing binary search tree with the additional property that recently
accessed elements are quick to access again. It performs basic operations such as insertion,
look-up and removal in O(log(n)) amortized time. For many non-uniform sequences of
operations, splay trees perform better than other search trees, even when the specific pattern of
the sequence is unknown.

All normal operations on a binary search tree are combined with one basic operation, called
splaying. Splaying the tree for a certain element rearranges the tree so that the element is
placed at the root of the tree. One way to do this is to first perform a standard binary tree search
for the element in question, and then use tree rotations in a specific fashion to bring the
element to the top. Alternatively, a top-down algorithm can combine the search and the tree
reorganization into a single phase.

The splay operation

When a node x is accessed, a splay operation is performed on x to move it to the root. To


perform a splay operation we carry out a sequence of splay steps, each of which moves x closer
to the root. By performing a splay operation on the node of interest after every access, the
recently accessed nodes are kept near the root and the tree remains roughly balanced, so that
we achieve the desired amortized time bounds.

Each particular step depends on three factors:

41
Whether x is the left or right child of its parent node, p,
whether p is the root or not, and if not
whether p is the left or right child of its parent, g (the grandparent of x).

The three types of splay steps are:

Zig Step: This step is done when p is the root. The tree is rotated on the edge between x and p.
Zig steps exist to deal with the parity issue and will be done only as the last step in a splay
operation and only when x has odd depth at the beginning of the operation.

Zig-zig Step: This step is done when p is not the root and x and p are either both right children
or are both left children. The picture below shows the case where x and p are both left children.
The tree is rotated on the edge joining p with its parent g, then rotated on the edge joining x
with p. Note that zig-zig steps are the only thing that differentiate splay trees from the rotate to
root method introduced by Allen and Munro prior to the introduction of splay trees.

Zig-zag Step: This step is done when p is not the root and x is a right child and p is a left child
or vice versa. The tree is rotated on the edge between x and p, then rotated on the edge between
x and its new parent g.

B-tree

42
The Structure of B-Trees

Unlike a binary-tree, each node of a b-tree may have a variable number of keys and children.
The keys are stored in non-decreasing order. Each key has an associated child that is the root of
a subtree containing all nodes with keys less than or equal to the key but greater than the
preceeding key. A node also has an additional rightmost child that is the root for a subtree
containing all keys greater than any keys in the node.

A b-tree has a minumum number of allowable children for each node known as the
minimization factor. If t is this minimization factor, every node must have at least t - 1 keys.
Under certain circumstances, the root node is allowed to violate this property by having fewer
than t - 1 keys. Every node may have at most 2t - 1 keys or, equivalently, 2t children.

Since each node tends to have a large branching factor (a large number of children), it is
typically neccessary to traverse relatively few nodes before locating the desired key. If access
to each node requires a disk access, then a b-tree will minimize the number of disk accesses
required. The minimzation factor is usually chosen so that the total size of each node
corresponds to a multiple of the block size of the underlying storage device. This choice
simplifies and optimizes disk access. Consequently, a b-tree is an ideal data structure for
situations where all data cannot reside in primary storage and accesses to secondary storage are
comparatively expensive (or time consuming).

Operations on B-Trees

The algorithms for the search, create, and insert operations are shown below. Note that these
algorithms are single pass; in other words, they do not traverse back up the tree. Since b-trees
strive to minimize disk accesses and the nodes are usually stored on disk, this single-pass
approach will reduce the number of node visits and thus the number of disk accesses. Simpler
double-pass approaches that move back up the tree to fix violations are possible.

Since all nodes are assumed to be stored in secondary storage (disk) rather than primary
storage (memory), all references to a given node be be preceeded by a read operation denoted
by Disk-Read. Similarly, once a node is modified and it is no longer needed, it must be written
out to secondary storage with a write operation denoted by Disk-Write. The algorithms below
assume that all nodes referenced in parameters have already had a corresponding Disk-Read
operation. New nodes are created and assigned storage with the Allocate-Node call. The
implementation details of the Disk-Read, Disk-Write, and Allocate-Node functions are
operating system and implementation dependent.

B-Tree-Search(x, k)
i <- 1
while i <= n[x] and k > keyi[x]
do i <- i + 1
if i <= n[x] and k = keyi[x]
then return (x, i)
if leaf[x]
then return NIL
else Disk-Read(ci[x])
return B-Tree-Search(ci[x], k)
43
The search operation on a b-tree is analogous to a search on a binary tree. Instead of choosing
between a left and a right child as in a binary tree, a b-tree search must make an n-way choice.
The correct child is chosen by performing a linear search of the values in the node. After
finding the value greater than or equal to the desired value, the child pointer to the immediate
left of that value is followed. If all values are less than the desired value, the rightmost child
pointer is followed. Of course, the search can be terminated as soon as the desired node is
found. Since the running time of the search operation depends upon the height of the tree, B-
Tree-Search is O(logt n).

B-Tree-Create(T)
x <- Allocate-Node()
leaf[x] <- TRUE
n[x] <- 0
Disk-Write(x)
root[T] <- x

The B-Tree-Create operation creates an empty b-tree by allocating a new root node that has no
keys and is a leaf node. Only the root node is permitted to have these properties; all other nodes
must meet the criteria outlined previously. The B-Tree-Create operation runs in time O(1).

B-Tree-Split-Child(x, i, y)
z <- Allocate-Node()
leaf[z] <- leaf[y]
n[z] <- t - 1
for j <- 1 to t - 1
do keyj[z] <- keyj+t[y]
if not leaf[y]
then for j <- 1 to t
do cj[z] <- cj+t[y]
n[y] <- t - 1
for j <- n[x] + 1 downto i + 1
do cj+1[x] <- cj[x]
ci+1 <- z
for j <- n[x] downto i
do keyj+1[x] <- keyj[x]
keyi[x] <- keyt[y]
n[x] <- n[x] + 1
Disk-Write(y)
Disk-Write(z)
Disk-Write(x)

If is node becomes "too full," it is necessary to perform a split operation. The split operation
moves the median key of node x into its parent y where x is the ith child of y. A new node, z, is
allocated, and all keys in x right of the median key are moved to z. The keys left of the median
key remain in the original node x. The new node, z, becomes the child immediately to the right
of the median key that was moved to the parent y, and the original node, x, becomes the child
immediately to the left of the median key that was moved into the parent y.

44
The split operation transforms a full node with 2t - 1 keys into two nodes with t - 1 keys each.
Note that one key is moved into the parent node. The B-Tree-Split-Child algorithm will run in
time O(t) where t is constant.

B-Tree-Insert(T, k)
r <- root[T]
if n[r] = 2t - 1
then s <- Allocate-Node()
root[T] <- s
leaf[s] <- FALSE
n[s] <- 0
c1 <- r
B-Tree-Split-Child(s, 1, r)

B-Tree-Insert-Nonfull(s, k)
else B-Tree-Insert-Nonfull(r, k)

B-Tree-Insert-Nonfull(x, k)
i <- n[x]
if leaf[x]
then while i >= 1 and k < keyi[x]
do keyi+1[x] <- keyi[x]
i <- i - 1
keyi+1[x] <- k
n[x] <- n[x] + 1
Disk-Write(x)
else while i >= and k < keyi[x]
do i <- i - 1
i <- i + 1
Disk-Read(ci[x])
if n[ci[x]] = 2t - 1
then B-Tree-Split-Child(x, i, ci[x])
if k > keyi[x]
then i <- i + 1
B-Tree-Insert-Nonfull(ci[x], k)

To perform an insertion on a b-tree, the appropriate node for the key must be located using an
algorithm similiar to B-Tree-Search. Next, the key must be inserted into the node. If the node is
not full prior to the insertion, no special action is required; however, if the node is full, the
node must be split to make room for the new key. Since splitting the node results in moving
one key to the parent node, the parent node must not be full or

another split operation is required. This process may repeat all the way up to the root and may
require splitting the root node. This approach requires two passes. The first pass locates the
node where the key should be inserted; the second pass performs any required splits on the
ancestor nodes.

45
Since each access to a node may correspond to a costly disk access, it is desirable to avoid the
second pass by ensuring that the parent node is never full. To accomplish this, the presented
algorithm splits any full nodes encountered while descending the tree. Although this approach
may result in unecessary split operations, it guarantees that the parent never needs to be split
and eliminates the need for a second pass up the tree. Since a split runs in linear time, it has
little effect on the O(t logt n) running time of B-Tree-Insert.

Splitting the root node is handled as a special case since a new root must be created to contain
the median key of the old root. Observe that a b-tree will grow from the top.

B-Tree-Delete

Deletion of a key from a b-tree is possible; however, special care must be taken to ensure that
the properties of a b-tree are maintained. Several cases must be considered. If the deletion
reduces the number of keys in a node below the minimum degree of the tree, this violation
must be corrected by combining several nodes and possibly reducing the height of the tree. If
the key has children, the children must be rearranged. For a detailed discussion of deleting
from a b-tree, refer to Section 19.3, pages 395-397, of Cormen, Leiserson, and Rivest or to
another reference listed below.

Examples

Sample B-Tree

Searching a B-Tree for Key 21

Inserting Key 33 into a B-Tree (w/ Split)

46
Unit-3 Hashing and Sets
Hashing – Separate chaining – open addressing – rehashing – extendible hashing –
Disjoint Set ADT – dynamic equivalence problem – smart union algorithms – path
compression – applications of Sets

HASHING

Hashing is a method to store data in an array so that storing, searching, inserting and deleting
data is fast (in theory it's O(1)). For this every record needs an unique key. The basic idea is
not to search for the correct position of a record with comparisons but to compute the position
within the array. The function that returns the position is called the 'hash function' and the array
is called a 'hash table'.

Hash table

In computer science, a hash table, or a hash map, is a data structure that associates keys with
values. The primary operation it supports efficiently is a lookup: given a key (e.g. a person's
name), find the corresponding value (e.g. that person's telephone number). It works by
transforming the key using a hash function into a hash, a number that is used as an index in an
array to locate the desired location ("bucket") where the values should be.

Hash tables support the efficient insertion of new entries, in expected O(1) time. The time
spent in searching depends on the hash function and the load of the hash table; both insertion
and search approach O (1) time with well chosen values and hashes.

Basic operation

A hash table works by transforming the key using a hash function into a hash, a number that is
used as an index in an array to locate the desired location ("bucket") where the values should
be. The number is normally converted into the index by taking a modulo, or sometimes bit
masking is used where the array size is a power of two. The optimal hash function for any
given use of a hash table can vary widely, however, depending on the nature of the key.

Typical operations on a hash table include insertion, deletion and lookup (although some hash
tables are precalculated so that no insertions or deletions, only lookups are done on a live
system). These operations are all performed in amortized constant time, which makes
maintaining and accessing a huge hash table very efficient.

It is also possible to create a hash table statically where, for example, there is a fairly limited
fixed set of input values - such as the value in a single byte (or possibly two bytes ) from which
an index can be constructed directly (see section below on creating hash tables). The hash table
can also be used simultaneously for tests of validity on the values that are disallowed.

Hashing Methods - 1. Division Method

Perhaps the simplest of all the methods of hashing an integer x is to divide x by M and then to
use the remainder modulo M. This is called the division method of hashing . In this case, the
hash function is h(x) = x mod M.

47
Generally, this approach is quite good for just about any value of M. However, in
certain situations some extra care is needed in the selection of a suitable value for M. For
example, it is often convenient to make M an even number. But this means that h(x) is even if x
is even; and h(x) is odd if x is odd. If all possible keys are equiprobable, then this is not a
problem. However if, say, even keys are more likely than odd keys, the function h(x) = x mod
M.will not spread the hashed values of those keys evenly. Similarly, it is often tempting to let
M be a power of two. E.g., for some integer k>1. In this case, the hash function
simply extracts the bottom k bits of the binary representation of x. While this
hash function is quite easy to compute, it is not a desirable function because it does not depend
on all the bits in the binary representation of x. For these reasons M is often chosen to be a
prime number. For example, suppose there is a bias in the way the keys are created that makes
it more likely for a key to be a multiple of some small constant, say two or three. Then making
M a prime increases the likelihood that those keys are spread out evenly. Also, if M is a prime
number, the division of x by that prime number depends on all the bits of x, not just the bottom
k bits, for some small constant k. The division method is extremely simple to implement.

2. Middle Square Method

Since integer division is usually slower than integer multiplication, by avoiding


division we can potentially improve the running time of the hashing algorithm. We can avoid
division by making use of the fact that a computer does finite-precision integer arithmetic. E.g.,
all arithmetic is done modulo W where is a power of two such that w is the word size
of the computer. The middle-square hashing method works as follows. First, we assume that
M is a power of two, say for some k>=1. Then, to hash an integer x, we use the
following hash function:

Notice that since M and W are both powers of two, the ratio is also a power two.
Therefore, in order to multiply the term by M/W we simply shift it to the right by
w-k bits! In effect, we are extracting k bits from the middle of the square of the key--hence the
name of the method.

3. Multiplication Method

A very simple variation on the middle-square method that alleviates its deficiencies is the so-
called, multiplication hashing method . Instead of multiplying the key x by itself, we multiply
the key by a carefully chosen constant a, and then extract the middle k bits from the result. In
this case, the hashing function is

What is a suitable choice for the constant a? If we want to avoid the problems that the middle-
square method encounters with keys having a large number of leading or trailing zeroes, then
we should choose an a that has neither leading nor trailing zeroes.

48
Collision resolution

If two keys hash to the same index, the corresponding records cannot be stored in the same
location. So, if it's already occupied, we must find another location to store the new record, and
do it so that we can find it when we look it up later on.

To give an idea of the importance of a good collision resolution strategy, consider the
following result, derived using the birthday paradox. Even if we assume that our hash function
outputs random indices uniformly distributed over the array, and even for a hash table with 1
million indices, there is a 95% chance of at least one collision occurring before it contains 2500
records.

There are a number of collision resolution techniques, but the most popular are open
addressing and chaining.

OPEN HASHING (SEPARATE CHAINING)


Hash collision resolved by chaining.Sometimes called simply chaining or direct
chaining, this technique in its simplest form has a linked list of inserted records at each slot in
the array references. Each linked list has each element that collides to the same slot. Insertion
requires finding the correct slot, and appending to either end of the list in that slot; deletion
requires searching the list and removal.

Chained hash tables have advantages over open addressed hash tables in that the removal
operation is simple and resizing the table can be postponed for a much longer time because
performance degrades more gracefully even when every slot is used. Indeed, many chaining
hash tables may not require resizing at all since performance degradation is linear as the table
fills. For example, a chaining hash table containing twice its recommended capacity of data
would only be about twice as slow on average as the same table at its recommended capacity.

Chained hash tables inherit the disadvantages of linked lists. When storing small
records, the overhead of the linked list can be significant. An additional disadvantage is that
traversing a linked list has poor cache performance.

Alternative data structures can be used for chains instead of linked lists. By using a
self-balancing tree, for example, the theoretical worst-case time of a hash table can be brought
down to O(log n) rather than O(n). However, since each list is intended to be short, this
approach is usually inefficient unless the hash table is designed to run at full capacity or there
are unusually high collision rates, as might occur in input designed to cause collisions.
Dynamic arrays can also be used to decrease space overhead and improve cache performance
when records are small.

Some chaining implementations use an optimization where the first record of each
chain is stored in the table. The purpose is to increase cache efficiency of hash table access. In
order to avoid wasting large amounts of space, such hash tables would maintain a load factor
of 1.0 or greater. The term direct chaining is sometimes used to describe implementations that
do not use this optimization.

49
CLOSED HASHING (OPEN ADDRESSING)

Hash collision resolved by linear probing (interval=1). Open addressing hash tables store the
records directly within the array. This approach is also called closed hashing. A hash collision
is resolved by probing, or searching through alternate locations in the array (the probe
sequence) until either the target record is found, or an unused array slot is found, which
indicates that there is no such key in the table. [2] Well known probe sequences include:
Linear probing
in which the interval between probes is fixed - often at 1.
Quadratic probing
in which the interval between probes increases proportional to the hash value (the
interval thus increasing linearly and the indices are described by a quadratic function).
Double hashing
in which the interval between probes is computed by another hash function.

Open addressing versus chaining

Chained hash tables have the following benefits over open addressing:

They are simple to implement effectively and only require basic data structures.
From the point of view of writing suitable hash functions, chained hash tables are
insensitive to clustering, only requiring minimization of collisions. Open addressing
depends upon better hash functions to avoid clustering. This is particularly important if
novice programmers can add their own hash functions, but even experienced
programmers can be caught out by unexpected clustering effects.
They degrade in performance more gracefully. Although chains grow longer as the
table fills, a chained hash table cannot "fill up" and does not exhibit the sudden
increases in lookup times that occur in a near-full table with open addressing. (see
right)
If the hash table stores large records, about 5 or more words per record, chaining uses
less memory than open addressing.
If the hash table is sparse (that is, it has a big array with many free array slots), chaining
uses less memory than open addressing even for small records of 2 to 4 words per
record due to its external storage.

This graph compares the average number of cache misses required to lookup elements in tables
with chaining and linear probing. As the table passes the 80%-full mark, linear probing's
performance drastically degrades.

For small record sizes (a few words or less) the benefits of in-place open addressing compared
to chaining are:

They can be more space-efficient than chaining since they don't need to store any
pointers or allocate any additional space outside the hash table. Simple linked lists
require a word of overhead per element.
Insertions avoid the time overhead of memory allocation, and can even be implemented
in the absence of a memory allocator.
Because it uses internal storage, open addressing avoids the extra indirection required
for chaining's external storage. It also has better locality of reference, particularly with

50
linear probing. With small record sizes, these factors can yield better performance than
chaining, particularly for lookups.
They can be easier to serialize, because they don't use pointers.

On the other hand, normal open addressing is a poor choice for large elements, since these
elements fill entire cache lines (negating the cache advantage), and a large amount of space is
wasted on large empty table slots. If the open addressing table only stores references to
elements (external storage), it uses space comparable to chaining even for large records but
loses its speed advantage.

Generally speaking, open addressing is better used for hash tables with small records that can
be stored within the table (internal storage) and fit in a cache line. They are particularly
suitable for elements of one word or less. In cases where the tables are expected to have high
load factors, the records are large, or the data is variable-sized, chained hash tables often
perform as well or better.

Ultimately, used sensibly, any kind of hash table algorithm is usually fast enough; and the
percentage of a calculation spent in hash table code is low. Memory usage is rarely considered
excessive. Therefore, in most cases the differences between these algorithms is marginal, and
other considerations typically come into play.

DOUBLE HASHING
The last collision resolution method we will examine is double hashing. For double hashing,
one popular choice is f(i) = i h2(x). This formula says that we apply a second hash function to
x and probe at a distance h2(x), 2h2(x), . . ., and so on. A poor choice of h2(x) would be
disastrous.

REHASHING
If the table gets too full, the running time for the operations will start taking too long
and inserts might fail for closed hashing with quadratic resolution. This can happen if there are
too many deletions intermixed with insertions. A solution, then, is to build another table that is
about twice as big (with associated new hash function) and scan down the entire original hash
table, computing the new hash value for each (non-deleted) element and inserting it in the new
table.

EXTENDIBLE HASHING

If either open hashing or closed hashing is used, the major problem is that collisions
could cause several blocks to be examined during a find, even for a well-distributed hash table.
Furthermore, when the table gets too full, an extremely expensive rehashing step must be
performed, which requires O(n) disk accesses. A clever alternative, known as extendible
hashing, allows a find to be performed in two disk accesses. Insertions also require few disk
accesses.

51
THE DISJOINT SET ADT

The data structure is simple to implement. Each routine requires only a few lines of code, and a
simple array can be used. The implementation is also extremely fast, requiring constant
average time per operation. This data structure is also very interesting from a theoretical point
of view, because its analysis is extremely difficult; the functional form of the worst case is
unlike any we have yet seen. For the disjoint set ADT, we will

Show how it can be implemented with minimal coding effort.

Greatly increase its speed, using just two simple observations.

Analyze the running time of a fast implementation.

See a simple application.

Equivalence Relations

A relation R is defined on a set S if for every pair of elements (a, b), a, b S, a R b is either true
or false. If a R b is true, then we say that a is related to b.

An equivalence relation is a relation R that satisfies three properties:

1. (Reflexive) a R a, for all a S.

2. (Symmetric) a R b if and only if b R a.

3. (Transitive) a R b and b R c implies that a R c.

We'll consider several examples.

The relationship is not an equivalence relationship. Although it is reflexive, since a a, and


transitive, since a b and b c implies a c, it is not symmetric, since a b does not imply b a.

Electrical connectivity, where all connections are by metal wires, is an equivalence relation.
The relation is clearly reflexive, as any component is connected to itself. If a is electrically
connected to b, then b must be electrically connected to a, so the relation is symmetric. Finally,
if a is connected to b and b is connected to c, then a is connected to c. Thus electrical
connectivity is an equivalence relation.

Two cities are related if they are in the same country. It is easily verified that this is an
equivalence relation. Suppose town a is related to b if it is possible to travel from a to b by
taking roads. This relation is an equivalence relation if all the roads are two-way.

THE DYNAMIC EQUIVALENCE PROBLEM

Given an equivalence relation ~, the natural problem is to decide, for any a and b, if a ~
b. If the relation is stored as a two-dimensional array of booleans, then, of course, this can be

52
done in constant time. The problem is that the relation is usually not explicitly, but rather
implicitly, defined.

As an example, suppose the equivalence relation is defined over the five-element set {a1, a2,
a3, a4, a5}. Then there are 25 pairs of elements, each of which is either related or not. However,
the information a1 ~ a2, a3 ~ a4, a5 ~ a1, a4 ~ a2 implies that all pairs are related. We would like
to be able to infer this quickly.

The equivalence class of an element a S is the subset of S that contains all the elements that
are related to a. Notice that the equivalence classes form a partition of S: Every member of S
appears in exactly one equivalence class. To decide if a ~ b, we need only to check whether a
and b are in the same equivalence class. This provides our strategy to solve the equivalence
problem.

The input is initially a collection of n sets, each with one element. This initial representation is
that all relations (except reflexive relations) are false. Each set has a different element, so that
Si Sj = ; this makes the sets disjoint.

There are two permissible operations. The first is find, which returns the name of the set (that
is, the equivalence class) containing a given element. The second operation adds relations. If
we want to add the relation a ~ b, then we first see if a and b are already related. This is done
by performing finds on both a and b and checking whether they are in the same equivalence
class. If they are not, then we apply union. This operation merges the two equivalence classes
containing a and b into a new equivalence class. From a set point of view, the result of is to
create a new set Sk = Si Sj, destroying the originals and preserving the disjointness of all the
sets. The algorithm to do this is frequently known as the disjoint set union/find algorithm for
this reason.

This algorithm is dynamic because, during the course of the algorithm, the sets can change via
the union operation. The algorithm must also operate on-line: When a find is performed, it
must give an answer before continuing. Another possibility would be an off-line algorithm.
Such an algorithm would be allowed to see the entire sequence of unions and finds. The answer
it provides for each find must still be consistent with all the unions that were performed up
until the find, but the algorithm can give all its answers after it has seen all the questions. The
difference is similar to taking a written exam (which is generally off-line--you only have to
give the answers before time expires), and an oral exam (which is on-line, because you must
answer the current question before proceeding to the next question).

Notice that we do not perform any operations comparing the relative values of elements, but
merely require knowledge of their location. For this reason, we can assume that all the
elements have been numbered sequentially from 1 to n and that the numbering can be
determined easily by some hashing scheme. Thus, initially we have Si = {i} for i = 1 through n.

Our second observation is that the name of the set returned by find is actually fairly abitrary.
All that really matters is that find(x) = find( ) if and only if x and are in the same set.

These operations are important in many graph theory problems and also in compilers which
process equivalence (or type) declarations. We will see an application later.

53
There are two strategies to solve this problem. One ensures that the find instruction can
be executed in constant worst-case time, and the other ensures that the union instruction can be
executed in constant worst-case time. It has recently been shown that both cannot be done
simultaneously in constant worst-case time.

We will now briefly discuss the first approach. For the find operation to be fast, we
could maintain, in an array, the name of the equivalence class for each element. Then find is
just a simple O(1) lookup. Suppose we want to perform union(a, b). Suppose that a is in
equivalence class i and b is in equivalence class j. Then we scan down the array, changing all is
to j. Unfortunately, this scan takes (n). Thus, a sequence of n - 1 unions (the maximum, since
then everything is in one set), would take (n2) time. If there are (n2) find operations, this
performance is fine, since the total running time would then amount to O(1) for each union or
find operation over the course of the algorithm. If there are fewer finds, this bound is not
acceptable.

One idea is to keep all the elements that are in the same equivalence class in a linked
list. This saves time when updating, because we do not have to search through the entire array.
This by itself does not reduce the asymptotic running time, because it is still possible to
perform (n2) equivalence class updates over the course of the algorithm.

If we also keep track of the size of each equivalence class, and when performing unions
we change the name of the smaller equivalence class to the larger, then the total time spent for
n - 1 merges isO (n log n). The reason for this is that each element can have its equivalence
class changed at most log n times, since every time its class is changed, its new equivalence
class is at least twice as large as its old. Using this strategy, any sequence of m finds and up to
n - 1 unions takes at most O(m + n log n) time.

In the remainder of this chapter, we will examine a solution to the union/find problem
that makes unions easy but finds hard. Even so, the running time for any sequences of at most
m finds and up to n - 1 unions will be only a little more than O(m + n).

Basic Data Structure

Recall that the problem does not require that a find operation return any specific name,
just that finds on two elements return the same answer if and only if they are in the same set.
One idea might be to use a tree to represent each set, since each element in a tree has the same
root. Thus, the root can be used to name the set. We will represent each set by a tree. (Recall
that a collection of trees is known as a forest.) Initially, each set contains one element. The
trees we will use are not necessarily binary trees, but their representation is easy, because the
only information we will need is a parent pointer. The name of a set is given by the node at the
root. Since only the name of the parent is required, we can assume that this tree is stored
implicitly in an array: each entry p[i] in the array represents the parent of element i. If i is a
root, then p[i] = 0. In the forest in Figure 8.1, p[i] = 0 for 1 i 8. As with heaps, we will draw
the trees explicitly, with the understanding that an array is being used. Figure 8.1 shows the
explicit representation. We will draw the root's parent pointer vertically for convenience.

To perform a union of two sets, we merge the two trees by making the root of one tree point to
the root of the other. It should be clear that this operation takes constant time. Figures 8.2, 8.3,
and 8.4 represent the forest after each of union(5,6) union(7,8), union(5,7), where we have

54
adopted the convention that the new root after the union(x,y) is x. The implicit representation
of the last forest is shown in Figure 8.5.

A find(x) on element x is performed by returning the root of the tree containing x. The
time to perform this operation is proportional to the depth of the node representing x, assuming,
of course, that we can find the node representing x in constant time. Using the strategy above,
it is possible to create a tree of depth n - 1, so the worst-case running time of a find is O(n).
Typically, the running time is computed for a sequence of m intermixed instructions. In this
case, m consecutive operations could take O(mn) time in the worst case.

The code in Figures 6 through 9 represents an implementation of the basic algorithm, assuming
that error checks have already been performed. In our routine, unions are performed on the
roots of the trees. Sometimes the operation is performed by passing any two elements, and
having the union perform two finds to determine the roots.

The average-case analysis is quite hard to do. The least of the problems is that the answer
depends on how to define average (with respect to the union operation). For instance, in the
forest in Figure 8.4, we could say that since there are five trees, there are 5 4 = 20 equally
likely results of the next union (as any two different trees can be unioned). Of course, the
implication of this model is that there is only a chance that the next union will involve the
large tree. Another model might say that all unions between any two elements in different trees
are equally likely, so a larger tree is more likely to be involved in the next union than a smaller
tree. In the example above, there is an chance that the large tree is involved in the next
union, since (ignoring symmetries) there are 6 ways in which to merge two elements in {1, 2,
3, 4}, and 16 ways to merge an element in {5, 6, 7, 8} with an element in {1, 2, 3, 4}. There
are still more models and no general agreement on which is the best. The average running time
depends on the model; (m), (m log n), and (mn) bounds have actually been shown for
three different models, although the latter bound is thought to be more realistic.

Figure 1 Eight elements, initially in different sets

55
Figure 2 After union (5, 6)

Figure 3 After union (7, 8)

Figure 4 After union (5, 7)

Figure 5 Implicit representation of previous tree


typedef int DISJ_SET[ NUM_SETS+1 ];
typedef unsigned int set_type;
typedef unsigned int element_type;

Figure 6 Disjoint set type declaration


void
initialize( DISJ_SET S )
{
int i;
for( i = NUN_SETS; i > 0; i-- )
S[i] = 0;
}

Figure 7 Disjoint set initialization routine


/* Assumes root1 and root2 are roots. */
/* union is a C keyword, so this routine is named set_union. */
void
set_union( DISJ_SET S, set_type root1, set_type root2 )
{
S[root2] = root1;
56
}

Figure 8 Union (not the best way)


set_type
find( element_type x, DISJ_SET S )
{
if( S[x] <= 0 )
return x;
else
return( find( S[x], S ) );
}

Figure 9 A simple disjoint set find algorithm


Quadratic running time for a sequence of operations is generally unacceptable. Fortunately,
there are several ways of easily ensuring that this running time does not occur.

Smart Union Algorithms


The unions above were performed rather arbitrarily, by making the second tree a
subtree of the first. A simple improvement is always to make the smaller tree a subtree of the
larger, breaking ties by any method; we call this approach union-by-size. The three unions in
the preceding example were all ties, and so we can consider that they were performed by size.
If the next operation were union (4, 5), then the forest in Figure 10 would form. Had the size
heuristic not been used, a deeper forest would have been formed (Fig.11).

Figure 10 Result of union-by-size

57
Figure 11 Result of an arbitrary union

Figure 12 Worst-case tree for n = 16


We can prove that if unions are done by size, the depth of any node is never more than
log n. To see this, note that a node is initially at depth 0. When its depth increases as a result of
a union, it is placed in a tree that is at least twice as large as before. Thus, its depth can be
increased at most log n times. This implies that the running time for a find operation is O(log
n), and a sequence of m operations takes O(m log n). The tree in Figure 12 shows the worst tree
possible after 16 unions and is obtained if all unions are between equal-sized trees (the worst-
case trees are binomial trees).

To implement this strategy, we need to keep track of the size of each tree. Since we are
really just using an array, we can have the array entry of each root contain the negative of the
size of its tree. Thus, initially the array representation of the tree is all -1s (and Fig 8.7 needs to
be changed accordingly). When a union is performed, check the sizes; the new size is the sum
of the old. Thus, union-by-size is not at all difficult to implement and requires no extra space.
It is also fast, on average. For virtually all reasonable models, it has been shown that a
sequence of m operations requires O(m) average time if union-by-size is used. This is because
when random unions are performed, generally very small (usually one-element) sets are
merged with large sets throughout the algorithm.

An alternative implementation, which also guarantees that all the trees will have depth
at most O(log n), is union-by-height. We keep track of the height, instead of the size, of each
tree and perform unions by making the shallow tree a subtree of the deeper tree. This is an easy
algorithm, since the height of a tree increases only when two equally deep trees are joined (and
then the height goes up by one). Thus, union-by-height is a trivial modification of union-by-
size.

The following figures show a tree and its implicit representation for both union-by-size
and union-by-height. The code in Figure13 implements union-by-height.

58
Path Compression
The union/find algorithm, as described so far, is quite acceptable for most cases. It is very
simple and linear on average for a sequence of m instructions (under all models). However, the
worst case of O(m log n ) can occur fairly easily and naturally.

/* assume root1 and root2 are roots */


/* union is a C keyword, so this routine is named set_union */
void
set_union (DISJ_SET S, set_type root1, set_type root2 )
{
if( S[root2] < S[root1] ) /* root2 is deeper set */
S[root1] = root2; /* make root2 new root */
else
{
if( S[root2] == S[root1] ) /* same height, so update */
S[root1]--;
S[root2] = root1; /* make root1 new root */
}
}

Figure 13 Code for union-by-height (rank)


For instance, if we put all the sets on a queue and repeatedly dequeue the first two sets
and enqueue the union, the worst case occurs. If there are many more finds than unions, this
running time is worse than that of the quick-find algorithm. Moreover, it should be clear that
there are probably no more improvements possible for the union algorithm. This is based on
the observation that any method to perform the unions will yield the same worst-case trees,
since it must break ties arbitrarily. Therefore, the only way to speed the algorithm up, without
reworking the data structure entirely, is to do something clever on the find operation.

59
The clever operation is known as path compression. Path compression is performed during a
find operation and is independent of the strategy used to perform unions. Suppose the operation
is find(x). Then the effect of path compression is that every node on the path from x to the root
has its parent changed to the root. Figure 14 shows the effect of path compression after find
(15) on the generic worst tree of Figure 12.

The effect of path compression is that with an extra two pointer moves, nodes 13 and 14 are
now one position closer to the root and nodes 15 and 16 are now two positions closer. Thus,
the fast future accesses on these nodes will pay (we hope) for the extra work to do the path
compression.

As the code in Figure 15 shows, path compression is a trivial change to the basic find
algorithm. The only change to the find routine is that S[x] is made equal to the value returned
by find; thus after the root of the set is found recursively, x is made to point directly to it. This
occurs recursively to every node on the path to the root, so this implements path compression.
As we stated when we implemented stacks and queues, modifying a parameter to a function
called is not necessarily in line with current software engineering rules. Some languages will
not allow this, so this code may well need changes.

Figure 14 An example of path compression


set_type
find( element_type x, DISJ_SET S )
{
if( S[x] <= 0 )
return x;
else
return( S[x] = find( S[x], S ) );
}

Figure 15 Code for disjoint set find with path compression


When unions are done arbitrarily, path compression is a good idea, because there is an
abundance of deep nodes and these are brought near the root by path compression. It has been
proven that when path compression is done in this case, a sequence of m operations requires at
most O(m log n) time. It is still an open problem to determine what the average-case behavior
is in this situation.

60
Path compression is perfectly compatible with union-by-size, and thus both routines can be
implemented at the same time. Since doing union-by-size by itself is expected to execute a
sequence of m operations in linear time, it is not clear that the extra pass involved in path
compression is worthwhile on average. Indeed, this problem is still open. However, as we shall
see later, the combination of path compression and a smart union rule guarantees a very
efficient algorithm in all cases.

Path compression is not entirely compatible with union-by-height, because path compression
can change the heights of the trees. It is not at all clear how to re-compute them efficiently. The
answer is do not!! Then the heights stored for each tree become estimated heights (sometimes
known as ranks), but it turns out that union-by-rank (which is what this has now become) is
just as efficient in theory as union-by-size. Furthermore, heights are updated less often than
sizes. As with union-by-size, it is not clear whether path compression is worthwhile on
average. What we will show in the next section is that with either union heuristic, path
compression significantly reduces the worst-case running time

Applications of Sets

Because of its very general or abstract nature, set theory has many applications in the
branch of mathematics. In the branch called analysis, of which differential and integral calculus
are important parts, an understanding of limit points and what is meant by the continuity of a
function are based on set theory. The algebraic treatment of set operations leads to boolean
algebra, in which the operations of intersection, union, and difference are interpreted as
corresponding to the logical operations "and," "or," and "not," respectively. Boolean Algebra in
turn is used extensively in the design of digital electronic circuitry, such as that found in
calculators and personal computers. Set theory provides the basis of topology, the study of sets
together with the properties of various collections of subsets.

61
Unit-4 Graphs

Definitions – Topological sort – breadth-first traversal - shortest-path algorithms –


minimum spanning tree – Prim's and Kruskal's algorithms – Depth-first traversal –
biconnectivity – Euler circuits – applications of graphs

GRAPHS (DATA STRUCTURE)


In computer science, a graph is a kind of data structure, specifically an abstract data
type (ADT) that consists of a set of nodes (also called vertices) and a set of edges that establish
relationships (connections) between the nodes. The graph ADT follows directly from the graph
concept from mathematics.

Informally, G=(V,E) consists of vertices, the elements of V, which are connected by edges, the
elements of E. Formally, a graph, G, is defined as an ordered pair, G=(V,E), where V is a set
(usually finite) and E is a set consisting of two element subsets of V.

Choices of representation

Two main data structures for the representation of graphs are used in practice. The first is
called an adjacency list, and is implemented by representing each node as a data structure that
contains a list of all adjacent nodes. The second is an adjacency matrix, in which the rows and
columns of a two-dimensional array represent source and destination vertices and entries in the
array indicate whether an edge exists between the vertices. Adjacency lists are preferred for
sparse graphs; otherwise, an adjacency matrix is a good choice. Finally, for very large graphs
with some regularity in the placement of edges, a symbolic graph is a possible choice of
representation.

Comparison with other data structures

Graph data structures are non-hierarchical and therefore suitable for data sets where the
individual elements are interconnected in complex ways. For example, a computer network can
be modeled with a graph.

Hierarchical data sets can be represented by a binary or nonbinary tree. It is worth mentioning,
however, that trees can be seen as a special form of graph.

Testing for the representation

Operations

Graph algorithms are a significant field of interest within computer science. Typical operations
associated with graphs are: finding a path between two nodes, like depth-first search and
breadth-first search and finding the shortest path from one node to another, like Dijkstra's
algorithm. A solution to finding the shortest path from each node to every other node also
exists in the form of the Floyd-Warshall algorithm.

62
A directed graph can be seen as a flow network, where each edge has a capacity and each edge
receives a flow. The Ford-Fulkerson algorithm is used to find out the maximum flow from a
source to a sink in a graph.

The graphs can be represented in two ways. One is adjacency matrix and adjacency list.

For example, let us consider the following graph

A----------->B
| ^
| |
| |
V |
C ------------

Adjacency Matrix

A B C
A 0 1 1
B 0 0 0
C 0 1 0

Adjacency List

A ----> | B | ----> | C | ---- NULL


B ----> ---- NULL
C ----> | B | ---- NULL

A graph is the basic object of study in graph theory. Informally speaking, a graph is a set
of objects called points, nodes, or vertices connected by links called lines or edges. In a proper
graph, which is by default undirected, a line from point A to point B is considered to be the
same thing as a line from point B to point A. In a digraph, short for directed graph, the two
directions are counted as being distinct arcs or directed edges. Typically, a graph is depicted in
diagrammatic form as a set of dots (for the points, vertices, or nodes), joined by curves (for the
lines or edges

DEFINITIONS
Definitions in graph theory vary. The following are some of the more basic ways of defining
graphs and related mathematical structures.

Graph
A general example of a graph with three vertices and six edges. A graph or undirected graph
G is an ordered pair G: = (V,E) that is subject to the following conditions:

V is a set, whose elements are called vertices or nodes,


E is a multiset of unordered pairs of vertices (not necessarily distinct), called
edges or lines.

63
(Note that this defines the most general type of graph. Some authors call this a multigraph and
reserve the term "graph" for simple graphs.)

The vertices belonging to an edge are called the ends, endpoints, or end vertices of the edge.

V (and hence E) are usually taken to be finite, and many of the well-known results are not true
(or are rather different) for infinite graphs because many of the arguments fail in the infinite
case. The order of a graph is | V | (the number of vertices). A graph's size is | E | , the number
of edges. The degree of a vertex is the number of edges that connect to it, where an edge that
connects to the vertex at both ends (a loop) is counted twice.

The edges E induce a symmetric binary relation ~ on V which is called the adjacency relation
of G. Specifically, for each edge {u,v} the vertices u and v are said to be adjacent to one
another, which is denoted u ~ v. For an edge {u, v}, graph theorists usually use the somewhat
shorter notation uv.
Types of graphs

Directed graph

A directed graph or digraph G is an ordered pair G: = (V,A) with

V is a set, whose elements are called vertices or nodes,


A is a set of ordered pairs of vertices, called directed edges, arcs, or arrows.

An arc e = (x,y) is considered to be directed from x to y; y is called the head and x is called the
tail of the arc; y is said to be a direct successor of x, and x is said to be a direct predecessor
of y. If a path leads from x to y, then y is said to be a successor of x, and x is said to be a
predecessor of y. The arc (y,x) is called the arc (x,y) inverted.

A directed graph G is called symmetric if, for every arc that belongs to G, the corresponding
inverted arc also belongs to G. A symmetric loopless directed graph is equivalent to an
undirected graph with the pairs of inverted arcs replaced with edges; thus the number of edges
is equal to the number of arcs halved.

A variation on this definition is the oriented graph, which is a graph (or multigraph; see
below) with an orientation or direction assigned to each of its edges. A distinction between a
directed graph and an oriented simple graph is that if x and y are vertices, a directed graph
allows both (x,y) and (y,x) as edges, while only one is permitted in an oriented graph. A more
fundamental difference is that, in a directed graph (or multigraph), the directions are fixed, but
in an oriented graph (or multigraph), only the underlying graph is fixed, while the orientation
may vary.

A directed acyclic graph, occasionally called a dag or DAG, is a directed graph with no
directed cycles.

In the theory of Lie groups, a quiver Q is a directed graph serving as the domain of, and thus
characterizing the shape of, a representation V defined as a functor, specifically an object of
the functor category FinVctKF(Q) where F(Q) is the free category on Q consisting of paths in Q
and FinVctK is the category of finite dimensional vector spaces over a field K. Representations
64
of a quiver label its vertices with vector spaces and its edges (and hence paths) compatibly with
linear transformations between them, and transform via natural transformations.

Undirected graph

A graph G = {V,E} in which every edge is undirected. This is the same as a digraph (look
above) where for an edge (v,u) there is an edge from v to u and u to v.

Finite graph

A finite graph is a graph G = <V,E> such that V(G) and E(G) are finite sets.

Simple graph
A simple graph with three vertices and three edges. Each vertex has degree two, so this is also
a regular graph.

A simple graph is an undirected graph that has no self-loops and no more than one edge
between any two different vertices. In a simple graph the edges of the graph form a set (rather
than a multiset) and each edge is a pair of distinct vertices. In a simple graph with p vertices
every vertex has a degree that is less than p.

Regular graph

A regular graph is a graph where each vertex has the same number of neighbors, i.e., every
vertex has the same degree or valency. A regular graph with vertices of degree k is called a
k-regular graph or regular graph of degree k.

Weighted graph

A graph is a weighted graph if a number (weight) is assigned to each edge. Such weights might
represent, for example, costs, lengths or capacities, etc. depending on the problem.

Weight of the graph is sum of the weights given to all edges.

Mixed graph

A mixed graph G is a graph in which some edges may be directed and some may be
undirected. It is written as an ordered triple G := (V, E, A) with V, E, and A defined as above.
Directed and undirected graphs are special cases.

Complete graph

Complete graphs have the feature that each pair of vertices has an edge connecting them.

Loop

A loop is an edge (directed or undirected) which starts and ends on the same vertex;
these may be permitted or not permitted according to the application. In this context, an edge
with two different ends is called a link.
65
Multi graph

The term "multigraph" is generally understood to mean that multiple edges (and
sometimes loops) are allowed. Where graphs are defined so as to allow loops and multiple
edges, a multigraph is often defined to mean a graph without loops, however, where graphs are
defined so as to disallow loops and multiple edges, the term is often defined to mean a "graph"
which can have both multiple edges and loops, although many use the term "pseudo graph" for
this meaning.

Half-edges, loose edges

In exceptional situations it is even necessary to have edges with only one end, called
half-edges, or no ends (loose edges); see for example signed graphs and biased graphs.

Properties of graphs

Two edges of a graph are called adjacent (sometimes coincident) if they share a common
vertex. Two arrows of a directed graph are called consecutive if the head of the first one is at
the nock (notch end) of the second one. Similarly, two vertices are called adjacent if they
share a common edge (consecutive if they are at the notch and at the head of an arrow), in
which case the common edge is said to join the two vertices. An edge and a vertex on that edge
are called incident.

The graph with only one vertex and no edges is called the trivial graph. A graph with only
vertices and no edges is known as an edgeless graph. The graph with no vertices and no edges
is sometimes called the null graph or empty graph, but not all mathematicians allow this
object.

In a weighted graph or digraph, each edge is associated with some value, variously called its
cost, weight, length or other term depending on the application; such graphs arise in many
contexts, for example in optimal routing problems such as the traveling salesman problem.

Normally, the vertices of a graph, by their nature as elements of a set, are distinguishable. This
kind of graph may be called vertex-labeled. However, for many questions it is better to treat
vertices as indistinguishable; then the graph may be called unlabeled. (Of course, the vertices
may be still distinguishable by the properties of the graph itself, e.g., by the numbers of
incident edges). The same remarks apply to edges, so that graphs which have labeled edges are
called edge-labeled graphs. Graphs with labels attached to edges or vertices are more generally
designated as labeled. Consequently, graphs in which vertices are indistinguishable and edges
are indistinguishable are called unlabeled. (Note that in the literature the term labeled may
apply to other kinds of labeling, besides that which serves only to distinguish different vertices
or edges.)

Examples
A graph with six nodes.

The picture is a graphic representation of the following graph

V: = {1,2,3,4,5,6}
66
E: = {{1,2},{1,5},{2,3},{2,5},{3,4},{4,5},{4,6}}

The fact that vertex 1 is adjacent to vertex 2 is sometimes denoted by 1 ~ 2.

In category theory a category can be considered a directed multigraph with the objects
as vertices and the morphisms as directed edges. The functors between categories
induce then some, but not necessarily all, of the digraph . In computer science directed
graphs are used to represent finite state machines and many other discrete structures.
A binary relation R on a set X is a directed graph. Two edges x, y of X are connected by
an arrow if xRy.

Important graphs

Basic examples are:

In a complete graph each pair of vertices is joined by an edge, that is, the graph
contains all possible edges.
In a bipartite graph, the vertices can be divided into two sets, W and X, so that every
edge has one vertex in each of the two sets.
In a complete bipartite graph, the vertex set is the union of two disjoint subsets, W and
X, so that every vertex in W is adjacent to every vertex in X but there are no edges
within W or X.
In a path of length n, the vertices can be listed in order, v0, v1, ..., vn, so that the edges
are vi−1vi for each i = 1, 2, ..., n.
A cycle or circuit of length n is a closed path without self-intersections; equivalently, it
is a connected graph with degree 2 at every vertex. Its vertices can be named v1, ..., vn
so that the edges are vi−1vi for each i = 2,...,n and vnv1
A planar graph can be drawn in a plane with no crossing edges (i.e., embedded in a
plane).
A forest is a graph with no cycles.
A tree is a connected graph with no cycles.

More advanced kinds of graphs are:

The Petersen graph and its generalizations


Perfect graphs
Co graphs
Other graphs with large automorphism groups: vertex-transitive, arc-transitive, and
distance-transitive graphs.
Strongly regular graphs and their generalization distance-regular graphs.

TOPOLOGICAL SORTING
In graph theory, a topological sort or topological ordering of a directed acyclic graph (DAG)
is a linear ordering of its nodes in which each node comes before all nodes to which it has
outbound edges. Every DAG has one or more topological sorts. Topological sorting is
sometimes also referred to as ancestral ordering (Neapolitan 2004).

67
More formally, define the partial order relation R over the nodes of the DAG such that xRy if
and only if there is a directed path from x to y. Then, a topological sort is a linear extension of
this partial order, that is, a total order compatible with the partial order.

Examples

The canonical application of topological sorting (topological order) is in scheduling a sequence


of jobs or tasks; topological sorting algorithms were first studied in the early 1960s in the
context of the PERT technique for scheduling in project management (Jarnagin 1960). The
jobs are represented by vertices, and there is an edge from x to y if job x must be completed
before job y can be started (for example, when washing clothes, the washing machine must
finish before we put the clothes to dry). Then, a topological sort gives an order in which to
perform the jobs.

In computer science, applications of this type arise in instruction scheduling, ordering of


formula cell evaluation when recomputing formula values in spreadsheets, determining the
order of compilation tasks to perform in makefiles, and resolving symbol dependencies in
linkers.

The graph shown to the left has many valid topological sorts, including:

7,5,3,11,8,2,10,9
7,5,11,2,3,10,8,9
3,7,8,5,11,10,9,2
3,5,7,11,10,2,8,9

Algorithms

The usual algorithms for topological sorting have running time linear in the number of nodes
plus the number of edges (O(|V|+|E|)).

One of these algorithms, first described by Kahn (1962), works by choosing vertices in the
same order as the eventual topological sort. First, find a list of "start nodes" which have no
incoming edges and insert them into a queue Q (at least one such node must exist if graph is
acyclic). L is the list that will contain the nodes in topological sorted order after the end of the
algorithm. Then,

L ← Empty list where we put the sorted elements


Q ← Set of all nodes with no incoming edges
while Q is non-empty do
remove a node n from Q
insert n into L
for each node m with an edge e from n to m do
remove edge e from the graph
if m has no other incoming edges then
insert m into Q
if graph has edges then
output error message (graph has a cycle)
else
output message (proposed topologically sorted order: L)

68
If the graph was a DAG, a solution is contained in the list L (the solution is not unique).
Otherwise, the graph has at least one cycle and therefore a topological sorting is impossible.

Note that, reflecting the non-uniqueness of the resulting sort, the structure Q need not be a
queue; it may be a stack or simply a set. Depending on the order that nodes n are removed from
set Q, a different solution is created.

An alternative algorithm for topological sorting is based on depth-first search. Loop through
the vertices of the graph, in any order, initiating a depth-first search for any vertex that has not
already been visited by a previous search. The desired topological sorting is the reverse
postorder of these searches. That is, we can construct the ordering as a list of vertices, by
adding each vertex to the start of the list at the time when the depth-first search is processing
that vertex and has returned from processing all children of that vertex. Since each edge and
vertex is visited once, the algorithm runs in linear time. This depth-first search based algorithm
is the one described by Cormen, Leiserson & Rivest (1990); it seems to have been first
described in print by Tarjan (1976).

BREADTH FIRST TRAVERSAL:


In Breadth first search we start at vertex v and mark it as having been reached. The
vertex v at this time is said to be unexplored. A vertex is said to have been explored by an
algorithm when the algorithm has visited all vertices adjacent from it. All unvisited vertices
adjacent from v are visited next. There are new unexplored vertices. Vertex v has now been
explored. The newly visited vertices have not been explored and are put onto the end of the list
of unexplored vertices. The first vertex on this list is the next to be explored. Exploration
continues until no unexplored vertex is left. The list of unexplored vertices acts as a queue and
can be represented using any of the standard queue representations.

SHORTEST PATH ALGORITHMS

Dijkstra's algorithm

Dijkstra's algorithm, conceived by Dutch computer scientist Edsger Dijkstra in 1959


is a graph search algorithm that solves the single-source shortest path problem for a graph with
non negative edge path costs, outputting a shortest path tree. This algorithm is often used in
routing.

For a given source vertex (node) in the graph, the algorithm finds the path with lowest
cost (i.e. the shortest path) between that vertex and every other vertex. It can also be used for
finding costs of shortest paths from a single vertex to a single destination vertex by stopping
the algorithm once the shortest path to the destination vertex has been determined. For
example, if the vertices of the graph represent cities and edge path costs represent driving
distances between pairs of cities connected by a direct road, Dijkstra's algorithm can be used to
find the shortest route between one city and all other cities.

Algorithm

It should be noted that distance between nodes can also be referred to as weight.

1. Create a distance list, a previous vertex list, a visited list, and a current vertex.
69
2. All the values in the distance list are set to infinity except the starting vertex which is
set to zero.
3. All values in visited list are set to false.
4. All values in the previous list are set to a special value signifying that they are
undefined, such as null.
5. Current vertex is set as the starting vertex.
6. Mark the current vertex as visited.
7. Update distance and previous lists based on those vertices which can be immediately
reached from the current vertex.
8. Update the current vertex to the unvisited vertex that can be reached by the shortest
path from the starting vertex.
9. Repeat (from step 6) until all nodes are visited.

Lay descriptions of the algorithm

Suppose you create a knotted web of strings, with each knot corresponding to a node,
and the strings corresponding to the edges of the web: the length of each string is proportional
to the weight of each edge. Now you compress the web into a small pile without making any
knots or tangles in it. You then grab your starting knot and pull straight up. As new knots start
to come up with the original, you can measure the straight up-down distance to these knots:
this must be the shortest distance from the starting node to the destination node. The acts of
"pulling up" and "measuring" must be abstracted for the computer, but the general idea of the
algorithm is the same: you have two sets, one of knots that are on the table, and another of
knots that are in the air. Every step of the algorithm, you take the closest knot from the table
and pull it into the air, and mark it with its length. If any knots are left on the table when you're
done, you mark them with the distance infinity.

Or, using a street map, suppose you're marking over the streets (tracing the street with a
marker) in a certain order, until you have a route marked in from the starting point to the
destination. The order is conceptually simple: from all the street intersections of the already
marked routes, find the closest unmarked intersection - closest to the starting point (the
"greedy" part). It's the whole marked route to the intersection, plus the street to the new,
unmarked intersection. Mark that street to that intersection, draw an arrow with the direction,
then repeat. Never mark to any intersection twice. When you get to the destination, follow the
arrows backwards. There will be only one path back against the arrows, the shortest one.

Pseudocode

In the following algorithm, u := node in Q with smallest dist[] searches for the vertex
u in the vertex set Q that has the least dist[u] value. That vertex is removed from the set Q and
returned to the user. dist_between(u, v) calculates the length between the two neighbor-
nodes u and v. alt on line 11 is the length of the path from the root node to the neighbor node v
if it were to go through u. If this path is shorter than the current shortest path recorded for v,
that current path is replaced with this alt path. The previous array is populated with a pointer to
the "next-hop" node on the source graph to get the shortest route to the source.

1 function Dijkstra(Graph, source):


2 for each vertex v in Graph: // Initializations
3 dist[v] := infinity // Unknown distance function
from source to v
70
4 previous[v] := undefined // Previous node in optimal
path from source
5 dist[source] := 0 // Distance from source to
source
6 Q := the set of all nodes in Graph // All nodes in the graph are
unoptimized - thus are in Q
7 while Q is not empty: // The main loop
8 u := node in Q with smallest dist[]
9 remove u from Q
10 for each neighbor v of u: // where v has not yet been
removed from Q.
11 alt := dist[u] + dist_between(u, v)
12 if alt < dist[v] // Relax (u,v)
13 dist[v] := alt
14 previous[v] := u
15 return previous[]

If we are only interested in a shortest path between vertices source and target, we can
terminate the search at line 10 if u = target. Now we can read the shortest path from source to
target by iteration:

1 S := empty sequence
2 u := target
3 while defined previous[u]
4 insert u at the beginning of S
5 u := previous[u]

Now sequence S is the list of vertices constituting one of the shortest paths from source to
target, or the empty sequence if no path exists.

A more general problem would be to find all the shortest paths between source and target
(there might be several different ones of the same length). Then instead of storing only a single
node in each entry of previous[] we would store all nodes satisfying the relaxation condition.
For example, if both r and source connect to target and both of them lie on different shortest
paths through target (because the edge cost is the same in both cases), then we would add both
r and source to previous[target]. When the algorithm completes, previous[] data structure will
actually describe a graph that is a subset of the original graph with some edges removed. Its
key property will be that if the algorithm was run with some starting node, then every path
from that node to any other node in the new graph will be the shortest path between those
nodes in the original graph, and all paths of that length from the original graph will be present
in the new graph. Then to actually find all these short paths between two given nodes we
would use a path finding algorithm on the new graph, such as depth-first search.

MINIMUM SPANNING TREES

Spanning tree

A spanning tree of a graph is just a subgraph that contains all the vertices and is a tree. A graph
may have many spanning trees; for instance the complete graph on four vertices
o---o
|\ /|
| X |
|/ \|
o---o
71
has sixteen spanning trees:
o---o o---o o o o---o
| | | | | |
| | | | | |
| | | | | |
o o o---o o---o o---o

o---o o o o o o o
\ / |\ / \ / \ /|
X | X X X |
/ \ |/ \ / \ / \|
o o o o o---o o o

o o o---o o o o---o
|\ | / | /| \
| \ | / | / | \
| \| / |/ | \
o o o---o o o o---o

o---o o o o o o---o
|\ | / \
| /|
| \ | / \ | / |
| \ |/ \| / |
o o o---o o---o o o

Now suppose the edges of the graph have weights or lengths. The weight of a tree is
just the sum of weights of its edges. Obviously, different trees have different lengths. The
problem: how to find the minimum length spanning tree?

This problem can be solved by many different algorithms. It is the topic of some very recent
research. There are several "best" algorithms, depending on the assumptions you make:

A randomized algorithm can solve it in linear expected time. [Karger, Klein, and
Tarjan, "A randomized linear-time algorithm to find minimum spanning trees", J.
ACM, vol. 42, 1995, pp. 321-328.]
It can be solved in linear worst case time if the weights are small integers. [Fredman
and Willard, "Trans-dichotomous algorithms for minimum spanning trees and shortest
paths", 31st IEEE Symp. Foundations of Comp. Sci., 1990, pp. 719--725.]
Otherwise, the best solution is very close to linear but not exactly linear. The exact
bound is O(m log beta(m,n)) where the beta function has a complicated definition: the
smallest i such that log(log(log(...log(n)...))) is less than m/n, where the logs are nested i
times. [Gabow, Galil, Spencer, and Tarjan, Efficient algorithms for finding minimum
spanning trees in undirected and directed graphs. Combinatorica, vol. 6, 1986, pp. 109--
122.]

These algorithms are all quite complicated, and probably not that great in practice unless you're
looking at really huge graphs. The book tries to keep things simpler, so it only describes one
algorithm but (in my opinion) doesn't do a very good job of it. I'll go through three simple
classical algorithms (spending not so much time on each one).

Why minimum spanning trees? The standard application is to a problem like phone
network design. You have a business with several offices; you want to lease phone lines to
connect them up with each other; and the phone company charges different amounts of money

72
to connect different pairs of cities. You want a set of lines that connects all your offices with a
minimum total cost. It should be a spanning tree, since if a network isn't a tree you can always
remove some edges and save money.

A less obvious application is that the minimum spanning tree can be used to approximately
solve the traveling salesman problem. A convenient formal way of defining this problem is to
find the shortest path that visits each point at least once.

Note that if you have a path visiting all points exactly once, it's a special kind of tree. For
instance in the example above, twelve of sixteen spanning trees are actually paths. If you have
a path visiting some vertices more than once, you can always drop some edges to get a tree. So
in general the MST weight is less than the TSP weight, because it's a minimization over a
strictly larger set.

On the other hand, if you draw a path tracing around the minimum spanning tree, you trace
each edge twice and visit all points, so the TSP weight is less than twice the MST weight.
Therefore this tour is within a factor of two of optimal. There is a more complicated way
(Christofides' heuristic) of using minimum spanning trees to find a tour within a factor of 1.5
of optimal; I won't describe this here but it might be covered in ICS 163 (graph algorithms)
next year.

How to find minimum spanning tree?

The stupid method is to list all spanning trees, and find minimum of list. We already
know how to find minima... But there are far too many trees for this to be efficient. It's also not
really an algorithm, because you'd still need to know how to list all the trees.

A better idea is to find some key property of the MST that lets us be sure that some edge is part
of it, and use this property to build up the MST one edge at a time.

For simplicity, we assume that there is a unique minimum spanning tree. (Problem 4.3 of
Baase is related to this assumption). You can get ideas like this to work without this
assumption but it becomes harder to state your theorems or write your algorithms precisely.

Lemma: Let X be any subset of the vertices of G, and let edge e be the smallest edge
connecting X to G-X. Then e is part of the minimum spanning tree.

Proof: Suppose you have a tree T not containing e; then I want to show that T is not the MST.
Let e=(u,v), with u in X and v not in X. Then because T is a spanning tree it contains a unique
path from u to v, which together with e forms a cycle in G. This path has to include another
edge f connecting X to G-X. T+e-f is another spanning tree (it has the same number of edges,
and remains connected since you can replace any path containing f by one going the other way
around the cycle). It has smaller weight than t since e has smaller weight than f. So T was not
minimum, which is what we wanted to prove.

PRIM’S ALGORITHM
A greedy method to obtain a minimum-cost spanning tree builds this tree edge by edge. The
next edge to include is chosen according to some optimization criterion. The simplest such
criterion is to choose an edge that results in a minimum increase in the sum of the costs of the
73
edges so far included. There are two possible ways to interpret this criterion. In the first, the
set of edges so far selected form a tree. Thus, if A is the set of edges selected so far, then A
forms a tree. The next edge(u,v) to be included in A is a minimum-cost edge not in A with the
property that A U {(u,v)} is also a tree. The corresponding algorithm is known as prim‟s
algorithm.

For Prim‟s algorithm draw n isolated vertices and label them v1, v2, v3,…vn. Tabulate the
given weights of the edges of g in an n by n table. Set the non existent edges as very large.
Start from vertex v1 and connect it to its nearest neighbor (i.e., to the vertex, which has the
smallest entry in row1 of table) say Vk. Now consider v1 and vk as one subgraph and connect
this subgraph to its closest neighbor. Let this new vertex be vi. Next regard the tree with v1
vk and vi as one subgraph and continue the process until all n vertices have been connected
by n-1 edges.

Consider the graph shown in fig 7.3. There are 6 vertices and 12 edges. The weights are
tabulated in table given below.

V1 V2 V3 V4 V5 V6
V1 - 10 16 11 10 17
V2 10 - 9.5 Inf Inf 19.5
V3 16 9.5 - 7 Inf 12
V4 11 Inf 7 - 8 7
V5 10 Inf Inf 8 - 9
V6 17 19.5 12 7 9 -

Start with v1 and pick the smallest entry in row1, which is either (v1,v2) or (v1,v5). Let us
pick (v1, v5). The closest neighbor of the subgraph (v1,v5) is v4 as it is the smallest in the
rows v1 and v5. The three remaining edges selected following the above procedure turn out to

74
be (v4,v6) (v4,v3) and (v3, v2) in that sequence. The resulting shortest spanning tree is shown
in fig 7.4. The weight of this tree is 41.5.
KRUSKAL’S ALGORITHM:
There is a second possible interpretation of the optimization criteria mentioned earlier in
which the edges of the graph are considered in non-decreasing order of cost. This
interpretation is that the set t of edges so far selected for the spanning tree be such that it is
possible to complete t into a tree. Thus t may not be a tree at all stages in the algorithm. In
fact, it will generally only be a forest since the set of edges t can be completed into a tree if
and only if there are no cycles in t. this method is due to kruskal.
The Kruskal algorithm can be illustrated as folows, list out all edges of graph G in order of non-
decreasing weight. Next select a smallest edge that makes no circuit with previously selected
edges. Continue this process until (n-1) edges have been selected and these edges will constitute
the desired shortest spanning tree.
For above fig kruskal solution is as follows,
V1 to v2 =10
V1 to v3 = 16
V1 to v4 = 11
V1 to v5 = 10
V1 to v6 = 17
V2 to v3 = 9.5
V2 to v6 = 19.5
V3 to v4 = 7
V3 to v6 =12
V4 to v5 = 8
V4 to v6 = 7
V5 to v6 = 9
The above path in ascending order is
V3 to v4 = 7
V4 to v6 = 7
V4 to v5 = 8
V5 to v6 = 9
V2 to v3 = 9.5
V1 to v5 = 10
V1 to v2 =10
V1 to v4 = 11
V3 to v6 =12
V1 to v3 = 16
V1 to v6 = 17
V2 to v6 = 19.5

Select the minimum, i.e., v3 to v4 connect them, now select v4 to v6 and then v4 to v5, now if
we select v5 to v6 then it forms a circuit so drop it and go for the next. Connect v2 and v3 and
finally connect v1 and v5. Thus, we have a minimum spanning tree, which is similar to the
figure 7.4.
Techniques for graphs:
A fundamental problem concerning graphs is the reachability problem. In its simplest form it
requires us to determine whether there exists a path in the given graph G=(V,E) such that this

75
path starts at vertex v and ends at vertex u. A more general form is to determine for a given
starting
Vertex v belonging to V all vertices u such that there is a path from v to u. This latter problem
can be solved by starting at vertex v and systematically searching the graph G for vertices that
can be reached from v. The 2 search methods for this are :
1) Breadth first search.
2) Depth first search.
DEPTH FIRST TRAVERSAL:
A depth first search of a graph differs from a breadth first search in that the exploration
of a vertex v is suspended as soon as a new vertex is reached. At this time the exploration of
the new vertex u begins. When this new vertex has been explored, the exploration of u
continues. The search terminates when all reached vertices have been fully explored. This
search process is best-described recursively.
Algorithm DFS(v)
{ visited[v]=1
for each vertex w adjacent from v do
{
If (visited[w]=0)then
DFS(w);}}
Applications of Depth first traversal
BICONNECTIVITY

A connected undirected graph is biconnected if there are no vertices whose removal


disconnects the rest of the graph. If the nodes are computers and the edges are links, then if any
computer goes down, network mail is unaffected, except, of course, at the down computer.
Similarly, if a mass transit system is biconnected, users always have an alternate route should
some terminal be disrupted.

Articulation Points

The vertices whose removal would disconnect the graph are known as articulation points. The
graph is not biconnected, if it has articulation points. Depth first search provides a linear time
algorithm to find all articulation points in a connected graph.

Steps to find Articulation Points :

Step 1 : Perform Depth first search, starting at any vertex

Step 2 : Number the vertex as they are visited, as Num (v).

Step 3 : Compute the lowest numbered vertex for every vertex v in the Depth first

76
spanning tree, which we call as low (w), that is reachable from v by taking zero

or more tree edges and then possibly one back edge. By definition, Low(v) is the

minimum of

(i) Num (v)

(ii) The lowest Num (w) among all back edges (v, w)

(iii) The lowest Low (w) among all tree edges (v, w)

Step 4 : (i) The root is an articulation if and only if it has more than two children.

(ii) Any vertex v other than root is an articulation point if and only if v has same child w such
that Low (w) Num (v). .

Note :For any edge (v, w) we can tell whether it is a tree edge or back edge merely by checking
Num (v) and Num (w). If Num (w) > Num (v) then the edge (v, w) is a back edge

EULER PATHS AND CIRCUITS

Let¡¦s look at the graph below:

A vertex in an intersection of 2 edges. An edge is an arc joining any 2 vertices.

Euler path: A graph is said to be containing an Euler path if it can be traced in 1 sweep without
lifting the pencil from the paper and without tracing the same edge more than once. Vertices
may be passed through more than once. The starting and ending points need not be the same.

Euler circuit: An Euler circuit is similar to an Euler path, except that the starting and ending
points must be the same.

Let¡¦s look at the graphs below; do they contain an Euler circuit or an Euler path?

77
Graph 1 Graph 2

Graph 3 Graph 4

Graph 5 Graph 6

What is the relationship between the nature of the vertices and the kind of path/circuit that the
graph contains? We¡¦ll have the answer after looking at the table below.

78
Graph Number of odd vertices Number of even vertices What does the path
(vertices connected to an (vertices connected to an contain?(Euler path
odd number of edges) even number of edges) = P;Euler circuit =
C;Neither = N)

1 0 10 C

2 0 6 C

3 2 6 P

4 2 4 P

5 4 1 N

6 8 0 N

From the above table, we can observe that:

1. A graph with all vertices being even contains an Euler circuit.


2. A graph with 2 odd vertices and some even vertices contains an Euler path.
3. A graph with more than 2 odd vertices does not contain any Euler path or circuit.

Application of Graphs
Graph theory is widely used in computer science. The applications of graph theory are
 Graph is used in computer networking such as Local Area network, Wide Area network
and internetworks.
 Graph theory is effectively used in telephone cabling.
 Job scheduling algorithms make use of graphs.

79
UNIT 5 Algorithm Design and Analysis

Introduction to algorithm design techniques: Greedy algorithms, Divide and conquer,


Dynamic programming, backtracking, branch and bound, Randomized algorithms –
Introduction to algorithm analysis: asymptotic notations, recurrences – Introduction to
NP-complete problems

INTRODUCTION TO ALGORITHMIC DESIGN TECHNIQUES

1. DIVIDE AND CONQUER


There are a number of general and powerful computational strategies that are repeatedly used
in computer science. It is often possible to phrase any problem in terms of these general
strategies. These general strategies are Divide and Conquer, Dynamic Programming. The
techniques of Greedy Search, Backtracking and Branch and Bound evaluation are variations of
dynamic programming idea. All these strategies and techniques are discussed in the subsequent
chapters.
The most widely known and often used of these is the divide and conquer strategy.
The basic idea of divide and conquer is to divide the original problem into two or more sub-
problems which can be solved by the same technique. If it is possible to split the problem
further into smaller and smaller sub-problems, a stage is reached where the sub-problems are
small enough to be solved without further splitting. Combining the solutions of the individuals
we get the final conquering. Combining need not mean, simply the union of individual
solutions.
Divide and Conquer involves four steps
1) Divide
2) Conquer [Initial Conquer occurred due to solving]
3) Combine
4) Conquer [Final Conquer].
In precise, forward journey is divide and backward journey is Conquer. A general binary
divide and conquer algorithm is :
Procedure D&C (P,Q) //the data size is from p to q
{
If size(P,Q) is small Then
Solve(P,Q)
Else
M divide(P,Q)
Combine (D&C(P,M), D&C(M+1,Q))
}
Sometimes, this type of algorithm is known as control abstract algorithms as they give an
abstract flow. This way of breaking down the problem has found wide application in sorting,
selection and searching algorithm.
Binary Search: Algorithm:
m (p+q)/2
If (p m q) Then do the following Else Stop
If (A(m) = Key Then „successful‟ stop
Else
If (A(m) < key Then
q=m-1;
Else

80
p m+1
End Algorithm.
Illustration :
Consider the data set with elements {12,18,22,32,46,52,59,62,68}. First let us consider the
simulation for successful cases.
Successful cases:

Key=12 P Q m Search
1 9 5 x
1 4 2 x
1 1 1 successful
To search 12, 3 units of time is required

Key=18 P Q m Search
1 9 5 x
1 4 2 successful
To search 18, 2 units of time is required

Key=22 P Q m Search
1 9 5 x
1 4 2 x
3 4 3 successful
To search 22, 3 units of time is required

Key=32 P Q m Search
1 9 5 x
1 4 2 x
3 4 3 x
4 4 4 successful
To search 32, 4 units of time is required

Key=46 P Q m Search
1 9 5 successful
To search 46, 1 unit of time is required

Key=52 P Q m Search
1 9 5 x
6 9 7 x
6 6 6 successful
To search 52, 3 units of time is required

Key=59 P Q m Search
1 9 5 x
6 9 7 successful
To search 59, 2 units of time is required

Key=62 P Q m Search
1 9 5 x
6 9 7 x

81
8 9 8 successful

To search 62, 3 units of time is required

Key=68 P Q m Search
1 9 5 x
6 9 7 x
8 9 8 x
9 9 9 successful
To search 68, 4 units of time is required
3+2+3+4+1+3+2+4
Successful average search time= -------------------------
9
unsuccessful cases

Key=25 P Q m Search
1 9 5 x
1 4 2 x
3 4 3 x
4 4 4 x
To search 25, 4 units of time is required

Key=65 P Q m Search
1 9 5 x
6 9 7 x
8 9 8 x
9 9 9 x
To search 65, 4 units of time is required
4+4
Unsuccessful search time =--------------------
2
average (sum of unsuccessful search time
search = + sum of Successful search time)/(n+(n+1))
time
Max-Min Search:
Max-Min search problem aims at finding the smallest as well as the biggest element in a vector
A of n elements.
Following the steps of Divide and Conquer the vector can be divided into sub-problem as
shown below.

82
The search has now reduced to comparison of 2 numbers. The time is spent in conquering and
comparing which is the major step in the algorithm.
Algorithm: Max-Min (p, q, max, min)
{
If (p = q) Then
max = a(p)
min = a(q)
Else
If ( p – q-1) Then
If a(p) > a(q) Then
max = a(p)
min = a(q)
Else
max = a(q)
min = a(p)
If End
Else
m (p+q)/2
max-min(p,m,max1,min1)
max-min(m+1,q,max2,min2)

max large(max1,max2)
min small(min1,min2)
If End
If End
Algorithm End.
Illustration

Consider a data set with elements {82,36,49,91,12,14,06,76,92}. Initially the max and min
variables have null values. In the first call, the list is broken into two equal halves.. The list is
again broken down into two. This process is continued till the length of the list is either two or
83
one. Then the maximum and minimum values are chosen from the smallest list and these
values are returned to the preceding step where the length of the list is slightly big. This
process is continued till the entire list is searched. The detail description is shown in fig 1
Integer multiplication

There are various methods of obtaining the product of two numbers. The repeated addition
method is left as an assignment for the reader. The reader is expected to find the product of
some bigger numbers using the repeated addition method.
Another way of finding the product is the one we generally use i.e., the left shift method.
left shift method

981*1234
3924
2943*
1962**
981***
1210554

In this method, a=981 is the multiplicand and b=1234 is the multiplier. A is multiplied by
every digit of b starting from right to left. On each multiplication the subsequent products are
shifted one place left. Finally the products obtained by multiplying a by each digit of b is
summed up to obtain the final product.
The above product can also be obtained by a right shift method, which can be illustrated as
follows,
right shift method

981*1234
981
1962
*2943
**3924

1210554
In the above method, a is multiplied by each digit of b from leftmost digit to rightmost digit.
On every multiplication the product is shifted one place to the right and finally all the products
obtained by multiplying „a‟ by each digit of „b‟ is added to obtain the final result.
84
The product of two numbers can also be obtained by dividing „a‟ and multiplying „b‟ by 2
repeatedly until a<=1.
halving and doubling method
Let a=981 and b=1234
The steps to be followed are
1. If a is odd store b
2. A=a/2 and b=b*2
3. Repeat step 2 and step 1 till a<=1

a b result
981 1234 1234
490 2468 ----------
--
245 4936 4936
122 9872 ---------
61 19744 19744
30 39488 ----------
--
15 78976 78976
7 157952 157952
3 315904 315904
1 631808 631808
Sum=1210554
The above method is called the halving and doubling method.
Speed up algorithm:
In this method we split the number till it is easier to multiply. i.e., we split 0981 into 09 and 81
and 1234 into 12 and 34. 09 is then multiplied by both 12 and 34 but, the products are shifted
„n‟ places left before adding. The number of shifts „n‟ is decided as follows

Multiplication shifts
sequence
09*12 4 108****

85
09*34 2 306**
81*12 2 972**
81*34 0 2754
Sum=1210554

For 0981*1234, multiplication of 34 and 81 takes zero shifts, 34*09 takes 2 shifts, 12 and 81
takes 2 shifts and so on.

2. GREEDY METHOD
Greedy method is a method of choosing a subset of the dataset as the solution set that results in
some profit. Consider a problem having n inputs, we are required to obtain the solution which
is a series of subsets that satisfy some constraints or conditions. Any subset, which satisfies
these constraints, is called a feasible solution. It is required to obtain the feasible solution that
maximizes or minimizes the objective function. This feasible solution finally obtained is
called optimal solution.
If one can devise an algorithm that works in stages, considering one input at a time and
at each stage, a decision is taken on whether the data chosen results with an optimal solution or
not. If the inclusion of a particular data results with an optimal solution, then the data is added
into the partial solution set. On the other hand, if the inclusion of that data results with
infeasible solution then the data is eliminated from the solution set.
The general algorithm for the greedy method is
1) Choose an element e belonging to dataset D.
2) Check whether e can be included into the solution set S if Yes solution set is s s U e.
3) Continue until s is filled up or D is exhausted whichever is earlier.
2.1 Cassette Filling
Consider n programs that are to be stored on a tape of length L. Each program I is of length li
where i lies between 1 and n. All programs can be stored on the tape iff the sum of the lengths
of the programs is at most L. It is assumed that, whenever a program is to be retrieved the tape
is initially positioned at the start end.
Let tj be the time required retrieving program ij where programs are stored in the order
I = i1, i2, i3, …,in.
The time taken to access a program on the tape is called the mean retrieval time (MRT)
i.e., tj = lik k=1,2,…,j

86
Now the problem is to store the programs on the tape so that MRT is minimized. From the
above discussion one can observe that the MRT can be minimized if the programs are stored in
an increasing order i.e., l1 l2 l3, … ln.

Hence the ordering defined minimizes the retrieval time. The solution set obtained need not be
a subset of data but may be the data set itself in a different sequence.
Illustration
Assume that 3 sorted files are given. Let the length of files A, B and C be 7, 3 and 5 units
respectively. All these three files are to be stored on to a tape S in some sequence that reduces
the average retrieval time. The table shows the retrieval time for all possible orders.

Order of Retrieval time MRT


recording
ABC 7+(7+3)+(7+3+5)=32 32/3
ACB 7+(7+5)+(7+5+3)=34 34/3
BAC 3+(3+7)+(3+7+5)=28 28/3
BCA 3+(3+5)+(3+5+7)=26 26/3
CAB 5+(5+7)+(5+7+3)=32 32/3
CBA 5+(5+3)+(5+3+7)=28 28/3
General Knapsack problem:
Greedy method is best suited to solve more complex problems such as a knapsack problem. In
a knapsack problem there is a knapsack or a container of capacity M n items where, each item i
is of weight wi and is associated with a profit pi. The problem of knapsack is to fill the
available items into the knapsack so that the knapsack gets filled up and yields a maximum
profit. If a fraction xi of object i is placed into the knapsack, then a profit pixi is earned. The
constrain is that all chosen objects should sum up to M
Illustration
Consider a knapsack problem of finding the optimal solution where, M=15, (p1,p2,p3…p7) =
(10, 5, 15, 7, 6, 18, 3) and (w1, w2, …., w7) = (2, 3, 5, 7, 1, 4, 1).
In order to find the solution, one can follow three different srategies.

Strategy 1 : non-increasing profit values

87
Let (a,b,c,d,e,f,g) represent the items with profit (10,5,15,7,6,18,3) then the sequence of objects
with non-increasing profit is (f,c,a,d,e,b,g).

Item chosen Quantity of item Remaining PiXi


for inclusion included space in M
f 1 full unit 15-4=11 18*1=18
C 1 full unit 11-5=6 15*1=15
A 1 full unit 6-2=4 10*1=10
d 4/7 unit 4-4=0 4/7*7=04
Profit= 47 units
The solution set is (1,0,1,4/7,0,1,0).

Strategy 2: non-decreasing weights


The sequence of objects with non-decreasing weights is (e,g,a,b,f,c,d).

Item chosen Quantity of item Remaining PiX I


for inclusion included space in M
E 1 full unit 15-1=14 6*1=6
G 1 full unit 14-1=13 3*1=3
A 1 full unit 13-2=11 10*1=10
b 1 full unit 11-3=8 5*1=05
f 1 full unit 8-4=4 18*1=18
c 4/5 unit 4-4=0 4/5*15=12
Profit= 54 units
The solution set is (1,1,4/5,0,1,1,1).

Strategy 2: maximum profit per unit of capacity used


(This means that the objects are considered in decreasing order of the ratio Pi/wI)
a: P1/w1 =10/2 = 5 b: P2/w2 =5/3=1.66 c: P3/w3 =15/5 = 3
d: P4/w4 =7/7=1 e: P5/w5 =6/1=6 f: P6/w6 =18/4 = 4.5
g: P7/w7 =3/1=3
Hence, the sequence is (e,a,f,c,g,b,d)

88
Item chosen Quantity of item Remaining PiX I
for inclusion included space in M
E 1 full unit 15-1=14 6*1=6
A 1 full unit 14-2=12 10*1=10
F 1 full unit 12-4=8 18*1=18
C 1 full unit 8-5=3 15*1=15
g 1 full unit 3-1=2 3*1=3
b 2/3 unit 2-2=0 2/3*5=3.33
Profit= 55.33 units
The solution set is (1,2/3,1,0,1,1,1).
In the above problem it can be observed that, if the sum of all the weights is M then all xi =
1, is an optimal solution. If we assume that the sum of all weights exceeds M, all xi‟s cannot
be one. Sometimes it becomes necessary to take a fraction of some items to completely fill the
knapsack. This type of knapsack problems is a general knapsack problem.

Job Scheduling:
In a 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 maximum sum of profits is to be
obtained now.
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.
Steps in finding the subset J are as follows:
a) pi i J is the objective function chosen for optimization measure.
b) Using this measure, the next job to be included should be the one which increases pi i
J.
c) Begin with J = and pi = 0 i J
d) Add a job to J which has the largest profit
e) Add another job to this J keeping in mind the following condition:
i) Search for job which has the next maximum profit.
ii) See if this job is union with J is feasible or not.

89
iii) If yes go to step (e) and continue else go to (iv)
iv) Search for the job with next maximum profit and go to step (b)
f) Terminate when addition of no more jobs is feasible.
Illustration:
Consider 5 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 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.
There are 3 units of time
Time Slot
[0-1] [1-2] [2-3] Profit
Job
1 - yes - 20
2 yes - - 15
3 cannot accommodate --
4 - - yes 5

40
In the first unit of time job 2 is done and a profit of 15 is gained, in the second unit job 1 is
done and a profit 20 is obtained finally in the 3rd unit since the third job is not available 4th job
is done and 5 is obtained as the profit in the above job 3 and 5 could not be accommodated due
to their deadlines.
3. BRANCH AND BOUND and BACKTRACKING
Backtracking
Problems, which deal with searching a set of solutions, or which ask for an optimal solution
satisfying some constraints can be solved using the backtracking formulation. The
backtracking algorithm yields the proper solution in fewer trials.
The basic idea of backtracking is to build up a vector one component at a time and to
test whether the vector being formed has any chance of success. The major advantage of this
algorithm is that if it is realized that the partial vector generated does not lead to an optimal
solution then that vector may be ignored.

90
Backtracking algorithm determine the solution by systematically searching the solution space
for the given problem. This search is accomplished by using a free organization. Backtracking
is a depth first search with some bounding function. All solutions using backtracking are
required to satisfy a complex set of constraints. The constraints may be explicit or implicit.
Explicit constraints are rules, which restrict each vector element to be chosen from the given
set. Implicit constraints are rules, which determine which of the tuples in the solution space,
actually satisfy the criterion function.
3.1Cassette filling problem:
There are n programs that are to be stored on a tape of length L. Every program „i‟ is of length
li. All programs can be stored on the tape if and only if the sum of the lengths of the programs
is at most L. In this problem, it is assumed that whenever a program is to be retrieved, the tape
is positioned at the start end. Hence, the time tj needed to retrieve program ij from a tape
having the programs in the order i1,i2, …,in is called mean retrieval time(MRT) and is given by
tj = lik k=1,2,…,j
In the optimal storage on tape problem, we are required to find a permutation for the n
programs so that when they are stored on the tape, the MRT is minimized.
Let n=3 and (l1,l2,l3)=(5,10,3),there are n!=6 possible orderings. These orderings and their
respective MRT is given in the fig 6.1. Hence, the best order of recording is 3,1,2.

3.2Subset problem:
There are n positive numbers given in a set. The desire is to find all possible subsets of this
set, the contents of which add onto a predefined value M.

91
Let there be n elements in the main set. W=w[1..n] represent the elements of the set. i.e., w =
(w1,w2,w3,…,wn) vector x = x[1..n] assumes either 0 or 1 value. If element w(i) is included
in the subset then x(i) =1.
Consider n=6 m=30 and w[1..6]={5,10,12,13,15,18}. The partial backtracking tree is shown in
fig 6.2. The label to the left of a node represents the item number chosen for insertion and the
label to the right represents the space occupied in M. S represents a solution to the given
problem and B represents a bounding criteria if no solution can be reached. For the above
problem the solution could be (1,1,0,0,1,0), (1,0,1,1,0,0) and (0,0,1,0,0,1). Completion of the
tree structure is left as an assignment for the reader.

3.3 8 queen problem:


The 8 queen problem can be stated as follows. Consider a chessboard of order 8X8. The
problem is to place 8 queens on this board such that no two queens are attack can attack each
other.
Illustration.
Consider the problem of 4 queens, backtracking solution for this is as shown in the fig 6.3.

92
The figure shows a partial backtracking tree. Completion of the tree is left as an assignment for
the reader.
4. Branch and Bound:
The term branch and bound refer to all state space search methods in which all possible
branches are derived before any other node can become the E-node. In other words the
exploration of a new node cannot begin until the current node is completely explored.
Concept of algorithm
A common man‟s belief is that a computer can do anything and everything that he imagines. It
is very difficult to make people realize that it is not really the computer but the man behind
computer who does everything.

In the modern internet world man feels that just by entering what he wants to search into the
computers he can get information as desired by him. He believes that, this is done by computer.
A common man seldom understands that a man made procedure called search has done the
entire job and the only support provided by the computer is the executional speed and
organized storage of information.

In the above instance, a designer of the information system should know what one frequently
searches for. He should make a structured organization of all those details to store in memory
of the computer. Based on the requirement, the right information is brought out. This is
accomplished through a set of instructions created by the designer of the information system to
search the right information matching the requirement of the user. This set of instructions is
termed as program. It should be evident by now that it is not the computer, which generates
automatically the program but it is the designer of the information system who has created this.

Thus, the program is the one, which through the medium of the computer executes to perform
all the activities as desired by a user. This implies that programming a computer is more
important than the computer itself while solving a problem using a computer and this part of
programming has got to be done by the man behind the computer. Even at this stage, one
should not quickly jump to a conclusion that coding is programming. Coding is perhaps the last
stage in the process of programming. Programming involves various activities form the stage
of conceiving the problem upto the stage of creating a model to solve the problem. The formal
representation of this model as a sequence of instructions is called an algorithm and coded
algorithm in a specific computer language is called a program.
One can now experience that the focus is shifted from computer to computer programming and
then to creating an algorithm. This is algorithm design, heart of problem solving.
Characteristics of an algorithm
Let us try to present the scenario of a man brushing his own teeth(natural denture) as an
algorithm as follows.
Step 1. Take the brush
Step 2. Apply the paste
Step 3. Start brushing
Step 4. Rinse
93
Step 5. Wash
Step 6. Stop
If one goes through these 6 steps without being aware of the statement of the problem, he could
possibly feel that this is the algorithm for cleaning a toilet. This is because of several
ambiguities while comprehending every step. The step 1 may imply tooth brush, paint brush,
toilet brush etc. Such an ambiguity doesn‟t an instruction an algorithmic step. Thus every step
should be made unambiguous. An unambiguous step is called definite instruction. Even if the
step 2 is rewritten as apply the tooth paste, to eliminate ambiguities yet the conflicts such as,
where to apply the tooth paste and where is the source of the tooth paste, need to be resolved.
Hence, the act of applying the toothpaste is not mentioned. Although unambiguous, such
unrealizable steps can‟t be included as algorithmic instruction as they are not effective.
The definiteness and effectiveness of an instruction implies the successful termination of that
instruction. However the above two may not be sufficient to guarantee the termination of the
algorithm. Therefore, while designing an algorithm care should be taken to provide a proper
termination for algorithm.
Thus, every algorithm should have the following five characteristic feature
1. Input
2. Output
3. Definiteness
4. Effectiveness
5. Termination
Therefore, an algorithm can be defined as a sequence of definite and effective instructions,
which terminates with the production of correct output from the given input.
In other words, viewed little more formally, an algorithm is a step by step formalization of a
mapping function to map input set onto an output set.The problem of writing down the correct
algorithm for the above problem of brushing the teeth is left to the reader.
For the purpose of clarity in understanding, let us consider the following examples.
Example 1:
Problem : finding the largest value among n>=1 numbers.
Input : the value of n and n numbers
Output : the largest value
Steps :
1. Let the value of the first be the largest value denoted by BIG
2. Let R denote the number of remaining numbers. R=n-1
3. If R != 0 then it is implied that the list is still not exhausted. Therefore look the
next number called NEW.
4. Now R becomes R-1
5. If NEW is greater than BIG then replace BIG by the value of NEW
6. Repeat steps 3 to 5 until R becomes zero.
7. Print BIG
8. Stop
End of algorithm
Example 2: quadratic equation
Example 3: listing all prime numbers between two limits n1 and n2.

94
THE ANALYSIS OF ALGORITHMS

The analysis of algorithms is made considering both qualitative and quantitative aspects to get
a solution that is economical in the use of computing and human resources which improves the
performance of an algorithm. A good algorithm usually possess the following qualities and
capabilities.

1. They are simple but powerful and general solutions.

2. They are user friendly

3. They can be easily updated.

4. They are correct.

5. They are able to be understood on a number of levels.

6. They are economical in the use of computer time, storage and peripherals.

7. They are well documented.

8. They are independent to run on a particular computer.

9. They can be used as subprocedures for other problems.

10. The solution is pleasing and satisfying to its designer.

Computational Complexity

The computational complexity can be measured in terms of space and time required by an
algorithm.

Space Complexity

The space complexity of an algorithm is the amount of memory it needs to run the algorithm.

Time Complexity

The time complexity of an algorithm is the amount of time it needs to run the algorithm.

The time taken by the program is the sum of the compile time and run time.

To make a quantitative measure of an algorithm's performance, the computational model must


capture the essence of the computation while at the same time it must be divorced from any
programming language.

95
ASYMTOTIC NOTATION

Asymtotic notations are method used to estimate and represent the efficiency of an algorithm
using simple formula. This can be useful for seperating algorithms that leads to different
amounts of work for large inputs.

Comparing or classify functions that ignores constant factors and small inputs is called as
asymtotic growth rate or asymtotic order or simply the order of functions. Complexity of an
algorithm is usually represented in O, o, , notations.

Big - oh notation (O)

This is a standard notation that has been developed to represent functions which bound the
computing time for algorithms and it is used to define the worst case running time of an
algorithm and concerned with very large values of N.

Definition : - T(N) = O(f(N)), if there are positive constants C and no such that T(N) Cf(N)
when N no

Big - Omega Notation ( )

This notation is used to describe the best case running time of algorithms and concerned with
very large values of N.

Definition : - T(N) = omega(f(N)), if there are positive constants C and no such that T(N)
CF(N) when N no

Big - Theta Notation

This notation is used to describe the average case running time of algorithms and concerned
with very large values of n.

Definition : - T(N) = theta (F(N)), if there are positive constants C1, C2 and no such that

T(N) = O(F(N)) and T(N) = (F(N)) for all N no

Definition : - T(N) = (F(N)), if there are positive constants C1, C2 and no such that

T(N) = O(F(N)) and T(N) = (F(N)) for all N no.

Little - Oh Notation (o)

This notation is used to describe the worstcase analysis of algorithms and concerned with small
values of n.

Basic Asymptotic Efficiency Classes

Computing Time Name

96
0(1) constant

0(log n) Logarithmic function

0(n) Linear

0(n2) quadratic

0(n3) cubic

0(2n) exponential

0(nlogn) n - log - n Logarithmic

n! factorial

Worst - Case, Best - Case And Average - Case Efficiencies

Worst - Case - Efficiency

The worst - case efficiency of an algorithm is its efficiency for the worst - case input of size n,
which is an input of size n for which the algorithm runs the longest among all possible inputs
of that size

Best - Case Efficiency

The best - case efficiency of an algorithm is its efficiency for the best case input of size n,
which is an input of size n for which the algorithm runs the fastest among all possible inputs of
that size.

Average - Case Efficiency

The average - case efficiency of an algorithm is its efficiency for the random input of size n,
which makes some assumptions about possible inputs of size n.

for example, let us consider sequential search

Algorithm

SequentialSearch (A[0...n-1],K)

// Input : An array A[0..n-1] and a search key k.

// Output : Returns the index of the first element of A that matches R or -1 if there are no
matching elements.

while i < n and A[i] # k do

i i+1
97
if i < n return i

else return - 1

Here, the best - case efficiency is 0(1) where the first element is itself the search element and
the worst - case efficiency is 0(n) where the last element is the search element or the search
element may not be found in the given array.

Algorithmic Notations
In this section we present the pseudocode that we use through out the book to describe
algorithms. The pseudo code used resembles PASCAL and C language control structures.
Hence, it is expected that the reader be aware of PASCAL/C. Even otherwise atleast now it is
required that the reader should know preferably C to practically test the algorithm in this
course work.
However, for the sake of completion we present the commonly employed control constructs
present in the algorithms.
1. A conditional statement has the following form
If < condition> then
Block 1
Else
Block 2
If end.
This pseudocode executes block1 if the condition is true otherwise block2 is executed.
2. The two types of loop structures are counter based and conditional based and they are as
follows
For variable = value1 to value2 do
Block
For end
Here the block is executed for all the values of the variable from value 1 to value 2.
There are two types of conditional looping, while type and repeat type.
While (condition) do
Block
While end.
Here block gets executed as long as the condition is true.
Repeat
Block
Until<condition>
Here block is executed as long as condition is false. It may be observed that the block is
executed atleast once in repeat type.

INTRODUCTION TO NP - COMPLETE PROBLEMS

A decision problem D is said to be NP complete if 1. It belongs to class NP. 2. Every problem


in NP is polynomially reducible to D.

A problem P1 can be reduced to P2 as follows

Provide a mapping so that any instance of P1 can be transformed to an instance of P2. Solve P2
and then map the answer back to the original. As an example, numbers are entered into a
98
pocket calculator in decimal. The decimal numbers are converted to binary, and all calculations
are performed in binary. Then the final answer is converted back to decimal for display. For P 1
to be polynomially reducible to P2, all the work associated with the transformations must be
performed in polynomial time.

The reason that NP - complete problems are the hardest NP problems is that a problem that is
NP - complete can essentially be used as a subroutine for any problem in NP, with only a
polynomial amount of overhead. Suppose we have an NP - complete problem P1. Suppose P2 is
known to be in NP. Suppose further that P1 polynomially reduces to P2, so that we can solve P1
by using P2 with only a polynomial time penalty. Since P1 is NP - complete, every problem in
NP polynomially reduces to P1. By applying the closure property to polynomials, we see that
every problem in NP is polynomially reducible to P2; we reduce the problem to P1 and then
reduce P1 to P2. Thus, P2 is NP - Complete. Travelling salesman problem is NP - complete. It is
easy to see that a solution can be checked in polynomial time, so it is certainly in NP.

Importance of NP-Completeness

Most algorithms we have studied so far have polynomial-time running times. According to
Cormen, Leiserson, and Rivest, polynomial-time algorithms can be considered tractable for the
following reasons.
(1)
Although a problem which has a running time of say O(n20) or O(n100) can be called
intractable, there are very few practical problems with such orders of polynomial
complexity.
(2)
For reasonable models of computation, a problem that can be solved in polynomial time
in one model can also be solved in polynomial time on another.
(3)
The class of polynomial-time solvable problems has nice closure properties (since
polynomials are closed under addition, multiplication, etc.)
The class of NP-complete (Non-deterministic polynomial time complete) problems is a very
important and interesting class of problems in Computer Science. The interest surrounding this
class of problems can be attributed to the following reasons.
1.
No polynomial-time algorithm has yet been discovered for any NP-complete problem;
at the same time no NP-complete problem has been shown to have a super polynomial-
time (for example exponential time) lower bound.
2.
If a polynomial-time algorithm is discovered for even one NP-complete problem, then
all NP-complete problems will be solvable in polynomial-time.
It is believed (but so far no proof is available) that NP-complete problems do not have
polynomial-time algorithms and therefore are intractable. The basis for this belief is the second
fact above, namely that if any single NP-complete problem can be solved in polynomial time,
then every NP-complete problem has a polynomial-time algorithm. Given the wide range of
NP-complete problems that have been discovered to date, it will be sensational if all of them
could be solved in polynomial time.

It is important to know the rudiments of NP-completeness for anyone to design "sound"


algorithms for problems. If one can establish a problem as NP-complete, there is strong reason

99
to believe that it is intractable. We would then do better by trying to design a good
approximation algorithm rather than searching endlessly seeking an exact solution. An
example of this is the TSP (Traveling Salesman Problem), which has been shown to be
intractable. A practical strategy to solve TSP therefore would be to design a good
approximation algorithm. This is what we did in Chapter 8, where we used a variation of
Kruskal's minimal spanning tree algorithm to approximately solve the TSP. Another important
reason to have good familiarity with NP-completeness is many natural interesting and
innocuous-looking problems that on the surface seem no harder than sorting or searching, are
in fact NP-complete.

100

You might also like