0% found this document useful (0 votes)
5 views

Data Structure Using C++

Uploaded by

Naivedya Sinha
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views

Data Structure Using C++

Uploaded by

Naivedya Sinha
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 65

DATA STRUCTURE USING C++

Sub Code: CSUCTO1

UNIT-I

• INTRODUCTION: Functions and parameter, Dynamic memory allocation,


Recursion.
• LINEAR LISTS: Data objects and structures, Linear list data structures, Array
Representation, Vector Representation, Singly Linked lists and chains. L1, L2

UNIT-II

• ARRAYS AND MATRICES: Arrays, Matrices, Special matrices, Sparse matrices.


• STACKS: The abstract data types, Array Representation, Linked Representation,
Applications-Parenthesis Matching & Towers of Hanoi. L1, L2, L3

UNIT-III

• QUEUES: The abstract data types, Array Representation, Linked Representation,


Applications-Railroad car arrangement.
• HASHING: Dictionaries, Linear representation, Hash table representation. L1, L2, L3

UNIT-IV

• BINARY AND OTHER TREES: Trees, Binary trees, Properties and representation
of binary trees, Common binary tree operations, Binary tree traversal the ADT binary
tree, ADT binary tree and the class linked binary tree. L1, L2, L3

UNIT-V

• PRIORITY QUEUES: Linear lists, Heaps, Applications-Heap Sorting.


• SEARCH TREES: Binary search trees operations and implementation, Binary Search
trees with duplicates. L1, L2, L3
UNIT-I:

INTRODUCTION

1. Functions and Parameters:

• Function Definition: A function is a block of code designed to perform a particular


task.
• Function Parameters: Parameters are the inputs to a function. They are specified
within the parentheses in the function definition.
o Actual Parameters: The values or arguments passed to a function when it is
called.
o Formal Parameters: Variables defined in the function definition that receive
the values of actual parameters.
• Function Call: The process of invoking a function by passing the necessary arguments.
• Return Values: The output or result that a function sends back after execution.

2. Dynamic Memory Allocation:

• Memory Management: In C++, memory can be dynamically allocated using pointers.


• Functions for Dynamic Allocation:
o malloc: Allocates a block of memory of a specified size and returns a pointer
to the beginning of the block.
o calloc: Similar to malloc, but initializes all bytes in the allocated memory to
zero.
o realloc: Resizes the memory block allocated by malloc or calloc.
o free: Deallocates the previously allocated memory block, making it available
for future use.
• Usage: Dynamic memory allocation is useful for managing data structures like linked
lists, trees, and graphs where the size is not known in advance.

3. Recursion:

• Recursion Definition: A function that calls itself directly or indirectly to solve a


problem.
• Base Case and Recursive Case:
o Base Case: The condition under which the recursion stops.
o Recursive Case: The part of the function where the function calls itself to solve
a smaller instance of the problem.
• Examples of Recursion:
o Factorial Calculation: n! = n * (n-1)!
o Fibonacci Sequence: F(n) = F(n-1) + F(n-2)
• Advantages:
o Simplifies complex problems by breaking them into smaller sub-problems.
• Disadvantages:
o May lead to high memory usage and stack overflow if not properly handled.
Dynamic Memory Allocation

Dynamic memory allocation is a technique in programming that allows the allocation of


memory during the runtime, as opposed to static memory allocation where memory is allocated
at compile time. This is particularly useful when the amount of memory required is not known
beforehand.

1. Memory Allocation Functions in C++:

• malloc (Memory Allocation):


o Allocates a block of memory of a specified size (in bytes) and returns a pointer
of type void* to the beginning of the block.
o The memory allocated is uninitialized.
o Syntax: void* malloc(size_t size);
o Example:

int* ptr = (int*)malloc(10 * sizeof(int)); // Allocates memory


for an array of 10 integers

• calloc (Contiguous Allocation):


o Similar to malloc but allocates memory
for an array of elements and initializes
all bytes to zero.
o Syntax: void* calloc(size_t num, size_t size);
o Example:

int* ptr = (int*)calloc(10, sizeof(int)); // Allocates memory


for an array of 10 integers and initializes all to zero

• realloc (Reallocation):
o Changes the size of the previously allocated memory block, which might move
the memory block to a new location.
o If the new size is larger, the additional memory is uninitialized.
o Syntax: void* realloc(void* ptr, size_t newSize);
o Example:

ptr = (int*)realloc(ptr, 20 * sizeof(int)); // Resizes the memory


block to store 20 integers

• free (Deallocate Memory):


o Deallocates the memory previously allocated by malloc, calloc, or realloc.
o It doesn't change the value of the pointer itself but frees up the allocated memory
space.
o Syntax: void free(void* ptr);
o Example:
free(ptr); // Deallocates the memory block pointed to by ptr

2. Advantages of Dynamic Memory Allocation:

• Efficient Memory Usage: Only allocates memory when needed, reducing wastage.
• Flexibility: Allows for the allocation of memory blocks of varying sizes, which is
particularly useful for data structures like linked lists, trees, and graphs.
• Resizability: Memory can be resized at runtime as needed.

3. Disadvantages of Dynamic Memory Allocation:

• Complexity: Managing dynamically allocated memory (like ensuring it's properly


freed) adds complexity to the program.
• Risk of Memory Leaks: If memory is allocated but not deallocated using free, it can
lead to memory leaks, where the memory is no longer accessible but not returned to the
system.
• Fragmentation: Frequent allocation and deallocation can lead to fragmented memory,
where there are many small blocks of free memory that are not contiguous.

4. Common Use Cases:

• Linked Lists: Nodes are created dynamically as elements are added.


• Dynamic Arrays: Arrays that can grow or shrink in size during runtime.
• Graphs and Trees: Dynamic structures where the size and shape are not known in
advance.

Example:

#include <iostream>
#include <cstdlib>

int main() {
int n;
std::cout << "Enter number of elements: ";
std::cin >> n;

// Dynamically allocate memory using malloc()


int* arr = (int*)malloc(n * sizeof(int));

if (arr == NULL) {
std::cerr << "Memory allocation failed\n";
return 1;
}

// Store and print elements


for (int i = 0; i < n; ++i) {
arr[i] = i + 1;
std::cout << arr[i] << " ";
}

// Free the allocated memory


free(arr);
return 0;
}

Linear Lists

Linear lists are a type of data structure where elements are arranged in a sequential or linear
order. Each element in a linear list is positioned in a specific sequence, and there is a first and
last element with each element (except the first) having a unique predecessor and each element
(except the last) having a unique successor.

1. Types of Linear Lists:

• Arrays:
o Definition: A collection of elements of the same type, stored in contiguous
memory locations.
o Access: Elements are accessed using indices, where the first element is at index
0.
o Advantages:
▪ Quick access to elements using an index.
▪ Simple and easy to implement.
o Disadvantages:
▪ Fixed size, which limits flexibility.
▪ Insertion and deletion operations can be costly in terms of time
complexity.
• Linked Lists:
o Definition: A collection of nodes where each node contains a data element and
a reference (or link) to the next node in the sequence.
o Types:
▪ Singly Linked List: Each node contains a single reference to the next
node.
▪ Doubly Linked List: Each node contains references to both the next
and the previous nodes.
▪ Circular Linked List: The last node points back to the first node,
forming a circle.
o Advantages:
▪ Dynamic size, can grow or shrink as needed.
▪ Efficient insertions and deletions (especially at the beginning or middle
of the list).
o Disadvantages:
▪ Slower access time compared to arrays since elements are accessed
sequentially.
▪ Extra memory required for storing references to the next (and previous)
nodes.

2. Representation of Linear Lists:

• Array Representation:
o Elements are stored in contiguous memory locations.
o Each element can be accessed directly using its index.
o Operations:
▪ Insertion: Adding a new element involves shifting elements to make
space.
▪ Deletion: Removing an element involves shifting elements to fill the
gap.
• Linked List Representation:
o Singly Linked List:
▪ Each node contains a data field and a pointer/reference to the next node.
▪ Insertion: A new node can be inserted by adjusting the pointers, which
is efficient.
▪ Deletion: Deleting a node involves re-linking the adjacent nodes.
o Doubly Linked List:
▪ Each node contains a data field and two pointers/references (one to the
next node and one to the previous node).
▪ Insertion/Deletion: Easier in some cases because you can traverse the
list in both directions.
o Circular Linked List:
▪ The last node's next pointer points to the first node, forming a circle.
▪ Usage: Useful in scenarios where the list needs to be looped through
continuously.

3. Operations on Linear Lists:

• Traversal: Visiting each element in the list to perform some operation (e.g., printing).
• Insertion: Adding an element at a specific position in the list.
• Deletion: Removing an element from the list.
• Search: Finding a specific element in the list based on some criteria.
• Update: Modifying the value of an existing element in the list.

4. Applications of Linear Lists:

• Array-Based Lists:
o Used in situations where fast access to elements is required (e.g., databases,
lookup tables).
o Commonly used in implementing other data structures like stacks and queues.
• Linked Lists:
o Used in scenarios where the size of the data structure is dynamic and frequent
insertions and deletions are needed (e.g., memory management, adjacency lists
in graphs).

Example: Singly Linked List in C++

#include <iostream>

struct Node {
int data;
Node* next;
};

