0% found this document useful (0 votes)
44 views29 pages

Data Struct UNIT 2

Stack, queues, hashing
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)
44 views29 pages

Data Struct UNIT 2

Stack, queues, hashing
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/ 29

UNIT 2

Abstract Data Types (ADTs) are theoretical models that define a set of
values and operations on those values, without specifying
implementation details. Examples include:

1. Stack: A collection of elements with two primary operations, push and


pop, following Last-In-First-Out (LIFO) order.
2. Queue: A collection of elements with two primary operations, enqueue
and dequeue, following First-In-First-Out (FIFO) order.
3. List: A sequence of elements with various operations like insertion,
deletion, and traversal.
4. Tree: A hierarchical structure composed of nodes, with operations like
insertion, deletion, and searching.
5. Graph: A collection of nodes connected by edges, with operations for
adding/removing nodes and edges, and traversing the graph.

Stack

Definition:

 A stack is an abstract data type that follows the Last-In-First-Out


(LIFO) principle.
 It represents a collection of elements with two main operations:
push (to add elements) and pop (to remove elements).
 Other common operations include peek (to view the top element
without removing it) and isEmpty (to check if the stack is empty).

Basic Operations:

1. Push: Adds an element to the top of the stack.


2. Pop: Removes and returns the top element from the stack.
3. Peek: Returns the top element without removing it.
4. isEmpty: Checks if the stack is empty.
Usage:

 Stacks are used in various applications such as expression


evaluation, parsing, backtracking algorithms, and managing
function calls in programming languages.
 In programming, function call stacks are used to manage the flow
of execution and store local variables and function call information .

Implementation:

 Stacks can be implemented using arrays or linked


lists.
 Arrays provide constant-time access to elements
but may require resizing if the capacity is exceeded.
 Linked lists offer dynamic memory allocation and
don't require resizing but may have slightly slower
access times due to pointer traversal.
Algorithms of Stack Operations
Stack:
Array stack[MAX_SIZE]
Integer top = -1

Push(element):
if top equals MAX_SIZE - 1:
Display "Stack Overflow"
else:
Increment top by 1
stack[top] = element

Pop():
if top equals -1:
Display "Stack Underflow"
else:
Element = stack[top]
Decrement top by 1
Return Element

Peek():
if top equals -1:
Display "Stack Underflow"
else:
Return stack[top]

isEmpty():
if top equals -1:
Return true
else:
Return false

isFull():
if top equals MAX_SIZE - 1:
Return true
else:
Return false
Application of Stack Data Structure:
 Function calls and recursion: When a function is called, the current
state of the program is pushed onto the stack. When the function
returns, the state is popped from the stack to resume the previous
function’s execution.
 Undo/Redo operations: The undo-redo feature in various
applications uses stacks to keep track of the previous actions. Each
time an action is performed, it is pushed onto the stack. To undo the
action, the top element of the stack is popped, and the reverse
operation is performed.
 Expression evaluation: Stack data structure is used to evaluate
expressions in infix, postfix, and prefix notations. Operators and
operands are pushed onto the stack, and operations are performed
based on the stack’s top elements.
 Browser history: Web browsers use stacks to keep track of the web
pages you visit. Each time you visit a new page, the URL is pushed
onto the stack, and when you hit the back button, the previous URL is
popped from the stack.
 Backtracking Algorithms: The backtracking algorithm uses stacks to
keep track of the states of the problem-solving process. The current
state is pushed onto the stack, and when the algorithm backtracks, the
previous state is popped from the stack.

Prefix and Postfix Expressions in Data Structure


The way to write arithmetic expression is known as a notation. An arithmetic
expression can be written in three different but equivalent notations, i.e., without
changing the essence or output of an expression. These notations are –
 Infix
 Prefix
 Postfix
Infix notations are normal notations, that are used by us while write different
mathematical expressions. The Prefix and Postfix notations are quite different.

Prefix Notation
In this notation, operator is prefixed to operands, i.e. operator is written ahead of
operands. For example, +ab. This is equivalent to its infix notation a + b. Prefix
notation is also known as Polish Notation.

