0% found this document useful (0 votes)
6 views120 pages

data structures5

The document provides an overview of linear data structures, including their definition, types (Array, Linked List, Stack, Queue), and the concept of Abstract Data Types (ADTs). It also discusses time and space complexity analysis, comparing linear and binary search algorithms, and outlines sorting techniques such as Bubble Sort and Selection Sort. Each section includes definitions, examples, and complexities associated with the algorithms and data structures.

Uploaded by

villanmshuhaiv05
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views120 pages

data structures5

The document provides an overview of linear data structures, including their definition, types (Array, Linked List, Stack, Queue), and the concept of Abstract Data Types (ADTs). It also discusses time and space complexity analysis, comparing linear and binary search algorithms, and outlines sorting techniques such as Bubble Sort and Selection Sort. Each section includes definitions, examples, and complexities associated with the algorithms and data structures.

Uploaded by

villanmshuhaiv05
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 120

Data structures

1.1)definition and importance of linear data structures:

 A linear data structure is a way to store data in a sequential order,with each


element connected to the next and previous elements
 Linear data structures are easy to understand and implement
Importance:
 Linear data structures are the building blocks for more complex
algorithms and structures
 They are used in many applications from simple data storage to
complex problem solving

Types of Linear Data Structure

There are four types of linear data structure:

1. Array.
2. Linked list.
3. Stack.
4. Queue.

1.2) Abstract Data Types



 In this article, we will learn about Abstract Data Types (ADTs).
But before understanding what an ADT is, let us consider
different built-in data types provided by programming
languages. Data types such as int, float, double, and long are
built-in types that allow us to perform basic operations like
addition, subtraction, division, and multiplication. However,
there are scenarios where we need custom operations for
different data types. These operations are defined based on
specific requirements and are tailored as needed. To address
such needs, we can create data structures along with their
operations, which are known as Abstract Data Types (ADTs).

Abstract Data Type (ADT)


Data types such as int,float,double,and long are built in types to perform basic
operations like addition,substraction,multiplication and division

An Abstract Data Type (ADT) is a conceptual model that defines a


set of operations and behaviours for a data type, without
specifying how these operations are implemented or how data is
organized in memory.

For example, we use primitive values like int, float, and char with
the understanding that these data types can operate and be
performed on without any knowledge of their implementation
details. ADTs operate similarly by defining what operations are
possible without detailing their implementation.
Defining ADTs: Examples
Now, let’s understand three common ADT’s: List ADT, Stack ADT,
and Queue ADT.
1. List ADT

Vies of list

The List ADT need to store the required data in the sequence and
should have the following operations:
 get(): Return an element from the list at any given position.
 insert(): Insert an element at any position in the list.
 remove(): Remove the first occurrence of any element from a
non-empty list.
 removeAt(): Remove the element at a specified location from
a non-empty list.
 replace(): Replace an element at any position with another
element.
 size(): Return the number of elements in the list.
 isEmpty(): Return true if the list is empty; otherwise, return
false.
 isFull(): Return true if the list is full; otherwise, return false.
2. Stack ADT
View of stack

In Stack ADT, the order of insertion and deletion should be


according to the FILO or LIFO Principle. Elements are inserted and
removed from the same end, called the top of the stack. It should
also support the following operations:
 push(): Insert an element at one end of the stack called the
top.
 pop(): Remove and return the element at the top of the stack,
if it is not empty.
 peek(): Return the element at the top of the stack without
removing it, if the stack is not empty.
 size(): Return the number of elements in the stack.
 isEmpty(): Return true if the stack is empty; otherwise, return
false.
 isFull(): Return true if the stack is full; otherwise, return false.
3. Queue ADT
View of Queue

The Queue ADT follows a design similar to the Stack ADT, but the
order of insertion and deletion changes to FIFO. Elements are
inserted at one end (called the rear) and removed from the other
end (called the front). It should support the following operations:
 enqueue(): Insert an element at the end of the queue.
 dequeue(): Remove and return the first element of the queue,
if the queue is not empty.
 peek(): Return the element of the queue without removing it,
if the queue is not empty.
 size(): Return the number of elements in the queue.
 isEmpty(): Return true if the queue is empty; otherwise,
return false.
1.3)overview of time and space complexity analysis for linear data structures

 Time and space complexity analysis for linear data structures measures
how much time and memory an algorithm takes to run.its important for
designing software, building web-sites and analyzing large datasets
Time complexity
 The amount of time it takes an algorithm to run
 The number of operations like comparisions,required to complete the
algorithm
 The worst-case time complexity is the maximum time it takes for any input
 The average-case time complexity is the average time it takes for an input
 The best-case time complexity is the minimum time it takes for an input
Space complexity
 The amount of memory an algorithm uses
 The fixed amount of space required by the algorithm
 The variable amount of space required by the algorithm which
depends on the input size
Calculating time and space complexity
 Identify the basic operation in the algorithm
 Count how many times the basic operation is performed
 Express the count as a function of the input size
 Simplify the expression and identify the dominant term
 Express the time complexity using big o notation

Example: Linear search algorithm

 The time complexity is o(n).where n is the number of elements in the array


 The space complexity is o(1).which means it uses a constant amount of
extra space

1.4)Searching techniques:Linear and binary search

Linear Search Algorithm



Given an array, arr of n integers, and an integer element x, find


whether element x is present in the array. Return the index of the
first occurrence of x in the array, or -1 if it doesn’t exist.
Input: arr[] = [1, 2, 3, 4], x = 3
Output: 2

Input: arr[] = [10, 8, 30], x = 6


Output: -1
Explanation: The element to be searched is 6 and its not
present, so we return -1.

In Linear Search, we iterate over all the elements of the array and
check if it the current element is equal to the target element. If
we find any element to be equal to the target element, then
return the index of the current element. Otherwise, if no element
is equal to the target element, then return -1 as the element is
not found. Linear search is also known as sequential search.
For example: Consider the array arr[] = {10, 50, 30, 70, 80,
20, 90, 40} and key = 30

Binary Search Algorithm – Iterative and Recursive Implementation

Binary Search is a highly efficient algorithm used to find an element in a sorted


array or list. Unlike linear search, which checks each element sequentially, binary
search works by repeatedly dividing the search interval in half, which makes it
much faster than linear search, especially for large datasets.

How Binary Search Works:

1. Initial Setup: Binary search begins by looking at the middle element of the
sorted array.
2. Comparison:
o If the middle element matches the target value, the search is complete.
o If the middle element is greater than the target, the target must be in
the left half of the array (since the array is sorted). So, the search
continues in the left half.
o If the middle element is less than the target, the target must be in the
right half of the array, and the search continues there.
3. Repeat: This process repeats, halving the search range each time, until the
element is found or the search range is empty (indicating the element is not
in the array).

Binary Search Algorithm

1. Start with the whole array.


2. Calculate the middle index: mid = (low + high) // 2
3. Compare the middle element with the target:
o If arr[mid] == target, return the index.
o If arr[mid] > target, repeat the search in the left half (high
= mid - 1).
o If arr[mid] < target, repeat the search in the right half (low
= mid + 1).
4. Repeat the process until the element is found or the search range becomes
invalid (low > high).

Example:

Given a sorted array: [2, 5, 8, 12, 15, 19, 25, 32, 37, 40] and
the target element 15.

1. Initial Range: low = 0, high = 9 (the indices of the array).


o mid = (0 + 9) // 2 = 4, so arr[mid] = 15.
o arr[mid] == 15, so we return mid = 4.

Time and Space Complexity:

1. Time Complexity:
o Best case: O(1)O(1)O(1) — The target is found in the first
comparison (if it's the middle element).
o Average case: O(log⁡n)O(\log n)O(logn) — With each iteration, the
search space is halved, so the time complexity grows logarithmically
with the size of the array.
o Worst case: O(log⁡n)O(\log n)O(logn) — The algorithm may need to
make log⁡n\log nlogn comparisons to exhaust the search space.
2. Space Complexity:
o Space Complexity: O(1)O(1)O(1) — Binary search operates in
constant space, as it only requires a few variables to track the low,
high, and mid indices.

Requirements for Binary Search:

 Sorted Array/List: The array must be sorted before applying binary search.
If the array is not sorted, you would need to sort it first, which would take
O(nlog⁡n)O(n \log n)O(nlogn) time.
 Efficient Search Space Reduction: Binary search reduces the search space
by half each time, which is why it is much more efficient than linear search
for large datasets.

Advantages:
 Efficient for Large Datasets: With a time complexity of O(log⁡n)O(\log
n)O(logn), binary search is very efficient compared to linear search,
especially for large datasets.
 Constant Space: It uses O(1)O(1)O(1) space, making it very memory-
efficient.

Disadvantages:

 Sorted Data Requirement: The array or list must be sorted beforehand.


 Not Efficient for Small Datasets: For small datasets, binary search may not
be worth the overhead compared to simpler algorithms like linear search.



1.5) Sorting techniques: Bubble Sort, Selection Sort,


Insertion Sort:
Bubble Sort Algorithm



Bubble Sort is a simple comparison-based sorting algorithm in computer science.


It is named "bubble sort" because the smaller elements "bubble" to the top
(beginning of the array) with each pass through the list.

How Bubble Sort Works:

1. Iterate through the list: Starting at the first element, compare the current
element with the next element.
2. Swap if necessary: If the current element is greater than the next one (for
ascending order), swap the two elements.
3. Repeat: Continue this process for the entire list. After each pass, the largest
element "bubbles" to the correct position at the end of the list.
4. Optimization: If during a pass, no swaps are made, the list is already sorted,
and the algorithm can terminate early.

Example:

Let's sort an array: [5, 3, 8, 4, 2] in ascending order.

1. First Pass:
o Compare 5 and 3, swap → [3, 5, 8, 4, 2]
o Compare 5 and 8, no swap → [3, 5, 8, 4, 2]
o Compare 8 and 4, swap → [3, 5, 4, 8, 2]
o Compare 8 and 2, swap → [3, 5, 4, 2, 8]
o After the first pass, the largest element (8) is in its correct position at
the end of the list.
2. Second Pass:
o Compare 3 and 5, no swap → [3, 5, 4, 2, 8]
o Compare 5 and 4, swap → [3, 4, 5, 2, 8]
o Compare 5 and 2, swap → [3, 4, 2, 5, 8]
o After the second pass, the second-largest element (5) is in its correct
position.
3. Third Pass:
o Compare 3 and 4, no swap → [3, 4, 2, 5, 8]
o Compare 4 and 2, swap → [3, 2, 4, 5, 8]
o After the third pass, the third-largest element (4) is in its correct
position.
4. Fourth Pass:
o Compare 3 and 2, swap → [2, 3, 4, 5, 8]
o The list is now sorted.

Time Complexity:

 Best case (already sorted array): O(n)O(n)O(n) — when the algorithm


terminates early due to no swaps.
 Average case: O(n2)O(n^2)O(n2) — when the array is unsorted.
 Worst case: O(n2)O(n^2)O(n2) — when the array is sorted in reverse order.