class LinkedList {
public:
Node* head;

LinkedList() {
head = nullptr;
}

// Insertion at the beginning


void insertAtBeginning(int value) {
Node* newNode = new Node();
newNode->data = value;
newNode->next = head;
head = newNode;
}

// Traversal and printing the list


void printList() {
Node* temp = head;
while (temp != nullptr) {
std::cout << temp->data << " -> ";
temp = temp->next;
}
std::cout << "NULL" << std::endl;
}
};

int main() {
LinkedList list;
list.insertAtBeginning(10);
list.insertAtBeginning(20);
list.insertAtBeginning(30);

list.printList(); // Output: 30 -> 20 -> 10 -> NULL

return 0;
}

Explanation:

• This code demonstrates a simple singly linked list where new elements are inserted at
the beginning of the list. The printList function traverses the list and prints all the
elements.

Data Objects and Structures

1. Data Objects:

• Definition:
o A data object is a collection of data that is treated as a unit. It represents an
instance of a data type and stores the actual values that an application processes.
o In programming, data objects can be variables, constants, or data structures that
hold data values.
• Examples:
o Primitive Data Objects: These include basic data types like integers, floats,
characters, and booleans.
o Composite Data Objects: These are made up of multiple primitive data
objects. Examples include arrays, structures, and classes.
• Characteristics:
o Type: The data type of the object (e.g., integer, float).
o Value: The actual data held by the object.
o Memory Location: The address where the data object is stored in memory.

2. Data Structures:

• Definition:
o A data structure is a way of organizing and storing data in a computer so that it
can be accessed and modified efficiently.
o Data structures are used to implement abstract data types (ADTs) like lists,
stacks, queues, trees, and graphs.
• Types of Data Structures:
o Linear Data Structures:
▪ Arrays: A collection of elements of the same type stored in contiguous
memory locations.
▪ Linked Lists: A sequence of nodes where each node contains data and
a reference (or link) to the next node.
▪ Stacks: A collection of elements that follows the Last In, First Out
(LIFO) principle.
▪ Queues: A collection of elements that follows the First In, First Out
(FIFO) principle.
o Non-Linear Data Structures:
▪ Trees: A hierarchical structure where each node has a value and
references to its child nodes. A binary tree is a common example.
▪ Graphs: A collection of nodes (or vertices) connected by edges. Graphs
can be directed or undirected.
o Hash Tables: A structure that maps keys to values using a hash function. It
allows for efficient data retrieval based on key values.
• Operations on Data Structures:
o Traversal: Visiting each element in the data structure to perform an operation
(e.g., printing, searching).
o Insertion: Adding a new element to the data structure.
o Deletion: Removing an element from the data structure.
o Search: Finding an element in the data structure that meets certain criteria.
o Sorting: Arranging the elements in a data structure in a specific order.

3. Importance of Data Structures:

• Efficiency:
o The choice of data structure affects the efficiency of algorithms. Some
operations are faster on certain data structures than others.
o For example, searching for an element in a sorted array is faster with binary
search (O(log n)) than with a linear search (O(n)).
• Scalability:
oProper use of data structures ensures that applications can handle growing
amounts of data efficiently.
o Data structures like trees and graphs are essential for representing hierarchical
or networked data.
• Maintainability:
o Data structures help in organizing code better, making it easier to understand,
maintain, and debug.

4. Examples of Data Structures in Use:

• Arrays: Used in scenarios where quick access to elements is needed, such as in matrix
operations.
• Linked Lists: Useful in situations where frequent insertion and deletion operations are
required, such as in dynamic memory allocation.
• Stacks: Used in function call management in programming languages (call stack) and
in algorithms like depth-first search (DFS).
• Queues: Commonly used in scheduling processes in operating systems, breadth-first
search (BFS) algorithms, and handling asynchronous data (like printers, IO buffers).
• Trees: Widely used in databases (B-trees, AVL trees), in organizing hierarchical data
like file systems, and in parsing expressions.
• Graphs: Essential in representing networks like social networks, transportation
networks, and in algorithms like shortest path (Dijkstra’s) and minimum spanning tree
(Kruskal’s).

Linear List Data Structures in C++

1. Definition: A linear list is a data structure where elements are stored in a sequential manner,
and each element is connected to the previous and next elements. This structure allows for
straightforward access and manipulation of data.

2. Types:

• Array:
o Description: A collection of elements stored in contiguous memory locations.
o Access Time: O(1) for access by index.
o Insertion/Deletion Time: O(n) in the worst case.
o Advantages: Fast access by index, simple implementation.
o Disadvantages: Fixed size, costly resizing.

int arr[10]; // Array of 10 integers


arr[0] = 5; // Access element at index 0

• Linked List:
o Description: A collection of nodes where each node contains data and a
reference (or pointer) to the next node.
o Access Time: O(n) for searching.
o Insertion/Deletion Time: O(1) if the position is known.
o Advantages: Dynamic size, efficient insertions/deletions.
o Disadvantages: Slower access time, additional memory for pointers.
struct Node {
int data;
Node* next;
};
Node* head = nullptr; // Initialize empty list

o Types of Linked Lists:


▪ Singly Linked List: Nodes have a single pointer to the next node.
▪ Doubly Linked List: Nodes have two pointers (next and previous).
▪ Circular Linked List: The last node points back to the first node.

3. Comparison:

• Array vs. Linked List:


o Memory Usage: Arrays are more memory-efficient due to contiguous
allocation. Linked lists use extra memory for pointers.
o Dynamic Size: Linked lists can grow and shrink dynamically, while arrays have
a fixed size (or need resizing).
o Performance: Arrays provide constant-time access, while linked lists require
linear-time traversal.

4. Common Operations:

• Array:
o Accessing an element: arr[index]
o Inserting an element: Requires shifting elements.
o Deleting an element: Requires shifting elements.
• Linked List:
o Inserting at the beginning, end, or specific position: O(1) if the position is
known.
o Deleting a node: O(1) if the node reference is known.

5. Use Cases:

• Arrays: Suitable for scenarios requiring fast access and fixed-size data.
• Linked Lists: Useful for dynamic data where frequent insertions and deletions are
required.

Array Representation in C++

In C++, arrays are a fundamental data structure that stores elements of the same type in
contiguous memory locations. Here's a detailed overview of array representation:

1. Definition:

An array is a collection of elements of the same type, indexed by contiguous integers. The size
of an array is fixed at the time of declaration.

2. Declaration and Initialization:


• Basic Declaration:

int arr[10]; // Declares an array of 10 integers

• Declaration with Initialization:

int arr[5] = {1, 2, 3, 4, 5}; // Declares and initializes an array of


5 integers

• Partial Initialization:

int arr[5] = {1, 2}; // The remaining elements are initialized to 0

• Initialization with Default Values:

int arr[5] = {}; // All elements initialized to 0

3. Accessing Elements:

Elements are accessed using an index, which starts from 0:

int value = arr[2]; // Accesses the third element of the array

4. Modification:

You can modify elements by assigning new values to specific indices:

arr[2] = 10; // Sets the third element to 10

5. Array Size:

• Determining Array Size:

int size = sizeof(arr) / sizeof(arr[0]); // Computes the number of


elements in the array

6. Multi-dimensional Arrays:

Arrays can be extended to more dimensions:


• Two-Dimensional Array:

int matrix[3][4]; // Declares a 2D array with 3 rows and 4 columns


matrix[0][0] = 1; // Sets the element at row 0, column 0

• Initialization of 2D Array:

int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};

7. Array vs. Pointer:

• Pointer to Array:

int* ptr = arr; // Pointer to the first element of the array

• Pointer Arithmetic:

int first = *ptr; // Accesses the first element (same as arr[0])


int second = *(ptr + 1); // Accesses the second element (same as arr[1])

8. Limitations:

• Fixed Size: Once an array is created, its size cannot be changed.


• Contiguous Memory: Requires contiguous memory allocation, which might not be
available for large arrays.

9. Common Operations:

• Traversing an Array:

for (int i = 0; i < size; ++i) {


std::cout << arr[i] << " ";
}

• Finding an Element:

bool found = false;


for (int i = 0; i < size; ++i) {
if (arr[i] == target) {
found = true;
break;
}
}

Arrays in C++ are simple and efficient for many use cases, but for dynamic sizing and more
flexible operations, other data structures like vectors (from the C++ Standard Library) might
be preferred.

Vector Representation in C++

In C++, the std::vector is a dynamic array provided by the Standard Library, which offers
flexible and efficient handling of arrays with dynamic sizes. It is part of the <vector> header
and is widely used for its versatility and convenience.

1. Definition:

A std::vector is a sequence container that encapsulates dynamic size arrays. Unlike regular
arrays, vectors can grow or shrink in size, and they manage their own memory.

2. Basic Operations:

• Including the Vector Header:

#include <vector>

• Declaration and Initialization:

std::vector<int> vec; // Creates an empty vector of integers


std::vector<int> vec2(5); // Creates a vector with 5 default-
initialized integers (0)
std::vector<int> vec3(5, 10); // Creates a vector with 5 integers, all
initialized to 10
std::vector<int> vec4 = {1, 2, 3, 4, 5}; // Creates and initializes a
vector with specific values

• Accessing Elements:

int value = vec4[2]; // Accesses the third element (index 2)


