Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 43

Introduction to Data Structures

Chapter 1

Introduction

Every computer science curriculum in the world includes a course on


data structures and algorithms. Data structures are that important; they
improve our quality of life and even save lives on a regular basis. If we
think about it for even a few minutes, we realize that we interact with
data structures constantly.
• Open a file: File system data structures are used to locate the parts of
that file on disk so they can be retrieved. This isn’t easy; disks contain
hundreds of millions of blocks. The contents of your file could be stored
on any one of them.
• Look up a contact on phone: A data structure is used to lookup a phone
number based on partial information even before you finish
dialing/typing. This isn’t easy;your phone may contain information
about a lot of people—everyone you have ever had phone or email
contact with—and your phone doesn’t have a very fast processor
or a lot of memory.
• Login to a social network: The network servers use your login
information look up the account information. This isn’t easy; the most
popular social networks have hundreds of millions of active users.
• Do a web search: The search engine uses data structures to find the
web pages containing your search terms. This isn’t easy; there are over
8.5 billion web pages on the Internet and each page contains a lot of
potential search terms.
• Phone emergency services (9-1-1): The emergency services network
looks up your phone number in a data structure that maps phone
numbers to addresses so that police cars, ambulances, or fire trucks can
be sent there without delay. This is important; the person making the call
may not be able to provide the exact address they are calling from and a
delay can mean the difference between life or death.

The Need for Efficiency


In the next section, we look at the operations supported by the most
commonly used data structures. Anyone with even a little bit of
programming experience will see that these operations are not hard to
implement correctly. We can store the data in an array or a linked list
and each operation can be implemented by iterating over all the elements
of the array or list and possibly adding or removing an element.
This kind of implementation is easy, but not very efficient. Does it really
matter? Computers are getting faster and faster. Maybe the obvious
implementation is good enough. Let’s do some back-of-the-envelope
calculations to find out.
Number of operations: Imagine an application with a moderately-sized
data set, say of one million (10)6, items. It is reasonable, in most
applications, to assume that the application will want to look up each
item at least once. This means we can expect to do at least one million
(10)6 lookups in this data. If each of these (10) 6 lookups inspects each of
the (10)6 items, this gives a total of (10)6×(10)6=(10)12 (one thousand
billion) inspections.
Processor speeds: At the time of writing, even a very fast desktop
computer can not do more than one billion (10)9 operations per second.
This means that this application will take at least (10) 12 /(10)9= 1000
seconds, or roughly 16 minutes and 40 seconds. 16 minutes is an eon in
computer time, but a person might be willing to put up with it (if they
were headed out for a coffee break).
Bigger data sets: Now consider a company like Google, that indexes
over 8.5 billion web pages. By our calculations, doing any kind of query
over this data would take at least 8.5 seconds. We already know that this
isn’t the case; web searches complete in much less than 8.5 seconds, and
they do much more complicated queries than just asking if a particular
page is in their list of indexed pages. At the time of writing, Google
receives approximately 4,500 queries per second, meaning that they
would require at least 4,500 × 8:5 = 38;250 very fast servers just to keep
up.
The solution: These examples tell us that the obvious implementations
of data structures do not scale well when the number of items, n, in the
data structure and the number of operations, m, performed on the data
structure are both large. In these cases, the time (measured in, say,
machine instructions) is roughly n × m.
The solution, of course, is to carefully organize data within the data
structure so that not every operation requires inspecting every data item.
Although it sounds impossible at first, we will see data structures where
a search requires looking at only 2 items on average, independent of the
number of items stored in the data structure. In our billion instruction per
second computer it takes only 0:000000002 seconds to search in a data
structure containing a billion items (or a trillion, or a quadrillion, or even
a quintillion items).
We will also see implementations of data structures that keep the items
in sorted order, where the number of items inspected during an operation
grows very slowly as a function of the number of items in the data
structure. For example, we can maintain a sorted set of one billion items
while inspecting at most 60 items during any operation.
In our billion instruction per second computer, these operations take
0:00000006 seconds each.
Interfaces
In discussing data structures, it is important to understand the difference
between a data structure’s interface and its implementation. An interface
describes what a data structure does, while an implementation describes
how the data structure does it.
An interface, sometimes also called an abstract data type, defines the set
of operations supported by a data structure and the semantics, or
meaning, of those operations. An interface tells us nothing about how
the data structure implements these operations, it only provides the list
of supported operations along with specifications about what types of
arguments each operation accepts and the value returned by each
operation.
A data structure implementation on the other hand, includes the internal
representation of the data structure as well as the definitions of the
algorithms that implement the operations supported by the data
structure. Thus, there can be many implementations of a single interface.
For example, we have implementations of the List interface using arrays
and implementations of the List interface using
pointer-based data structures. Each implements the same interface, List,
but in different ways.]
Chapter 2