Space Complexity:
 Space Complexity: O(1)O(1)O(1) — Bubble Sort is an in-place sorting
algorithm, meaning it doesn’t require additional storage proportional to the
input size.

Advantages:

 Simple to understand and implement.


 In-place sorting with O(1)O(1)O(1) extra space.

Disadvantages:

 Inefficient for large datasets due to its O(n2)O(n^2)O(n2) time complexity.


 Not suitable for large-scale sorting when compared to algorithms like
Merge Sort or Quick Sort.

Selection Sort is a simple and intuitive sorting algorithm. It repeatedly selects the
smallest (or largest) element from the unsorted portion of the array and swaps it
with the element at the beginning of the unsorted portion. This process is repeated
until the entire array is sorted.

How Selection Sort Works:

1. Start with the first element of the list.


2. Find the smallest element in the unsorted part of the list (from the current
element to the last element).
3. Swap the smallest element with the first unsorted element.
4. Move the boundary of the unsorted portion by one position forward (i.e.,
move the left pointer to the next element).
5. Repeat this process until the entire array is sorted.

Example:

Let's walk through Selection Sort on the following array:

[64, 25, 12, 22, 11]

Step-by-step Process:

First Pass:

 The initial array is [64, 25, 12, 22, 11].


 We need to find the smallest element from the entire array.
o Compare 64 with 25, 25 is smaller.
o Compare 25 with 12, 12 is smaller.
o Compare 12 with 22, 12 is still smaller.
o Compare 12 with 11, 11 is smaller.
o The smallest element in this pass is 11.

 Swap 11 with 64 (the first element of the array).


 The array now becomes:

[11, 25, 12, 22, 64]


Second Pass:

 Now, we focus on the unsorted portion of the array: [25, 12, 22, 64].
 Find the smallest element in this subarray:
o Compare 25 with 12, 12 is smaller.
o Compare 12 with 22, 12 is smaller.
o Compare 12 with 64, 12 is still smaller.
o The smallest element is 12.

 Swap 12 with 25 (the first element in this unsorted subarray).


 The array now becomes:

[11, 12, 25, 22, 64]


Third Pass:

 Now, focus on the subarray [25, 22, 64].


 Find the smallest element in this subarray:
o Compare 25 with 22, 22 is smaller.
o Compare 22 with 64, 22 is smaller.
o The smallest element is 22.

 Swap 22 with 25.


 The array now becomes:

[11, 12, 22, 25, 64]


Fourth Pass:

 Focus on the subarray [25, 64].


 Find the smallest element in this subarray:
o 25 is smaller than 64.

 No need to swap since 25 is already in the correct position.


 The array remains:

[11, 12, 22, 25, 64]


Fifth Pass:

 The remaining subarray is just [64], which is already sorted.

Final Sorted Array:

After completing all passes, the array is sorted:

[11, 12, 22, 25, 64]

Time and Space Complexity:

1. Time Complexity:
o Best, Average, and Worst Case: O(n2)O(n^2)O(n2) — Selection Sort
always compares every element with every other element in the
unsorted portion, resulting in a quadratic number of comparisons.
 Best Case: Even if the array is already sorted, Selection Sort
still performs O(n2)O(n^2)O(n2) comparisons.
 Worst Case: The worst case happens when the array is sorted
in reverse order, which still requires O(n2)O(n^2)O(n2)
comparisons.
2. Space Complexity:
o Space Complexity: O(1)O(1)O(1) — Selection Sort is an in-place
sorting algorithm, meaning it requires only a constant amount of
additional space for the temporary variable used in swapping.

Advantages of Selection Sort:

 Simplicity: Very easy to understand and implement.


 In-place sorting: It sorts the array without using additional memory (i.e., no
auxiliary array).
 Not affected by the initial order of elements: Unlike algorithms like Bubble
Sort or Insertion Sort, which can perform better if the array is partially
sorted, Selection Sort always performs the same number of comparisons.

Disadvantages of Selection Sort:

 Inefficient for large datasets: Because of its O(n2)O(n^2)O(n2) time


complexity, it is very slow for large arrays, especially when compared to
more advanced algorithms like Merge Sort or Quick Sort, which have
O(nlog⁡n)O(n \log n)O(nlogn) time complexity.
 Not adaptive: It doesn't improve even when the array is partially sorted.
This makes it inefficient in many practical scenarios.

Insertion Sort in Data Structures with Example

Insertion Sort is a simple comparison-based sorting algorithm that builds the final
sorted array one element at a time. It is much like sorting playing cards in your
hands, where you take one card at a time and place it in the correct position relative
to the cards already sorted.

How Insertion Sort Works:

1. Start with the second element (since a single element is trivially sorted).
2. Compare the current element with the elements in the sorted portion of
the array (to its left).
3. Shift all elements that are greater than the current element one position to
the right to make space for the current element.
4. Insert the current element into its correct position.
5. Repeat this process for all the elements in the array.

Example:

Consider the array: [5, 2, 9, 1, 5, 6]

Step-by-step Process:

Initial Array:
[5, 2, 9, 1, 5, 6]

We will start with the second element (index 1), because the first element is
trivially considered sorted.

First Pass (i = 1):

 Current element: 2
 Compare 2 with 5 (the element to its left).
o 2 < 5, so shift 5 to the right.
o The array now looks like: [5, 5, 9, 1, 5, 6].
 Insert 2 into its correct position: Place 2 at the start.
 The array becomes: [2, 5, 9, 1, 5, 6].

Second Pass (i = 2):

 Current element: 9
 Compare 9 with 5 (the element to its left).
o 9 > 5, no shift needed.
 Insert 9: It's already in the correct position.
 The array remains: [2, 5, 9, 1, 5, 6].

Third Pass (i = 3):

 Current element: 1
 Compare 1 with 9 (shift 9 right).
o The array becomes: [2, 5, 9, 9, 5, 6].
 Compare 1 with 5 (shift 5 right).
o The array becomes: [2, 5, 5, 9, 5, 6].
 Compare 1 with 2 (shift 2 right).
o The array becomes: [2, 2, 5, 9, 5, 6].
 Insert 1 at the start.
 The array becomes: [1, 2, 5, 9, 5, 6].
Fourth Pass (i = 4):

 Current element: 5
 Compare 5 with 9 (shift 9 right).
o The array becomes: [1, 2, 5, 9, 9, 6].
 Compare 5 with 5 (no shift needed).
 Insert 5 after the first 5.
 The array becomes: [1, 2, 5, 5, 9, 6].

Fifth Pass (i = 5):

 Current element: 6
 Compare 6 with 9 (shift 9 right).
o The array becomes: [1, 2, 5, 5, 9, 9].
 Compare 6 with 5 (no shift needed).
 Insert 6 after the second 5.
 The array becomes: [1, 2, 5, 5, 6, 9].

Final Sorted Array:

After completing all passes, the array is sorted:

[1, 2, 5, 5, 6, 9]

Insertion Sort Algorithm Code (in Python):

Time and Space Complexity:

1. Time Complexity:
o Best case: O(n)O(n)O(n) — This happens when the array is already
sorted. In this case, only one comparison is made for each element,
and no shifting occurs.
o Worst case: O(n2)O(n^2)O(n2) — This occurs when the array is
sorted in reverse order. Each element needs to be compared and
shifted to the beginning.
o Average case: O(n2)O(n^2)O(n2) — On average, the algorithm will
need to perform about n2/2n^2 / 2n2/2 comparisons and shifts.

2. Space Complexity:
o Space Complexity: O(1)O(1)O(1) — Insertion sort is an in-place
sorting algorithm, meaning it doesn't require any additional space
besides the input array.

Advantages of Insertion Sort:

 Simple to understand and implement: It is one of the easiest sorting


algorithms to understand and code.
 Efficient for small datasets: For small arrays, the overhead of more
advanced algorithms may not be justified, and Insertion Sort can perform
well.
 Adaptive: It is adaptive, meaning if the array is already partially sorted, the
algorithm will perform better (i.e., fewer shifts and comparisons).
 Stable: Insertion sort is stable, meaning that it maintains the relative order
of elements with equal values.

Disadvantages of Insertion Sort:

 Inefficient for large datasets: Due to its O(n2)O(n^2)O(n2) time complexity,


it is not suitable for large arrays.
 Not ideal for large-scale data: For larger datasets, more efficient algorithms
like Quick Sort, Merge Sort, or Heap Sort are typically preferred.

When to Use Insertion Sort:

 Small datasets: It is efficient for small arrays where the overhead of more
complex algorithms isn't necessary.
 Partially sorted arrays: If the array is already partially sorted, Insertion Sort
can be much faster than other algorithms.
 Memory-constrained environments: It uses only O(1)O(1)O(1) extra space,
so it is useful when memory is limited.
Chapter -2

2.1)What is Linked List?

A linked list is a linear data structure which can store a collection


of "nodes" connected together via links i.e. pointers. Linked lists
nodes are not stored at a contiguous location, rather they are
linked using pointers to the different memory locations. A node
consists of the data value and a pointer to the address of the next
node within the linked list.

A linked list is a dynamic linear data structure whose memory size


can be allocated or de-allocated at run time based on the
operation insertion or deletion, this helps in using system memory
efficiently. Linked lists can be used to implement various data
structures like a stack, queue, graph, hash maps, etc.

A linked list starts with a head node which points to the first
node. Every node consists of data which holds the actual data
(value) associated with the node and a next pointer which holds
the memory address of the next node in the linked list. The last
node is called the tail node in the list which points
to null indicating the end of the list.

Types of Linked List

Following are the various types of linked list.

Singly Linked Lists

Singly linked lists contain two "buckets" in one node; one bucket
holds the data and the other bucket holds the address of the next
node of the list. Traversals can be done in one direction only as
there is only a single link between two nodes of the same list.
Doubly Linked Lists

Doubly Linked Lists contain three "buckets" in one node; one


bucket holds the data and the other buckets hold the addresses
of the previous and next nodes in the list. The list is traversed
twice as the nodes in the list are connected to each other from
both sides.

Circular Linked Lists

Circular linked lists can exist in both singly linked list and doubly
linked list.

Since the last node and the first node of the circular linked list are
connected, the traversal in this linked list will go on forever until it
is broken.

Basic Operations in Linked List:-

The basic operations in the linked lists are insertion, deletion,


searching, display, and deleting an element at a given key. These
operations are performed on Singly Linked Lists as given below −

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


 Deletion − Deletes an element at the beginning of the list.
 Display − Displays the complete list.
 Search − Searches an element using the given key.
 Delete − Deletes an element using the given key.

Linked List - Insertion Operation

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

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


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

NewNode.next -> RightNode;

It should look like this −

Now, the next node at the left should point to the new node.
LeftNode.next -> NewNode;

This will put the new node in the middle of the two. The new list
should look like this −

Insertion in linked list can be done in three different ways. They


are explained as follows −

Insertion at Beginning

In this operation, we are adding an element at the beginning of


the list.

Algorithm

1. START
2. Create a node to store the data
3. Check if the list is empty
4. If the list is empty, add the data to the node and
assign the head pointer to it.
5. If the list is not empty, add the data to a node and link to the
current head. Assign the head to the newly added node.
6. END