int front = vec4.front(); // Accesses the first element
int back = vec4.back(); // Accesses the last element

• Modifying Elements:

vec4[2] = 10; // Sets the third element to 10


vec4.push_back(6); // Adds an element with value 6 to the end of the
vector
vec4.pop_back(); // Removes the last element from the vector

• Size and Capacity:

size_t size = vec4.size(); // Number of elements in the vector


size_t capacity = vec4.capacity(); // Number of elements that can be
stored without reallocating

• Resizing and Reserving:

vec4.resize(10); // Resizes the vector to 10 elements (new elements


are default-initialized)
vec4.resize(8, 20); // Resizes to 8 elements, with new elements
initialized to 20
vec4.reserve(20); // Requests that the vector can hold at least 20
elements without reallocating

3. Common Operations:

• Traversing a Vector:

for (size_t i = 0; i < vec4.size(); ++i) {


std::cout << vec4[i] << " ";
}

Using Range-based for Loop:

for (int value : vec4) {


std::cout << value << " ";
}

• Finding an Element:

auto it = std::find(vec4.begin(), vec4.end(), 10); // Finds the first


occurrence of 10
if (it != vec4.end()) {
std::cout << "Element found at position: " <<
std::distance(vec4.begin(), it);
}

4. Vector Methods:

• Empty Check:
bool isEmpty = vec4.empty(); // Checks if the vector is empty

• Clearing the Vector:

vec4.clear(); // Removes all elements from the vector

5. Advantages Over Arrays:

• Dynamic Size: Unlike arrays, vectors can grow and shrink as needed.
• Memory Management: Automatically handles memory allocation and deallocation.
• Flexible Operations: Provides various methods for manipulation and access.
• Efficiency: Maintains performance through amortized constant-time complexity for
insertions at the end.

6. Limitations:

• Overhead: More memory overhead compared to arrays due to additional features and
management.
• Element Access: Slightly slower than array access due to boundary checks in debug
mode.

7. Example Usage:

#include <iostream>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};

// Adding an element
numbers.push_back(6);

// Accessing elements
std::cout << "First element: " << numbers.front() << std::endl;
std::cout << "Last element: " << numbers.back() << std::endl;

// Traversing
for (const int& num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

// Size and capacity


std::cout << "Size: " << numbers.size() << std::endl;
std::cout << "Capacity: " << numbers.capacity() << std::endl;

return 0;
}

Vector Representation in C++


In C++, the std::vector is a dynamic array provided by the Standard Library, which offers
flexible and efficient handling of arrays with dynamic sizes. It is part of the <vector> header
and is widely used for its versatility and convenience.

1. Definition:

A std::vector is a sequence container that encapsulates dynamic size arrays. Unlike regular
arrays, vectors can grow or shrink in size, and they manage their own memory.

2. Basic Operations:

• Including the Vector Header:

#include <vector>

• Declaration and Initialization:

std::vector<int> vec; // Creates an empty vector of integers


std::vector<int> vec2(5); // Creates a vector with 5 default-
initialized integers (0)
std::vector<int> vec3(5, 10); // Creates a vector with 5 integers, all
initialized to 10
std::vector<int> vec4 = {1, 2, 3, 4, 5}; // Creates and initializes a
vector with specific values

• Accessing Elements:

int value = vec4[2]; // Accesses the third element (index 2)


int front = vec4.front(); // Accesses the first element
int back = vec4.back(); // Accesses the last element

• Modifying Elements:

vec4[2] = 10; // Sets the third element to 10


vec4.push_back(6); // Adds an element with value 6 to the end of the
vector
vec4.pop_back(); // Removes the last element from the vector

• Size and Capacity:

size_t size = vec4.size(); // Number of elements in the vector


size_t capacity = vec4.capacity(); // Number of elements that can be
stored without reallocating

• Resizing and Reserving:


vec4.resize(10); // Resizes the vector to 10 elements (new elements
are default-initialized)
vec4.resize(8, 20); // Resizes to 8 elements, with new elements
initialized to 20
vec4.reserve(20); // Requests that the vector can hold at least 20
elements without reallocating

3. Common Operations:

• Traversing a Vector:

for (size_t i = 0; i < vec4.size(); ++i) {


std::cout << vec4[i] << " ";
}

Using Range-based for Loop:

for (int value : vec4) {


std::cout << value << " ";
}

• Finding an Element:

auto it = std::find(vec4.begin(), vec4.end(), 10); // Finds the first


occurrence of 10
if (it != vec4.end()) {
std::cout << "Element found at position: " <<
std::distance(vec4.begin(), it);
}

4. Vector Methods:

• Empty Check:

bool isEmpty = vec4.empty(); // Checks if the vector is empty

• Clearing the Vector:

vec4.clear(); // Removes all elements from the vector

5. Advantages Over Arrays:

• Dynamic Size: Unlike arrays, vectors can grow and shrink as needed.
• Memory Management: Automatically handles memory allocation and deallocation.
• Flexible Operations: Provides various methods for manipulation and access.
• Efficiency: Maintains performance through amortized constant-time complexity for insertions
at the end.

6. Limitations:

• Overhead: More memory overhead compared to arrays due to additional features and
management.
• Element Access: Slightly slower than array access due to boundary checks in debug mode.

7. Example Usage:

#include <iostream>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};

// Adding an element
numbers.push_back(6);

// Accessing elements
std::cout << "First element: " << numbers.front() << std::endl;
std::cout << "Last element: " << numbers.back() << std::endl;

// Traversing
for (const int& num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

// Size and capacity


std::cout << "Size: " << numbers.size() << std::endl;
std::cout << "Capacity: " << numbers.capacity() << std::endl;

return 0;
}

This example demonstrates basic operations with a std::vector, showing how to create,
modify, and access elements effectively.

You said:
Singly Linked lists and chains

ChatGPT said:
ChatGPT

Singly Linked Lists and Chains in C++

1. Definition:
A singly linked list is a linear data structure where each element, known as a node, contains
data and a pointer to the next node in the sequence. It allows for dynamic memory allocation
and efficient insertions and deletions.

2. Structure of a Node:

A node in a singly linked list typically has two parts:

• Data: Stores the actual value.


• Next: A pointer to the next node in the list.

struct Node {
int data; // Data part
Node* next; // Pointer to the next node
};

3. Basic Operations:

• Creating a New Node:

Node* createNode(int value) {


Node* newNode = new Node();
newNode->data = value;
newNode->next = nullptr;
return newNode;
}

• Inserting at the Beginning:

void insertAtBeginning(Node*& head, int value) {


Node* newNode = createNode(value);
newNode->next = head;
head = newNode;
}

• Inserting at the End:

void insertAtEnd(Node*& head, int value) {


Node* newNode = createNode(value);
if (head == nullptr) {
head = newNode;
} else {
Node* temp = head;
while (temp->next != nullptr) {
temp = temp->next;
}
temp->next = newNode;
}
}

• Deleting a Node:

void deleteNode(Node*& head, int value) {


if (head == nullptr) return;

// Special case: deleting the head


if (head->data == value) {
Node* temp = head;
head = head->next;
delete temp;
return;
}

Node* current = head;


Node* previous = nullptr;

while (current != nullptr && current->data != value) {


previous = current;
current = current->next;
}

if (current != nullptr) {
previous->next = current->next;
delete current;
}
}

• Traversing the List:

void printList(Node* head) {


Node* temp = head;
while (temp != nullptr) {
std::cout << temp->data << " ";
temp = temp->next;
}
std::cout << std::endl;
}

4. Advantages:

• Dynamic Size: The list can grow and shrink as needed.


• Efficient Insertions/Deletions: Adding or removing elements is efficient when the node's
location is known.

5. Disadvantages:

• Sequential Access: Accessing elements requires traversing from the head to the desired
position, which can be slow (O(n) time complexity).
• Extra Memory: Each node requires additional memory for the pointer.

6. Chains:
Chains are a concept where nodes are connected in a way similar to linked lists, but they can
represent various types of linked data structures, such as hash tables or more complex
structures. In the context of singly linked lists, "chains" typically refer to sequences of nodes
where each node points to the next in a single direction.

Example of a Complete Program:

#include <iostream>

struct Node {
int data;
Node* next;
};

// Function to create a new node


Node* createNode(int value) {
Node* newNode = new Node();
newNode->data = value;
newNode->next = nullptr;
return newNode;
}

// Function to insert at the beginning


void insertAtBeginning(Node*& head, int value) {
Node* newNode = createNode(value);
newNode->next = head;
head = newNode;
}

// Function to insert at the end


void insertAtEnd(Node*& head, int value) {
Node* newNode = createNode(value);
if (head == nullptr) {
head = newNode;
} else {
Node* temp = head;
while (temp->next != nullptr) {
temp = temp->next;
}
temp->next = newNode;
}
}

// Function to delete a node


void deleteNode(Node*& head, int value) {
if (head == nullptr) return;

if (head->data == value) {
Node* temp = head;
head = head->next;
delete temp;
return;
}

Node* current = head;


Node* previous = nullptr;

while (current != nullptr && current->data != value) {


previous = current;
current = current->next;
}

if (current != nullptr) {
previous->next = current->next;
delete current;
}
}

// Function to print the list


void printList(Node* head) {
Node* temp = head;
while (temp != nullptr) {
std::cout << temp->data << " ";
temp = temp->next;
}
std::cout << std::endl;
}