Sorting and Searching Techniques

Bubble sort
The bubble sort is also known as the ripple sort. The bubble sort is
probably the first, reasonably complex module that any beginning
programmer has to write. It is a very simple construct which introduces
the student to the fundamentals of how sorting works.
A bubble sort makes use of an array and some sort of "swapping"
mechanism. Most programming languages have a built-in function to
swap elements of an array. Even if a swapping function does not exist,
only a couple of extra lines of code are required to store one array
element in a temporary field in order to swap a second element into its
place. Then the first element is moved out of the temporary field and
back into the array at the second element's position. The bubble sort gets
its name because elements tend to move up into the correct order like
bubbles rising to the surface
Here is a simple example of how a bubble sort works: Suppose you have
a row of children's toy blocks with letters on them. They are in random
order and you wish to arrange them in alphabetical order from left to
right.

Step 1. Begin with the first block. In this case, the letter G. (Fig. 1.)

Fig. 1

Step 2. Look at the block just to the right of it.


Step 3. If the block to the right should come before the block on the left,
swap them so that they are in order (Fig. 2.)

Fig. 2

If you were doing this by hand, you might just pick the blocks to be
moved with one in each hand and cross your arms to swap them. Or you
might move the first one out of its position temporarily, move the second
one in its place, them move the first one to the now empty position (this
is the difference between having a single function to do the swap, or
writing some code to do it).
Step 4. Compare the next block in line with the first, and repeat step 3.
Do this until you run out of blocks. Then begin step one again with the
second block. (Fig. 3,4,5,6)

Fig. 3 - Pass #2

Fig. 4 - Pass #3
Fig. 5 - Pass #4

Fig. 6 - Completed Sort

Step-by-step example
Let us take the array of numbers "5 1 4 2 8", and sort the array from
lowest number to greatest number using bubble sort. In each step,
elements written in bold are being compared. Three passes will be
required.
First Pass:
( 5 1 4 2 8 )   ( 1 5 4 2 8 ),Here, algorithm compares the first two elements, and
swaps since 5 > 1.
( 1 5 4 2 8 )   ( 1 4 5 2 8 ), Swap since 5 > 4
( 1 4 5 2 8 )   ( 1 4 2 5 8 ), Swap since 5 > 2
( 1 4 2 5 8 )   ( 1 4 2 5 8 ), Now, since these elements are already in order(8 > 5),
algorithm does not swap them.
Second Pass:
( 1 4 2 5 8 )   ( 1 4 2 5 8 )
( 1 4 2 5 8 )   ( 1 2 4 5 8 ), Swap since 4 > 2
( 1 2 4 5 8 )   ( 1 2 4 5 8 )
( 1 2 4 5 8 )   ( 1 2 4 5 8 )
Now, the array is already sorted, but our algorithm does not know if it is
completed. The algorithm needs one whole pass without any swap to
know it is sorted.
Third Pass:
( 1 2 4 5 8 )   ( 1 2 4 5 8 )
( 1 2 4 5 8 )   ( 1 2 4 5 8 )
( 1 2 4 5 8 )   ( 1 2 4 5 8 )
( 1 2 4 5 8 )   ( 1 2 4 5 8 )
Programming code for bubble sort function is as follows

Void bubbleSort(int *array,int length)//Bubble sort function