Insertion at Ending

In this operation, we are adding an element at the ending of the


list.
Algorithm

1. START
2. Create a new node and assign the data
3. Find the last node
4. Point the last node to new node
5. END

Insertion at a Given Position

In this operation, we are adding an element at any position within


the list.

Algorithm

1. START
2. Create a new node and assign data to it
3. Iterate until the node at position is found
4. Point first to new first node
5. END

Linked List - Deletion Operation

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

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

LeftNode.next -> TargetNode.next;


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

TargetNode.next -> NULL;

We need to use the deleted node. We can keep that in memory


otherwise we can simply deallocate memory and wipe off the
target node completely.

Similar steps should be taken if the node is being inserted at the


beginning of the list. While inserting it at the end, the second last
node of the list should point to the new node and the new node
will point to NULL.

Deletion in linked lists is also performed in three different ways.


They are as follows −
Deletion at Beginning

In this deletion operation of the linked, we are deleting an


element from the beginning of the list. For this, we point the head
to the second node.

Algorithm

1. START
2. Assign the head pointer to the next node in the list
3. END

Deletion at Ending

In this deletion operation of the linked, we are deleting an


element from the ending of the list.

Algorithm

1. START
2. Iterate until you find the second last element in the list.
3. Assign NULL to the second last element in the list.
4. END

Deletion at a Given Position

In this deletion operation of the linked, we are deleting an


element at any position of the list.

Algorithm

1. START
2. Iterate until find the current node at position in the list.
3. Assign the adjacent node of current node in the list
to its previous node.
4. END

Linked List - Reversal Operation:-

This operation is a thorough one. We need to make the last node


to be pointed by the head node and reverse the whole linked list.

First, we traverse to the end of the list. It should be pointing to


NULL. Now, we shall make it point to its previous node −

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

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

Algorithm

Step by step process to reverse a linked list is as follows −

1. START
2. We use three pointers to perform the reversing:
prev, next, head.
3. Point the current node to head and assign its next value to
the prev node.
4. Iteratively repeat the step 3 for all the nodes in the list.
5. Assign head to the prev node.

Linked List - Search Operation:-

Searching for an element in the list using a key element. This


operation is done in the same way as array search; comparing
every element in the list with the key element given.

Algorithm

1 START
2 If the list is not empty, iteratively check if the list
contains the key
3 If the key element is not present in the list, unsuccessful
search
4 END

Linked List - Traversal Operation:-

The traversal operation walks through all the elements of the list
in an order and displays the elements in that order.

Algorithm

1. START
2. While the list is not empty and did not reach the end of the list,
print the data in each node
3. END

2.2)Comparing arrays and Linked lists

Linked List vs Array




Array: Arrays store elements in contiguous memory locations,


resulting in easily calculable addresses for the elements stored
and this allows faster access to an element at a specific index.
Data storage scheme of an array

Linked List: Linked lists are less rigid in their storage structure
and elements are usually not stored in contiguous locations,
hence they need to be stored with additional tags giving a
reference to the next element.

Linked-List representation

Advantages of Linked List over arrays :


 Efficient insertion and deletion. : We only need to change
few pointers (or references) to insert (or delete) an item in the
middle. Insertion and deletion at any point in a linked list take
O(1) time. Whereas in an array data structure, insertion /
deletion in the middle takes O(n) time.
 Implementation of Queue and Deque : Simple array
implementation is not efficient at all. We must use circular
array to efficiently implement which is complex. But with linked
list, it is easy and straightforward. That is why most of the
language libraries use Linked List internally to implement these
data structures.
 Space Efficient in Some Cases : Linked List might turn out
to be more space efficient compare to arrays in cases where
we cannot guess the number of elements in advance.
 Circular List with Deletion/Addition : Circular Linked Lists
are useful to implement CPU round robin scheduling or similar
requirements in the real world because of the quick
deletion/insertion in a circular manner.
Advantages of Arrays over Linked List :
 Random Access. : We can access ith item in O(1) time (only
some basic arithmetic required using base address). In case of
linked lists, it is O(n) operation due to sequential access.
 Cache Friendliness : Array items (Or item references) are
stored at contiguous locations which makes array cache
friendly (Please refer Spatial locality of reference for more
details)
 Easy to use : Arrays are relatively very easy to use and are
available as core of programming languages
 Less Overhead : Unlike linked list, we do not have any extra
references / pointers to be stored with every item.
2.3) Applications of linked lists:-
Linked lists have several practical applications in computer science and real-
world scenarios. Due to their dynamic nature and efficient
insertion/deletion operations, they are used in various areas where flexible
and efficient memory usage is required. Here are the major applications of
linked lists in data structures:

1. Dynamic Memory Management

Linked lists are often used in dynamic memory allocation and management,
where the size of data structures needs to change at runtime.

Example: Implementing stacks, queues, and heaps.


2. Implementation of Other Data Structures
Linked lists are used as a base to implement other data structures such as:

Stacks

Queues

Hash Tables (for handling collisions via chaining)

Graphs (for adjacency lists)

Sparse Matrices
3. Undo Mechanism in Applications

Many applications (e.g., text editors or Photoshop) use linked lists to store
states for the "Undo" and "Redo" operations.

Each node represents a state of the document, and traversal allows going
back and forth between changes.
4. Dynamic Data Storage

Linked lists are suitable for storing data where the size is unknown or
changes frequently.

Example:

Maintaining a dynamic playlist in a media player.

Managing a list of active users in a chat application.


5. Efficient Insertion/Deletion
Linked lists allow efficient insertion and deletion operations compared to
arrays, especially in scenarios where elements are frequently added or
removed.

Example: Maintaining jobs in a printer queue or task scheduling systems.


6. File Systems

File systems like FAT (File Allocation Table) use linked lists to represent file
blocks. Each block contains a pointer to the next block, enabling sequential
storage of file data.
7. Networking

Linked lists are used in routers and switches to maintain dynamically


changing lists of packet buffers.
8. Polynomial Representation

Linked lists can be used to represent and manipulate polynomials. Each


node represents a term in the polynomial (coefficient and exponent), and
operations like addition and multiplication are easier to implement.
9. Memory-Efficient Data Storage

Doubly and circular linked lists are useful in memory-critical systems where
efficient memory utilization is a priority.

Example: Circular linked lists are used in implementing round-robin


scheduling algorithms.

10. Browser History Management

A doubly linked list is used to manage the history of visited web pages,
allowing users to navigate forward and backward efficiently.
11. Music and Media Players
Circular linked lists are used to implement playlists in media players, where
the last song is connected to the first, allowing continuous playback.

12. Graph and Tree Traversal

Linked lists (particularly adjacency lists) are used in graph algorithms for
efficient representation and traversal of nodes.

Example: Breadth-First Search (BFS) and Depth-First Search (DFS).


13. Operating System Applications

Linked lists are used in:

Process scheduling (ready queue).

Paging and memory management systems.


UNIT-3

3.1)Stack in Data Structure: What is Stack and Its Applications

Stacks in Data Structures is a linear type of data structure that follows the
LIFO (Last-In-First-Out) principle and allows insertion and deletion
operations from one end of the stack data structure, that is top.
Implementation of the stack can be done by contiguous memory which is
an array, and non-contiguous memory which is a linked list. Stack plays a
vital role in many applications.

Introduction to Stack in Data Structures

The stack data structure is a linear data structure that accompanies a


principle known as LIFO (Last In First Out) or FILO (First In Last Out).
Real-life examples of a stack are a deck of cards, piles of books, piles of
money, and many more.

Stack Representation in Data Structures

Working of Stack in Data Structures

Now, assume that you have a stack of books.

You can only see the top, i.e., the top-most book, namely 40, which is kept
top of the stack.
If you want to insert a new book first, namely 50, you must update the top
and then insert a new text.

And if you want to access any other book other than the topmost book that
is 40, you first remove the topmost book from the stack, and then the top
will point to the next topmost book.

After working on the representation of stacks in data structures, you will


see some basic operations performed on the stacks in data structures.

Basic Operations on Stack in Data Structures

There following are some operations that are implemented on the stack.

Push Operation
Push operation involves inserting new elements in the stack. Since you
have only one end to insert a unique element on top of the stack, it inserts
the new element at the top of the stack.

Pop Operation

Pop operation refers to removing the element from the stack again since
you have only one end to do all top of the stack. So removing an element
from the top of the stack is termed pop operation.
Peek Operation

Peek operation refers to retrieving the topmost element in the stack without
removing it from the collections of data elements.

isFull()

isFull function is used to check whether or not a stack is empty.

isEmpty()

isEmpty function is used to check whether or not a stack is empty.

First, you will learn about the functions:

isFull()
The following is the algorithm of the isFull() function:

Begin

If

top equals to maxsize

return true

else

return false

else if

end

The implementation of the isFull() function is as follows:

Bool isFull()

if(top == maxsize)
return true;

else

return false;

isEmpty()

The following is the algorithm of the isEmpty() function:

Begin

If

topless than 1

return true

else

return false

else if
end

The implementation of the isEmpty() function is:

Bool isEmpty()

if(top = = -1)

return true;

else

return false;

Push Operation

Push operation includes various steps, which are as follows :

Step 1: First, check whether or not the stack is full

Step 2: If the stack is complete, then exit


Step 3: If not, increment the top by one

Step 4: Insert a new element where the top is pointing

Step 5: Success

The algorithm of the push operation is:

Begin push: stack, item

If the stack is complete, return null

end if

top ->top+1;

stack[top] <- item

end

This is how you implement a push operation:

if(! isFull ())

{
top = top + 1;

stack[top] = item;

else {

printf(“stack is full”);

Pop Operation

Step 1: First, check whether or not the stack is empty

Step 2: If the stack is empty, then exit

Step 3: If not, access the topmost data element

Step 4: Decrement the top by one

Step 5: Success

The following is the algorithm of the pop operation:

Begin pop: stack


If the stack is empty

return null

end if

item -> stack[top] ;

Top -> top - 1;

Return item;

End

Implementing a pop operation is as follows:

int pop( int item){

If isEmpty()) {

item = stack[top];

top = top - 1;
return item;

else{

printf(“stack if empty”);

Peek Operation

The algorithm of a peek operation is:

begin to peek

return stack[top];

end

The implementation of the peek operation is:


int peek()

return stack[top];

3.2) Implementation of Stack in Data Structures

You can perform the implementation of stacks in data structures using two
data structures that are an array and a linked list.

 Array: In array implementation, the stack is formed using an array. All


the operations are performed using arrays. You will see how all
operations can be implemented on the stack in data structures using an
array data structure.
Properties of a stack:-
1.)LIFO Behaviour:- The fundamental principle of a stack is that the last
element added will be the first one removed like a pile of plates where you
only access the top plate
2.)Single access point:-All insertions and deletions occur only at the top of
the stack
Implementing Stacks using linked list:-

 Linked-List: Every new element is inserted as a top element in the linked


list implementation of stacks in data structures. That means every newly
inserted element is pointed to the top. Whenever you want to remove an
element from the stack, remove the node indicated by the top, by
moving the top to its previous node in the list.
3.3)Applications of stacks in expression evaluation, backtracking, &
reversing List