// Main function to demonstrate linked list operations


int main() {
Node* head = nullptr; // Initialize empty list

insertAtBeginning(head, 10);
insertAtBeginning(head, 20);
insertAtEnd(head, 30);

std::cout << "Linked List: ";


printList(head);

deleteNode(head, 20);
std::cout << "After deleting 20: ";
printList(head);

return 0;
}

This example demonstrates creating a singly linked list, inserting nodes at both the beginning
and end, deleting a node, and printing the list.

This example demonstrates basic operations with a std::vector, showing how to create,
modify, and access elements effectively.

Working with Two Singly Linked Lists (L1 and L2)

1. Definition:

L1 and L2 are two separate singly linked lists. Each list contains nodes where each node has a
data value and a pointer to the next node.

2. Basic Node Structure:


struct Node {
int data;
Node* next;
};

3. Common Operations:

a. Merging Two Sorted Lists:

Suppose you have two sorted linked lists and want to merge them into a single sorted list.

Node* mergeSortedLists(Node* l1, Node* l2) {


Node dummy;
Node* tail = &dummy;
dummy.next = nullptr;

while (l1 != nullptr && l2 != nullptr) {


if (l1->data < l2->data) {
tail->next = l1;
l1 = l1->next;
} else {
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}

// Append remaining nodes


tail->next = (l1 != nullptr) ? l1 : l2;

return dummy.next;
}

b. Finding the Intersection of Two Lists:

To find if two linked lists intersect and find the intersection node.

Node* getIntersectionNode(Node* headA, Node* headB) {


if (headA == nullptr || headB == nullptr) return nullptr;

Node* a = headA;
Node* b = headB;

// Traverse both lists. When reaching the end of one list, start at the
beginning of the other list
while (a != b) {
a = (a == nullptr) ? headB : a->next;
b = (b == nullptr) ? headA : b->next;
}

return a; // Either intersection node or nullptr if no intersection


}

c. Concatenating Two Lists:


To append the nodes of one list to the end of another list.

void concatenateLists(Node*& l1, Node* l2) {


if (l1 == nullptr) {
l1 = l2;
return;
}

Node* temp = l1;


while (temp->next != nullptr) {
temp = temp->next;
}
temp->next = l2;
}

d. Comparing Two Lists:

To check if two linked lists are identical (same data in the same order).

bool areListsIdentical(Node* l1, Node* l2) {


while (l1 != nullptr && l2 != nullptr) {
if (l1->data != l2->data) return false;
l1 = l1->next;
l2 = l2->next;
}

return (l1 == nullptr && l2 == nullptr);


}

Example Program:

Here’s a full example demonstrating some of these operations:

#include <iostream>

struct Node {
int data;
Node* next;
};

// Helper function to create a new node


Node* createNode(int value) {
Node* newNode = new Node();
newNode->data = value;
newNode->next = nullptr;
return newNode;
}

// Function to print a linked list


void printList(Node* head) {
while (head != nullptr) {
std::cout << head->data << " ";
head = head->next;
}
std::cout << std::endl;
}

// Function to merge two sorted lists


Node* mergeSortedLists(Node* l1, Node* l2) {
Node dummy;
Node* tail = &dummy;
dummy.next = nullptr;

while (l1 != nullptr && l2 != nullptr) {


if (l1->data < l2->data) {
tail->next = l1;
l1 = l1->next;
} else {
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}

tail->next = (l1 != nullptr) ? l1 : l2;

return dummy.next;
}

// Function to concatenate two lists


void concatenateLists(Node*& l1, Node* l2) {
if (l1 == nullptr) {
l1 = l2;
return;
}

Node* temp = l1;


while (temp->next != nullptr) {
temp = temp->next;
}
temp->next = l2;
}

// Main function
int main() {
// Create two linked lists
Node* l1 = createNode(1);
l1->next = createNode(3);
l1->next->next = createNode(5);

Node* l2 = createNode(2);
l2->next = createNode(4);
l2->next->next = createNode(6);

// Merge the lists


Node* mergedList = mergeSortedLists(l1, l2);
std::cout << "Merged List: ";
printList(mergedList);

// Concatenate lists
concatenateLists(l1, l2);
std::cout << "Concatenated List: ";
printList(l1);
return 0;
}

This example shows how to create, merge, and concatenate linked lists. Modify the code and
functions as needed for specific operations involving two singly linked lists.
Unit II

Arrays and Matrices in C++

This section covers arrays and matrices in C++, focusing on their definition, operations, and
applications.

1. Arrays

1.1 Definition:

An array is a collection of elements of the same type, stored in contiguous memory locations.
The size of an array is fixed at the time of declaration.

1.2 Declaration and Initialization:

• Single-Dimensional Array:

int arr[5]; // Declares an array of 5 integers


int arr[5] = {1, 2, 3, 4, 5}; // Initializes an array with values

• Accessing Elements:

int firstElement = arr[0]; // Accesses the first element

• Modification:

arr[2] = 10; // Sets the third element to 10

• Size of Array:

size_t size = sizeof(arr) / sizeof(arr[0]); // Computes the number of


elements

1.3 Multi-Dimensional Arrays:

• Two-Dimensional Array:

int matrix[3][4]; // 3 rows and 4 columns


matrix[0][0] = 1; // Access element at row 0, column 0
• Initialization of 2D Array:

int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};

• Accessing Elements:

int value = matrix[1][2]; // Accesses element in the second row, third


column

• Traversing a 2D Array:

for (int i = 0; i < 2; ++i) {


for (int j = 0; j < 3; ++j) {
std::cout << matrix[i][j] << " ";
}
std::cout << std::endl;
}

2. Matrices

Matrices are a specialized form of 2D arrays used in mathematical computations. In C++,


matrices are typically represented as 2D arrays or using std::vector.

2.1 Matrix Operations:

• Addition of Two Matrices:

void addMatrices(int mat1[3][3], int mat2[3][3], int result[3][3]) {


for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
result[i][j] = mat1[i][j] + mat2[i][j];
}
}
}

• Multiplication of Two Matrices:

void multiplyMatrices(int mat1[3][3], int mat2[3][3], int


result[3][3]) {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
result[i][j] = 0;
for (int k = 0; k < 3; ++k) {
result[i][j] += mat1[i][k] * mat2[k][j];
}
}
}
}

2.2 Dynamic Matrix with std::vector:

• Declaration and Initialization:

#include <vector>
std::vector<std::vector<int>> matrix(3, std::vector<int>(4, 0)); //
3x4 matrix initialized with 0

• Accessing and Modifying Elements:

matrix[1][2] = 5; // Set element in second row, third column to 5

• Traversing a Matrix:

for (const auto& row : matrix) {


for (int val : row) {
std::cout << val << " ";
}
std::cout << std::endl;
}

Example Program: Working with Arrays and Matrices

Here is a sample program demonstrating operations on arrays and matrices:

#include <iostream>
#include <vector>

void printMatrix(const std::vector<std::vector<int>>& matrix) {


for (const auto& row : matrix) {
for (int val : row) {
std::cout << val << " ";
}
std::cout << std::endl;
}
}

int main() {
// 1D Array Example
int arr[5] = {1, 2, 3, 4, 5};
std::cout << "1D Array: ";
for (int i : arr) {
std::cout << i << " ";
}
std::cout << std::endl;

// 2D Array Example
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
std::cout << "2D Array: " << std::endl;
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << matrix[i][j] << " ";
}
std::cout << std::endl;
}

// Dynamic Matrix with std::vector


std::vector<std::vector<int>> mat1 = {
{1, 2, 3},
{4, 5, 6}
};
std::vector<std::vector<int>> mat2 = {
{7, 8, 9},
{10, 11, 12}
};
std::vector<std::vector<int>> result(2, std::vector<int>(3, 0));

// Matrix Addition
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
result[i][j] = mat1[i][j] + mat2[i][j];
}
}

std::cout << "Matrix Addition Result: " << std::endl;


printMatrix(result);

// Matrix Multiplication
result = std::vector<std::vector<int>>(2, std::vector<int>(3, 0));
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
result[i][j] = 0;
for (int k = 0; k < 3; ++k) {
result[i][j] += mat1[i][k] * mat2[k][j];
}
}
}

std::cout << "Matrix Multiplication Result: " << std::endl;


printMatrix(result);

return 0;
}
Summary

• Arrays are fixed-size containers for elements of the same type.


• Matrices are 2D arrays, useful for mathematical operations.
• Dynamic arrays and matrices can be handled using std::vector for more flexibility.
This overview covers the essentials of arrays and matrices, including basic operations and
examples of code. Let me know if you need further details or specific examples!

1. Arrays

1.1 Definition:

An array is a collection of elements of the same type, stored in contiguous memory locations.
It allows access to its elements using indices.

1.2 Basic Operations:

• Declaration and Initialization:

int arr[5] = {1, 2, 3, 4, 5}; // Declares and initializes an integer


array

• Accessing Elements:

int element = arr[2]; // Accesses the third element (value 3)

• Modification:

arr[2] = 10; // Sets the third element to 10

• Size Calculation:

size_t size = sizeof(arr) / sizeof(arr[0]); // Number of elements in


the array

2. Matrices

2.1 Definition:

A matrix is a two-dimensional array where elements are arranged in rows and columns.

2.2 Operations:

• Declaration and Initialization:

int matrix[3][4] = { // 3 rows and 4 columns


{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};

• Accessing Elements:

int value = matrix[1][2]; // Accesses element in the second row, third


column (value 7)

• Traversing a Matrix:

for (int i = 0; i < 3; ++i) {


for (int j = 0; j < 4; ++j) {
std::cout << matrix[i][j] << " ";
}
std::cout << std::endl;
}

2.3 Matrix Operations:

• Addition of Two Matrices:

void addMatrices(int mat1[3][3], int mat2[3][3], int result[3][3]) {


for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
result[i][j] = mat1[i][j] + mat2[i][j];
}
}
}

• Multiplication of Two Matrices:

void multiplyMatrices(int mat1[3][3], int mat2[3][3], int


result[3][3]) {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
result[i][j] = 0;
for (int k = 0; k < 3; ++k) {
result[i][j] += mat1[i][k] * mat2[k][j];
}
}
}
}

3. Special Matrices

Special matrices are matrices with particular properties that are useful in mathematical and
computational problems.
3.1 Identity Matrix:

An identity matrix is a square matrix with 1s on the diagonal and 0s elsewhere.

void printIdentityMatrix(int size) {


for (int i = 0; i < size; ++i) {
for (int j = 0; j < size; ++j) {
if (i == j) {
std::cout << "1 ";
} else {
std::cout << "0 ";
}
}
std::cout << std::endl;
}
}

3.2 Diagonal Matrix:

A diagonal matrix is a square matrix where non-diagonal elements are zero.

void printDiagonalMatrix(int size, const std::vector<int>& diagElements) {


for (int i = 0; i < size; ++i) {
for (int j = 0; j < size; ++j) {
if (i == j) {
std::cout << diagElements[i] << " ";
} else {
std::cout << "0 ";
}
}
std::cout << std::endl;
}
}

3.3 Upper Triangular Matrix:

An upper triangular matrix has non-zero elements only on or above the main diagonal.

void printUpperTriangularMatrix(int size, const std::vector<int>&


upperElements) {
int index = 0;
for (int i = 0; i < size; ++i) {
for (int j = 0; j < size; ++j) {
if (j >= i) {
std::cout << upperElements[index++] << " ";
} else {
std::cout << "0 ";
}
}
std::cout << std::endl;
}
}
3.4 Lower Triangular Matrix:

A lower triangular matrix has non-zero elements only on or below the main diagonal.

void printLowerTriangularMatrix(int size, const std::vector<int>&


lowerElements) {
int index = 0;
for (int i = 0; i < size; ++i) {
for (int j = 0; j < size; ++j) {
if (j <= i) {
std::cout << lowerElements[index++] << " ";
} else {
std::cout << "0 ";
}
}
std::cout << std::endl;
}
}

4. Sparse Matrices

4.1 Definition:

A sparse matrix is a matrix in which most of the elements are zero. Sparse matrices are stored
more efficiently than dense matrices by only storing non-zero elements.

4.2 Representation:

• List of Triples Representation:


o Each non-zero element is stored as a triplet (row index, column index, value).

#include <vector>

struct Triplet {
int row;
int col;
int value;
};

void printSparseMatrix(const std::vector<Triplet>& sparseMatrix, int rows,


int cols) {
std::vector<std::vector<int>> matrix(rows, std::vector<int>(cols, 0));

for (const auto& triplet : sparseMatrix) {


matrix[triplet.row][triplet.col] = triplet.value;
}

for (const auto& row : matrix) {


for (int val : row) {
std::cout << val << " ";
}
std::cout << std::endl;
}
}

4.3 Operations on Sparse Matrices:

• Addition of Sparse Matrices:

std::vector<Triplet> addSparseMatrices(const std::vector<Triplet>& mat1,


const std::vector<Triplet>& mat2) {
std::vector<Triplet> result;
std::map<std::pair<int, int>, int> elements;

for (const auto& triplet : mat1) {


elements[{triplet.row, triplet.col}] += triplet.value;
}
for (const auto& triplet : mat2) {
elements[{triplet.row, triplet.col}] += triplet.value;
}

for (const auto& [key, value] : elements) {


if (value != 0) {
result.push_back({key.first, key.second, value});
}
}

return result;
}
Summary

• Arrays: Fixed-size collections of elements.


• Matrices: Two-dimensional arrays with operations such as addition and multiplication.
• Special Matrices: Includes identity, diagonal, upper triangular, and lower triangular matrices.
• Sparse Matrices: Efficiently stores matrices with mostly zero elements using representations
like the list of triples.

These concepts and operations are fundamental in various fields including computer science,
engineering, and mathematics.

Stacks: Abstract Data Types, Array Representation, Linked Representation,


Applications

A stack is a fundamental data structure used to manage and store data in a Last-In-First-Out
(LIFO) manner. This section covers the abstract data type of stacks, their array and linked
representations, and some common applications.

1. Abstract Data Type (ADT) for Stack

1.1 Definition:

A stack is an abstract data type that follows the Last-In-First-Out (LIFO) principle. It supports
the following operations:
• Push: Add an element to the top of the stack.
• Pop: Remove the element from the top of the stack.
• Peek/Top: Retrieve the top element without removing it.
• isEmpty: Check if the stack is empty.

1.2 Operations and Complexity:

• Push: O(1)
• Pop: O(1)
• Peek/Top: O(1)
• isEmpty: O(1)

2. Array Representation of Stack

2.1 Definition:

An array-based stack uses a fixed-size array to store the stack elements. It maintains a pointer
(or index) to the top element.

2.2 Implementation:

#include <iostream>
#define MAX 100 // Define maximum size of stack

class Stack {
private:
int arr[MAX];
int top;

public:
Stack() { top = -1; }

bool isEmpty() {
return top == -1;
}

bool isFull() {
return top == MAX - 1;
}

void push(int x) {
if (isFull()) {
std::cout << "Stack Overflow" << std::endl;
return;
}
arr[++top] = x;
}

void pop() {
if (isEmpty()) {
std::cout << "Stack Underflow" << std::endl;
return;
}
--top;
}

int peek() {
if (isEmpty()) {
std::cout << "Stack is Empty" << std::endl;
return -1; // or some sentinel value
}
return arr[top];
}
};

2.3 Usage:

int main() {
Stack s;
s.push(10);
s.push(20);
std::cout << "Top element: " << s.peek() << std::endl;
s.pop();
std::cout << "Top element after pop: " << s.peek() << std::endl;
return 0;
}

3. Linked Representation of Stack

3.1 Definition:

A linked representation of a stack uses a linked list where each node points to the next node in
the stack.

3.2 Implementation:

#include <iostream>

struct Node {
int data;
Node* next;
};

class Stack {
private:
Node* top;

public:
Stack() { top = nullptr; }

bool isEmpty() {
return top == nullptr;
}

void push(int x) {
Node* newNode = new Node();
newNode->data = x;
newNode->next = top;
top = newNode;
}

void pop() {
if (isEmpty()) {
std::cout << "Stack Underflow" << std::endl;
return;
}
Node* temp = top;
top = top->next;
delete temp;
}

int peek() {
if (isEmpty()) {
std::cout << "Stack is Empty" << std::endl;
return -1; // or some sentinel value
}
return top->data;
}
};

3.3 Usage:

int main() {
Stack s;
s.push(10);
s.push(20);
std::cout << "Top element: " << s.peek() << std::endl;
s.pop();
std::cout << "Top element after pop: " << s.peek() << std::endl;
return 0;
}

4. Applications of Stack

4.1 Parenthesis Matching

Stacks are often used to check if parentheses in an expression are balanced.

#include <iostream>
#include <stack>
#include <string>

bool isValidParentheses(const std::string& s) {


std::stack<char> stk;
for (char ch : s) {
if (ch == '(' || ch == '{' || ch == '[') {
stk.push(ch);
} else {
if (stk.empty()) return false;
char top = stk.top();
stk.pop();
if ((ch == ')' && top != '(') ||
(ch == '}' && top != '{') ||
(ch == ']' && top != '[')) {
return false;
}
}
}
return stk.empty();
}

int main() {
std::string expr = "{[()]}";
if (isValidParentheses(expr)) {
std::cout << "Balanced Parentheses" << std::endl;
} else {
std::cout << "Unbalanced Parentheses" << std::endl;
}
return 0;
}

4.2 Towers of Hanoi

The Towers of Hanoi problem can be solved using a stack-based approach, but is often
explained recursively. The problem involves moving a set of disks from one peg to another,
using an auxiliary peg.

Recursive Solution:

#include <iostream>

void hanoi(int n, char from_rod, char to_rod, char aux_rod) {


if (n == 1) {
std::cout << "Move disk 1 from rod " << from_rod << " to rod " <<
to_rod << std::endl;
return;
}
hanoi(n - 1, from_rod, aux_rod, to_rod);
std::cout << "Move disk " << n << " from rod " << from_rod << " to rod
" << to_rod << std::endl;
hanoi(n - 1, aux_rod, to_rod, from_rod);
}

int main() {
int n = 3; // Number of disks
hanoi(n, 'A', 'C', 'B'); // A, B, and C are names of rods
return 0;
}
Summary