{
int i,j;
for (i=0;i<10;i++)
{
for (j=0;j<i;j++)
{
if(array[i]>array[j])
{
int temp=array[i]; //swap
array[i]=array[j];
array[j]=temp;
}

}
}

Selection Sort

Selection sort is one of the O(n2) sorting algorithms, which makes it


quite inefficient for sorting large data volumes. Selection sort is notable
for its programming simplicity and it can over perform other sorts in
certain situations (see complexity analysis for more details).

Algorithm

The idea of algorithm is quite simple. Array is imaginary divided into


two parts - sorted one and unsorted one. At the beginning, sorted
part is empty, while unsorted one contains whole array. At every
step, algorithm finds minimal element in the unsorted part and adds it to
the end of the sorted one. When unsorted part becomes empty,
algorithm stops.

When algorithm sorts an array, it swaps first element of unsorted part


with minimal element and then it is included to the sorted part. This
implementation of selection sort in not stable. In case of linked list is
sorted, and, instead of swaps, minimal element is linked to the unsorted
part, selection sort is stable.

Let us see an example of sorting an array to make the idea of selection


sort clearer.Example. Sort {5, 1, 12, -5, 16, 2, 12, 14} using selection
sort.
Code for selection sort function is as follows

Void Selectionsort(int *array,int length


{
int i,j,first,temp;
for (i=0;i<10;i++)
{
first=0;
for(j=1;j<=I;j++)
{
If array[j]<array[first])
first=j
}
Temp=array[first];
array[first]=array[i];
array[i]=temp;
}
return;
}

Insertion Sort

Insertion sort belongs to the O(n2) sorting algorithms. Unlike many


sorting algorithms with quadratic complexity, it is actually applied in
practice for sorting small arrays of data. For instance, it is used to
improve quick sort routine. Some sources notice, that people use same
algorithm ordering items, for example, hand of cards.

Algorithm

Insertion sort algorithm somewhat resembles selection sort. Array is


imaginary divided into two parts - sorted one and unsorted one. At the
beginning, sorted part contains first element of the array and unsorted
one contains the rest. At every step, algorithm takes first element in
the unsorted part and inserts it to the right place of the sorted
one. When unsorted part becomes empty, algorithm stops. Sketchy,
insertion sort algorithm step looks like this:
becomes

Let us see an example of insertion sort routine to make the idea of


algorithm clearer.

Example. Sort {7, -5, 2, 16, 4} using insertion sort.


The ideas of insertion

The main operation of the algorithm is insertion. The task is to insert a


value into the sorted part of the array. Let us see the variants of how we
can do it.

"Sifting down" using swaps

The simplest way to insert next element into the sorted part is to sift it
down, until it occupies correct position. Initially the element stays right
after the sorted part. At each step algorithm compares the element with
one before it and, if they stay in reversed order, swap them. Let us see an
illustration.

This approach writes sifted element to temporary position many times.


Next implementation eliminates those unnecessary writes.

Shifting instead of swapping

We can modify previous algorithm, so it will write sifted element only to


the final correct position. Let us see an illustration.
It is the most commonly used modification of the insertion sort.

Using binary search

It is reasonable to use binary search algorithm to find a proper place for


insertion. This variant of the insertion sort is calledbinary insertion
sort. After position for insertion is found, algorithm shifts the part of the
array and inserts the element. This version has lower number of
comparisons, but overall average complexity remains O(n 2). From a
practical point of view this improvement is not very important, because
insertion sort is used on quite small data sets.

Shell Sort
Shellsort, also known as Shell sort or Shell's method, is an in-
place comparison sort. It generalizes an exchanging sort, such
as insertion or bubble sort, by starting the comparison and exchange of
elements with elements that are far apart before finishing with
neighboring elements. Starting with far apart elements can move some
out-of-place elements into position faster than a simple nearest neighbor
exchange.  The running time of Shellsort is heavily dependent on the gap
sequence it uses.
Shellsort is a multi-pass algorithm. Each pass is an insertion sort of the
sequences consisting of every h-th element for a fixed gap h (also known
as the increment). This is referred to as h-sorting.
An example run of Shellsort with gaps 5, 3 and 1 is shown below.

The first pass, 5-sorting, performs insertion sort on separate subarrays


(a1, a6, a11), (a2, a7, a12), (a3, a8), (a4, a9), (a5, a10). For instance, it changes
the subarray (a1, a6, a11) from (62, 17, 25) to (17, 25, 62). The next pass,
3-sorting, performs insertion sort on the subarrays (a1, a4, a7, a10),
(a2, a5, a8, a11), (a3, a6, a9, a12). The last pass, 1-sorting, is an ordinary
insertion sort of the entire array (a1,..., a12).
As the example illustrates, the subarrays that Shellsort operates on are
initially short; later they are longer but almost ordered. In both cases
insertion sort works efficiently.
Shellsort is unstable: it may change the relative order of elements with
equal values. It has "natural" behavior, in that it executes faster when the
input is partially sorted.
void shellsort(int A[],int max)
{
int stop,swap,limit,temp;
int x=(int)(max/2)-1;
while(x>0)
{
stop=0;
limit=max-x;
while(stop==0)
{
swap=0;
for(int k=0;k<limit;k++)
{
if(A[k]>A[k+x])
{
temp=A[k];
 A[k]=A[k+x];
A[k+x]=temp;
swap=k;
}
}
limit=swap-x;
if(swap==0)
stop=1;
}
x=(int)(x/2);
}
}

Quick Sort
Quicksort, or partition-exchange sort, is a sorting algorithm that, on
average, makes O(n log n) comparisons to sort n items. In the worst
case, it makes O(n2) comparisons, though this behavior is rare. Quicksort
is often faster in practice than other O(n log n) algorithms. Additionally,
quicksort's sequential and localized memory references work well with
a cache. Quicksort can be implemented with an in-place partitioning
algorithm, so the entire sort can be done with only O(log n) additional
space.[2]
Quicksort is a divide and conquer algorithm. Quicksort first divides a
large list into two smaller sub-lists: the low elements and the high
elements. Quicksort can then recursively sort the sub-lists.
The steps are:

1. Pick an element, called a pivot, from the list.


2. Reorder the list so that all elements with values less than the pivot
come before the pivot, while all elements with values greater than
the pivot come after it (equal values can go either way). After this
partitioning, the pivot is in its final position. This is called
the partition operation.
3. Recursively sort the sub-list of lesser elements and the sub-list of
greater elements.
Notice that we only examine elements by comparing them to other
elements. This makes quicksort a comparison sort. This version is also a
stable sort (assuming that the "for each" method retrieves elements in
original order, and the pivot selected is the last among those of equal
value).
The correctness of the partition algorithm is based on the following two
arguments:

 At each iteration, all the elements processed so far are in the desired
position: before the pivot if less than the pivot's value, after the pivot
if greater than the pivot's value (loop invariant).
 Each iteration leaves one fewer element to be processed (loop
variant).
The correctness of the overall algorithm can be proven via induction: for
zero or one element, the algorithm leaves the data unchanged; for a
larger data set it produces the concatenation of two parts, elements less
than the pivot and elements greater than it, themselves sorted by the
recursive hypothesis.
Following is the example of quicksort on a random set of numbers. The
shaded element is the pivot. It is always chosen as the last element of the
partition. However, always choosing the last element in the partition as
the pivot in this way results in poor performance on already sorted lists,
or lists of identical elements. Since sub-lists of sorted / identical
elements crop up a lot towards the end of a sorting procedure on a large
set, versions of the quicksort algorithm which choose the pivot as the
middle element run much more quickly than the algorithm described in
this diagram on large sets of numbers.
Following is the program code for quick sort
void quickSort(int arr[], int left, int right)
{
      int i = left, j = right;
      int tmp;
      int pivot = arr[(left + right) / 2];
 
      /* partition */
      while (i <= j) {
            while (arr[i] < pivot)
                  i++;
            while (arr[j] > pivot)
                  j--;
            if (i <= j) {
                  tmp = arr[i];
                  arr[i] = arr[j];
                  arr[j] = tmp;
                  i++;
                  j--;
            }
      };
 
      /* recursion */
      if (left < j)
            quickSort(arr, left, j);
      if (i < right)
            quickSort(arr, i, right);
}

Merge sort
Merge sort (also commonly spelled mergesort) is
an O(n log n) comparison-based sorting algorithm. Most
implementations produce a stable sort, which means that the
implementation preserves the input order of equal elements in the sorted
output. Merge sort is a divide and conquer algorithm that was invented
by John von Neumann in 1945.[1] A detailed description and analysis of
bottom-up mergesort appeared in a report by Goldstine and Neumann as
early as 1948.[2]
Conceptually, a merge sort works as follows

1. Divide the unsorted list into n sublists, each containing 1 element


(a list of 1 element is considered sorted).
2. Repeatedly merge sublists to produce new sublists until there is
only 1 sublist remaining. This will be the sorted list.

The following code devides the list into n sublists.

void merge_sort(int low,int high)


{
int mid;
if(low<high)
{
mid=(low+high)/2;
merge_sort(low,mid);
merge_sort(mid+1,high);
merge(low,mid,high);
}
}

The following code is for the merging of n sublists.

void merge(int low,int mid,int high)


{
int h,i,j,b[50],k;
h=low;
i=low;
j=mid+1;
while((h<=mid)&&(j<=high))
{
if(a[h]<=a[j])
{
b[i]=a[h];
h++;
}
else
{
b[i]=a[j];
j++;
}
i++;
}
if(h>mid)
{
for(k=j;k<=high;k++)
{
b[i]=a[k];
i++;
}
}
else
{
for(k=h;k<=mid;k++)
{
b[i]=a[k];
i++;
}
}
for(k=low;k<=high;k++) a[k]=b[k];
}

Searching Techniques
Searching refers to the operation of finding the location of a given item
in a collection of items. The search is said to be successful if ITEM does
appear in DATA and unsuccessful otherwise.
The following searching algorithms are discussed here.
1. Sequential Searching
2. Binary Search

Sequential Search
This is the most natural searching method. The most intuitive way to
search for a given ITEM in DATA is to compare ITEM with each
element of DATA one by one. .The algorithm for a sequential search
procedure is now presented
INPUT : List of Size N. Target Value T
OUTPUT : Position of T in the list-1

BEGIN
Set FOUND = false
Set I := 0
While (I <= N) and (FOUND is false)
IF List[i] ==t THEN
FOUND = true
ELSE
I = I+1
IF FOUND==false THEN
T is not present in the List
END

Binary Search

Suppose DATA is an array which is sorted in increasing numerical


order. Then there is an extremely efficient searching algorithm, called
binary search, which can be used to find the location LOC of a given
ITEM of information in DATA.
The binary search algorithm applied to our array DATA works as
follows. During each stage of our algorithm, our search for ITEM is
reduced to a segment of elements of DATA:
DATA[BEG], DATA[BEG + 1], DATA[BEG + 2], ...... DATA[END].

Note that the variable BEG and END denote the beginning and end
locations of the segment respectively. The algorithm compares ITEM
with the middle element DATA[MID] of the
segment, where MID is obtained by
MID = INT((BEG + END) / 2)
(We use INT(A) for the integer value of A.) If DATA[MID] = ITEM,
then the search is successful and we set LOC: = MID. Otherwise a new
segment of DATA is obtained as follows:

(a) If ITEM < DATA[MID], then ITEM can appear only in the left half
of the segment: DATA[BEG],DATA[BEG + 1],….. ,DATA[MID - 1]
So we reset END := MID - 1 and begin searching again.

(b) If ITEM > DATA[MID], then ITEM can appear only in the right half
of the segment: DATA[MID + 1], DATA[MID + 2],....,DATA[END]
So we reset BEG := MID + 1 and begin searching again.
Initially, we begin with the entire array DATA; i.e. we begin with
BEG = 1 and END = n, If ITEM is not in DATA, then eventually we
obtain END< BEG
This condition signals that the search is unsuccessful, and in this case we
assign LOC: = NULL. Here NULL is a value that lies outside the set of
indices of DATA. We now formally state the binary search algorithm

Algorithm:-
(Binary Search) BINARY(DATA, LB, UB, TEM, LOC)
Here DATA is sorted array with lower bound LB and upper bound
UB, and ITEM is a given item of information. The variables BEG,
END and MID denote, respectively, the beginning, end and middle
locations of a segment of elements of DATA. This algorithm finds
the location LOC of ITEM in DATA or sets LOC=NULL.
1. [Initialize segment variables.]
Set BEG := LB, END := UB and MID = INT((BEG + END)/ 2).
2. Repeat Steps 3 and 4 while BEG ≤ END and
DATA[MID] ≠ ITEM.
3. If ITEM<DATA[MID], then:
Set END := MID - 1.
Else:
Set BEG := MID + 1.
[End of If structure]
4. Set MID := INT((BEG + END)/2).
[End of Step 2 loop.]
5. If DATA[MID] :=ITEM, then:
Set LOC:=MID.
Else:
Set LOC := NULL.
[End of If structure.]
6. Exit.
Chapter 3

Stacks and Queues


Two of the more common data objects found in computer algorithms are
stacks and queues. Both of these objects are special cases of the more
general data object, an ordered list.

A stack is an ordered list in which all insertions and deletions are made
at one end, called the top. A queue is an ordered list in which all
insertions take place at one end, the rear, while all deletions take place
at the other end, the front. Given a stack S=(a[1],a[2],.......a[n]) then we
say that a1 is the bottommost element and element a[i]) is on top of
element a[i-1], 1<i<=n. When viewed as a queue with a[n] as the rear
element one says that a[i+1] is behind a[i], 1<i<=n.