1. Expression Evaluation:

Stacks are widely used in evaluating mathematical expressions, especially those in


postfix (Reverse Polish Notation, RPN) and infix notation.

 Infix to Postfix Conversion: The stack helps convert infix expressions (like
A + B * C) to postfix notation (A B C * +) by handling operator
precedence and associativity.
 Postfix Expression Evaluation: After conversion to postfix, the expression
can be evaluated using a stack. Operands are pushed onto the stack, and
when an operator is encountered, operands are popped off, the operation is
performed, and the result is pushed back onto the stack.

Example: For postfix expression 2 3 4 * +, evaluate:

 Push 2, 3, 4 onto the stack.


 Encounter *: pop 3 and 4, multiply to get 12, push 12.
 Encounter +: pop 2 and 12, add to get 14, push 14.
 Final result: 14.

2. Backtracking:

Backtracking is a problem-solving technique used in decision-making processes.


Stacks are helpful for backtracking, as they allow us to "remember" previous
states.

 Example: Solving puzzles like the N-Queens problem or Sudoku, where


you need to explore different possible configurations and "undo" certain
decisions when you reach a dead end.
 When exploring potential solutions, you push choices onto the stack. If a
choice leads to a dead end, you pop from the stack to backtrack to a previous
state and try a different choice.

Example: In a maze, you might explore paths by moving forward (pushing each
decision onto the stack). If you hit a dead end, you backtrack by popping the stack
to find the previous decision point.

3. Reversing a List:

A stack can be used to reverse a list because stacks operate on a Last In, First Out
(LIFO) principle, meaning the last element added will be the first one to be
removed.

 Algorithm: Push each element of the list onto a stack, and then pop all
elements from the stack, which will result in the reversed order.

Example: For a list: [1, 2, 3, 4]

 Push 1, 2, 3, 4 onto the stack.


 Pop each element: first 4, then 3, 2, and finally 1, resulting in [4, 3, 2,
1].

UNIT-4

Queues:-

4.1)Introduction to queues properties and operations:-


A queue is a linear data structure that follows the FIFO (First In, First Out)
principle. This means that the element inserted first is the one to be removed
first, similar to a queue of people waiting in line.

Properties of a Queue:-

1. FIFO Principle: The first element added is the first to be removed.

2. Sequential Order: Elements are processed in the order they arrive.

3. Two Ends:

Front: The position where elements are removed.

Rear: The position where elements are inserted.

4. Dynamic Size: The size of a queue may grow or shrink as elements are
added or removed.

5. Can Be Implemented Using:

Arrays (Static Queue)

Linked Lists (Dynamic Queue)

Operations on a Queue:-

1. Enqueue (Insertion)

Adds an element at the rear of the queue.

If the queue is full (in an array-based implementation), it results in overflow.

2. Dequeue (Deletion)

Removes an element from the front of the queue.

If the queue is empty, it results in underflow.

3. Peek (Front)

Returns the front element of the queue without removing it.


4. isEmpty()

Checks if the queue is empty.

5. isFull()

Checks if the queue is full (in a static array-based implementation).

Types of Queues:-

1. Simple Queue – Follows FIFO without modifications.

2. Circular Queue – The rear wraps around when it reaches the end of the array.

3. Priority Queue – Elements are dequeued based on priority instead of FIFO.

4. Deque (Double-Ended Queue) – Insertion and deletion can be done from both
ends.

4.2)Implementing queues using arrays and linked lists:-

Queue Using Linked List

A queue is a linear data structure that follows the First In, First Out (FIFO)
principle. It is characterized by two primary operations. Enqueue adds an
element to the rear end of the queue, and dequeue removes an element
from the front end. how to implement queue using Linked List.

What is a Queue?
A queue is a linear data structure with both ends open for operations and
follows the principle of First In, First Out (FIFO).
The FIFO principle states that the first element getting inside a queue (i.e.,
enqueue) has to be the first element that gets out of the queue(i.e.,
dequeue). To better understand a queue, think of a line to board a bus. The
first person in the line will be the first person to board the bus and vice-
versa.
Take a look at the image below for reference.
What is a Linked List?
A linked list is a node-based linear data structure used to store data
elements. Each node in a linked list is made up of two key components,
namely data and next, that store the data and the address of the next none
in the linked list, respectively.
A single node in a linked list appears in the image below.

A linked list with multiple nodes typically looks like in the image below.
In the above image, 'HEAD' refers to the first node of the linked list, and
'Null' in Node 3's 'next' pointer/reference indicates that there is no additional
node following it, meaning the linked list ends at the third node.

Implementing Queue using Linked list


Follow the below steps to implement a queue using a linked list
Create a New Node
1. When enqueuing an element, first create a new node (say NEW_NODE).
2. Initialize two pointers, FRONT and REAR, which represent the head and
the tail of the queue, respectively. These should be set to NULL initially
when the queue is created (this step is typically done during the
initialization of the queue, not during each enqueue operation).
Set Data and Next Pointer of the New Node
1. Assign the value to be enqueued to the data field
of NEW_NODE(say DATA).
2. Set the next pointer of NEW_NODE to NULL.
Enqueue Operation
1. Check if the queue is empty, indicated by FRONT being NULL.
If the queue is empty ( FRONT is NULL)
 Set both FRONT and REAR to NEW_NODE, as this node will be the
only one in the queue.
If the queue is not empty ( FRONT is not NULL)
 Update the next pointer of the current REAR node to point
to NEW_NODE.
 Update REAR to NEW_NODE, making it the new last node in the queue.
Exit Enqueue Process
This step marks the completion of the enqueue operation. (In the context of
coding, this would be the end of the function, and there is no explicit "exit"
command required.)
Algorithm
The algorithm would look like the below for the above-mentioned steps.

Step 1: [Initialization] (done once when the queue is first created)


- SET FRONT = NULL, SET REAR = NULL
Step 2: [Enqueue Operation]
- CREATE NEW_NODE
- SET DATA of NEW_NODE = VALUE, NEW_NODE NEXT = NULL

- If FRONT is NULL (Queue is empty)


-> SET FRONT = NEW_NODE
-> SET REAR = NEW_NODE
- Else
-> SET REAR NEXT = NEW_NODE
-> SET REAR = NEW_NODE
Real-World Application of Queue
Below listed are some real-world use cases of the queue data structure
 Operating Systems: Queues are often used to schedule processes to
be executed by the CPU. Queues are used to prioritize tasks by various
scheduling algorithms within the operating system. It is also used to
manage the I/O requests in operating systems.
 Data Traffic Management: Devices like Routers and switches use
queues to manage data buffers for data packet transmission.
 Web Servers: Web servers use a queue data structure to manage and
optimize incoming requests. All requests to the server is added to the
queue and are processed using the FIFO principle.
 Printers: Printers, while printing multiple documents, use queues to
manage the workload and process each printing task based on the
queue.
 Banking Systems: Banking transactions like RTGS are managed using
queues, as the bank receives many customer transaction requests. Each
such request is put into a queue and is processed one at a time.
 Call Center Systems: The queue data structure manages customer
connect requests to the support representative. It works on a first-come,
first-serve basis.
 Television Broadcast: Television stations manage the broadcast
queue, ensuring programs or advertisements are played in the intended
sequence.

Array representation of Queue


We can easily represent queue by using linear arrays. There are two
variables i.e. front and rear, that are implemented in the case of every
queue. Front and rear variables point to the position from where insertions
and deletions are performed in a queue. Initially, the value of front and
queue is -1 which represents an empty queue. Array representation of a
queue containing 5 elements along with the respective values of front and
rear, is shown in the following figure.

The above figure shows the queue of characters forming the English
word "HELLO". Since, No deletion is performed in the queue till now,
therefore the value of front remains -1 . However, the value of rear
increases by one every time an insertion is performed in the queue. After
inserting an element into the queue shown in the above figure, the queue
will look something like following. The value of rear will become 5 while the
value of front remains same.
After deleting an element, the value of front will increase from -1 to 0.
however, the queue will look something like following.

Algorithm to insert any element in a queue


Check if the queue is already full by comparing rear to max - 1. if so, then
return an overflow error.

If the item is to be inserted as the first element in the list, in that case set
the value of front and rear to 0 and insert the element at the rear end.
Otherwise keep increasing the value of rear and insert each element one
by one having rear as the index.

C Function
1. void insert (int queue[], int max, int front, int rear, int item)
2. {
3. if (rear + 1 == max)
4. {
5. printf("overflow");
6. }
7. else
8. {
9. if(front == -1 && rear == -1)
10. {
11. front = 0;
12. rear = 0;
13. }
14. else
15. {
16. rear = rear + 1;
17. }
18. queue[rear]=item;
19. }
20. }
Algorithm to delete an element from the queue
If, the value of front is -1 or value of front is greater than rear , write an
underflow message and exit.

Otherwise, keep increasing the value of front and return the item stored at
the front end of the queue at each time.