Postfix Notation
This notation style is known as Reversed Polish Notation. In this notation style,
the operator is postfixed to the operands i.e., the operator is written after the
operands. For example, ab+. This is equivalent to its infix notation a + b.

Example
Expression No Infix Notation Prefix Notation Postfix Notation

1 a+b +ab ab+

2 (a + b) * c *+abc ab+c*

3 a * (b + c) *a+bc abc+*

4 a/b+c/d +/ab/cd ab/cd/+

5 (a + b) * (c + d) *+ab+cd ab+cd+*

6 ((a + b) * c) - d -*+abcd ab+c*d-

Postfix Expression Evaluation

Postfix Expression Evaluation using Stack Data


Structure
A postfix expression can be evaluated using the Stack data structure. To evaluate a postfix
expression using Stack data structure we can use the following steps...

1. Read all the symbols one by one from left to right in the given Postfix
Expression
2. If the reading symbol is operand, then push it on to the Stack.
3. If the reading symbol is operator (+ , - , * , / etc.,), then perform TWO pop
operations and store the two popped oparands in two different variables
(operand1 and operand2). Then perform reading symbol operation using
operand1 and operand2 and push result back on to the Stack.
4. Finally! perform a pop operation and display the popped value as final result.

Example
Consider the following Expression...
What is Recursion?
Recursion is defined as a process which calls itself directly or indirectly
and the corresponding function is called a recursive function.

Properties of Recursion:

Recursion has some important properties. Some of which are mentioned


below:
 The primary property of recursion is the ability to solve a problem by
breaking it down into smaller sub-problems, each of which can be
solved in the same way.
 A recursive function must have a base case or stopping criteria to
avoid infinite recursion.
 Recursion involves calling the same function within itself, which leads
to a call stack.
 Recursive functions may be less efficient than iterative solutions in
terms of memory and performance.

Types of Recursion:

1. Direct recursion: When a function is called within itself directly it is


called direct recursion. This can be further categorised into four types:
 Tail recursion,
 Head recursion,

Tail Recursion: Tail recursion is defined as a recursive function in


which the recursive call is the last statement that is executed by the
function. So basically nothing is left to execute after the recursion call.
void print(int n)
{
if (n < 0)
return;
printf("%d ", n);

// The last executed statement is recursive call


print(n - 1);
}
Head Recursion: If a recursive function calling itself and that
recursive call is the first statement in the function then it’s known as Head
Recursion. There’s no statement, no operation before the call. The
function doesn’t have to process or perform any operation at the time of
calling and all operations are done at returning time.
void fun(int n)
{
if (n > 0) {

// First statement in the function


fun(n - 1);

printf("%d ", n);


}
}

2. Indirect recursion: Indirect recursion occurs when a function calls


another function that eventually calls the original function and it forms
a cycle.

What is the base condition in recursion?


In the recursive program, the solution to the base case is provided and
the solution to the bigger problem is expressed in terms of smaller
problems.

int fact(int n)
{
if (n < = 1) // base case
return 1;
else
return n*fact(n-1);
}

Binary Search
Binary Search is defined as a searching algorithm used in a sorted array
by repeatedly dividing the search interval in half. The idea of binary
search is to use the information that the array is sorted and reduce the
time complexity to O(log N).

Conditions for when to apply Binary Search in a


Data Structure:
To apply binary search in any data structure, the data structure must
maintain the following properties:
 The data structure must be sorted.
 Access to any element of the data structure takes constant time.

The Binary Search Algorithm can be implemented in the following two


ways
 Iterative Binary Search Algorithm
 Recursive Binary Search Algorithm

Iterative Binary Search Algorithm:

Pseudocode of Iterative Binary Search Algorithm:


binarySearch(arr, x, low, high):
repeat till low = high
mid = (low + high)/2
if (x = arr[mid])
return mid
else if (x > arr[mid])
low = mid + 1
else
high = mid – 1

Time Complexity: O(log N)


Auxiliary Space: O(1)