• Stack ADT: LIFO structure supporting push, pop, peek, and isEmpty operations.
• Array Representation: Fixed-size implementation with array and index.
• Linked Representation: Dynamic-size implementation using a linked list.
• Applications:
o Parenthesis Matching: Ensures expressions have balanced parentheses.
o Towers of Hanoi: Classic problem solvable using stack principles.

These concepts are fundamental in computer science and are widely used in algorithm design
and problem-solving.
UNIT-III

Queues: Abstract Data Types, Array Representation, Linked Representation,


and Applications

Queues are essential data structures that manage elements in a First-In-First-Out (FIFO)
manner. This section provides an overview of the queue abstract data type, its array and linked
representations, and an application example.

1. Abstract Data Type (ADT) for Queue

1.1 Definition:

A queue is an abstract data type that follows the First-In-First-Out (FIFO) principle. It supports
the following operations:

• Enqueue: Add an element to the rear of the queue.


• Dequeue: Remove an element from the front of the queue.
• Front/Peek: Retrieve the front element without removing it.
• isEmpty: Check if the queue is empty.
• isFull: Check if the queue is full (in fixed-size implementations).

1.2 Operations and Complexity:

• Enqueue: O(1)
• Dequeue: O(1)
• Front/Peek: O(1)
• isEmpty: O(1)
• isFull: O(1) (if applicable)

2. Array Representation of Queue

2.1 Definition:

An array-based queue uses a fixed-size array and two pointers (or indices) to manage the front
and rear of the queue.

2.2 Implementation:

#include <iostream>
#define MAX 100 // Define maximum size of queue

class Queue {
private:
int arr[MAX];
int front, rear, size;

public:
Queue() : front(0), rear(0), size(0) {}

bool isEmpty() {
return size == 0;
}

bool isFull() {
return size == MAX;
}

void enqueue(int x) {
if (isFull()) {
std::cout << "Queue Overflow" << std::endl;
return;
}
arr[rear] = x;
rear = (rear + 1) % MAX;
size++;
}

void dequeue() {
if (isEmpty()) {
std::cout << "Queue Underflow" << std::endl;
return;
}
front = (front + 1) % MAX;
size--;
}

int frontElement() {
if (isEmpty()) {
std::cout << "Queue is Empty" << std::endl;
return -1; // or some sentinel value
}
return arr[front];
}
};

2.3 Usage:

int main() {
Queue q;
q.enqueue(10);
q.enqueue(20);
std::cout << "Front element: " << q.frontElement() << std::endl;
q.dequeue();
std::cout << "Front element after dequeue: " << q.frontElement() <<
std::endl;
return 0;
}

3. Linked Representation of Queue


3.1 Definition:

A linked representation of a queue uses a linked list where each node points to the next node.
Two pointers, front and rear, manage the front and rear of the queue.

3.2 Implementation:

#include <iostream>

struct Node {
int data;
Node* next;
};

class Queue {
private:
Node* front;
Node* rear;

public:
Queue() : front(nullptr), rear(nullptr) {}

bool isEmpty() {
return front == nullptr;
}

void enqueue(int x) {
Node* newNode = new Node();
newNode->data = x;
newNode->next = nullptr;
if (isEmpty()) {
front = rear = newNode;
} else {
rear->next = newNode;
rear = newNode;
}
}

void dequeue() {
if (isEmpty()) {
std::cout << "Queue Underflow" << std::endl;
return;
}
Node* temp = front;
front = front->next;
delete temp;
if (front == nullptr) {
rear = nullptr;
}
}

int frontElement() {
if (isEmpty()) {
std::cout << "Queue is Empty" << std::endl;
return -1; // or some sentinel value
}
return front->data;
}
};

3.3 Usage:

int main() {
Queue q;
q.enqueue(10);
q.enqueue(20);
std::cout << "Front element: " << q.frontElement() << std::endl;
q.dequeue();
std::cout << "Front element after dequeue: " << q.frontElement() <<
std::endl;
return 0;
}

4. Applications of Queue

4.1 Railroad Car Arrangement

One interesting application of queues is the railroad car arrangement problem. In this problem,
train cars need to be arranged in a specific order using a stack (acting as a temporary holding
area) and a queue (for final arrangement).

Problem Description: Given a sequence of train cars, we want to rearrange them into a specific
order. We can only use a stack to temporarily hold the cars and a queue to achieve the final
arrangement.

Solution Approach:

1. Enqueue cars into the queue.


2. Dequeue cars and push them onto the stack until the stack's top matches the next car in the
desired order.
3. Pop from the stack and enqueue to the final arrangement queue.
4. Continue until the desired order is achieved.

Here’s a conceptual implementation in C++:

#include <iostream>
#include <queue>
#include <stack>

void rearrangeCars(std::queue<int>& inputQueue, std::queue<int>&


outputQueue) {
std::stack<int> tempStack;

while (!inputQueue.empty()) {
int car = inputQueue.front();
inputQueue.pop();
// Move cars to the stack until we find the correct order
while (!tempStack.empty() && tempStack.top() == outputQueue.front())
{
outputQueue.push(tempStack.top());
tempStack.pop();
}

// Push the current car onto the stack


tempStack.push(car);
}

// Transfer remaining cars from stack to output queue


while (!tempStack.empty()) {
outputQueue.push(tempStack.top());
tempStack.pop();
}
}

int main() {
std::queue<int> inputQueue;
std::queue<int> outputQueue;

// Input cars
inputQueue.push(1);
inputQueue.push(2);
inputQueue.push(3);
inputQueue.push(4);

// Desired order (example)


outputQueue.push(2);
outputQueue.push(1);
outputQueue.push(4);
outputQueue.push(3);

rearrangeCars(inputQueue, outputQueue);

std::cout << "Final arrangement: ";


while (!outputQueue.empty()) {
std::cout << outputQueue.front() << " ";
outputQueue.pop();
}
std::cout << std::endl;

return 0;
}
Summary

• Queue ADT: FIFO structure supporting enqueue, dequeue, front, and isEmpty operations.
• Array Representation: Fixed-size implementation with array and circular indexing.
• Linked Representation: Dynamic-size implementation using a linked list.
• Applications:
o Railroad Car Arrangement: Uses queues and stacks to rearrange train cars.

Understanding these concepts and implementations helps in efficiently managing and


processing data in various computational scenarios.

Hashing: Dictionaries, Linear Representation, Hash Table Representation


Hashing is a technique used to efficiently store and retrieve data. It involves mapping keys to
indices in a table using a hash function. This section covers dictionaries, linear representation,
and hash table representation in hashing.

1. Dictionaries

1.1 Definition:

A dictionary is a data structure that stores key-value pairs, where each key is unique and maps
to a specific value. It is also known as an associative array or map.

1.2 Operations:

• Insertion: Add a key-value pair to the dictionary.


• Deletion: Remove a key-value pair by key.
• Search: Retrieve the value associated with a given key.
• Update: Modify the value associated with a given key.

Example Implementation:

Using std::unordered_map in C++:

#include <iostream>
#include <unordered_map>
#include <string>

int main() {
std::unordered_map<std::string, int> dict;

// Insertion
dict["apple"] = 3;
dict["banana"] = 2;

// Search
if (dict.find("apple") != dict.end()) {
std::cout << "Apple count: " << dict["apple"] << std::endl;
}

// Update
dict["apple"] = 5;

// Deletion
dict.erase("banana");

// Display all key-value pairs


for (const auto& pair : dict) {
std::cout << pair.first << ": " << pair.second << std::endl;
}

return 0;
}
2. Linear Representation of Hashing

2.1 Definition:

Linear hashing techniques use a hash function to map keys to indices in an array. Collisions
(when multiple keys hash to the same index) are resolved using methods like linear probing.

2.2 Linear Probing:

Linear probing resolves collisions by searching for the next available slot in a sequential
manner.

Implementation:

#include <iostream>
#include <vector>

class HashTable {
private:
std::vector<int> table;
int size;
public:
HashTable(int s) : size(s) {
table.resize(size, -1); // Initialize table with -1 (indicating empty
slots)
}

int hashFunction(int key) {


return key % size;
}

void insert(int key) {


int index = hashFunction(key);
while (table[index] != -1) {
index = (index + 1) % size; // Linear probing
}
table[index] = key;
}

bool search(int key) {


int index = hashFunction(key);
int startIndex = index;
while (table[index] != -1) {
if (table[index] == key) return true;
index = (index + 1) % size;
if (index == startIndex) break; // Prevent infinite loop
}
return false;
}

void remove(int key) {


int index = hashFunction(key);
int startIndex = index;
while (table[index] != -1) {
if (table[index] == key) {
table[index] = -1; // Mark as deleted
return;
}
index = (index + 1) % size;
if (index == startIndex) break; // Prevent infinite loop
}
}
};

Usage:

int main() {
HashTable ht(10);
ht.insert(5);
ht.insert(15);

std::cout << "5 present: " << ht.search(5) << std::endl;


std::cout << "15 present: " << ht.search(15) << std::endl;

ht.remove(5);
std::cout << "5 present after removal: " << ht.search(5) << std::endl;

return 0;
}

3. Hash Table Representation

3.1 Definition:

A hash table is an implementation of a dictionary using a hash function to map keys to indices
in an array. Collisions are resolved through various methods, including chaining and open
addressing.

3.2 Chaining:

Chaining involves using a linked list (or another data structure) to handle collisions at each
index in the hash table.

Implementation:

#include <iostream>
#include <list>
#include <vector>

class HashTableChaining {
private:
std::vector<std::list<int>> table;
int size;
public:
HashTableChaining(int s) : size(s) {
table.resize(size);
}

int hashFunction(int key) {


return key % size;
}

void insert(int key) {


int index = hashFunction(key);
table[index].push_back(key);
}

bool search(int key) {


int index = hashFunction(key);
for (const int& k : table[index]) {
if (k == key) return true;
}
return false;
}

void remove(int key) {


int index = hashFunction(key);
table[index].remove(key);
}
};

Usage:

int main() {
HashTableChaining ht(10);
ht.insert(5);
ht.insert(15);

std::cout << "5 present: " << ht.search(5) << std::endl;


std::cout << "15 present: " << ht.search(15) << std::endl;

ht.remove(5);
std::cout << "5 present after removal: " << ht.search(5) << std::endl;

return 0;
}

3.3 Open Addressing:

Open addressing handles collisions by finding another open slot within the array using
techniques such as linear probing (discussed above), quadratic probing, or double hashing.

Quadratic Probing Implementation:

#include <iostream>
#include <vector>

class HashTableQuadraticProbing {
private:
std::vector<int> table;
std::vector<bool> occupied;
int size;

public:
HashTableQuadraticProbing(int s) : size(s) {
table.resize(size, -1);
occupied.resize(size, false);
}

int hashFunction(int key) {


return key % size;
}

void insert(int key) {


int index = hashFunction(key);
int i = 0;
while (occupied[(index + i * i) % size]) {
i++;
}
table[(index + i * i) % size] = key;
occupied[(index + i * i) % size] = true;
}

bool search(int key) {


int index = hashFunction(key);
int i = 0;
while (occupied[(index + i * i) % size]) {
if (table[(index + i * i) % size] == key) return true;
i++;
}
return false;
}

void remove(int key) {


int index = hashFunction(key);
int i = 0;
while (occupied[(index + i * i) % size]) {
if (table[(index + i * i) % size] == key) {
occupied[(index + i * i) % size] = false;
return;
}
i++;
}
}
};

Usage:

int main() {
HashTableQuadraticProbing ht(10);
ht.insert(5);
ht.insert(15);

std::cout << "5 present: " << ht.search(5) << std::endl;


std::cout << "15 present: " << ht.search(15) << std::endl;
ht.remove(5);
std::cout << "5 present after removal: " << ht.search(5) << std::endl;

return 0;
}
Summary

• Dictionaries: Key-value pair data structures allowing efficient lookup, insertion, and deletion.
• Linear Representation: Uses array and linear probing to handle collisions.
• Hash Table Representation: Includes chaining and open addressing (linear probing, quadratic
probing) for collision resolution.

Hashing techniques are crucial for optimizing performance in data retrieval and storage
operations across various applications.
UNIT-IV

Binary and Other Trees: An Overview

Trees are hierarchical data structures that are essential in various computational scenarios. This
section covers binary trees, their properties, representations, common operations, and traversal
methods, as well as the abstract data type (ADT) for binary trees and implementations using
linked structures.

1. Trees

1.1 Definition:

A tree is a non-linear data structure consisting of nodes connected by edges. Each node contains
a value and pointers to child nodes. The top node is called the root, and nodes without children
are called leaves.

1.2 Terminology:

• Root: The top node of the tree.


• Parent: A node that has one or more child nodes.
• Child: A node that is a descendant of another node.
• Leaf: A node with no children.
• Subtree: A tree formed by a node and its descendants.
• Height of a Tree: The length of the longest path from the root to a leaf.
• Depth of a Node: The length of the path from the root to the node.

2. Binary Trees

2.1 Definition:

A binary tree is a type of tree where each node has at most two children, typically referred to
as the left and right child.

2.2 Properties:

• Each node has at most two children.


• The left subtree contains nodes with values less than the root node.
• The right subtree contains nodes with values greater than the root node.

2.3 Representation:

Binary trees can be represented using nodes where each node has pointers to its left and right
children.
Node Structure in C++:

struct Node {
int data;
Node* left;
Node* right;

Node(int value) : data(value), left(nullptr), right(nullptr) {}


};

2.4 Common Binary Tree Operations:

• Insertion: Add a new node to the tree.


• Deletion: Remove a node from the tree.
• Searching: Find a node with a specific value.
• Traversal: Visit all nodes in a specific order.

Insertion Example:

void insert(Node*& root, int value) {


if (root == nullptr) {
root = new Node(value);
} else if (value < root->data) {
insert(root->left, value);
} else {
insert(root->right, value);
}
}

Deletion Example:

Node* deleteNode(Node* root, int key) {


if (root == nullptr) return nullptr;

if (key < root->data) {


root->left = deleteNode(root->left, key);
} else if (key > root->data) {
root->right = deleteNode(root->right, key);
} else {
if (root->left == nullptr) {
Node* temp = root->right;
delete root;
return temp;
} else if (root->right == nullptr) {
Node* temp = root->left;
delete root;
return temp;
}

Node* temp = minValueNode(root->right);


root->data = temp->data;
root->right = deleteNode(root->right, temp->data);
}
return root;
}

Search Example:

bool search(Node* root, int key) {


if (root == nullptr) return false;
if (root->data == key) return true;
return key < root->data ? search(root->left, key) : search(root->right,
key);
}

3. Binary Tree Traversal

Traversal refers to visiting all the nodes in a specific order. Common traversal methods include:

3.1 Inorder Traversal:

Visit left subtree, root, then right subtree.

void inorder(Node* root) {


if (root != nullptr) {
inorder(root->left);
std::cout << root->data << " ";
inorder(root->right);
}
}

3.2 Preorder Traversal:

Visit root, left subtree, then right subtree.

void preorder(Node* root) {


if (root != nullptr) {
std::cout << root->data << " ";
preorder(root->left);
preorder(root->right);
}
}

3.3 Postorder Traversal:

Visit left subtree, right subtree, then root.

void postorder(Node* root) {


if (root != nullptr) {
postorder(root->left);
postorder(root->right);
std::cout << root->data << " ";
}
}

4. Abstract Data Type (ADT) for Binary Tree

4.1 Definition:

The ADT for a binary tree defines the operations and properties without specifying the
implementation details.

ADT Operations:

• Insert: Add a node to the tree.


• Delete: Remove a node from the tree.
• Search: Find a node with a specific value.
• Traversal: Visit all nodes in a specific order.

5. Linked Binary Tree Class

5.1 Definition:

A linked binary tree class represents a binary tree using nodes with left and right pointers,
allowing for dynamic tree structures.

Linked Binary Tree Class Example:

class BinaryTree {
private:
Node* root;

void inorder(Node* node);


void preorder(Node* node);
void postorder(Node* node);

public:
BinaryTree() : root(nullptr) {}

void insert(int value);


void deleteNode(int value);
bool search(int value);
void inorderTraversal();
void preorderTraversal();
void postorderTraversal();
};

Class Implementation:
void BinaryTree::insert(int value) {
insert(root, value);
}

void BinaryTree::deleteNode(int value) {


root = deleteNode(root, value);
}

bool BinaryTree::search(int value) {


return search(root, value);
}

void BinaryTree::inorderTraversal() {
inorder(root);
std::cout << std::endl;
}

void BinaryTree::preorderTraversal() {
preorder(root);
std::cout << std::endl;
}

void BinaryTree::postorderTraversal() {
postorder(root);
std::cout << std::endl;
}

// Private traversal methods are the same as provided earlier


Summary

• Trees: Hierarchical structures with nodes and edges, where each node can have multiple
children.
• Binary Trees: Trees where each node has at most two children.
• Operations: Include insertion, deletion, searching, and traversal (inorder, preorder,
postorder).
• ADT Binary Tree: Defines operations and properties of binary trees without implementation
specifics.
• Linked Binary Tree Class: A practical implementation using linked nodes to create a dynamic
binary tree structure.

Understanding these concepts provides a solid foundation for working with hierarchical data
structures and performing operations efficiently.
UNIT-V

Priority Queues: Linear Lists, Heaps, and Applications

A priority queue is a data structure that allows for efficient retrieval and removal of elements
based on their priority. Unlike regular queues where elements are processed in a FIFO manner,
a priority queue processes elements according to their priority levels.

1. Priority Queues: Overview

1.1 Definition:

A priority queue is an abstract data type that supports operations to insert elements with an
associated priority and to remove the element with the highest priority.

1.2 Key Operations:

• Insert: Add an element with a specified priority.


• Extract-Max/Extract-Min: Remove and return the element with the highest/lowest priority.
• Peek/Front: Return the element with the highest/lowest priority without removing it.

2. Linear List Representation

2.1 Definition:

In a linear list representation of a priority queue, elements are stored in a list, and priorities are
managed explicitly.

2.2 Implementation:

Insertion: Insert an element into the list while maintaining order based on priority.

Extract-Max/Extract-Min: Find the element with the highest/lowest priority and remove it
from the list.

Example:

#include <iostream>
#include <list>
#include <algorithm>