C Function
1. int delete (int queue[], int max, int front, int rear)
2. {
3. int y;
4. if (front == -1 || front > rear)
5.
6. {
7. printf("underflow");
8. }
9. else
10. {
11. y = queue[front];
12. if(front == rear)
13. {
14. front = rear = -1;
15. else
16. front = front + 1;
17.
18. }
19. return y;
20. }
21. }
Menu driven program to implement queue using array
1. #include<stdio.h>
2. #include<stdlib.h>
3. #define maxsize 5
4. void insert();
5. void delete();
6. void display();
7. int front = -1, rear = -1;
8. int queue[maxsize];
9. void main ()
10. {
11. int choice;
12. while(choice != 4)
13. {
14. printf("\n*************************Main Menu**************************
***\n");
15. printf("\
n========================================================
=========\n");
16. printf("\n1.insert an element\n2.Delete an element\n3.Display th
e queue\n4.Exit\n");
17. printf("\nEnter your choice ?");
18. scanf("%d",&choice);
19. switch(choice)
20. {
21. case 1:
22. insert();
23. break;
24. case 2:
25. delete();
26. break;
27. case 3:
28. display();
29. break;
30. case 4:
31. exit(0);
32. break;
33. default:
34. printf("\nEnter valid choice??\n");
35. }
36. }
37. }
38. void insert()
39. {
40. int item;
41. printf("\nEnter the element\n");
42. scanf("\n%d",&item);
43. if(rear == maxsize-1)
44. {
45. printf("\nOVERFLOW\n");
46. return;
47. }
48. if(front == -1 && rear == -1)
49. {
50. front = 0;
51. rear = 0;
52. }
53. else
54. {
55. rear = rear+1;
56. }
57. queue[rear] = item;
58. printf("\nValue inserted ");
59.
60. }
61. void delete()
62. {
63. int item;
64. if (front == -1 || front > rear)
65. {
66. printf("\nUNDERFLOW\n");
67. return;
68.
69. }
70. else
71. {
72. item = queue[front];
73. if(front == rear)
74. {
75. front = -1;
76. rear = -1 ;
77. }
78. else
79. {
80. front = front + 1;
81. }
82. printf("\nvalue deleted ");
83. }
84.
85.
86. }
87.
88. void display()
89. {
90. int i;
91. if(rear == -1)
92. {
93. printf("\nEmpty queue\n");
94. }
95. else
96. { printf("\nprinting values .....\n");
97. for(i=front;i<=rear;i++)
98. {
99. printf("\n%d\n",queue[i]);
100. }
101. }
102. }
Output:

*************Main Menu**************

=======================================
=======

1.insert an element
2.Delete an element
3.Display the queue
4.Exit

Enter your choice ?1

Enter the element


123

Value inserted

*************Main Menu**************

=======================================
=======

1.insert an element
2.Delete an element
3.Display the queue
4.Exit

Enter your choice ?1

Enter the element


90

Value inserted

*************Main Menu**************
===================================

1.insert an element
2.Delete an element
3.Display the queue
4.Exit

Enter your choice ?2

value deleted

*************Main Menu**************
=======================================
=======

1.insert an element
2.Delete an element
3.Display the queue
4.Exit

Enter your choice ?3

printing values .....

90

*************Main Menu**************

=======================================
=======

1.insert an element
2.Delete an element
3.Display the queue
4.Exit

Enter your choice ?4


Drawback of array implementation
Although, the technique of creating a queue is easy, but there are some
drawbacks of using this technique to implement a queue.

o Memory wastage : The below figure shows how the memory space
is wasted in the array representation of queue. In the above figure, a
queue of size 10 having 3 elements, is shown.

4.3) Applications of Queues in Breadth-First Search (BFS) and Scheduling


in Data Structures

Queues play a critical role in both Breadth-First Search (BFS) and Scheduling
Algorithms, thanks to their FIFO (First-In, First-Out) property.

1. Applications of Queues in BFS:-

BFS is a graph traversal algorithm that explores all nodes at the current level
before moving to the next level. A queue helps maintain the correct order of
traversal.

Key Applications of BFS Using Queues:

Graph Traversal: BFS is used to explore all reachable nodes from a source node.

Shortest Path in Unweighted Graphs: Since BFS explores layer by layer, it finds
the shortest path (in terms of edges) from the source node.

Social Networking (Friend Suggestions): BFS helps find the shortest connection
path between users.
Web Crawling: A queue is used to visit web pages level by level.

Maze Solving: BFS finds the shortest path from start to finish in a maze.

AI and Game Pathfinding: Used in grid-based games for AI movement.

2. Applications of Queues in Scheduling:-

Queues are widely used in CPU scheduling, process scheduling, and task
scheduling to manage jobs in an orderly manner.

Key Scheduling Applications Using Queues:-

CPU Scheduling:

Round-Robin Scheduling: Each process is given a time slice and placed back into
the queue if not finished.

First Come, First Served (FCFS): The process that arrives first is executed first.

Job Scheduling in Operating Systems: Jobs are placed in a queue and processed
sequentially.

Print Spoolers: Printing tasks are queued and executed in order.

Disk Scheduling (I/O Scheduling): Requests are handled in FIFO or priority-based


order.

Network Packet Scheduling: Packets are placed in a queue before being


processed by routers.

Conclusion:-
In BFS, queues help explore nodes level by level.

In Scheduling, queues manage task execution efficiently.

The FIFO principle makes queues ideal for both graph traversal (BFS) and
task scheduling in operating systems and networks.
4.4)dequeues:-

Deque (or double-ended queue)


In this article, we will discuss the double-ended queue or deque. We should
first see a brief description of the queue.

What is a queue?

A queue is a data structure in which whatever comes first will go out first,
and it follows the FIFO (First-In-First-Out) policy. Insertion in the queue is
done from one end known as the rear end or the tail, whereas the deletion
is done from another end known as the front end or the head of the
queue.

The real-world example of a queue is the ticket queue outside a cinema


hall, where the person who enters first in the queue gets the ticket first, and
the person enters last in the queue gets the ticket at last.

What is a Deque (or double-ended queue)

The deque stands for Double Ended Queue. Deque is a linear data
structure where the insertion and deletion operations are performed from
both ends. We can say that deque is a generalized version of the queue.

Though the insertion and deletion in a deque can be performed on both


ends, it does not follow the FIFO rule. The representation of a deque is
given as follows -
Types of deque

There are two types of deque -

o Input restricted queue


o Output restricted queue
Input restricted Queue

In input restricted queue, insertion operation can be performed at only one


end, while deletion can be performed from both ends.

Output restricted Queue

In output restricted queue, deletion operation can be performed at only one


end, while insertion can be performed from both ends.
Operations performed on deque

There are the following operations that can be applied on a deque -

o Insertion at front
o Insertion at rear
o Deletion at front
o Deletion at rear
We can also perform peek operations in the deque along with the
operations listed above. Through peek operation, we can get the deque's
front and rear elements of the deque. So, in addition to the above
operations, following operations are also supported in deque -

o Get the front item from the deque


o Get the rear item from the deque
o Check whether the deque is full or not
o Checks whether the deque is empty or not
Now, let's understand the operation performed on deque using an example.

Insertion at the front end

In this operation, the element is inserted from the front end of the queue.
Before implementing the operation, we first have to check whether the
queue is full or not. If the queue is not full, then the element can be inserted
from the front end by using the below conditions -

o If the queue is empty, both rear and front are initialized with 0. Now,
both will point to the first element.
o Otherwise, check the position of the front if the front is less than 1
(front < 1), then reinitialize it by front = n - 1, i.e., the last index of the
array.

Insertion at the rear end

In this operation, the element is inserted from the rear end of the queue.
Before implementing the operation, we first have to check again whether
the queue is full or not. If the queue is not full, then the element can be
inserted from the rear end by using the below conditions -

o If the queue is empty, both rear and front are initialized with 0. Now,
both will point to the first element.
o Otherwise, increment the rear by 1. If the rear is at last index (or size
- 1), then instead of increasing it by 1, we have to make it equal to 0.
Deletion at the front end

In this operation, the element is deleted from the front end of the queue.
Before implementing the operation, we first have to check whether the
queue is empty or not.

If the queue is empty, i.e., front = -1, it is the underflow condition, and we
cannot perform the deletion. If the queue is not full, then the element can
be inserted from the front end by using the below conditions -

If the deque has only one element, set rear = -1 and front = -1.

Else if front is at end (that means front = size - 1), set front = 0.

Else increment the front by 1, (i.e., front = front + 1).


Deletion at the rear end

In this operation, the element is deleted from the rear end of the queue.
Before implementing the operation, we first have to check whether the
queue is empty or not.

If the queue is empty, i.e., front = -1, it is the underflow condition, and we
cannot perform the deletion.

If the deque has only one element, set rear = -1 and front = -1.

If rear = 0 (rear is at front), then set rear = n - 1.

Else, decrement the rear by 1 (or, rear = rear -1).


Check empty

This operation is performed to check whether the deque is empty or not. If


front = -1, it means that the deque is empty.

Check full

This operation is performed to check whether the deque is full or not. If


front = rear + 1, or front = 0 and rear = n - 1 it means that the deque is full.

The time complexity of all of the above operations of the deque is O(1), i.e.,
constant.

Applications of deque

o Deque can be used as both stack and queue, as it supports both


operations.
o Deque can be used as a palindrome checker means that if we read
the string from both ends, the string would be the same.

Implementation of deque
Now, let's see the implementation of deque in C programming language.

1. #include <stdio.h>
2. #define size 5
3. int deque[size];
4. int f = -1, r = -1;
5. // insert_front function will insert the value from the front
6. void insert_front(int x)
7. {
8. if((f==0 && r==size-1) || (f==r+1))
9. {
10. printf("Overflow");
11. }
12. else if((f==-1) && (r==-1))
13. {
14. f=r=0;
15. deque[f]=x;
16. }
17. else if(f==0)
18. {
19. f=size-1;
20. deque[f]=x;
21. }
22. else
23. {
24. f=f-1;
25. deque[f]=x;
26. }
27. }
28.
29. // insert_rear function will insert the value from the rear
30. void insert_rear(int x)
31. {
32. if((f==0 && r==size-1) || (f==r+1))
33. {
34. printf("Overflow");
35. }
36. else if((f==-1) && (r==-1))
37. {
38. r=0;
39. deque[r]=x;
40. }
41. else if(r==size-1)
42. {
43. r=0;
44. deque[r]=x;
45. }
46. else
47. {
48. r++;
49. deque[r]=x;
50. }
51.
52. }
53.
54. // display function prints all the value of deque.
55. void display()
56. {
57. int i=f;
58. printf("\nElements in a deque are: ");
59.
60. while(i!=r)
61. {
62. printf("%d ",deque[i]);
63. i=(i+1)%size;
64. }
65. printf("%d",deque[r]);
66. }
67.
68. // getfront function retrieves the first value of the deque.
69. void getfront()
70. {
71. if((f==-1) && (r==-1))
72. {
73. printf("Deque is empty");
74. }
75. else
76. {
77. printf("\nThe value of the element at front is: %d", deque[f]);
78. }
79.
80. }
81.
82. // getrear function retrieves the last value of the deque.
83. void getrear()
84. {
85. if((f==-1) && (r==-1))
86. {
87. printf("Deque is empty");
88. }
89. else
90. {
91. printf("\nThe value of the element at rear is %d", deque[r]);
92. }
93.
94. }
95.
96. // delete_front() function deletes the element from the front
97. void delete_front()
98. {
99. if((f==-1) && (r==-1))
100. {
101. printf("Deque is empty");
102. }
103. else if(f==r)
104. {
105. printf("\nThe deleted element is %d", deque[f]);
106. f=-1;
107. r=-1;
108.
109. }
110. else if(f==(size-1))
111. {
112. printf("\nThe deleted element is %d", deque[f]);
113. f=0;
114. }
115. else
116. {
117. printf("\nThe deleted element is %d", deque[f]);
118. f=f+1;
119. }
120. }
121.
122. // delete_rear() function deletes the element from the rear
123. void delete_rear()
124. {
125. if((f==-1) && (r==-1))
126. {
127. printf("Deque is empty");
128. }
129. else if(f==r)
130. {
131. printf("\nThe deleted element is %d", deque[r]);
132. f=-1;
133. r=-1;
134.
135. }
136. else if(r==0)
137. {
138. printf("\nThe deleted element is %d", deque[r]);
139. r=size-1;
140. }
141. else
142. {
143. printf("\nThe deleted element is %d", deque[r]);
144. r=r-1;
145. }
146. }
147.
148. int main()
149. {
150. insert_front(20);
151. insert_front(10);
152. insert_rear(30);
153. insert_rear(50);
154. insert_rear(80);
155. display(); // Calling the display function to retrieve the values of de
que
156. getfront(); // Retrieve the value at front-end
157. getrear(); // Retrieve the value at rear-end
158. delete_front();
159. delete_rear();
160. display(); // calling display function to retrieve values after deletion
161. return 0;
162. }
Output:

.UNIT-5

5.1)Introduction to trees:-