The restrictions on a stack imply that if the elements A,B,C,D,E are


added to the stack, n that order, then the first element to be
removed/deleted must be E. Equivalently we say that the last element to
be inserted into the stack will be the first to be removed. For this reason
stacks are sometimes referred to as Last In First Out (LIFO) lists. The
restrictions on queue imply that the first element which is inserted into
the queue will be the first one to be removed. Thus A is the first letter to
be removed, and queues are known as First In First Out (FIFO) lists.
Note that the data object queue as defined here need not necessarily
correspond to the mathematical concept of queue in which the
insert/delete rules may be different.
Adding into stack

procedure add(item : items); 
{add item to the global stack stack; 
top is the current top of stack 
and n is its maximum size} 
begin 
    if top = n then stackfull; 
    top := top+1; 
    stack(top) := item; 
end: {of add}

Deletion in stack

procedure delete(var item : items); 
{remove top element from the stack stack and put it in the item} 
begin 
    if top = 0 then stackempty; 
    item := stack(top); 
    top := top-1; 
end; {of delete}

These two procedures are so simple that they perhaps need no more
explanation. Procedure delete actually combines the functions TOP and
DELETE, stackfull and stackempty are procedures which are left
unspecified since they will depend upon the particular application. Often
a stackfull condition will signal that more storage needs to be allocated
and the program re-run. Stackempty is often a meaningful
condition.Procedures for addition and deletion in queue is given below.