Recursive Binary Search Algorithm:


Pseudocode of Recursive Binary Search Algorithm:
binarySearch(arr, x, low, high)
if low > high
return False
else
mid = (low + high) / 2
if x = arr[mid]
return mid
else if x > arr[mid]
return binarySearch(arr, x, mid + 1, high)
else
return binarySearch(arr, x, low, mid – 1)
Fibonacci Series Program in C
What is Fibonacci Series?
The Fibonacci series is the sequence where each number is the sum of the previous two
numbers of the sequence. The first two numbers of the Fibonacci series are 0 and 1 and are
used to generate the Fibonacci series.
In mathematical terms, the number at the nth position can be represented by:
Fn = Fn-1 + Fn-2
with seed values, F0 = 0 and F1 = 1.
For example, Fibonacci series upto 10 terms is: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

The Fibonacci programs in C that print the first n terms of the series can be coded using
two methods specified below:
 Fibonacci series using recursion
 Fibonacci series using loops

 Fibonacci series using recursion

int fib(int n)
{
if (n <= 1)
return n;
return fib(n - 1) + fib(n - 2);
}

#include <stdio.h>
int fibonacci(int n) {
if (n <= 1)
return n;
else
return fibonacci(n-1) + fibonacci(n-2);
}

int main() {
int i;
// Print the first 10 Fibonacci numbers
for (i = 0; i < 10; i++) {
printf("%d\n", fibonacci(i));
}
return 0;
}
Fibonacci Iterative Algorithm
First we try to draft the iterative algorithm for Fibonacci series.

Procedure Fibonacci(n)
declare f0, f1, fib, loop

set f0 to 0
set f1 to 1

display f0, f1

for loop ← 1 to n

fib ← f0 + f1
f0 ← f1
f1 ← fib

display fib
end for

end procedure

Queue Data Structure

Definition: A Queue is a linear data structure that follows the First In First Out
(FIFO) principle. In simpler terms, the first element inserted into the queue is the
first one to be removed. It resembles a real-world queue where people stand in
line to wait for service.

Operations on Queue:

1. enqueue(item): Adds an item to the end of the queue.


2. dequeue(): Removes the item from the front of the queue.
3. isEmpty(): Checks if the queue is empty.
4. peek(): Returns the element at the front of the queue without removing it.
5. size(): Returns the number of elements in the queue.

Implementation:

Queues can be implemented using arrays or linked lists. Below is a simple


implementation using arrays.
Applications:

 Job scheduling in operating systems.


 Printer queues.
 Breadth-first search (BFS) algorithm.
 Traffic management in networking.
 Process synchronization in operating systems.

Type of Queue Data Structure


There are several types of queue data structures, each designed to serve specific
requirements and optimize different aspects of queue operations. Some common
types of queue data structures include:

1. Simple Queue: This is the basic form of a queue where elements are inserted at
the rear and removed from the front. It follows the FIFO (First In First Out)
principle.

2. Circular Queue (Ring Buffer): Circular queues are a variation of simple queues
where the rear of the queue is connected to the front, forming a circular structure.
This allows efficient space utilization and avoids wastage of memory.
3. Priority Queue: Priority queues are queues where each element has an
associated priority. Elements with higher priorities are dequeued before elements
with lower priorities, irrespective of their arrival order. Priority queues can be
implemented using various data structures like binary heaps, balanced binary
search trees, or arrays.

4. Double-ended Queue (Deque): Deques support insertion and deletion of


elements from both the front and the rear ends. This flexibility allows operations
like inserting at both ends, removing from both ends, and peeking at elements
from both ends.

Double-ended Queue (Deque):

Simple Queue Algorithms

Enqueue Algorithm:

Algorithm enqueue(value):
if rear == MAX_SIZE - 1:
Print "Queue Overflow"
return
if front == -1 and rear == -1:
Set front = rear = 0
else:
Set rear = rear + 1
Set queue[rear] = value
Dequeue Algorithm:

Algorithm dequeue():
if front == -1:
Print "Queue Underflow"
return
if front == rear:
Set front = rear = -1
else:
Set front = front + 1