In data structures, trees are hierarchical structures that consist of nodes connected
by edges. Trees are widely used to represent relationships or structures where there
is a clear parent-child relationship. Each node in a tree contains a value or data, and
may have zero or more children. Here’s an introduction to trees and their key
concepts:

Basic Terminology:

1. Node: The fundamental unit of a tree, which contains data.


2. Root: The topmost node of the tree. It has no parent.
3. Parent: A node that has one or more child nodes.
4. Child: A node that has a parent node.
5. Leaf: A node with no children.
6. Subtree: Any node in the tree along with all its descendants.
7. Edge: The connection between two nodes in the tree.
8. Depth: The number of edges from the root to the node.
9. Height: The number of edges on the longest path from the node to a leaf.

5.2)Binary search tree:-Insertion,Deletion & Traversal:-

What is a Binary Search tree?

A binary search tree follows some order to arrange the elements. In a


Binary search tree, the value of left node must be smaller than the parent
node, and the value of right node must be greater than the parent node.
This rule is applied recursively to the left and right subtrees of the root.

Let's understand the concept of Binary search tree with an example.


In the above figure, we can observe that the root node is 40, and all the
nodes of the left subtree are smaller than the root node, and all the nodes
of the right subtree are greater than the root node.

Similarly, we can see the left child of root node is greater than its left child
and smaller than its right child. So, it also satisfies the property of binary
search tree. Therefore, we can say that the tree in the above image is a
binary search tree.

Suppose if we change the value of node 35 to 55 in the above tree, check


whether the tree will be binary search tree or not.

In the above tree, the value of root node is 40, which is greater than its left
child 30 but smaller than right child of 30, i.e., 55. So, the above tree does
not satisfy the property of Binary search tree. Therefore, the above tree is
not a binary search tree.

Advantages of Binary search tree

o Searching an element in the Binary search tree is easy as we always


have a hint that which subtree has the desired element.
o As compared to array and linked lists, insertion and deletion
operations are faster in BST.

Example of creating a binary search tree

Now, let's see the creation of binary search tree using an example.
Suppose the data elements are - 45, 15, 79, 90, 10, 55, 12, 20, 50

o First, we have to insert 45 into the tree as the root of the tree.
o Then, read the next element; if it is smaller than the root node, insert
it as the root of the left subtree, and move to the next element.
o Otherwise, if the element is larger than the root node, then insert it as
the root of the right subtree.
Now, let's see the process of creating the Binary search tree using the
given data element. The process of creating the BST is shown below -

Step 1 - Insert 45.

Step 2 - Insert 15.

As 15 is smaller than 45, so insert it as the root node of the left subtree.

Step 3 - Insert 79.

As 79 is greater than 45, so insert it as the root node of the right subtree.
Step 4 - Insert 90.

90 is greater than 45 and 79, so it will be inserted as the right subtree of 79.

Step 5 - Insert 10.

10 is smaller than 45 and 15, so it will be inserted as a left subtree of 15.


Step 6 - Insert 55.

55 is larger than 45 and smaller than 79, so it will be inserted as the left
subtree of 79.

Step 7 - Insert 12.

12 is smaller than 45 and 15 but greater than 10, so it will be inserted as


the right subtree of 10.
Step 8 - Insert 20.

20 is smaller than 45 but greater than 15, so it will be inserted as the right
subtree of 15.
Step 9 - Insert 50.

50 is greater than 45 but smaller than 79 and 55. So, it will be inserted as a
left subtree of 55.

Now, the creation of binary search tree is completed. After that, let's move
towards the operations that can be performed on Binary search tree.

We can perform insert, delete and search operations on the binary search
tree.

Let's understand how a search is performed on a binary search tree.

Searching in Binary search tree


Searching means to find or locate a specific element or node in a data
structure. In Binary search tree, searching a node is easy because
elements in BST are stored in a specific order. The steps of searching a
node in Binary Search tree are listed as follows -

1. First, compare the element to be searched with the root element of


the tree.
2. If root is matched with the target element, then return the node's
location.
3. If it is not matched, then check whether the item is less than the root
element, if it is smaller than the root element, then move to the left
subtree.
4. If it is larger than the root element, then move to the right subtree.
5. Repeat the above procedure recursively until the match is found.
6. If the element is not found or not present in the tree, then return
NULL.

Now, let's understand the searching in binary tree using an example. We


are taking the binary search tree formed above. Suppose we have to find
node 20 from the below tree.

Step1:

Step2:
Step3:

Now, let's see the algorithm to search an element in the Binary search tree.

Algorithm to search an element in Binary search tree

1. Search (root, item)


2. Step 1 - if (item = root → data) or (root = NULL)
3. return root
4. else if (item < root → data)
5. return Search(root → left, item)
6. else
7. return Search(root → right, item)
8. END if
9. Step 2 - END
Now let's understand how the deletion is performed on a binary search
tree. We will also see an example to delete an element from the given tree.

Deletion in Binary Search tree

In a binary search tree, we must delete a node from the tree by keeping in
mind that the property of BST is not violated. To delete a node from BST,
there are three possible situations occur -

o The node to be deleted is the leaf node, or,


o The node to be deleted has only one child, and,
o The node to be deleted has two children
We will understand the situations listed above in detail.

When the node to be deleted is the leaf node

It is the simplest case to delete a node in BST. Here, we have to replace


the leaf node with NULL and simply free the allocated space.

We can see the process to delete a leaf node from BST in the below
image. In below image, suppose we have to delete node 90, as the node to
be deleted is a leaf node, so it will be replaced with NULL, and the
allocated space will free.

When the node to be deleted has only one child

In this case, we have to replace the target node with its child, and then
delete the child node. It means that after replacing the target node with its
child node, the child node will now contain the value to be deleted. So, we
simply have to replace the child node with NULL and free up the allocated
space.

We can see the process of deleting a node with one child from BST in the
below image. In the below image, suppose we have to delete the node 79,
as the node to be deleted has only one child, so it will be replaced with its
child 55.

So, the replaced node 79 will now be a leaf node that can be easily
deleted.

When the node to be deleted has two children

This case of deleting a node in BST is a bit complex among other two
cases. In such a case, the steps to be followed are listed as follows -

o First, find the inorder successor of the node to be deleted.


o After that, replace that node with the inorder successor until the target
node is placed at the leaf of tree.
o And at last, replace the node with NULL and free up the allocated
space.
The inorder successor is required when the right child of the node is not
empty. We can obtain the inorder successor by finding the minimum
element in the right child of the node.

We can see the process of deleting a node with two children from BST in
the below image. In the below image, suppose we have to delete node 45
that is the root node, as the node to be deleted has two children, so it will
be replaced with its inorder successor. Now, node 45 will be at the leaf of
the tree so that it can be deleted easily.

Now let's understand how insertion is performed on a binary search tree.

Insertion in Binary Search tree

A new key in BST is always inserted at the leaf. To insert an element in


BST, we have to start searching from the root node; if the node to be
inserted is less than the root node, then search for an empty location in the
left subtree. Else, search for the empty location in the right subtree and
insert the data. Insert in BST is similar to searching, as we always have to
maintain the rule that the left subtree is smaller than the root, and right
subtree is larger than the root.

Now, let's see the process of inserting a node into BST using an example.
Binary Search Tree (BST) Traversals – Inorder, Preorder, Post Order


Given a Binary Search Tree, The task is to print the elements in


inorder, preorder, and postorder traversal of the Binary Search
Tree.
Input:
A Binary Search Tree

Output:
Inorder Traversal: 10 20 30 100 150 200 300
Preorder Traversal: 100 20 10 30 200 150 300
Postorder Traversal: 10 30 20 150 300 200 100
Input:

Binary Search Tree


Output:
Inorder Traversal: 8 12 20 22 25 30 40
Preorder Traversal: 22 12 8 20 30 25 40
Postorder Traversal: 8 20 12 25 40 30 22

5.3)Introduction to Hashing


Hashing refers to the process of generating a fixed-size output


from an input of variable size using the mathematical formulas
known as hash functions. This technique determines an index or
location for the storage of an item in a data structure.

Introduction to Hashing

Hash Data Structure Overview


 It is one of the most widely used data structure after arrays.
 It mainly supports search, insert and delete in O(1) time on
average which is more efficient than other popular data
structures like arrays, Linked List and Self Balancing BST.
 We use hashing for dictionaries, frequency counting,
maintaining data for quick access by key, etc.
 Real World Applications include Database Indexing,
Cryptography, Caches, Symbol Table and Dictionaries.
 There are mainly two forms of hash typically implemented in
programming languages.
Hash Set : Collection of unique keys.
Hash Map : Collection of key value pairs with keys being
unique Situations Where Hash is not Used
 Need to maintain sorted data along with search, insert and
delete. We use a self balancing BST in these cases.
 When Strings are keys and we need operations like prefix
search along with search, insert and delete. We use Trie in
these cases.
 When we need operations like floor and ceiling along with
search, insert and/or delete. We use Self Balancing BST in
these cases.
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: 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 typically an array of lists. It stores
values corresponding to the keys. Hash stores the data in an
associative manner in an array where each data value has its
own unique index.
How does Hashing work?
Suppose we have a set of strings {“ab”, “cd”, “efg”} and we
would like to store it in a table.
 Step 1: We know that hash functions (which is some
mathematical formula) are used to calculate the hash value
which acts as the index of the data structure where the value
will be stored.
 Step 2: So, let’s assign
o “a” = 1,
o “b”=2, .. etc, to all alphabetical characters.
 Step 3: Therefore, the numerical value by summation of all
characters of the string:
 “ab” = 1 + 2 = 3,
 “cd” = 3 + 4 = 7 ,
 “efg” = 5 + 6 + 7 = 18
 Step 4: Now, assume that we have a table of size 7 to store
these strings. The hash function that is used here is the sum of
the characters in key mod Table size . We can compute the
location of the string in the array by taking the sum(string)
mod 7 .
 Step 5: So we will then store
o “ab” in 3 mod 7 = 3,
o “cd” in 7 mod 7 = 0, and
o “efg” in 18 mod 7 = 4.

The above technique enables us to calculate the location


of a given string by using a simple hash function and
rapidly find the value that is stored in that location.
Therefore the idea of hashing seems like a great way to
store (key, value) pairs of the data in a table.
What is a Hash function?
A hash function creates a mapping from an input key to an index
in hash table, this is done through the use of mathematical
formulas known as hash functions.
For example: Consider phone numbers as keys and a hash table
of size 100. A simple example hash function can be to consider
the last two digits of phone numbers so that we have valid array
indexes as output.
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.
4. Multiplication Method
Properties of a Good hash function
A good hash function should have the following properties:
1. Efficient
2. Should uniformly distribute the keys to each index of hash
table.
3. Should minimize collisions (This and the below are mainly
derived from the above 2nd point)
4. Should have a low load factor (number of items in the table
divided by the size of the table).
How does Hashing work?
Suppose we have a set of strings {“ab”, “cd”, “efg”} and we
would like to store it in a table.
 Step 1: We know that hash functions (which is some
mathematical formula) are used to calculate the hash value
which acts as the index of the data structure where the value
will be stored.
 Step 2: So, let’s assign