Addition into a queue

procedure addq (item : items); 


{add item to the queue q} 
begin 
    if rear=n then queuefull 
    else begin 
         rear :=rear+1; 
         q[rear]:=item; 
    end; 
end;{of addq}

Deletion in a queue

procedure deleteq (var item : items); 


{delete from the front of q and put into item} 
begin 
    if front = rear then queueempty 
    else begin 
         front := front+1 
         item := q[front]; 
    end; 
end; {of deleteq} 

Circular Queue
A circular queue is a Queue but a particular implementation of a queue.
It is very efficient. It is also quite useful in low level code, because
insertion and deletion are totally independant, which means that you
don't have to worry about an interrupt handler trying to do an insertion at
the same time as your main code is doing a deletion
Algorithm for Insertion:-
Step-1: If "rear" of the queue is pointing to the last position then go to
step-2 or else step-3
Step-2: make the "rear" value as 0
Step-3: increment the "rear" value by one
Step-4:
1.if the "front" points where "rear" is pointing and the queue holds
a not NULL value for it, then its a "queue overflow" state, so quit; else
go to step-4.2
2. insert the new value for the queue position pointed by the "rear"
Algorithm for deletion:-
Step-1: If the queue is empty then say "empty queue" and quit; else
continue
Step-2: Delete the "front" element
Step-3: If the "front" is pointing to the last position of the queue then
step-4 else step-5
Step-4: Make the "front" point to the first position in the queue and quit
Step-5: Increment the "front" position by one