Program

#include <stdio.h>
#define MAX_SIZE 100

int queue[MAX_SIZE];
int front = -1, rear = -1;

void enqueue(int value) {


if (rear == MAX_SIZE - 1) {
printf("Queue Overflow\n");
return;
}
if (front == -1 && rear == -1) {
front = rear = 0;
} else {
rear++;
}
queue[rear] = value;
}

// Function to remove an element from the queue (dequeue)


void dequeue() {
if (front == -1) {
printf("Queue Underflow\n");
return;
}
if (front == rear) {
front = rear = -1;
} else {
front++;
}
}
// Function to display the elements of the queue
void display() {
if (front == -1) {
printf("Queue is empty\n");
return;
}
printf("Queue elements: ");
for (int i = front; i <= rear; i++) {
printf("%d ", queue[i]);
}
printf("\n");
}

int main() {
enqueue(10);
enqueue(20);
enqueue(30);

display();

dequeue();

display();

return 0;
}

Circular Queue

Enqueue Algorithm:

Algorithm enqueue(value):
if ((rear + 1) % MAX_SIZE == front):
Print "Queue Overflow"
return
if front == -1 and rear == -1:
Set front = rear = 0
else:
Set rear = (rear + 1) % MAX_SIZE
Set queue[rear] = value
Dequeue Algorithm:

Algorithm dequeue():
if front == -1:
Print "Queue Underflow"
return
if front == rear:
Set front = rear = -1
else:
Set front = (front + 1) % MAX_SIZE
Searching Algorithms
Searching Algorithms are designed to check for an element or retrieve an element
from any data structure where it is stored.
Sequential Search: In this, the list or array is traversed sequentially and every
element is checked. For example: Linear Search.
Linear Search is defined as a sequential search algorithm that starts at
one end and goes through each element of a list until the desired element
is found, otherwise the search continues till the end of the data set.

What is Hashing?
Hashing: Hashing is a popular technique for storing and retrieving data
as fast as possible.
Hashing is a technique or process of mapping keys, and values into the hash table by using
a hash function. It is done for faster access to elements. The efficiency of mapping depends
on the efficiency of the hash function used.
Let a hash function H(x) maps the value x at the index x%10 in an Array. For example if
the list of values is [11,12,13,14,15] it will be stored at positions {1,2,3,4,5} in the array or
Hash table respectively.

Components of Hashing
There are majorly three components of hashing:
1. Key: A Key can be anything string or integer which is fed as input in
the hash function the technique that determines an index or location
for storage of an item in a data structure.
2. Hash Function: The hash function receives the input key and
returns the index of an element in an array called a hash table. The
index is known as the hash index.
3. Hash Table: Hash table is a data structure that maps keys to values
using a special function called a hash function. Hash stores the data in
an associative manner in an array where each data value has its own
unique index.

Types of Hash functions:

There are many hash functions that use numeric or alphanumeric keys.
This article focuses on discussing different hash functions:

1. Division Method.
2. Mid Square Method.
3. Folding Method.