o “a” = 1,
o “b”=2, .. etc, to all alphabetical characters.
 Step 3: Therefore, the numerical value by summation of all
characters of the string:
 “ab” = 1 + 2 = 3,
 “cd” = 3 + 4 = 7 ,
 “efg” = 5 + 6 + 7 = 18
 Step 4: Now, assume that we have a table of size 7 to store
these strings. The hash function that is used here is the sum of
the characters in key mod Table size . We can compute the
location of the string in the array by taking the sum(string)
mod 7 .
 Step 5: So we will then store
o “ab” in 3 mod 7 = 3,
o “cd” in 7 mod 7 = 0, and
o “efg” in 18 mod 7 = 4.
The above technique enables us to calculate the location of a
given string by using a simple hash function and rapidly find the
value that is stored in that location. Therefore the idea of hashing
seems like a great way to store (key, value) pairs of the data in a
table.
5.4)Collision Resolution Techniques:-chaining and open
addressing



In Hashing, hash functions were used to generate hash values.


The hash value is used to create an index for the keys 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
There are mainly two methods to handle collision:
1. Separate Chaining
2. Open Addressing
1) Separate Chaining
The idea behind Separate Chaining is to make each cell of the
hash table point to a linked list of records that have the same
hash function value. Chaining is simple but requires additional
memory outside the table.
Example: We have given a hash function and we have to insert
some elements in the hash table using a separate chaining
method for collision resolution technique.
Hash function = key % 5,
Elements = 12, 15, 22, 25 and 37.
Let’s see step by step approach to how to solve the above
problem:

3/5

Hence In this way, the separate chaining method is used as the


collision resolution technique.
2) Open Addressing
In open addressing, all elements are stored in the hash table
itself. Each table entry contains either a record or NIL. When
searching for an element, we examine the table slots one by one
until the desired element is found or it is clear that the element is
not in the table.
2.a) Linear Probing
In linear probing, the hash table is searched sequentially that
starts from the original location of the hash. If in case the location
that we get is already occupied, then we check for the next
location.
Algorithm:
1. Calculate the hash key. i.e. key = data % size
2. Check, if hashTable[key] is empty
 store the value directly by hashTable[key] = data
3. If the hash index already has some value then
 check for next index using key = (key+1) % size
4. Check, if the next index is available hashTable[key] then store
the value. Otherwise try for next index.
5. Do the above process till we find the space.
Example: Let us consider a simple hash function as “key mod 5”
and a sequence of keys that are to be inserted are 50, 70, 76, 85,
93.
2/6

2.b) Quadratic Probing


Quadratic probing is an open addressing scheme in computer
programming for resolving hash collisions in hash tables.
Quadratic probing operates by taking the original hash index and
adding successive values of an arbitrary quadratic polynomial
until an open slot is found.
An example sequence using quadratic probing is:
H + 1 2 , H + 2 2 , H + 3 2 , H + 4 2 …………………. H + k 2
This method is also known as the mid-square method because in
this method we look for i2-th probe (slot) in i-th iteration and the
value of i = 0, 1, . . . n – 1. We always start from the original hash
location. If only the location is occupied then we check the other
slots.
Let hash(x) be the slot index computed using the hash function
and n be the size of the hash table.
If the slot hash(x) % n is full, then we try (hash(x) + 1 2 ) % n.
If (hash(x) + 1 2 ) % n is also full, then we try (hash(x) + 2 2 ) %
n.
If (hash(x) + 2 2 ) % n is also full, then we try (hash(x) + 3 2 ) %
n.
This process will be repeated for all the values of i until an empty
slot is found
Example: Let us consider table Size = 7, hash function as Hash(x)
= x % 7 and collision resolution strategy to be f(i) = i 2 . Insert =
22, 30, and 50

2/4

2.c) Double Hashing


Double hashing is a collision resolving technique in Open
Addressed Hash tables. Double hashing make use of two hash
function,
 The first hash function is h1(k) which takes the key and gives
out a location on the hash table. But if the new location is not
occupied or empty then we can easily place our key.
 But in case the location is occupied (collision) we will use
secondary hash-function h2(k) in combination with the first
hash-function h1(k) to find the new location on the hash table.
This combination of hash functions is of the form
h(k, i) = (h1(k) + i * h2(k)) % n
where
 i is a non-negative integer that indicates a collision number,
 k = element/key which is being hashed
 n = hash table size.
Complexity of the Double hashing algorithm:
Time complexity: O(n)
Example: Insert the keys 27, 43, 692, 72 into the Hash Table of
size 7. where first hash-function is h1(k) = k mod 7 and second
hash-function is h2(k) = 1 + (k mod 5)

5.5)Hash Tables implementations & operations

Hashing is a technique that is used to uniquely identify a specific object


from a group of similar objects. Some examples of how hashing is used in
our lives include:

 In universities, each student is assigned a unique roll number that


can be used to retrieve information about them.
 In libraries, each book is assigned a unique number that can be used
to determine information about the book, such as its exact position in
the library or the users it has been issued to etc.

In both these examples the students and books were hashed to a unique
number.

Assume that you have an object and you want to assign a key to it to make
searching easy. To store the key/value pair, you can use a simple array like
a data structure where keys (integers) can be used directly as an index to
store values. However, in cases where the keys are large and cannot be
used directly as an index, you should use hashing.
In hashing, large keys are converted into small keys by using hash
functions. The values are then stored in a data structure called hash
table. The idea of hashing is to distribute entries (key/value pairs) uniformly
across an array. Each element is assigned a key (converted key). By using
that key you can access the element in O(1) time. Using the key, the
algorithm (hash function) computes an index that suggests where an entry
can be found or inserted.

Hashing is implemented in two steps:

1. An element is converted into an integer by using a hash function. This


element can be used as an index to store the original element, which
falls into the hash table.
2. The element is stored in the hash table where it can be quickly
retrieved using hashed key.

hash = hashfunc(key)
index = hash % array_size

In this method, the hash is independent of the array size and it is then
reduced to an index (a number between 0 and array_size − 1) by using the
modulo operator (%).

Hash function
A hash function is any function that can be used to map a data set of an
arbitrary size to a data set of a fixed size, which falls into the hash table.
The values returned by a hash function are called hash values, hash
codes, hash sums, or simply hashes.

To achieve a good hashing mechanism, It is important to have a good hash


function with the following basic requirements:

1. Easy to compute: It should be easy to compute and must not become


an algorithm in itself.

2. Uniform distribution: It should provide a uniform distribution across


the hash table and should not result in clustering.

3. Less collisions: Collisions occur when pairs of elements are mapped


to the same hash value. These should be avoided.
Note: Irrespective of how good a hash function is, collisions are
bound to occur. Therefore, to maintain the performance of a hash
table, it is important to manage collisions through various collision
resolution techniques.

Need for a good hash function

Let us understand the need for a good hash function. Assume that you
have to store strings in the hash table by using the hashing technique
{“abcdef”, “bcdefa”, “cdefab” , “defabc” }.

To compute the index for storing the strings, use a hash function that states
the following:

The index for a specific string will be equal to the sum of the ASCII values
of the characters modulo 599.

As 599 is a prime number, it will reduce the possibility of indexing different


strings (collisions). It is recommended that you use prime numbers in case
of modulo. The ASCII values of a, b, c, d, e, and f are 97, 98, 99, 100, 101,
and 102 respectively. Since all the strings contain the same characters with
different permutations, the sum will 599.

The hash function will compute the same index for all the strings and the
strings will be stored in the hash table in the following format. As the index
of all the strings is the same, you can create a list on that index and insert
all the strings in that list.
Here, it will take O(n) time (where n is the number of strings) to access a
specific string. This shows that the hash function is not a good hash
function.

Let’s try a different hash function. The index for a specific string will be
equal to sum of ASCII values of characters multiplied by their respective
order in the string after which it is modulo with 2069 (prime number).

String Hash function Index


abcdef (971 + 982 + 993 + 1004 + 1015 + 1026)%2069 38
bcdefa (981 + 992 + 1003 + 1014 + 1025 + 976)%2069 23
cdefab (991 + 1002 + 1013 + 1024 + 975 + 986)%2069 14
defabc (1001 + 1012 + 1023 + 974 + 985 + 996)%2069 11
Hash table
A hash table is a data structure that is used to store keys/value pairs. It
uses a hash function to compute an index into an array in which an element
will be inserted or searched. By using a good hash function, hashing can
work well. Under reasonable assumptions, the average time required to
search for an element in a hash table is O(1).

Let us consider string S. You are required to count the frequency of all the
characters in this string.

string S = “ababcd”
Collision resolution techniques

Separate chaining (open hashing)

Separate chaining is one of the most commonly used collision resolution


techniques. It is usually implemented using linked lists. In separate
chaining, each element of the hash table is a linked list. To store an
element in the hash table you must insert it into a specific linked list. If there
is any collision (i.e. two different elements have same hash value) then
store both the elements in the same linked list.

The cost of a lookup is that of scanning the entries of the selected linked
list for the required key. If the distribution of the keys is sufficiently uniform,
then the average cost of a lookup depends only on the average number of
keys per linked list. For this reason, chained hash tables remain effective
even when the number of table entries (N) is much higher than the number
of slots.

For separate chaining, the worst-case scenario is when all the entries are
inserted into the same linked list. The lookup procedure may have to scan
all its entries, so the worst-case cost is proportional to the number (N) of
entries in the table.
In the following image, CodeMonk and Hashing both hash to the value 2.
The linked list at the index 2 can hold only one entry, therefore, the next
entry (in this case Hashing) is linked (attached) to the entry of CodeMonk.

Implementation of hash tables with separate chaining (open hashing)

Assumption

Hash function will return an integer from 0 to 19.

vector <string> hashTable[20];


int hashTableSize=20;

Insert

void insert(string s)
{
// Compute the index using Hash
Function
int index = hashFunc(s);
// Insert the element in the linked list at the
particular index
hashTable[index].push_back(s);
}

Search

void search(string s)
{
//Compute the index by using the hash function
int index = hashFunc(s);
//Search the linked list at that specific index
for(int i = 0;i < hashTable[index].size();i++)
{
if(hashTable[index][i] == s)
{
cout << s << " is found!" << endl;
return;
}
}
cout << s << " is not found!" << endl;
}

Linear probing (open addressing or closed hashing)

In open addressing, instead of in linked lists, all entry records are stored in
the array itself. When a new entry has to be inserted, the hash index of the
hashed value is computed and then the array is examined (starting with the
hashed index). If the slot at the hashed index is unoccupied, then the entry
record is inserted in slot at the hashed index else it proceeds in some
probe sequence until it finds an unoccupied slot.

The probe sequence is the sequence that is followed while traversing


through entries. In different probe sequences, you can have different
intervals between successive entry slots or probes.
When searching for an entry, the array is scanned in the same sequence
until either the target element is found or an unused slot is found. This
indicates that there is no such key in the table. The name "open
addressing" refers to the fact that the location or address of the item is not
determined by its hash value.