Code for insertion and deletion procedures is given below:-

class cqueue
{
int q[5],front,rare;
public:
cqueue()
{
front=-1;
rare=-1;
}
void push(int x)
{
if(front ==-1 && rare == -1)
{
q[++rare]=x;
front=rare;
return;
}
else if(front == (rare+1)%5 )
{
cout <<" Circular Queue over flow";
return;
}
rare= (rare+1)%5;
q[rare]=x;
}
 
void pop()
{
if(front==-1 && rare== -1)
{
cout <<"under flow";
return;
}
else if( front== rare )
{
front=rare=-1;
return;
}
front= (front+1)%5;

Chapter 4

Linked List
A linked list is a data structure consisting of a group of nodes which
together represent a sequence. Under the simplest form, each node is
composed of a datum and a reference (in other words, a link) to the next
node in the sequence; more complex variants add additional links. This
structure allows for efficient insertion or removal of elements from any
position in the sequence.

A linked list whose nodes contain two fields: an integer value and a link
to the next node. The last node is linked to a terminator used to signify
the end of the list.
Linked lists are among the simplest and most common data structures.
They can be used to implement several other common abstract data
types, including stacks, queues,associative arrays, and S-expressions,
though it is not uncommon to implement the other data structures
directly without using a list as the basis of implementation.
The principal benefit of a linked list over a conventional array is that the
list elements can easily be inserted or removed without reallocation or
reorganization of the entire structure because the data items need not be
stored contiguously in memory or on disk. Linked lists allow insertion
and removal of nodes at any point in the list, and can do so with a
constant number of operations if the link previous to the link being
added or removed is maintained during list traversal.
On the other hand, simple linked lists by themselves do not
allow random access to the data, or any form of efficient indexing. Thus,
many basic operations — such as obtaining the last node of the list
(assuming that the last node is not maintained as separate node reference
in the list structure), or finding a node that contains a given datum, or
locating the place where a new node should be inserted — may require
scanning most or all of the list elements.

Basically, a linked list is a collection of nodes. Each node in the list has
two components – a components to store information about that node
(i.e. data) and a component to store the next “linked” node. The last
node in a linked list indicates that it does not have a “linked” node –
usually by a null reference. The address of the first node is stored in a
separate location, called the head or first.
The code snippet below shows a basic structure of a node declaration in
c++.
struct nodeType
{
int data;
nodeType *link;
};
 
The variable declaration is as follows…
nodeType *head;
 
Basic Operations of a Linked List
There are a few basic operations that are typically needed with a linked
list

 Search a list to determine whether a particular item is in the list


 Insert an item in the list
 Delete an item from the list

Traversing/Searching a List
To traverse a linked list you need some way to move around the list. The
*head is a pointer in the list, but you cannot use it to traverse the list
because then you would loose a reference to the beginning of the list.
Typically to traverse the list, you will need to declare another pointer
that follows the same declaration as head, but that is not critical if you
change where it’s pointing to…
current = head;
while (current != NULL)
{
cout << current->data << " ";
current = current->link;

Item Insertion and Deletion
To insert a new node into a linked list, you need to do the following:

 Create a new node


 Determine the position in the list that you want to insert it and
assign it to current
 Make the new node is point to the same link that current is
pointing to
 Make current now point to the new node

In code, assuming current was at the correct position, then it would look
something like this…
nodeType *newNode;
newNode = new nodeType;
newNode->data = 123;
newNode->link = current->link;
current->link = newNode;
 

To delete a node from a linked list, you need to do the following:

 Determine the node that you want to remove and point current to
the node prior to that node
 Create a pointer to point to the node you want to delete (i.e. *p)
 Make current’s link now point to p’s link
 delete p from memory
In code, assuming current was at the correct position, then it would look
something like this…
deleteNode = new nodeType;
deleteNode = current->link;
current->link = deleteNode->link;
delete deleteNode;

Sorted & Unsorted Linked Lists


Typically there are two types of linked lists, those that are sorted and
those that are unsorted.
The advantages of knowing that a collection of data is sorted when
performing searches greatly reduces the complexity of the search
algorithms as well as improving the searches performance.
Typically to maintain a sorted listed requires adjusting the insert method
so that when a list is being built, the order is maintained.
Double Linked List
A doubly linked list is a linked data structure that consists of a set of
sequentially linked records called nodes. Each node contains two fields,
called links, that are references to the previous and to the next node in
the sequence of nodes. The beginning and ending
nodes' previous and next links, respectively, point to some kind of
terminator, typically a sentinel node or null, to facilitate traversal of the
list. If there is only one sentinel node, then the list is circularly linked via
the sentinel node. It can be conceptualized as two singly linked
lists formed from the same data items, but in opposite sequential orders.
A doubly linked list whose nodes contain three fields: an integer value,
the link to the next node, and the link to the previous node.
The two node links allow traversal of the list in either direction. While
adding or removing a node in a doubly linked list requires changing
more links than the same operations on a singly linked list, the
operations are simpler and potentially more efficient (for nodes other
than first nodes) because there is no need to keep track of the previous
node during traversal or no need to traverse the list to find the previous
node, so that its link can be modified.
Stacks using Linked list
The operations performed on a stack are 
1)push(): This is the function which is for insertion(pushing)of an
element into stack. It is similar to the insertion of an element at the end
of a single linked list
2)pop(): This is the function which is for deletion(popping up) of an
element from the stack.It is similar to the deletion of an element at the
end of a single linked list
      3)stack_display():This is the function which is for displaying the
elements of a stack it is similar to the forward traversal of a single linked
list.
The code is given below,

class stack
{
 int element;
 stack* next;
public:
 stack* push(stack*,int);
 stack* pop(stack*);
 void stack_display(stack*);
}*head,object;
stack* stack::push(stack* head,int key)
{
 stack* temp,*temp1;
 temp1=head;
 temp=new stack;
 temp->element=key;
 temp->next=NULL;
 if(head==NULL)
  head=temp;
 else
 {
 while(head->next!=NULL)
  head=head->next;
 head->next=temp;
 head=temp1;
 }
 return head;
}
stack* stack::pop(stack* head)
{
 stack* temp;
 if(head!=NULL)
 {
 temp=head;
 if(head->next==NULL)
 {
  cout<<”\nthe pooped element from the stack is: “<<head->element<<endl;
  return NULL;
 }
 while(head->next->next!=NULL)
 head=head->next;
 cout<<”the popped element from the stack is  “<<head->next->element;
 cout<<endl;
 head->next=head->next->next;
 head=temp;
 return head;
 }
 else
 {
  cout<<”\nit is impossible to pop an element from the stack as “;
  return head;
 }
}
void stack::stack_display(stack* head)
{
 if(head!=NULL)
 {
 while(head->next!=NULL)
 {
  cout<<head->element<<”->”;
  head=head->next;
 }
 cout<<head->element;
 cout<<endl;
 }
 else
  cout<<”the stack is empty\n”;
}  

Queues using linked list

Code for the implementation of queue using linked list is given below