1. Division Method:
This is the most simple and easiest method to generate a hash value.
The hash function divides the value k by M and then uses the remainder
obtained.
Formula:
h(K) = k mod M
Here,
k is the key value, and
M is the size of the hash table.
It is best suited that M is a prime number as that can make sure the keys
are more uniformly distributed. The hash function is dependent upon the
remainder of a division.
Example:
k = 12345
M = 95
h(12345) = 12345 mod 95
= 90
k = 1276
M = 11
h(1276) = 1276 mod 11
=0
Pros:
1. This method is quite good for any value of M.
2. The division method is very fast since it requires only a single division
operation.
Cons:
1. This method leads to poor performance since consecutive keys map
to consecutive hash values in the hash table.
2. Sometimes extra care should be taken to choose the value of M.
2. Mid Square Method:
The mid-square method is a very good hashing method. It involves two
steps to compute the hash value-
1. Square the value of the key k i.e. k 2
2. Extract the middle r digits as the hash value.
Formula:
h(K) = h(k x k)
Here,
k is the key value.
The value of r can be decided based on the size of the table.
Example:
Suppose the hash table has 100 memory locations. So r = 2 because two
digits are required to map the key to the memory location.
k = 60
k x k = 60 x 60
= 3600
h(60) = 60
The hash value obtained is 60
Pros:
1. The performance of this method is good as most or all digits of the key
value contribute to the result. This is because all digits in the key
contribute to generating the middle digits of the squared result.
2. The result is not dominated by the distribution of the top digit or
bottom digit of the original key value.
Cons:
1. The size of the key is one of the limitations of this method, as the key
is of big size then its square will double the number of digits.
2. Another disadvantage is that there will be collisions but we can try to
reduce collisions.
3. Digit Folding Method:
This method involves two steps:
1. Divide the key-value k into a number of parts i.e. k1, k2, k3,….,kn,
where each part has the same number of digits except for the last part
that can have lesser digits than the other parts.
2. Add the individual parts. The hash value is obtained by ignoring the
last carry if any.
Formula:
k = k1, k2, k3, k4, ….., kn
s = k1+ k2 + k3 + k4 +….+ kn
h(K)= s
Here,
s is obtained by adding the parts of the key k
Example:
k = 12345
k1 = 12, k2 = 34, k3 = 5
s = k1 + k2 + k3
= 12 + 34 + 5
= 51
h(K) = 51
Note:
The number of digits in each part varies depending upon the size of the
hash table. Suppose for example the size of the hash table is 100, then
each part must have two digits except for the last part which can have a
lesser number of digits.

Collision in Hashing
In this, the hash function is used to find the index of the array. The hash
value is used to create an index for the key in the hash table. The hash
function may return the same hash value for two or more keys. When two
or more keys have the same hash value, a collision happens. To handle
this collision, we use collision resolution techniques.
Collision Resolution Techniques

In hashing, data is stored in an array where the index of the array is


determined by a hash function. However, collisions can occur when two
or more keys generate the same hash value, which can cause data loss
or performance degradation. To resolve collisions, several techniques are
used, some of which are discussed below:

Open addressing: In open addressing, when a collision occurs, a


different index is calculated using a probing sequence until an empty slot
is found. There are different types of probing sequences, including linear
probing, quadratic probing, and double hashing. For example, in linear
probing, if index h(k) is already occupied, we calculate h(k)+1, h(k)+2,
h(k)+3, and so on, until we find an empty slot.

PRACTICE PROBLEM BASED ON OPEN


ADDRESSING-

Problem-

Using the hash function ‘key mod 7’, insert the following sequence of keys in the
hash table-
50, 700, 76, 85, 92, 73 and 101

Solution-

The given sequence of keys will be inserted in the hash table as-

Step-01:

 Draw an empty hash table.


 For the given hash function, the possible range of hash values is [0, 6].
 So, draw an empty hash table consisting of 7 buckets as-
Step-02:

 Insert the given keys in the hash table one by one.


 The first key to be inserted in the hash table = 50.
 Bucket of the hash table to which key 50 maps = 50 mod 7 = 1.
 So, key 50 will be inserted in bucket-1 of the hash table as-

Step-03:

 The next key to be inserted in the hash table = 700.


 Bucket of the hash table to which key 700 maps = 700 mod 7 = 0.
 So, key 700 will be inserted in bucket-0 of the hash table as-
Step-04:

 The next key to be inserted in the hash table = 76.


 Bucket of the hash table to which key 76 maps = 76 mod 7 = 6.
 So, key 76 will be inserted in bucket-6 of the hash table as-

Step-05:

 The next key to be inserted in the hash table = 85.


 Bucket of the hash table to which key 85 maps = 85 mod 7 = 1.
 Since bucket-1 is already occupied, so collision occurs.
 To handle the collision, linear probing technique keeps probing linearly until an empty
bucket is found.
 The first empty bucket is bucket-2.
 So, key 85 will be inserted in bucket-2 of the hash table as-
Step-06:

 The next key to be inserted in the hash table = 92.


 Bucket of the hash table to which key 92 maps = 92 mod 7 = 1.
 Since bucket-1 is already occupied, so collision occurs.
 To handle the collision, linear probing technique keeps probing linearly until an empty
bucket is found.
 The first empty bucket is bucket-3.
 So, key 92 will be inserted in bucket-3 of the hash table as-

Step-07:
 The next key to be inserted in the hash table = 73.
 Bucket of the hash table to which key 73 maps = 73 mod 7 = 3.
 Since bucket-3 is already occupied, so collision occurs.
 To handle the collision, linear probing technique keeps probing linearly until an
empty bucket is found.
 The first empty bucket is bucket-4.
 So, key 73 will be inserted in bucket-4 of the hash table as-

Step-08:

 The next key to be inserted in the hash table = 101.


 Bucket of the hash table to which key 101 maps = 101 mod 7 = 3.
 Since bucket-3 is already occupied, so collision occurs.
 To handle the collision, linear probing technique keeps probing linearly until
an empty bucket is found.
 The first empty bucket is bucket-5.
 So, key 101 will be inserted in bucket-5 of the hash table as-
Chaining: In chaining, each slot in the hash table contains a linked list of
elements that share the same hash value. When a collision occurs, the
new element is added to the linked list. Retrieval of data requires iterating
through the linked list.

Separate Chaining-

To handle the collision,


 This technique creates a linked list to the slot for which collision occurs.
 The new key is then inserted in the linked list.
 These linked lists to the slots appear like chains.
 That is why, this technique is called as separate chaining.

PRACTICE PROBLEM BASED ON SEPARATE


CHAINING-

Problem-

Using the hash function ‘key mod 7’, insert the following sequence of keys in the
hash table-
50, 700, 76, 85, 92, 73 and 101

Use separate chaining technique for collision resolution.

Solution-
The given sequence of keys will be inserted in the hash table as-

Step-01:

 Draw an empty hash table.


 For the given hash function, the possible range of hash values is [0, 6].
 So, draw an empty hash table consisting of 7 buckets as-
Step-02:

 Insert the given keys in the hash table one by one.


 The first key to be inserted in the hash table = 50.
 Bucket of the hash table to which key 50 maps = 50 mod 7 = 1.
 So, key 50 will be inserted in bucket-1 of the hash table as-

Step-03:

 The next key to be inserted in the hash table = 700.


 Bucket of the hash table to which key 700 maps = 700 mod 7 = 0.
 So, key 700 will be inserted in bucket-0 of the hash table as-
Step-04:

 The next key to be inserted in the hash table = 76.


 Bucket of the hash table to which key 76 maps = 76 mod 7 = 6.
 So, key 76 will be inserted in bucket-6 of the hash table as-

Step-05:

 The next key to be inserted in the hash table = 85.


 Bucket of the hash table to which key 85 maps = 85 mod 7 = 1.
 Since bucket-1 is already occupied, so collision occurs.
 Separate chaining handles the collision by creating a linked list to bucket-1.
 So, key 85 will be inserted in bucket-1 of the hash table as-
Step-06:

 The next key to be inserted in the hash table = 92.


 Bucket of the hash table to which key 92 maps = 92 mod 7 = 1.
 Since bucket-1 is already occupied, so collision occurs.
 Separate chaining handles the collision by creating a linked list to bucket-1.
 So, key 92 will be inserted in bucket-1 of the hash table as-

Step-07:

 The next key to be inserted in the hash table = 73.


 Bucket of the hash table to which key 73 maps = 73 mod 7 = 3.
 So, key 73 will be inserted in bucket-3 of the hash table as-
Step-08:

 The next key to be inserted in the hash table = 101.


 Bucket of the hash table to which key 101 maps = 101 mod 7 = 3.
 Since bucket-3 is already occupied, so collision occurs.
 Separate chaining handles the collision by creating a linked list to bucket-3.
 So, key 101 will be inserted in bucket-3 of the hash table as-

You might also like