class PriorityQueue {
private:
std::list<std::pair<int, int>> elements; // Pair of (priority, value)

public:
void insert(int value, int priority) {
elements.push_back({priority, value});
elements.sort([](const std::pair<int, int>& a, const std::pair<int,
int>& b) {
return a.first > b.first; // Sort in descending order of priority
});
}

int extractMax() {
if (elements.empty()) {
std::cerr << "Priority Queue is empty!" << std::endl;
return -1; // or some sentinel value
}
int value = elements.front().second;
elements.pop_front();
return value;
}

int peekMax() const {


if (elements.empty()) {
std::cerr << "Priority Queue is empty!" << std::endl;
return -1; // or some sentinel value
}
return elements.front().second;
}
};

Usage:

int main() {
PriorityQueue pq;
pq.insert(10, 2);
pq.insert(20, 1);
pq.insert(30, 3);

std::cout << "Max element: " << pq.peekMax() << std::endl;


std::cout << "Extracted Max: " << pq.extractMax() << std::endl;
std::cout << "New Max element: " << pq.peekMax() << std::endl;

return 0;
}

3. Heaps

3.1 Definition:

A heap is a complete binary tree that satisfies the heap property. There are two types of heaps:

• Max-Heap: The key at the root is the maximum among all keys in the heap, and each subtree
is a max-heap.
• Min-Heap: The key at the root is the minimum among all keys in the heap, and each subtree
is a min-heap.

3.2 Heap Operations:

• Insert: Add a new element while maintaining the heap property.


• Extract-Max/Extract-Min: Remove the root element and adjust the heap to maintain the heap
property.
• Peek/Front: Return the root element (max in a max-heap, min in a min-heap).

Implementation of a Max-Heap:

#include <iostream>
#include <vector>
#include <algorithm>

class MaxHeap {
private:
std::vector<int> heap;

void heapify(int index) {


int largest = index;
int left = 2 * index + 1;
int right = 2 * index + 2;

if (left < heap.size() && heap[left] > heap[largest])


largest = left;

if (right < heap.size() && heap[right] > heap[largest])


largest = right;

if (largest != index) {
std::swap(heap[index], heap[largest]);
heapify(largest);
}
}

public:
void insert(int value) {
heap.push_back(value);
int index = heap.size() - 1;

while (index != 0 && heap[(index - 1) / 2] < heap[index]) {


std::swap(heap[index], heap[(index - 1) / 2]);
index = (index - 1) / 2;
}
}

int extractMax() {
if (heap.empty()) {
std::cerr << "Heap is empty!" << std::endl;
return -1; // or some sentinel value
}

int root = heap[0];


heap[0] = heap.back();
heap.pop_back();
heapify(0);

return root;
}

int peekMax() const {


if (heap.empty()) {
std::cerr << "Heap is empty!" << std::endl;
return -1; // or some sentinel value
}
return heap[0];
}
};

Usage:

int main() {
MaxHeap maxHeap;
maxHeap.insert(10);
maxHeap.insert(20);
maxHeap.insert(30);

std::cout << "Max element: " << maxHeap.peekMax() << std::endl;


std::cout << "Extracted Max: " << maxHeap.extractMax() << std::endl;
std::cout << "New Max element: " << maxHeap.peekMax() << std::endl;

return 0;
}

4. Applications of Priority Queues

4.1 Heap Sorting:

Heap sorting is an efficient sorting algorithm that uses a binary heap. It works by building a
max-heap (or min-heap) from the input data and then repeatedly extracting the maximum (or
minimum) element to get the sorted output.

Heap Sort Implementation:

#include <iostream>
#include <vector>
#include <algorithm>

void heapify(std::vector<int>& arr, int n, int i) {


int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;

if (left < n && arr[left] > arr[largest])


largest = left;

if (right < n && arr[right] > arr[largest])


largest = right;

if (largest != i) {
std::swap(arr[i], arr[largest]);
heapify(arr, n, largest);
}
}

void heapSort(std::vector<int>& arr) {


int n = arr.size();

for (int i = n / 2 - 1; i >= 0; i--)


heapify(arr, n, i);

for (int i = n - 1; i >= 0; i--) {


std::swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}

Usage:

int main() {
std::vector<int> arr = {12, 11, 13, 5, 6, 7};
heapSort(arr);

std::cout << "Sorted array: ";


for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}
Summary

• Priority Queues: Data structures that allow elements to be processed based on priority.
• Linear List Representation: Simple but less efficient, using a sorted list to manage priorities.
• Heaps: Efficient data structures (max-heaps, min-heaps) for implementing priority queues.
• Heap Sorting: A sorting algorithm that utilizes the heap data structure to achieve efficient
sorting.

Priority queues are essential for various applications such as scheduling tasks, managing
resources, and implementing algorithms like Dijkstra's shortest path. Understanding these
concepts enables efficient management and processing of data based on priority.

Search Trees: Binary Search Trees (BST) Operations and Implementation

Search trees are hierarchical data structures used to manage and retrieve data efficiently. The
most common type of search tree is the Binary Search Tree (BST). This section covers BST
operations, implementation, and handling duplicates.

1. Binary Search Trees (BST)

1.1 Definition:

A Binary Search Tree (BST) is a binary tree in which each node has at most two children, and
the left child contains values less than the parent node, while the right child contains values
greater than the parent node. This property allows for efficient searching, insertion, and
deletion operations.

1.2 Properties:

• Inorder Traversal: Results in a sorted order of values.


• Search Operation: Average time complexity is O(log⁡n)O(\log n)O(logn) for balanced trees,
but can degrade to O(n)O(n)O(n) if the tree becomes unbalanced.

2. BST Operations

2.1 Insertion:

Inserting a new node involves finding the appropriate location in the tree where the new node
will maintain the BST property.

Insertion Algorithm:

1. Start at the root.


2. If the new value is less than the current node's value, move to the left child.
3. If the new value is greater, move to the right child.
4. Insert the new node when a suitable position (a null child) is found.

Insertion Implementation:

struct Node {
int data;
Node* left;
Node* right;

Node(int value) : data(value), left(nullptr), right(nullptr) {}


};

Node* insert(Node* root, int value) {


if (root == nullptr) {
return new Node(value);
}
if (value < root->data) {
root->left = insert(root->left, value);
} else {
root->right = insert(root->right, value);
}
return root;
}

2.2 Search:

Searching involves comparing the target value with the current node's value and recursively
moving left or right depending on whether the target is smaller or larger.
Search Implementation:

bool search(Node* root, int value) {


if (root == nullptr) return false;
if (root->data == value) return true;
return (value < root->data) ? search(root->left, value) : search(root-
>right, value);
}

2.3 Deletion:

Deletion requires three cases:

1. Node to delete has no children (leaf node): Simply remove the node.
2. Node to delete has one child: Replace the node with its child.
3. Node to delete has two children: Find the in-order successor (smallest value in the right
subtree), replace the node's value with the successor’s value, and delete the successor.

Deletion Implementation:

Node* minValueNode(Node* node) {


Node* current = node;
while (current && current->left != nullptr) {
current = current->left;
}
return current;
}

Node* deleteNode(Node* root, int key) {


if (root == nullptr) return nullptr;

if (key < root->data) {


root->left = deleteNode(root->left, key);
} else if (key > root->data) {
root->right = deleteNode(root->right, key);
} else {
if (root->left == nullptr) {
Node* temp = root->right;
delete root;
return temp;
} else if (root->right == nullptr) {
Node* temp = root->left;
delete root;
return temp;
}

Node* temp = minValueNode(root->right);


root->data = temp->data;
root->right = deleteNode(root->right, temp->data);
}
return root;
}

2.4 Traversal:
Traversal methods visit all nodes in a specific order:

• Inorder: Left subtree, root, right subtree.


• Preorder: Root, left subtree, right subtree.
• Postorder: Left subtree, right subtree, root.

Traversal Implementations:

void inorder(Node* root) {


if (root != nullptr) {
inorder(root->left);
std::cout << root->data << " ";
inorder(root->right);
}
}

void preorder(Node* root) {


if (root != nullptr) {
std::cout << root->data << " ";
preorder(root->left);
preorder(root->right);
}
}

void postorder(Node* root) {


if (root != nullptr) {
postorder(root->left);
postorder(root->right);
std::cout << root->data << " ";
}
}

3. Binary Search Trees with Duplicates

3.1 Handling Duplicates:

BSTs typically do not allow duplicate values. However, if duplicates are allowed, you need to
define a rule for their placement. Common strategies include:

• Insert duplicates in the right subtree if the new value is equal to the current node’s value.
• Insert duplicates in the left subtree if the new value is equal to the current node’s value.

Insertion with Duplicates Implementation:

Node* insertWithDuplicates(Node* root, int value) {


if (root == nullptr) {
return new Node(value);
}
if (value <= root->data) { // Allow duplicates on the right
root->right = insertWithDuplicates(root->right, value);
} else {
root->left = insertWithDuplicates(root->left, value);
}
return root;
}

Summary

• Binary Search Trees (BST): A hierarchical data structure where each node has at most two
children, with values organized to allow efficient searching and sorting.
• Operations: Include insertion, search, deletion, and various traversals.
• Handling Duplicates: Duplicates can be managed by defining specific rules for their placement
in the tree.

BSTs are foundational for many algorithms and data structures, and understanding their
operations and variations with duplicates is crucial for effective implementation and use in
applications.

You might also like