class queue
{
struct node *frnt,*rear;
public:
queue() // constructure
{
frnt=rear=NULL;
}
void insert();
void del();
void show(); //
};
void queue::insert()
{
int value;
struct node *ptr;
cout<<"\nInsertion\n";
cout<<"Enter a number to insert: ";
cin>>value;
ptr=new node;
ptr->data=value;
ptr->next=NULL;
if(frnt==NULL)
frnt=ptr;
else
rear->next=ptr;
rear=ptr;
cout<<"\nNew item is inserted to the Queue!!!";
getch();
}
 
void queue::del()
{
if(frnt==NULL)
{
cout<<"\nQueue is empty!!";
getch();
return;
}
struct node *temp;
temp=frnt;
frnt=frnt->next;
cout<<"\nDeletion Operation........\nDeleted value is "<<temp->data;
delete temp;
getch();
}
void queue::show()
{
struct node *ptr1=frnt;
if(frnt==NULL)
{
cout<<"The Queue is empty!!";
getch();
return;
}
cout<<"\nThe Queue is\n";
while(ptr1!=NULL)
{
cout<<ptr1->data<<" ->";
ptr1=ptr1->next;
}
cout<<"END";
getch();
}
 

Circular queue using linked list


class cqueue
{
int q[5],front,rare;
public:
cqueue()
{
front=-1;
rare=-1;
}
void push(int x)
{
if(front ==-1 && rare == -1)
{
q[++rare]=x;
front=rare;
return;
}
else if(front == (rare+1)%5 )
{
cout <<" Circular Queue over flow";
return;
}
rare= (rare+1)%5;
q[rare]=x;
}
 
void pop()
{
if(front==-1 && rare== -1)
{
cout <<"under flow";
return;
}
else if( front== rare )
{
front=rare=-1;
return;
}
front= (front+1)%5;
}
  void display()
{
int i;
if( front <= rare)
{
for(i=front; i<=rare;i++)
cout << q[i]<<" ";
}
else
{
for(i=front;i<=4;i++)
{
cout <<q[i] << " ";
}
for(i=0;i<=rare;i++)
{
cout << q[i]<< " ";
}
}
}
};
 
Chapter 5

Binary Trees
A  binary tree is a tree data structure in which each node has at most
two child nodes, usually distinguished as "left" and "right". Nodes with
children are parent nodes, and child nodes may contain references to
their parents. Outside the tree, there is often a reference to the "root"
node (the ancestor of all nodes), if it exists. Any node in the data
structure can be reached by starting at root node and repeatedly
following references to either the left or right child. A tree which does
not have any node other than root node is called a null tree. In a binary
tree a degree of every node is maximum two. A tree with n nodes has
exactly n−1 branches or degree.
Binary trees are used to implement binary search trees and binary heaps.

A simple binary tree of size 9 and height 3, with a root node whose value is 2. The above tree is
unbalanced and not sorted.

 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 depth (or height) of a tree is the length of the path from the root
to the deepest node in the tree. A (rooted) tree with only one node
(the root) has a depth of zero.
 Siblings are nodes that share the same parent node.
 A node p is an ancestor of a node q if it exists on the path from the
root to node q. The node q is then termed as a descendant of p.
 The size of a node is the number of descendants it has including
itself.
 In-degree of a node is the number of edges arriving at that node.
 Out-degree of a node is the number of edges leaving that node.
 The root is the only node in the tree with In-degree = 0.
 All the leaf nodes have Out-degree = 0.

Types of Binary Trees

 A rooted binary tree is a tree with a root node in which every node


has at most two children.
 A full binary tree (sometimes proper binary tree or 2-
tree or strictly binary tree) is a tree in which every node other than
the leaves has two children. Or, perhaps more clearly, every node in a
binary tree has exactly 0 or 2 children. Sometimes a full tree is
ambiguously defined as a perfect tree 
 A perfect binary tree is a full binary tree in which all leaves are at
the same depth or same level, and in which every parent has two
children.
 A complete binary tree is a binary tree in which every level, except
possibly the last, is completely filled, and all nodes are as far left as
possible.
 An infinite complete binary tree is a tree with a countably
infinite number of levels, in which every node has two children, so
that there are 2d nodes at level d. The set of all nodes is countably
infinite, but the set of all infinite paths from the root is uncountable:
 A balanced binary tree is commonly defined as a binary tree in
which the depth of the two subtrees of every node differ by 1 or less,
although in general it is a binary tree where no leaf is much farther
away from the root than any other leaf. Binary trees that are balanced
according to this definition 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   where   is the number of nodes on the balanced tree.
Example 1: balanced tree with 1 node,   (depth = 0).
Example 2: balanced tree with 3 nodes,   (depth=1).
Example 3: balanced tree with 5 nodes,   (depth of tree
is 2 nodes).
 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.

You might also like