Linear probing is when the interval between successive probes is fixed


(usually to 1). Let’s assume that the hashed index for a particular entry
is index. The probing sequence for linear probing will be:

index = index % hashTableSize


index = (index + 1) % hashTableSize
index = (index + 2) % hashTableSize
index = (index + 3) % hashTableSize

and so on…

Hash collision is resolved by open addressing with linear probing.


Since CodeMonk and Hashing are hashed to the same index i.e. 2,
store Hashing at 3 as the interval between successive probes is 1.

Implementation of hash table with linear probing


Assumption

 There are no more than 20 elements in the data set.


 Hash function will return an integer from 0 to 19.
 Data set must have unique elements.

string hashTable[21];
int hashTableSize = 21;

Insert

void insert(string s)
{
//Compute the index using the hash function
int index = hashFunc(s);
//Search for an unused slot and if the index
will exceed the hashTableSize then roll back
while(hashTable[index] != "")
index = (index + 1) % hashTableSize;
hashTable[index] = s;
}

Search

void search(string s)
{
//Compute the index using the hash function
int index = hashFunc(s);
//Search for an unused slot and if the index
will exceed the hashTableSize then roll back
while(hashTable[index] != s and
hashTable[index] != "")
index = (index + 1) % hashTableSize;
//Check if the element is present in the hash
table
if(hashTable[index] == s)
cout << s << " is found!" << endl;
else
cout << s << " is not found!" << endl;
}

Quadratic Probing

Quadratic probing is similar to linear probing and the only difference is the
interval between successive probes or entry slots. Here, when the slot at a
hashed index for an entry record is already occupied, you must start
traversing until you find an unoccupied slot. The interval between slots is
computed by adding the successive value of an arbitrary polynomial in the
original hashed index.

Let us assume that the hashed index for an entry is index and
at index there is an occupied slot. The probe sequence will be as follows:

index = index % hashTableSize


index = (index + 12) % hashTableSize
index = (index + 22) % hashTableSize
index = (index + 32) % hashTableSize

and so on…

Implementation of hash table with quadratic probing

Assumption

 There are no more than 20 elements in the data set.


 Hash function will return an integer from 0 to 19.
 Data set must have unique elements.

string hashTable[21];
int hashTableSize = 21;

Insert

void insert(string s)
{
//Compute the index using the hash function
int index = hashFunc(s);
//Search for an unused slot and if the index
will exceed the hashTableSize roll back
int h = 1;
while(hashTable[index] != "")
{
index = (index + h*h) % hashTableSize;
h++;
}
hashTable[index] = s;
}

Search

void search(string s)
{
//Compute the index using the Hash Function
int index = hashFunc(s);
//Search for an unused slot and if the index
will exceed the hashTableSize roll back
int h = 1;
while(hashTable[index] != s and
hashTable[index] != "")
{
index = (index + h*h) % hashTableSize;
h++;
}
//Is the element present in the hash table
if(hashTable[index] == s)
cout << s << " is found!" << endl;
else
cout << s << " is not found!" << endl;
}

Double hashing

Double hashing is similar to linear probing and the only difference is the
interval between successive probes. Here, the interval between probes is
computed by using two hash functions.
Let us say that the hashed index for an entry record is an index that is
computed by one hashing function and the slot at that index is already
occupied. You must start traversing in a specific probing sequence to look
for an unoccupied slot. The probing sequence will be:

index = (index + 1 * indexH) % hashTableSize;


index = (index + 2 * indexH) % hashTableSize;

and so on…

Here, indexH is the hash value that is computed by another hash function.

Implementation of hash table with double hashing

Assumption

 There are no more than 20 elements in the data set.


 Hash functions will return an integer from 0 to 19.
 Data set must have unique elements.

string hashTable[21];
int hashTableSize = 21;

Insert

void insert(string s)
{
//Compute the index using the hash function1
int index = hashFunc1(s);
int indexH = hashFunc2(s);
//Search for an unused slot and if the index
exceeds the hashTableSize roll back
while(hashTable[index] != "")
index = (index + indexH) % hashTableSize;
hashTable[index] = s;
}

Search
void search(string s)
{
//Compute the index using the hash function
int index = hashFunc1(s);
int indexH = hashFunc2(s);
//Search for an unused slot and if the index
exceeds the hashTableSize roll back
while(hashTable[index] != s and
hashTable[index] != "")
index = (index + indexH) % hashTableSize;
//Is the element present in the hash table
if(hashTable[index] == s)
cout << s << " is found!" << endl;
else
cout << s << " is not found!" << endl;
}

5.6) Applications of Hashing in Unique Identifier Generation and Caching

Hashing is widely used in various applications, particularly in generating unique


identifiers and implementing caching mechanisms. Below, we’ll explore both
applications in detail and provide examples for each.

1. Unique Identifier Generation using Hashing

In many systems, we need to generate unique identifiers for various data objects.
Hashing can provide a way to generate identifiers that are compact, fast to
compute, and minimize collisions (though not eliminate them).

Use Case: Generating a Unique Identifier for Files

When storing files, you may need a way to quickly generate a unique identifier for
a file based on its contents. Instead of using file names directly, you can hash the
contents of the file to generate a unique identifier (hash value) for the file.
Example: File Hashing (MD5 or SHA-256)

Let’s assume we want to generate a unique identifier for a file. We can hash its
contents using a cryptographic hash function like MD5 or SHA-256.

python
Copy
import hashlib

def generate_file_hash(file_path):
# Create a hash object
hash_object = hashlib.sha256()
# Read the file in chunks to avoid large memory
consumption
with open(file_path, 'rb') as file:
while chunk := file.read(4096):
hash_object.update(chunk) # Update the
hash object with file content
# Return the hexadecimal digest (unique identifier)
return hash_object.hexdigest()

# Example usage
file_path = 'example.txt'
file_hash = generate_file_hash(file_path)
print(f"Unique Identifier for the file: {file_hash}")
Explanation:

 We use SHA-256 (a cryptographic hash function) to generate a unique


identifier for a file.
 The hash function produces a 64-character hexadecimal string (256 bits)
that uniquely identifies the file's contents.
 If the file contents change, the hash will change, ensuring that the identifier
is always tied to the file's actual data.

Other Applications of Unique Identifiers:

 User IDs: Hashing can be used to generate unique user IDs based on user
data (email, username, etc.).
 URL Shortening: Services like URL shorteners (e.g., bit.ly) generate a
unique, short URL based on hashing the original long URL.
 Distributed Systems: In distributed systems, hashing is used for generating
unique keys for partitioning data across nodes (e.g., in consistent hashing).

2. Caching Using Hashing

Caching is a technique used to store data temporarily to reduce access time for
frequently requested data. Hashing is often employed in cache management to map
data or results to a specific cache location. Hashing makes it easier to quickly look
up cached data by its key.

Use Case: Caching Web Pages

When a user visits a website, the page content can be cached for future visits.
Rather than regenerating the same page each time, we can cache it and use a hash
of the URL as the key to retrieve the cached content.

Example: Simple Web Page Caching

Here’s a simple implementation of caching using a hash table for caching web
pages based on their URLs:

python
Copy
class WebCache:
def __init__(self):
self.cache = {}

def generate_cache_key(self, url):


# Hash the URL to generate a unique key for
caching
return hash(url)

def get_page(self, url):


cache_key = self.generate_cache_key(url)
if cache_key in self.cache:
print("Cache Hit: Returning cached page.")
return self.cache[cache_key] # Return the
cached page
else:
print("Cache Miss: Fetching the page.")
# Simulate fetching the page content from
the web (e.g., HTML content)
page_content = f"HTML content for {url}"
self.cache[cache_key] = page_content #
Store in cache
return page_content

# Example usage
cache = WebCache()

# Fetching a page (first time, cache miss)


url = "https://example.com"
print(cache.get_page(url)) # Cache Miss

# Fetching the same page again (cache hit)


print(cache.get_page(url)) # Cache Hit
Explanation:

 We define a class WebCache that uses a hash table (self.cache) to


store the cached web pages.
 The generate_cache_key method generates a hash key for a given
URL to use as a key for caching.
 The get_page method first checks if the URL’s hash key exists in the
cache:
o If it does, the cached page content is returned (cache hit).
o If it doesn’t, the page is fetched (simulated here) and stored in the
cache (cache miss).

Benefits of Caching with Hashing:

 Fast Lookup: Hashing provides constant time complexity for checking


whether a page is already cached.
 Space Efficient: By hashing the URLs, we avoid storing large amounts of
redundant data and instead store only the cache key and content.
 Scalable: The cache can easily scale as the number of cached items grows.

Other Caching Use Cases:

 Database Query Caching: Hashing can be used to cache results of


expensive database queries.
 API Response Caching: API responses can be cached based on request
parameters (hashing the request parameters).
 Image or File Caching: Caching frequently accessed images or files based on
their hash values (e.g., for fast loading in a web app).

3. LRU Cache (Least Recently Used) with Hashing

One of the most common types of caching is LRU (Least Recently Used)
caching, which removes the least recently used items when the cache reaches its
limit. Hashing is often combined with doubly linked lists or other structures to
implement efficient LRU caches.

Example: LRU Cache Implementation Using Hashing


python
Copy
from collections import OrderedDict

class LRUCache:
def __init__(self, capacity: int):
self.cache = OrderedDict() # Ordered
dictionary keeps the insertion order
self.capacity = capacity

def get(self, key: int) -> int:


if key not in self.cache:
return -1 # Return -1 if the key is not
found
else:
# Move the accessed item to the end (most
recent)
self.cache.move_to_end(key)
return self.cache[key] # Return the value
for the key

def put(self, key: int, value: int) -> None:


if key in self.cache:
self.cache.move_to_end(key) # If the key
exists, move it to the end
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # Pop the
first item (least recently used)

# Example usage
lru_cache = LRUCache(2)
lru_cache.put(1, 1)
lru_cache.put(2, 2)
print(lru_cache.get(1)) # Returns 1
lru_cache.put(3, 3) # Evicts key 2
print(lru_cache.get(2)) # Returns -1 (not found)
Explanation:

 The LRUCache uses an OrderedDict (from Python’s collections module) to


maintain the order of the keys.
 The get method moves the accessed key to the end, marking it as recently
used.
 The put method updates or inserts a new key-value pair and ensures that
if the cache exceeds its capacity, the least recently used item is evicted (by
removing the first item from the ordered dictionary).

Conclusion:

Hashing plays a vital role in both unique identifier generation and caching:

1. Unique Identifier Generation: Hashing is used to generate unique and


efficient identifiers for data such as files, URLs, and user information. Hash
functions like SHA-256 provide a compact, fast, and collision-resistant way
to generate unique identifiers.
2. Caching: Hashing helps in efficient lookups and management of cache
entries, whether it’s caching web pages, database queries, or API responses.
The speed and simplicity of hashing make it an essential technique in
optimizing data retrieval.

Both applications are critical in real-world systems, enabling faster data access,
improved performance, and scalability.

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy