0% found this document useful (0 votes)
3 views71 pages

Data Structures Using C

This document is a comprehensive guide on data structures using the C programming language, aimed at computer science students. It covers fundamental concepts such as data objects, data structures, abstract data types, and algorithm analysis, including time and space complexity. The document also explores specific data structures like arrays, their applications, and algorithms for searching and sorting, providing clear examples and C code implementations.

Uploaded by

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

Data Structures Using C

This document is a comprehensive guide on data structures using the C programming language, aimed at computer science students. It covers fundamental concepts such as data objects, data structures, abstract data types, and algorithm analysis, including time and space complexity. The document also explores specific data structures like arrays, their applications, and algorithms for searching and sorting, providing clear examples and C code implementations.

Uploaded by

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

Data Structures using C

Introduction

Welcome to the world of Data Structures using C! This book is designed to be a


comprehensive guide for computer science students, providing a clear and easy-to-
understand explanation of fundamental data structures and their implementations in
the C programming language. Data structures are essential for organizing and storing
data efficiently, which is crucial for developing effective and optimized algorithms. A
strong grasp of data structures is a cornerstone for any aspiring programmer or
computer scientist.

In this book, we will explore various data structures, starting from the basics and
progressing to more complex concepts. Each topic will be explained with clear
examples, illustrations, and C code implementations to help you understand the
theoretical concepts and their practical applications. We will also delve into algorithm
analysis, which is vital for evaluating the efficiency of different data structures and
algorithms.

Unit 1: Introduction to Data Structures and Algorithm


Analysis

1.1 Data object, Data Structure, Abstract Data Type (ADT)

To begin our journey, let's clarify some fundamental terms. A data object is a named
place in memory that stores a value. It's the most basic unit of data. For example, an
integer variable int x; declares a data object x that can store an integer value.

A data structure is a particular way of organizing data in a computer so that it can be


used efficiently. It's a collection of data objects and the relationships between them.
Data structures are not just about storing data; they are about how data is arranged to
facilitate specific operations, such as searching, sorting, insertion, and deletion. Think
of it like organizing your books on a shelf: you could stack them randomly, or you
could arrange them alphabetically by author, or by genre. Each method of
organization is a data structure, and each makes certain tasks (like finding a specific
book) easier or harder.

An Abstract Data Type (ADT) is a mathematical model for data types. It defines the
logical properties of a data type, such as the values it can hold and the operations that
can be performed on it, without specifying how these properties are implemented. In
simpler terms, an ADT tells you what a data structure does, but not how it does it. For
example, a

List ADT defines operations like add , remove , get , and size , but doesn't specify
whether it's implemented using an array or a linked list. This abstraction allows
programmers to use the data structure without worrying about its underlying
implementation details, promoting modularity and reusability.

1.2 Types of Data Structures

Data structures can be broadly classified into two categories: linear and non-linear
data structures.

Linear Data Structures: In linear data structures, data elements are arranged
sequentially, one after another. Each element has a predecessor and a successor,
except for the first and last elements. This arrangement makes it easy to traverse all
elements in a single run. Examples of linear data structures include:

Arrays: A collection of elements of the same data type stored at contiguous


memory locations. Elements are accessed using an index.

Linked Lists: A collection of nodes where each node contains data and a pointer
(or reference) to the next node in the sequence. Unlike arrays, elements are not
stored contiguously.

Stacks: A linear data structure that follows the Last-In, First-Out (LIFO) principle.
Operations are performed only at one end, called the 'top'.

Queues: A linear data structure that follows the First-In, First-Out (FIFO)
principle. Elements are added at one end (rear) and removed from the other end
(front).

Non-linear Data Structures: In non-linear data structures, data elements are not
arranged sequentially. Instead, they are organized in a hierarchical or network-like
manner. Each element can have multiple predecessors and successors, making them
more complex but also more flexible for representing complex relationships. Examples
of non-linear data structures include:

Trees: A hierarchical data structure consisting of nodes connected by edges. It


has a root node, and each node can have zero or more child nodes. Trees are
widely used to represent hierarchical relationships, such as file systems or
organizational charts.

Graphs: A collection of nodes (vertices) and edges that connect pairs of nodes.
Graphs are used to model relationships between objects, such as social
networks, road maps, or computer networks.

1.3 Algorithm analysis - Space complexity, time complexity

When we talk about data structures and algorithms, it's crucial to understand how
efficient they are. Algorithm analysis is the process of determining the amount of
resources (like time and space) required by an algorithm to solve a given problem. This
helps us compare different algorithms and choose the most efficient one for a
particular task. The two primary measures of efficiency are time complexity and
space complexity.

Time Complexity: Time complexity measures the amount of time an algorithm takes
to run as a function of the input size. It's not about the actual execution time in
seconds, which can vary depending on the hardware, programming language, and
other factors. Instead, it's about how the number of operations grows with the input
size. We typically express time complexity using Big O notation (O), which describes
the upper bound or worst-case scenario of an algorithm's growth rate. For example:

O(1) - Constant Time: The execution time remains constant regardless of the
input size. Accessing an element in an array by its index is an O(1) operation.

O(log n) - Logarithmic Time: The execution time grows logarithmically with the
input size. This often occurs in algorithms that divide the problem into smaller
halves in each step, like binary search.

O(n) - Linear Time: The execution time grows linearly with the input size.
Traversing a linked list or searching for an element in an unsorted array are O(n)
operations.
O(n log n) - Linearithmic Time: The execution time grows proportionally to n
log n. Many efficient sorting algorithms, like Merge Sort and Quick Sort, have this
time complexity.

O(n^2) - Quadratic Time: The execution time grows quadratically with the input
size. This often occurs in algorithms with nested loops, like simple sorting
algorithms such as Bubble Sort or Insertion Sort.

O(2^n) - Exponential Time: The execution time doubles with each addition to
the input size. These algorithms are usually impractical for even moderately
sized inputs.

O(n!) - Factorial Time: The execution time grows extremely rapidly. These
algorithms are typically only feasible for very small input sizes.

Space Complexity: Space complexity measures the amount of memory space an


algorithm requires to run as a function of the input size. This includes the space used
by the input itself, as well as any auxiliary space used by the algorithm (e.g., for
variables, data structures, or recursion stack). Similar to time complexity, space
complexity is also expressed using Big O notation. For example:

O(1) - Constant Space: The algorithm uses a constant amount of memory


regardless of the input size. This is ideal for memory efficiency.

O(n) - Linear Space: The memory usage grows linearly with the input size. For
example, if an algorithm creates a copy of the input array, its space complexity
would be O(n).

O(log n) - Logarithmic Space: The memory usage grows logarithmically with the
input size. This can occur in recursive algorithms where the depth of recursion is
logarithmic.

Understanding both time and space complexity is crucial for designing efficient
algorithms and choosing the right data structure for a given problem. Often, there's a
trade-off between time and space; an algorithm that is very fast might require a lot of
memory, and vice-versa. The goal is to find an optimal balance based on the specific
requirements of the application.
Unit 2: Array

2.1 ADT - Array

An Array is one of the simplest and most fundamental data structures. It is a collection
of elements, all of the same data type, stored in contiguous memory locations. This
contiguous storage is what makes arrays highly efficient for certain operations,
particularly direct access to elements. In C, arrays are declared with a fixed size,
meaning the number of elements they can hold is determined at compile time or
runtime and cannot be changed later. This fixed size is a key characteristic of arrays.

Key Characteristics of Arrays:

Homogeneous Elements: All elements in an array must be of the same data type
(e.g., all integers, all characters, all floats).

Contiguous Memory Allocation: Elements are stored in adjacent memory


locations. This allows for very fast access to any element.

Fixed Size: Once an array is declared with a certain size, its size cannot be
changed during program execution. If you need more space, you typically have to
create a new, larger array and copy the elements.

Direct Access (Random Access): Elements can be accessed directly using their
index. The index typically starts from 0 for the first element, 1 for the second, and
so on. This means accessing the first element is as fast as accessing the last
element.

Array as an ADT:

As an Abstract Data Type, an Array can be thought of as a collection of elements, each


identified by an index. The operations typically associated with an Array ADT include:

create(size) : Initializes an array of a specified size.

get(index) : Retrieves the element at a given index.

set(index, value) : Stores a value at a given index.

size() : Returns the total number of elements the array can hold.

While these are the core operations, other operations like insert (at a specific
position), delete (at a specific position), and search are also commonly performed
on arrays, though their efficiency can vary significantly depending on the
implementation.

Declaration and Initialization in C:

In C, an array can be declared and initialized in several ways:

// Declaration of an integer array of size 5


int numbers[5];

// Initialization at declaration time


int scores[3] = {100, 95, 88};

// Initialization without specifying size (compiler determines size)


int grades[] = {90, 85, 78, 92};

// Accessing elements
printf("First score: %d\n", scores[0]); // Output: 100

// Modifying elements
numbers[0] = 10;
numbers[1] = 20;

Arrays are fundamental to programming and serve as the building blocks for many
other complex data structures. Their simplicity and efficiency for direct access make
them invaluable for various applications.

2.2 Applications of Arrays

Arrays are incredibly versatile and are used in a wide range of applications due to their
direct access capabilities and efficient storage of homogeneous data. Some common
applications include:

Storing Lists of Data: The most straightforward application is to store a


collection of items, such as a list of student grades, product prices, or
temperatures.

Implementing Other Data Structures: Arrays are often used as the underlying
storage mechanism for other data structures like stacks, queues, hash tables, and
even for representing graphs (e.g., adjacency matrix).

Matrix Operations: Multi-dimensional arrays are perfect for representing


matrices and performing mathematical operations on them, such as addition,
multiplication, and transposition.
Image Processing: Images can be represented as 2D or 3D arrays of pixel values,
where each element stores the color intensity or RGB values of a pixel.

Database Records: In some simplified database systems, records can be stored


as arrays of structures or arrays of arrays.

Sorting and Searching Algorithms: Many sorting algorithms (like Bubble Sort,
Selection Sort, Insertion Sort, Merge Sort, Quick Sort) and searching algorithms
(like Linear Search, Binary Search) operate directly on arrays.

Let's delve into two crucial applications of arrays: searching and sorting.

2.2.1 Searching - Binary Search

Searching is the process of finding a specific element within a collection of elements.


While a simple linear search can be performed on any array, Binary Search is a much
more efficient algorithm for searching in sorted arrays. Its efficiency stems from its
divide-and-conquer approach.

How Binary Search Works:

1. Start in the Middle: The algorithm begins by comparing the target element with
the middle element of the sorted array.

2. Divide and Conquer:


If the target element matches the middle element, the search is successful.

If the target element is smaller than the middle element, the search
continues in the left half of the array.

If the target element is larger than the middle element, the search
continues in the right half of the array.

3. Repeat: This process is repeated on the selected half until the element is found
or the search space is exhausted.

Prerequisite: The array must be sorted for Binary Search to work correctly.

Time Complexity: The time complexity of Binary Search is O(log n). This is because
with each comparison, the search space is halved, significantly reducing the number of
operations required as the input size grows. For an array of 1 million elements, a linear
search might take up to 1 million comparisons in the worst case, while a binary search
would take at most about 20 comparisons (log₂ 1,000,000 ≈ 19.9).
Example Implementation in C (Iterative):

#include <stdio.h>

int binarySearch(int arr[], int size, int target) {


int low = 0;
int high = size - 1;

while (low <= high) {


int mid = low + (high - low) / 2; // To prevent potential overflow

if (arr[mid] == target) {
return mid; // Element found, return its index
} else if (arr[mid] < target) {
low = mid + 1; // Target is in the right half
} else {
high = mid - 1; // Target is in the left half
}
}
return -1; // Element not found
}

int main() {
int arr[] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
int size = sizeof(arr) / sizeof(arr[0]);
int target = 23;

int result = binarySearch(arr, size, target);

if (result != -1) {
printf("Element %d found at index %d\n", target, result);
} else {
printf("Element %d not found in the array\n", target);
}

target = 10;
result = binarySearch(arr, size, target);
if (result != -1) {
printf("Element %d found at index %d\n", target, result);
} else {
printf("Element %d not found in the array\n", target);
}

return 0;
}

2.2.2 Sorting - Insertion Sort, Merge Sort, Quick Sort

Sorting is the process of arranging elements in a specific order (ascending or


descending). It's a fundamental operation in computer science, as many algorithms
(like Binary Search) require sorted data, and sorted data is generally easier to process
and analyze. We will discuss three important sorting algorithms here:

1. Insertion Sort
Insertion Sort is a simple sorting algorithm that builds the final sorted array (or list)
one item at a time. It is much less efficient on large lists than more advanced
algorithms such as quicksort, heapsort, or merge sort. However, it has some
advantages:

Simple Implementation: It's easy to understand and implement.

Efficient for Small Data Sets: It is efficient for small data sets or data sets that
are already substantially sorted.

Stable: It maintains the relative order of elements with equal values.

In-place: It sorts the array without requiring extra space.

How Insertion Sort Works:

Imagine you have a hand of cards, and you want to sort them. You pick up one card at
a time and insert it into its correct position among the cards already in your hand.
Insertion sort works similarly:

1. The array is conceptually divided into a sorted and an unsorted part.

2. Initially, the first element is considered sorted.

3. For each subsequent element in the unsorted part, it is picked and compared
with elements in the sorted part.

4. Elements in the sorted part that are greater than the picked element are shifted
one position to the right to make space.

5. The picked element is then inserted into its correct position.

Time Complexity:

Worst Case: O(n^2) (e.g., reverse sorted array) - Each element might need to be
compared with and shifted past all elements in the sorted part.

Best Case: O(n) (e.g., already sorted array) - Only one comparison is needed for
each element.

Average Case: O(n^2)

Example Implementation in C:
#include <stdio.h>

void insertionSort(int arr[], int n) {


int i, key, j;
for (i = 1; i < n; i++) {
key = arr[i];
j = i - 1;

// Move elements of arr[0..i-1], that are


// greater than key, to one position ahead
// of their current position
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}

void printArray(int arr[], int size) {


int i;
for (i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}

int main() {
int arr[] = {12, 11, 13, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);

printf("Original array: ");


printArray(arr, n);

insertionSort(arr, n);

printf("Sorted array (Insertion Sort): ");


printArray(arr, n);
return 0;
}

2. Merge Sort

Merge Sort is a highly efficient, comparison-based sorting algorithm. It is a divide-


and-conquer algorithm. It works by recursively dividing an array into two halves,
sorting each half, and then merging the sorted halves back together.

How Merge Sort Works:

1. Divide: The unsorted list is divided into n sublists, each containing one element
(a list of one element is considered sorted).

2. Conquer (Recursively Sort): Repeatedly merge sublists to produce new sorted


sublists until there is only one sublist remaining. This remaining sublist will be
the sorted list.
3. Combine (Merge): The merging process is crucial. Two sorted sub-arrays are
combined to form a single sorted array. This is done by comparing the first
elements of both sub-arrays and repeatedly picking the smaller element to place
into the merged array.

Time Complexity: The time complexity of Merge Sort is O(n log n) in all cases (worst,
average, and best). This makes it a very reliable sorting algorithm, especially for large
datasets.

Space Complexity: Merge Sort typically requires O(n) auxiliary space because it
needs temporary arrays during the merging process. This can be a disadvantage for
memory-constrained environments.

Example Implementation in C:
#include <stdio.h>
#include <stdlib.h>

// Merges two subarrays of arr[].


// First subarray is arr[l..m]
// Second subarray is arr[m+1..r]
void merge(int arr[], int l, int m, int r) {
int i, j, k;
int n1 = m - l + 1;
int n2 = r - m;

// Create temporary arrays


int L[n1], R[n2];

// Copy data to temp arrays L[] and R[]


for (i = 0; i < n1; i++)
L[i] = arr[l + i];
for (j = 0; j < n2; j++)
R[j] = arr[m + 1 + j];

// Merge the temp arrays back into arr[l..r]


i = 0; // Initial index of first subarray
j = 0; // Initial index of second subarray
k = l; // Initial index of merged subarray
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}

// Copy the remaining elements of L[], if there are any


while (i < n1) {
arr[k] = L[i];
i++;
k++;
}

// Copy the remaining elements of R[], if there are any


while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}

// l is for left index and r is right index of the sub-array of arr to be


sorted
void mergeSort(int arr[], int l, int r) {
if (l < r) {
// Same as (l+r)/2, but avoids overflow for large l and r
int m = l + (r - l) / 2;

// Sort first and second halves


mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
}

int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int arr_size = sizeof(arr) / sizeof(arr[0]);

printf("Original array: ");


printArray(arr, arr_size);

mergeSort(arr, 0, arr_size - 1);

printf("Sorted array (Merge Sort): ");


printArray(arr, arr_size);
return 0;
}

3. Quick Sort

Quick Sort is another highly efficient, comparison-based sorting algorithm that also
follows the divide-and-conquer paradigm. It is generally considered one of the fastest
sorting algorithms in practice for large datasets. Its efficiency comes from its ability to
reduce the problem into smaller, independent sub-problems.

How Quick Sort Works:

1. Choose a Pivot: Select an element from the array, called the 'pivot'. The choice
of pivot significantly impacts performance. Common strategies include picking
the first, last, middle, or a random element.

2. Partition: Rearrange the array such that all elements smaller than the pivot
come before it, and all elements greater than the pivot come after it. Elements
equal to the pivot can go on either side. After partitioning, the pivot is in its final
sorted position.

3. Recursively Sort Sub-arrays: Recursively apply Quick Sort to the sub-array of


elements with smaller values and separately to the sub-array of elements with
greater values.

Time Complexity:

Worst Case: O(n^2) (e.g., already sorted array or reverse sorted array, if a bad
pivot choice is made consistently) - This happens when the partition always
results in one sub-array with n-1 elements and another with 0 elements.

Best Case: O(n log n) (e.g., pivot always divides the array into two roughly equal
halves).
Average Case: O(n log n) - In practice, Quick Sort performs very well on average.

Space Complexity: Quick Sort is an in-place sorting algorithm, meaning it typically


requires O(log n) auxiliary space due to the recursion stack. In the worst case, it can
be O(n) if the recursion depth is n.

Example Implementation in C:
#include <stdio.h>

// Function to swap two elements


void swap(int* a, int* b) {
int t = *a;
*a = *b;
*b = t;
}

// This function takes last element as pivot, places


// the pivot element at its correct position in sorted
// array, and places all smaller (smaller than pivot)
// to left of pivot and all greater elements to right
// of pivot
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // pivot
int i = (low - 1); // Index of smaller element

for (int j = low; j <= high - 1; j++) {


// If current element is smaller than the pivot
if (arr[j] < pivot) {
i++; // increment index of smaller element
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]);
return (i + 1);
}

// The main function that implements QuickSort


// arr[] --> Array to be sorted,
// low --> Starting index,
// high --> Ending index
void quickSort(int arr[], int low, int high) {
if (low < high) {
// pi is partitioning index, arr[pi] is now
// at right place
int pi = partition(arr, low, high);

// Separately sort elements before


// partition and after partition
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}

int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]);

printf("Original array: ");


printArray(arr, n);

quickSort(arr, 0, n - 1);

printf("Sorted array (Quick Sort): ");


printArray(arr, n);
return 0;
}
Strings as Character Arrays

In C programming, strings are essentially arrays of characters. A string is a sequence of


characters terminated by a null character ( \0 ). This null character signifies the end of
the string, allowing functions to determine its length. Understanding strings as
character arrays is fundamental to manipulating text data in C.

Declaration and Initialization of Strings:

Strings can be declared and initialized in several ways:

// Declaration of a character array (string)


char str1[20];

// Initialization at declaration time


char str2[] = "Hello"; // Compiler automatically adds null terminator and
determines size
char str3[10] = "World"; // Size must be at least length of string + 1 for null
terminator

// Accessing characters
printf("First character of str2: %c\n", str2[0]); // Output: H

// Iterating through a string


printf("str2: ");
for (int i = 0; str2[i] != '\0'; i++) {
printf("%c", str2[i]);
}
printf("\n");

String Manipulation Functions (from <string.h> ):

C provides a rich set of library functions in the <string.h> header for manipulating
strings. Some of the most commonly used functions include:

strlen(str) : Returns the length of the string str , excluding the null
terminator.

strcpy(destination, source) : Copies the source string to the destination


string.

strcat(destination, source) : Concatenates (joins) the source string to the


end of the destination string.

strcmp(str1, str2) : Compares two strings lexicographically. Returns 0 if


strings are equal, a negative value if str1 is less than str2 , and a positive value
if str1 is greater than str2 .
strncpy(destination, source, n) : Copies at most n characters from source
to destination .

strncat(destination, source, n) : Appends at most n characters from


source to destination .

strncmp(str1, str2, n) : Compares at most n characters of two strings.

Example of String Functions:

#include <stdio.h>
#include <string.h>

int main() {
char s1[50] = "Programming";
char s2[50] = " in C";
char s3[50];
int len;

// strlen example
len = strlen(s1);
printf("Length of s1: %d\n", len); // Output: 11

// strcpy example
strcpy(s3, s1);
printf("s3 after strcpy: %s\n", s3); // Output: Programming

// strcat example
strcat(s1, s2);
printf("s1 after strcat: %s\n", s1); // Output: Programming in C

// strcmp example
char s4[] = "apple";
char s5[] = "banana";
char s6[] = "apple";

printf("strcmp(s4, s5): %d\n", strcmp(s4, s5)); // Output: a negative value


printf("strcmp(s5, s4): %d\n", strcmp(s5, s4)); // Output: a positive value
printf("strcmp(s4, s6): %d\n", strcmp(s4, s6)); // Output: 0

return 0;
}

Understanding strings as character arrays and utilizing the standard library functions
is crucial for effective text processing in C. While simple, they form the basis for more
complex text manipulation and parsing tasks.
Unit 3: Linked List

3.1 Basics of Linked List

A Linked List is a linear data structure, similar to an array, but it stores elements non-
contiguously. Instead of storing data in adjacent memory locations, a linked list stores
elements at arbitrary locations and links them together using pointers. Each element
in a linked list is called a node. A node typically consists of two parts:

1. Data Part: Stores the actual value or data.

2. Pointer (or Link) Part: Stores the address of the next node in the sequence.

The first node of the linked list is called the head. The head pointer stores the address
of the first node. If the list is empty, the head pointer is NULL . The last node in the
linked list points to NULL , indicating the end of the list.

Key Characteristics of Linked Lists:

Dynamic Size: Unlike arrays, linked lists can grow or shrink in size during
runtime. Memory is allocated dynamically as needed.

Non-contiguous Memory Allocation: Elements are not stored in adjacent


memory locations. This allows for efficient insertion and deletion of elements
anywhere in the list without shifting elements.

No Random Access: Elements cannot be accessed directly by an index. To access


an element, you must traverse the list from the head until you reach the desired
position.

Efficient Insertions and Deletions: Inserting or deleting an element in a linked


list typically involves changing a few pointers, which is an O(1) operation once
the position is found. In contrast, arrays require shifting elements, which can be
O(n).

More Memory Overhead: Each node requires extra memory for the pointer part,
which can be a disadvantage compared to arrays for storing simple data types.

Advantages of Linked Lists over Arrays:

Dynamic Size: No need to pre-allocate memory. Can grow or shrink as needed.


Ease of Insertion/Deletion: Elements can be inserted or deleted without shifting
other elements.

Disadvantages of Linked Lists over Arrays:

No Random Access: Accessing an element takes O(n) time in the worst case.

More Memory: Requires extra space for pointers.

Cache Performance: Due to non-contiguous memory, linked lists can have


poorer cache performance compared to arrays.

3.2 Types of Linked List - Singly, Doubly, Circular linked list

Linked lists come in various forms, each with its own advantages and use cases:

1. Singly Linked List:

This is the simplest form of a linked list, as described above. Each node contains data
and a pointer to the next node. Traversal is possible only in one direction (forward).

Structure of a Singly Linked List Node in C:

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

2. Doubly Linked List:

In a doubly linked list, each node contains data, a pointer to the next node, and a
pointer to the previous node. This allows for traversal in both forward and backward
directions.

Advantages:

Can traverse in both directions.

Deletion of a given node is more efficient as you have access to the previous node
directly.

Disadvantages:

Requires more memory per node (for the prev pointer).


Insertion and deletion operations are slightly more complex due to managing
two pointers.

Structure of a Doubly Linked List Node in C:

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

3. Circular Linked List:

In a circular linked list, the last node points back to the first node (head), forming a
circle. This can be a singly or doubly linked list. There is no NULL pointer at the end of
the list.

Advantages:

Can traverse the entire list starting from any node.

Useful for applications where elements need to be accessed in a circular fashion


(e.g., round-robin scheduling).

Disadvantages:

Care must be taken to avoid infinite loops during traversal if not handled
correctly.

Structure of a Circular Singly Linked List Node in C:

struct Node {
int data;
struct Node* next;
}; // The last node's next pointer points to the head

3.3 Operations on Linked List - create, traverse, insert, delete, search

Let's explore the fundamental operations performed on a singly linked list. These
operations form the building blocks for more complex linked list applications.

1. Create (Insertion at the beginning):


Creating a linked list often starts with inserting the first node, or adding new nodes to
the beginning of an existing list. This is one of the simplest insertion operations.

#include <stdio.h>
#include <stdlib.h>

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

// Function to insert a new node at the beginning of the list


struct Node* insertAtBeginning(struct Node* head, int new_data) {
// Allocate memory for new node
struct Node* new_node = (struct Node*) malloc(sizeof(struct Node));

// Put in the data


new_node->data = new_data;

// Link the new node to the old head


new_node->next = head;

// Move the head to point to the new node


head = new_node;

return head;
}

// Function to print the linked list


void printList(struct Node* node) {
while (node != NULL) {
printf(" %d", node->data);
node = node->next;
}
printf("\n");
}

int main() {
struct Node* head = NULL; // Start with an empty list

head = insertAtBeginning(head, 10);


head = insertAtBeginning(head, 20);
head = insertAtBeginning(head, 30);

printf("Created Linked List: ");


printList(head);

return 0;
}

2. Traverse:

Traversing a linked list means visiting each node in the list, typically from the head to
the tail. This operation is essential for printing the list, searching for an element, or
performing any operation on all elements.
(See printList function in the insertAtBeginning example above for traversal
implementation.)

3. Insert (at a specific position - after a given node, at the end):

Inserting a node can be done at the beginning (as shown above), after a specific node,
or at the end of the list.

Insertion after a given node:

// Function to insert a new node after a given prev_node


void insertAfter(struct Node* prev_node, int new_data) {
// 1. Check if the given prev_node is NULL
if (prev_node == NULL) {
printf("The given previous node cannot be NULL");
return;
}

// 2. Allocate new node


struct Node* new_node = (struct Node*) malloc(sizeof(struct Node));

// 3. Put in the data


new_node->data = new_data;

// 4. Make next of new node as next of prev_node


new_node->next = prev_node->next;

// 5. Move the next of prev_node as new_node


prev_node->next = new_node;
}

// Example usage in main:


// insertAfter(head->next, 25); // Inserts 25 after the second node

Insertion at the end:


// Function to insert a new node at the end of the list
struct Node* insertAtEnd(struct Node* head, int new_data) {
// 1. Allocate new node
struct Node* new_node = (struct Node*) malloc(sizeof(struct Node));

// 2. Put in the data


new_node->data = new_data;

// 3. This new node is going to be the last node, so make its next as NULL
new_node->next = NULL;

// 4. If the Linked List is empty, then make the new node as head
if (head == NULL) {
head = new_node;
return head;
}

// 5. Else traverse till the last node


struct Node* last = head;
while (last->next != NULL) {
last = last->next;
}

// 6. Change the next of last node


last->next = new_node;
return head;
}

// Example usage in main:


// head = insertAtEnd(head, 5);

4. Delete:

Deleting a node from a linked list involves removing a specific node, which can be the
head, a node in the middle, or the tail. The key is to correctly update the pointers of
the surrounding nodes.

Deletion of a given key (data value):


// Function to delete the first occurrence of a given key in linked list
struct Node* deleteNode(struct Node* head, int key) {
struct Node *temp = head, *prev = NULL;

// If head node itself holds the key to be deleted


if (temp != NULL && temp->data == key) {
head = temp->next; // Changed head
free(temp); // Free old head
return head;
}

// Search for the key to be deleted, keep track of the previous node
// as we need to change 'prev->next'
while (temp != NULL && temp->data != key) {
prev = temp;
temp = temp->next;
}

// If key was not present in linked list


if (temp == NULL) return head;

// Unlink the node from linked list


prev->next = temp->next;

free(temp); // Free memory


return head;
}

// Example usage in main:


// head = deleteNode(head, 20);

5. Search:

Searching for an element in a linked list involves traversing the list from the head and
comparing the data of each node with the target value until a match is found or the
end of the list is reached.

// Function to search for a key in a linked list


bool search(struct Node* head, int key) {
struct Node* current = head; // Initialize current
while (current != NULL) {
if (current->data == key)
return true;
current = current->next;
}
return false;
}

// Example usage in main:


// if (search(head, 20)) {
// printf("20 is found in the list.\n");
// } else {
// printf("20 is not found in the list.\n");
// }
3.4 Applications of Linked List - Polynomial representation

Linked lists are used in various applications where dynamic data storage and efficient
insertions/deletions are required. One significant application is the representation of
polynomials.

A polynomial can be represented as a collection of terms, where each term has a


coefficient and an exponent. For example, the polynomial 5x^3 + 2x^1 + 7x^0 (or
5x^3 + 2x + 7 ) has three terms: (5, 3) , (2, 1) , and (7, 0) .

Using a linked list, each node can represent a term in the polynomial. A node would
typically contain:

coefficient : The coefficient of the term.

exponent : The exponent of the term.

next : A pointer to the next term in the polynomial.

This representation is particularly useful because polynomials can have varying


numbers of terms, and many terms might have zero coefficients (which can be
omitted). A linked list allows for flexible storage without wasting space for zero terms,
unlike an array-based representation which would require storing all possible
exponents up to the highest degree.

Example Structure for a Polynomial Term Node:

struct PolyNode {
int coeff;
int exp;
struct PolyNode* next;
};

Operations on Polynomials using Linked Lists:

Common operations like addition, subtraction, and multiplication of polynomials can


be efficiently implemented using this linked list representation. For example, to add
two polynomials, you would traverse both linked lists, adding coefficients of terms
with the same exponent and creating new nodes for terms that exist in only one
polynomial.

This dynamic nature of linked lists makes them a suitable choice for representing
sparse polynomials (polynomials with many zero terms) and for performing algebraic
operations on them.

Unit 4: Stack

4.1 Introduction to Stack

A Stack is a linear data structure that follows a particular order in which operations are
performed. The order is LIFO (Last In, First Out) or FILO (First In, Last Out). This
means the element that is inserted last is the first one to be removed. Think of a stack
of plates: you can only add a new plate to the top, and you can only remove the
topmost plate. The plate that was put on last is the first one you take off.

Key Characteristics of Stacks:

LIFO Principle: The last element added is the first one to be removed.

Single End Operations: All operations (insertion and deletion) occur at one end,
called the top of the stack.

Abstract Data Type: A stack is an ADT, meaning its behavior is defined by its
operations, not by its implementation.

Common Stack Operations:

push() : Adds an element to the top of the stack.

pop() : Removes the element from the top of the stack. It also returns the
removed element.

peek() or top() : Returns the top element of the stack without removing it.

isEmpty() : Checks if the stack is empty. Returns true if empty, false otherwise.

isFull() : Checks if the stack is full. This operation is relevant only for fixed-size
(array-based) stack implementations.

4.2 Implementation of Stack - Static and Dynamic

Stacks can be implemented in two primary ways: using arrays (static implementation)
or using linked lists (dynamic implementation).
Static Implementation (Array-based Stack)

An array-based stack is straightforward to implement. A fixed-size array is used to store


the stack elements, and a variable (often called top or _top ) is used to keep track of
the index of the topmost element in the stack. Initially, top is set to -1 to indicate an
empty stack.

Advantages:

Simple to implement.

Memory efficiency: No overhead for pointers (compared to linked list


implementation).

Faster access: Elements are stored contiguously, potentially leading to better


cache performance.

Disadvantages:

Fixed size: The maximum size of the stack must be defined at compile time. If the
stack overflows (attempts to push an element onto a full stack), it can lead to
errors.

Wasted space: If the stack is not always full, some memory allocated for the
array might be unused.

Structure for Array-based Stack:

#define MAX_SIZE 100 // Define maximum size of the stack

struct Stack {
int arr[MAX_SIZE];
int top;
};

Dynamic Implementation (Linked List-based Stack)

A linked list-based stack uses a singly linked list to store the stack elements. The push
and pop operations are performed at the head of the linked list, as this is the most
efficient place for insertions and deletions in a singly linked list (O(1) time complexity).
The head pointer of the linked list acts as the top of the stack.

Advantages:
Dynamic size: The stack can grow or shrink as needed, limited only by available
memory. No overflow issues due to fixed size.

No wasted space: Memory is allocated only when an element is pushed onto the
stack.

Disadvantages:

Memory overhead: Each node requires extra memory for the pointer.

Slightly slower access: Due to non-contiguous memory allocation, it might have


slightly worse cache performance.

More complex to implement than array-based.

Structure for Linked List-based Stack:

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

struct Stack {
struct Node* top;
};

4.3 Operations on Stack - init(), push(), pop(), isEmpty(), isFull(),


peek()

Let's look at the implementation of the core stack operations using both array-based
and linked list-based approaches.
Array-based Stack Operations

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define MAX_SIZE 5 // For demonstration, a small size

struct Stack {
int arr[MAX_SIZE];
int top;
};

// Initialize the stack


void initStack(struct Stack* s) {
s->top = -1; // -1 indicates an empty stack
}

// Check if the stack is empty


bool isEmpty(struct Stack* s) {
return (s->top == -1);
}

// Check if the stack is full


bool isFull(struct Stack* s) {
return (s->top == MAX_SIZE - 1);
}

// Push an element onto the stack


void push(struct Stack* s, int item) {
if (isFull(s)) {
printf("Stack Overflow! Cannot push %d\n", item);
return;
}
s->arr[++(s->top)] = item;
printf("Pushed %d to stack.\n", item);
}

// Pop an element from the stack


int pop(struct Stack* s) {
if (isEmpty(s)) {
printf("Stack Underflow! Cannot pop from empty stack.\n");
return -1; // Or throw an error, depending on design
}
return s->arr[(s->top)--];
}

// Peek at the top element without removing it


int peek(struct Stack* s) {
if (isEmpty(s)) {
printf("Stack is empty. No top element.\n");
return -1; // Or throw an error
}
return s->arr[s->top];
}

int main() {
struct Stack myStack;
initStack(&myStack);

push(&myStack, 10);
push(&myStack, 20);
push(&myStack, 30);

printf("Top element is: %d\n", peek(&myStack));

printf("Popped element: %d\n", pop(&myStack));


printf("Popped element: %d\n", pop(&myStack));

printf("Top element is: %d\n", peek(&myStack));

push(&myStack, 40);
push(&myStack, 50);
push(&myStack, 60); // This will cause overflow

printf("Is stack empty? %s\n", isEmpty(&myStack) ? "Yes" : "No");

return 0;
}
Linked List-based Stack Operations

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

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

struct Stack {
struct Node* top;
};

// Initialize the stack


void initStack(struct Stack* s) {
s->top = NULL;
}

// Check if the stack is empty


bool isEmpty(struct Stack* s) {
return (s->top == NULL);
}

// Push an element onto the stack


void push(struct Stack* s, int item) {
struct Node* newNode = (struct Node*) malloc(sizeof(struct Node));
if (newNode == NULL) {
printf("Memory allocation failed! Stack Overflow.\n");
return;
}
newNode->data = item;
newNode->next = s->top;
s->top = newNode;
printf("Pushed %d to stack.\n", item);
}

// Pop an element from the stack


int pop(struct Stack* s) {
if (isEmpty(s)) {
printf("Stack Underflow! Cannot pop from empty stack.\n");
return -1; // Or throw an error
}
struct Node* temp = s->top;
int popped_data = temp->data;
s->top = temp->next;
free(temp);
return popped_data;
}

// Peek at the top element without removing it


int peek(struct Stack* s) {
if (isEmpty(s)) {
printf("Stack is empty. No top element.\n");
return -1; // Or throw an error
}
return s->top->data;
}

int main() {
struct Stack myStack;
initStack(&myStack);

push(&myStack, 10);
push(&myStack, 20);
push(&myStack, 30);

printf("Top element is: %d\n", peek(&myStack));

printf("Popped element: %d\n", pop(&myStack));


printf("Popped element: %d\n", pop(&myStack));

printf("Top element is: %d\n", peek(&myStack));

push(&myStack, 40);
push(&myStack, 50);

printf("Is stack empty? %s\n", isEmpty(&myStack) ? "Yes" : "No");

// Clean up remaining nodes (important for linked list)


while (!isEmpty(&myStack)) {
pop(&myStack);
}

return 0;
}

4.4 Expression types - infix, prefix and postfix expression, evaluation


of postfix expression

Stacks play a crucial role in handling and evaluating arithmetic expressions, especially
when dealing with different notations: infix, prefix, and postfix.

Infix Expression: This is the most common way we write expressions, where
operators are placed between operands (e.g., A + B , (A + B) * C ).
Parentheses are often used to define the order of operations.

Prefix Expression (Polish Notation): In prefix notation, operators are placed


before their operands (e.g., + A B , * + A B C ). Parentheses are not needed
because the position of the operator unambiguously defines the order of
operations.

Postfix Expression (Reverse Polish Notation - RPN): In postfix notation,


operators are placed after their operands (e.g., A B + , A B + C * ). Like prefix,
parentheses are not needed.

Why different notations?


Infix expressions are easy for humans to read but difficult for computers to parse
directly due to operator precedence and associativity rules (e.g., multiplication before
addition). Prefix and postfix expressions, however, are unambiguous and can be
evaluated directly by computers using a stack.

Conversion between Notations:

Stacks are extensively used for converting expressions from one form to another,
particularly from infix to postfix or prefix.

Evaluation of Postfix Expression:

Evaluating a postfix expression is a classic application of stacks. The algorithm is as


follows:

1. Scan the postfix expression from left to right.

2. If an operand is encountered, push it onto the stack.

3. If an operator is encountered, pop the required number of operands (usually two


for binary operators) from the stack, perform the operation, and push the result
back onto the stack.

4. After scanning the entire expression, the final result will be the only element left
on the stack.

Example: Evaluate 2 3 + 4 *

Scanned Element Stack (Top to Bottom)

2 2

3 3, 2

+ 5 (2+3)

4 4, 5

* 20 (5*4)

Final Result: 20

C Implementation for Postfix Evaluation (simplified for single-digit operands):


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h> // For isdigit()

#define MAX_SIZE 100

struct Stack {
int arr[MAX_SIZE];
int top;
};

void initStack(struct Stack* s) {


s->top = -1;
}

bool isEmpty(struct Stack* s) {


return (s->top == -1);
}

bool isFull(struct Stack* s) {


return (s->top == MAX_SIZE - 1);
}

void push(struct Stack* s, int item) {


if (isFull(s)) {
printf("Stack Overflow!\n");
return;
}
s->arr[++(s->top)] = item;
}

int pop(struct Stack* s) {


if (isEmpty(s)) {
printf("Stack Underflow!\n");
return -1; // Error value
}
return s->arr[(s->top)--];
}

int evaluatePostfix(char* exp) {


struct Stack s;
initStack(&s);
int i;

for (i = 0; exp[i] != '\0'; i++) {


// If the scanned character is an operand (number), push it to the
stack.
if (isdigit(exp[i])) {
push(&s, exp[i] - '0'); // Convert char to int
}
// If the scanned character is an operator, pop two elements from
stack,
// apply the operator and push the result back.
else if (exp[i] == '+' || exp[i] == '-' || exp[i] == '*' || exp[i] ==
'/') {
int val2 = pop(&s);
int val1 = pop(&s);
switch (exp[i]) {
case '+': push(&s, val1 + val2); break;
case '-': push(&s, val1 - val2); break;
case '*': push(&s, val1 * val2); break;
case '/': push(&s, val1 / val2); break;
}
}
}
return pop(&s);
}

int main() {
char exp[] = "23+4*"; // Corresponds to (2+3)*4 = 20
printf("Postfix evaluation of %s: %d\n", exp, evaluatePostfix(exp));

char exp2[] = "123*+"; // Corresponds to 1+(2*3) = 7


printf("Postfix evaluation of %s: %d\n", exp2, evaluatePostfix(exp2));

return 0;
}

4.5 Applications

Stacks are fundamental data structures with a wide range of applications in computer
science. Some of the most common and important applications include:

Function Call Management (Runtime Stack): When a program executes,


function calls are managed using a stack. Each time a function is called, its local
variables, parameters, and return address are pushed onto the call stack. When
the function completes, its stack frame is popped, and execution returns to the
caller.

Expression Evaluation and Conversion: As discussed, stacks are essential for


converting infix expressions to postfix/prefix and for evaluating postfix/prefix
expressions.

Undo/Redo Functionality: Many applications (text editors, graphic design


software) implement undo/redo features using stacks. Each action is pushed
onto an

stack (for undo) or a separate stack (for redo). When undo is performed, the action is
popped from the undo stack and pushed onto the redo stack. * Browser History: Web
browsers use a stack to keep track of the pages visited. When you click the 'back'
button, the current page is popped, and the previous page is displayed. *
Backtracking Algorithms: Algorithms that involve exploring multiple paths (like
solving mazes, N-Queens problem, Sudoku solver) often use a stack to keep track of
the current path and to backtrack when a dead end is reached. * Syntax Parsing:
Compilers use stacks to parse the syntax of programming languages, checking for
balanced parentheses, brackets, and braces. * Recursion: Recursive function calls
implicitly use a call stack to manage their state.

Unit 5: Queue

5.1 Introduction to Queue

A Queue is a linear data structure that follows the FIFO (First In, First Out) principle.
This means the element that is inserted first is the first one to be removed. Think of a
queue of people waiting in line at a ticket counter: the first person to join the line is the
first person to be served. This behavior is in contrast to a stack, which follows the LIFO
principle.

Key Characteristics of Queues:

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

Two-End Operations: Operations occur at two different ends:


Front (or Head): Where elements are removed (dequeued).

Rear (or Tail): Where elements are added (enqueued).

Abstract Data Type: Like a stack, a queue is an ADT, defined by its operations.

Common Queue Operations:

enqueue() : Adds an element to the rear of the queue.

dequeue() : Removes the element from the front of the queue. It also returns the
removed element.

front() or peek() : Returns the front element of the queue without removing
it.

rear() : Returns the rear element of the queue without removing it.

isEmpty() : Checks if the queue is empty. Returns true if empty, false otherwise.

isFull() : Checks if the queue is full. This operation is relevant only for fixed-
size (array-based) queue implementations.
5.2 Types of Queue

While the basic concept of a queue remains FIFO, there are several variations of
queues, each designed for specific use cases:

1. Simple Queue (Linear Queue):

This is the most basic form of a queue, where elements are added at the rear and
removed from the front. Once an element is dequeued, the space it occupied is not
immediately reused, leading to a potential issue of

a full queue even if there are empty slots at the beginning (this is addressed by circular
queues).

2. Circular Queue:

To overcome the space limitation of a linear queue, a circular queue is used. In a


circular queue, the last element points to the first element, forming a circle. This
allows for efficient utilization of space as elements can be inserted and deleted from
any position in the queue, wrapping around to the beginning if necessary.

Advantages:

Better memory utilization than linear queues.

Prevents the queue from becoming full prematurely.

3. Priority Queue:

A priority queue is a special type of queue where each element has a priority. Elements
are dequeued based on their priority, not necessarily their arrival order. Elements with
higher priority are served before elements with lower priority. If two elements have
the same priority, they are served according to their order in the queue.

Applications:

Operating systems for process scheduling.

Event simulation.

Bandwidth management.

4. Deque (Double-Ended Queue):


A deque is a generalized version of a queue where elements can be inserted or deleted
from both the front and the rear ends. It combines the features of both a stack and a
queue.

Applications:

Work-stealing queues in multithreaded programming.

Implementing a history of user actions (like browser history).

5.3 Static implementation

Similar to stacks, queues can also be implemented using arrays. This is known as a
static or array-based implementation. We use an array to store the elements and two
pointers, front and rear , to keep track of the front and rear ends of the queue,
respectively.

Initially, both front and rear are set to -1 or 0, indicating an empty queue. When an
element is enqueued, rear is incremented. When an element is dequeued, front is
incremented. A key challenge with linear array-based queues is that front keeps
moving forward, potentially leading to a situation where the queue is logically empty
but rear has reached the end of the array, preventing further enqueues even if there's
space at the beginning. This is where circular queues become beneficial.

Structure for Array-based Queue:

#define MAX_QUEUE_SIZE 5 // Define maximum size of the queue

struct Queue {
int arr[MAX_QUEUE_SIZE];
int front;
int rear;
};

5.4 Operations on Queue - init(), enqueue(), dequeue(), isEmpty(),


isFull()

Let's implement the basic operations for a linear array-based queue.


Array-based Queue Operations

#include <stdio.h>
#include <stdbool.h>

#define MAX_QUEUE_SIZE 5

struct Queue {
int arr[MAX_QUEUE_SIZE];
int front;
int rear;
};

// Initialize the queue


void initQueue(struct Queue* q) {
q->front = -1;
q->rear = -1;
}

// Check if the queue is empty


bool isEmpty(struct Queue* q) {
return (q->front == -1 && q->rear == -1);
}

// Check if the queue is full


bool isFull(struct Queue* q) {
return (q->rear == MAX_QUEUE_SIZE - 1);
}

// Add an element to the rear of the queue


void enqueue(struct Queue* q, int item) {
if (isFull(q)) {
printf("Queue is full! Cannot enqueue %d\n", item);
return;
}
if (isEmpty(q)) {
q->front = 0; // Set front to 0 for the first element
}
q->arr[++(q->rear)] = item;
printf("Enqueued %d to queue.\n", item);
}

// Remove an element from the front of the queue


int dequeue(struct Queue* q) {
if (isEmpty(q)) {
printf("Queue is empty! Cannot dequeue.\n");
return -1; // Error value
}
int dequeued_item = q->arr[q->front];
if (q->front == q->rear) {
// Last element dequeued, reset queue
q->front = -1;
q->rear = -1;
} else {
q->front++;
}
return dequeued_item;
}

// Get the front element without removing it


int peekFront(struct Queue* q) {
if (isEmpty(q)) {
printf("Queue is empty! No front element.\n");
return -1;
}
return q->arr[q->front];
}

int main() {
struct Queue myQueue;
initQueue(&myQueue);

enqueue(&myQueue, 10);
enqueue(&myQueue, 20);
enqueue(&myQueue, 30);

printf("Front element is: %d\n", peekFront(&myQueue));

printf("Dequeued element: %d\n", dequeue(&myQueue));


printf("Dequeued element: %d\n", dequeue(&myQueue));

printf("Front element is: %d\n", peekFront(&myQueue));

enqueue(&myQueue, 40);
enqueue(&myQueue, 50);
enqueue(&myQueue, 60); // This will cause overflow

printf("Is queue empty? %s\n", isEmpty(&myQueue) ? "Yes" : "No");

return 0;
}

Linked List-based Queue Operations

Implementing a queue using a linked list provides dynamic sizing. We maintain two
pointers: front (pointing to the first node) and rear (pointing to the last node).
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

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

struct Queue {
struct Node *front, *rear;
};

// Initialize the queue


void initQueue(struct Queue* q) {
q->front = NULL;
q->rear = NULL;
}

// Check if the queue is empty


bool isEmpty(struct Queue* q) {
return (q->front == NULL);
}

// Add an element to the rear of the queue


void enqueue(struct Queue* q, int item) {
struct Node* newNode = (struct Node*) malloc(sizeof(struct Node));
if (newNode == NULL) {
printf("Memory allocation failed! Queue Overflow.\n");
return;
}
newNode->data = item;
newNode->next = NULL; // New node is always added at the end

if (q->rear == NULL) {
// If queue is empty, new node is both front and rear
q->front = newNode;
q->rear = newNode;
} else {
// Add the new node at the end of queue and change rear
q->rear->next = newNode;
q->rear = newNode;
}
printf("Enqueued %d to queue.\n", item);
}

// Remove an element from the front of the queue


int dequeue(struct Queue* q) {
if (isEmpty(q)) {
printf("Queue is empty! Cannot dequeue.\n");
return -1; // Error value
}
struct Node* temp = q->front;
int dequeued_item = temp->data;
q->front = q->front->next;

if (q->front == NULL) {
// If front becomes NULL, queue is empty, so rear must also be NULL
q->rear = NULL;
}
free(temp);
return dequeued_item;
}

// Get the front element without removing it


int peekFront(struct Queue* q) {
if (isEmpty(q)) {
printf("Queue is empty! No front element.\n");
return -1;
}
return q->front->data;
}

int main() {
struct Queue myQueue;
initQueue(&myQueue);

enqueue(&myQueue, 10);
enqueue(&myQueue, 20);
enqueue(&myQueue, 30);

printf("Front element is: %d\n", peekFront(&myQueue));

printf("Dequeued element: %d\n", dequeue(&myQueue));


printf("Dequeued element: %d\n", dequeue(&myQueue));

printf("Front element is: %d\n", peekFront(&myQueue));

enqueue(&myQueue, 40);
enqueue(&myQueue, 50);

printf("Is queue empty? %s\n", isEmpty(&myQueue) ? "Yes" : "No");

// Clean up remaining nodes


while (!isEmpty(&myQueue)) {
dequeue(&myQueue);
}

return 0;
}

5.5 Applications

Queues are widely used in various computing scenarios where elements need to be
processed in the order they arrive. Some common applications include:

Operating Systems:
CPU Scheduling: Processes waiting for CPU time are often managed in a
queue (e.g., Round Robin scheduling).

Spooling: Print jobs, keyboard buffers, and other I/O operations are
handled using queues.

Disk Scheduling: Requests for disk access are often processed in a queue.
Network Buffering: Routers and switches use queues to buffer data packets
when network traffic is high, ensuring packets are processed in the order they are
received.

Call Center Systems: Calls are typically placed in a queue and answered in the
order they are received.

Web Servers: Requests from web clients are often placed in a queue to be
processed by the server in a FIFO manner.

Breadth-First Search (BFS): This graph traversal algorithm uses a queue to


explore nodes level by level.

Simulation: Queues are used to simulate real-world scenarios, such as customer


service lines, traffic flow, or manufacturing processes.

Data Buffering: Used in various systems to temporarily hold data before it is


processed, ensuring smooth data flow between different components operating
at different speeds.

Unit 6: Non-linear data Structures

6.1 Introduction of Non-linear data Structures - Tree and Graph

So far, we have explored linear data structures like arrays, linked lists, stacks, and
queues, where elements are arranged sequentially. Now, we venture into the realm of
non-linear data structures, which allow for more complex relationships between data
elements. The two most prominent non-linear data structures are Trees and Graphs.

Non-linear data structures are crucial for representing hierarchical relationships,


networks, and complex connections that cannot be adequately modeled by linear
structures. They offer greater flexibility and efficiency for certain types of problems,
though they often come with increased complexity in implementation and traversal.

Trees: Trees are hierarchical data structures that simulate a tree structure with a root
value and subtrees of children, represented as a set of linked nodes. They are used to
represent data with a hierarchical relationship between elements, such as file systems,
organizational charts, or family trees.

Graphs: Graphs are more general than trees. They consist of a finite set of vertices (or
nodes) and a set of edges that connect pairs of vertices. Graphs are used to model
relationships between objects where there isn't necessarily a hierarchical order, such
as social networks, road networks, or electrical circuits.

Understanding trees and graphs is essential for solving a wide range of real-world
problems, from optimizing network routes to designing efficient search algorithms.

6.2 Concept and types of Binary trees - skewed tree, strictly binary
tree, full binary tree, complete binary tree, expression tree, binary
search tree, Heap

A Tree is a non-linear data structure that organizes data in a hierarchical manner. It


consists of nodes connected by edges. Every tree has a special node called the root,
which is at the top of the hierarchy. Each node can have zero or more child nodes.
Nodes with no children are called leaf nodes.

Key Tree Terminology:

Root: The topmost node of the tree. It has no parent.

Parent: A node that has one or more child nodes.

Child: A node that has a parent node.

Siblings: Nodes that share the same parent.

Leaf Node (External Node): A node with no children.

Internal Node: A node with at least one child.

Edge: The link between two nodes.

Path: A sequence of nodes and edges connecting one node to another.

Depth of a Node: The length of the path from the root to that node. The root
node has a depth of 0.

Height of a Node: The length of the longest path from that node to a leaf node.
The height of a leaf node is 0.

Height of a Tree: The height of its root node.

Subtree: A tree formed by a node and all its descendants.


Binary Trees

A Binary Tree is a special type of tree in which each node can have at most two
children, referred to as the left child and the right child. This constraint makes binary
trees particularly useful and easier to manage than general trees.

Structure of a Binary Tree Node in C:

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

Types of Binary Trees:

There are several specific types of binary trees, each with unique properties:

1. Skewed Tree:

A skewed tree is a degenerate binary tree where all nodes have only one child, either a
left child or a right child. It essentially behaves like a linked list.

Left Skewed Tree: Every node has only a left child (except the leaf).

Right Skewed Tree: Every node has only a right child (except the leaf).

2. Strictly Binary Tree (Full Binary Tree):

A strictly binary tree (also known as a proper binary tree or 2-tree) is a binary tree in
which every node has either zero or two children. No node has only one child.

3. Full Binary Tree:

A full binary tree is a binary tree in which every node has either zero or two children,
and all leaf nodes are at the same level. This means all levels, except possibly the last,
are completely filled, and all nodes are as far left as possible.

4. Complete Binary Tree:

A complete binary tree is a binary tree in which all levels are completely filled, except
possibly the last level, which is filled from left to right. This means that all nodes are as
far left as possible.

5. Expression Tree:
An expression tree is a binary tree used to represent arithmetic expressions. Internal
nodes are operators, and leaf nodes are operands. The order of operations is naturally
represented by the tree structure.

Example: For the expression (A + B) * C :

The root would be * .

Its left child would be the + operator, with A and B as its children.

Its right child would be C .

6. Binary Search Tree (BST):

A Binary Search Tree is a special type of binary tree that maintains a specific ordering
property: for every node, all values in its left subtree are less than the node's value,
and all values in its right subtree are greater than the node's value. This property
makes BSTs highly efficient for searching, insertion, and deletion operations.

Key Properties of BST:

The left subtree of a node contains only nodes with keys lesser than the node's
key.

The right subtree of a node contains only nodes with keys greater than the node's
key.

The left and right subtrees must also be binary search trees.

There must be no duplicate nodes.

Advantages:

Efficient searching (average O(log n)).

Efficient insertion and deletion (average O(log n)).

Disadvantages:

Worst-case performance can degrade to O(n) if the tree becomes skewed (like a
linked list).

7. Heap:

A Heap is a specialized tree-based data structure that satisfies the heap property. In a
Max-Heap, for any given node C, if P is a parent of C, then the value of P is greater than
or equal to the value of C. In a Min-Heap, the value of P is less than or equal to the
value of C.

Heaps are typically implemented using an array, taking advantage of the fact that a
complete binary tree can be efficiently represented in an array. They are crucial for
implementing priority queues and for the Heap Sort algorithm.

6.3 Traversal methods of Binary tree - preorder, inorder, postorder

Tree traversal refers to the process of visiting each node in the tree exactly once. There
are three common ways to traverse a binary tree:

1. Inorder Traversal (Left-Root-Right):

In this traversal, we first visit the left subtree, then the root node, and finally the right
subtree. For a Binary Search Tree, an inorder traversal will always visit the nodes in
ascending order of their values.

Steps:

1. Traverse the left subtree recursively.

2. Visit the root node.

3. Traverse the right subtree recursively.

Example (for BST): If you have a BST with values, inorder traversal will print them in
sorted order.

C Implementation:

void inorderTraversal(struct Node* node) {


if (node == NULL) return;
inorderTraversal(node->left);
printf("%d ", node->data);
inorderTraversal(node->right);
}

2. Preorder Traversal (Root-Left-Right):

In this traversal, we first visit the root node, then the left subtree, and finally the right
subtree. Preorder traversal is often used to create a copy of the tree or to get the prefix
expression of an expression tree.
Steps:

1. Visit the root node.

2. Traverse the left subtree recursively.

3. Traverse the right subtree recursively.

C Implementation:

void preorderTraversal(struct Node* node) {


if (node == NULL) return;
printf("%d ", node->data);
preorderTraversal(node->left);
preorderTraversal(node->right);
}

3. Postorder Traversal (Left-Right-Root):

In this traversal, we first visit the left subtree, then the right subtree, and finally the
root node. Postorder traversal is often used to delete a tree (deleting children before
the parent) or to get the postfix expression of an expression tree.

Steps:

1. Traverse the left subtree recursively.

2. Traverse the right subtree recursively.

3. Visit the root node.

C Implementation:

void postorderTraversal(struct Node* node) {


if (node == NULL) return;
postorderTraversal(node->left);
postorderTraversal(node->right);
printf("%d ", node->data);
}

6.4 Concept of graph and terminologies

A Graph is a non-linear data structure that consists of a finite set of vertices (or nodes)
and a set of edges that connect pairs of vertices. Graphs are used to model
relationships between objects, where the relationships can be complex and non-
hierarchical. They are incredibly versatile and can represent a wide variety of real-
world systems, such as social networks, transportation networks, computer networks,
and even dependencies between tasks.

Key Graph Terminology:

Vertex (Node): A fundamental unit of a graph. It can represent an entity, a


location, or an object.

Edge (Arc/Link): A connection between two vertices. Edges can be directed or


undirected.

Directed Edge: An edge with a specific direction, meaning the connection goes
from one vertex to another in a one-way fashion (e.g., a one-way street).

Undirected Edge: An edge without a specific direction, meaning the connection


is bidirectional (e.g., a two-way street).

Weighted Graph: A graph where each edge has a numerical value (weight)
associated with it. This weight can represent distance, cost, time, capacity, etc.

Unweighted Graph: A graph where edges do not have associated weights.

Path: A sequence of distinct vertices such that there is an edge between


consecutive vertices in the sequence.

Cycle: A path that starts and ends at the same vertex.

Degree of a Vertex: The number of edges incident to a vertex.


In-degree (for directed graphs): The number of incoming edges to a
vertex.

Out-degree (for directed graphs): The number of outgoing edges from a


vertex.

Connected Graph: An undirected graph is connected if there is a path between


every pair of vertices.

Strongly Connected Graph (for directed graphs): A directed graph is strongly


connected if there is a path from every vertex to every other vertex.

Adjacent Vertices: Two vertices are adjacent if they are connected by an edge.

Self-loop: An edge that connects a vertex to itself.

Multiple Edges: More than one edge connecting the same pair of vertices.

Simple Graph: A graph that has no self-loops and no multiple edges.


6.5 Representation of Graph - Adjacency matrix, Adjacency list

Graphs can be represented in computer memory using various techniques. The two
most common representations are the Adjacency Matrix and the Adjacency List.

Adjacency Matrix

An adjacency matrix is a square matrix (2D array) used to represent a finite graph. The
size of the matrix is V x V, where V is the number of vertices in the graph. Each cell
matrix[i][j] stores information about the connection between vertex i and vertex
j.

For an unweighted graph, matrix[i][j] is 1 if there is an edge from vertex i


to vertex j , and 0 otherwise.

For a weighted graph, matrix[i][j] stores the weight of the edge from vertex
i to vertex j , and 0 or infinity if there is no edge.

Example (Undirected Graph):

Consider a graph with 4 vertices (0, 1, 2, 3) and edges (0,1), (0,2), (1,2), (2,3).

0 1 2 3
0 |0 1 1 0|
1 |1 0 1 0|
2 |1 1 0 1|
3 |0 0 1 0|

Advantages:

Easy to implement.

Checking for an edge: Checking if an edge exists between two vertices (i, j) is
O(1) (just check matrix[i][j] ).

Adding/Removing edges: O(1).

Disadvantages:

Space complexity: O(V^2), which can be very inefficient for sparse graphs
(graphs with relatively few edges compared to the number of vertices). Even if
there are few edges, the matrix still requires V^2 space.
Finding all neighbors: To find all neighbors of a vertex, you need to iterate
through an entire row (O(V)).

Adjacency List

An adjacency list is a collection of linked lists or arrays. For each vertex u in the graph,
there is a list that contains all the vertices v such that there is an edge from u to v .
This is generally the preferred representation for sparse graphs.

Example (Undirected Graph):

Using the same graph with 4 vertices (0, 1, 2, 3) and edges (0,1), (0,2), (1,2), (2,3).

0: -> 1 -> 2
1: -> 0 -> 2
2: -> 0 -> 1 -> 3
3: -> 2

Implementation in C (using an array of linked lists):


#include <stdio.h>
#include <stdlib.h>

// A structure to represent an adjacency list node


struct AdjListNode {
int dest;
struct AdjListNode* next;
};

// A structure to represent an adjacency list


struct AdjList {
struct AdjListNode *head; // pointer to head node of list
};

// A structure to represent a graph. A graph


// is an array of adjacency lists. Size of
// array will be V (number of vertices)
struct Graph {
int V;
struct AdjList* array;
};

// A utility function to create a new adjacency list node


struct AdjListNode* newAdjListNode(int dest) {
struct AdjListNode* newNode = (struct AdjListNode*) malloc(sizeof(struct
AdjListNode));
newNode->dest = dest;
newNode->next = NULL;
return newNode;
}

// A utility function that creates a graph of V vertices


struct Graph* createGraph(int V) {
struct Graph* graph = (struct Graph*) malloc(sizeof(struct Graph));
graph->V = V;

// Create an array of adjacency lists. Size of array will be V


graph->array = (struct AdjList*) malloc(V * sizeof(struct AdjList));

// Initialize each adjacency list as empty by making head as NULL


for (int i = 0; i < V; ++i)
graph->array[i].head = NULL;

return graph;
}

// Adds an edge to an undirected graph


void addEdge(struct Graph* graph, int src, int dest) {
// Add an edge from src to dest. A new node is added to the adjacency
// list of src. The node is added at the beginning
struct AdjListNode* newNode = newAdjListNode(dest);
newNode->next = graph->array[src].head;
graph->array[src].head = newNode;

// Since graph is undirected, add an edge from dest to src also


newNode = newAdjListNode(src);
newNode->next = graph->array[dest].head;
graph->array[dest].head = newNode;
}

// A utility function to print the adjacency list representation of graph


void printGraph(struct Graph* graph) {
for (int v = 0; v < graph->V; ++v) {
struct AdjListNode* pCrawl = graph->array[v].head;
printf("\n Adjacency list of vertex %d\n head ", v);
while (pCrawl) {
printf("-> %d", pCrawl->dest);
pCrawl = pCrawl->next;
}
printf("\n");
}
}

int main() {
int V = 5;
struct Graph* graph = createGraph(V);
addEdge(graph, 0, 1);
addEdge(graph, 0, 4);
addEdge(graph, 1, 2);
addEdge(graph, 1, 3);
addEdge(graph, 1, 4);
addEdge(graph, 2, 3);
addEdge(graph, 3, 4);

printGraph(graph);

return 0;
}

Advantages:

Space efficiency: O(V + E), where E is the number of edges. This is much more
efficient for sparse graphs than an adjacency matrix.

Finding all neighbors: Efficiently find all neighbors of a vertex (just traverse its
linked list).

Disadvantages:

Checking for an edge: Checking if an edge exists between two vertices (i, j) is
O(V) in the worst case (you might have to traverse the entire list for vertex i).

6.6 Graph Traversals - Breadth First Search and Depth First Search

Graph traversal algorithms are used to visit every vertex and edge in a graph. The two
most common graph traversal algorithms are Breadth-First Search (BFS) and Depth-
First Search (DFS).
Breadth-First Search (BFS)

BFS is an algorithm for traversing or searching tree or graph data structures. It starts at
the tree root (or some arbitrary node of a graph, sometimes referred to as a 'search
key') and explores all of the neighbor nodes at the present depth prior to moving on to
the nodes at the next depth level. BFS uses a queue data structure to keep track of the
nodes to visit.

How BFS Works:

1. Start by putting any one of the graph's vertices at the back of a queue.

2. Take the front item of the queue and add it to the visited list.

3. Create a list of that vertex's adjacent nodes. Add the ones which are not yet in the
visited list to the back of the queue.

4. Keep repeating steps 2 and 3 until the queue is empty.

Applications of BFS:

Finding the shortest path in an unweighted graph.

Finding connected components in a graph.

Web crawlers.

Social networking websites to find people within a given distance.

C Implementation (using Adjacency List and Queue):


#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// Queue implementation for BFS


#define MAX_Q_SIZE 100

struct Queue {
int arr[MAX_Q_SIZE];
int front, rear;
};

void initQueue(struct Queue* q) {


q->front = -1;
q->rear = -1;
}

bool isQEmpty(struct Queue* q) {


return (q->front == -1);
}

void enqueue(struct Queue* q, int item) {


if (q->rear == MAX_Q_SIZE - 1) {
printf("Queue is full\n");
return;
}
if (q->front == -1) q->front = 0;
q->arr[++(q->rear)] = item;
}

int dequeue(struct Queue* q) {


if (isQEmpty(q)) {
printf("Queue is empty\n");
return -1;
}
int item = q->arr[q->front];
if (q->front == q->rear) {
q->front = -1;
q->rear = -1;
} else {
q->front++;
}
return item;
}

// Graph representation (Adjacency List)


struct AdjListNode {
int dest;
struct AdjListNode* next;
};

struct AdjList {
struct AdjListNode *head;
};

struct Graph {
int V;
struct AdjList* array;
};

struct AdjListNode* newAdjListNode(int dest) {


struct AdjListNode* newNode = (struct AdjListNode*) malloc(sizeof(struct
AdjListNode));
newNode->dest = dest;
newNode->next = NULL;
return newNode;
}

struct Graph* createGraph(int V) {


struct Graph* graph = (struct Graph*) malloc(sizeof(struct Graph));
graph->V = V;
graph->array = (struct AdjList*) malloc(V * sizeof(struct AdjList));
for (int i = 0; i < V; ++i)
graph->array[i].head = NULL;
return graph;
}

void addEdge(struct Graph* graph, int src, int dest) {


struct AdjListNode* newNode = newAdjListNode(dest);
newNode->next = graph->array[src].head;
graph->array[src].head = newNode;

newNode = newAdjListNode(src);
newNode->next = graph->array[dest].head;
graph->array[dest].head = newNode;
}

// BFS function
void BFS(struct Graph* graph, int startVertex) {
bool* visited = (bool*) malloc(graph->V * sizeof(bool));
for (int i = 0; i < graph->V; i++)
visited[i] = false;

struct Queue q;
initQueue(&q);

visited[startVertex] = true;
enqueue(&q, startVertex);

printf("BFS Traversal starting from vertex %d: ", startVertex);

while (!isQEmpty(&q)) {
int currentVertex = dequeue(&q);
printf("%d ", currentVertex);

struct AdjListNode* temp = graph->array[currentVertex].head;


while (temp) {
int adjVertex = temp->dest;
if (!visited[adjVertex]) {
visited[adjVertex] = true;
enqueue(&q, adjVertex);
}
temp = temp->next;
}
}
printf("\n");
free(visited);
}

int main() {
struct Graph* graph = createGraph(6);
addEdge(graph, 0, 1);
addEdge(graph, 0, 2);
addEdge(graph, 1, 3);
addEdge(graph, 1, 4);
addEdge(graph, 2, 4);
addEdge(graph, 3, 5);
addEdge(graph, 4, 5);

BFS(graph, 0);

return 0;
}

Depth-First Search (DFS)

DFS is an algorithm for traversing or searching tree or graph data structures. The
algorithm starts at the root (or some arbitrary node) and explores as far as possible
along each branch before backtracking. DFS uses a stack data structure (implicitly
through recursion or explicitly with an iterative approach).

How DFS Works:

1. Start by putting any one of the graph's vertices on top of a stack.

2. Take the top item of the stack and add it to the visited list.

3. Create a list of that vertex's adjacent nodes. Push the ones which are not yet in
the visited list to the top of the stack.

4. Keep repeating steps 2 and 3 until the stack is empty.

Applications of DFS:

Finding connected components in a graph.

Detecting cycles in a graph.

Topological sorting.

Solving puzzles with a single solution (e.g., mazes).

Pathfinding.

C Implementation (using Adjacency List and Recursion):


#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// Graph representation (Adjacency List) - same as BFS example


struct AdjListNode {
int dest;
struct AdjListNode* next;
};

struct AdjList {
struct AdjListNode *head;
};

struct Graph {
int V;
struct AdjList* array;
};

struct AdjListNode* newAdjListNode(int dest) {


struct AdjListNode* newNode = (struct AdjListNode*) malloc(sizeof(struct
AdjListNode));
newNode->dest = dest;
newNode->next = NULL;
return newNode;
}

struct Graph* createGraph(int V) {


struct Graph* graph = (struct Graph*) malloc(sizeof(struct Graph));
graph->V = V;
graph->array = (struct AdjList*) malloc(V * sizeof(struct AdjList));
for (int i = 0; i < V; ++i)
graph->array[i].head = NULL;
return graph;
}

void addEdge(struct Graph* graph, int src, int dest) {


struct AdjListNode* newNode = newAdjListNode(dest);
newNode->next = graph->array[src].head;
graph->array[src].head = newNode;

newNode = newAdjListNode(src);
newNode->next = graph->array[dest].head;
graph->array[dest].head = newNode;
}

// DFS function (recursive helper)


void DFSUtil(int v, bool visited[], struct Graph* graph) {
visited[v] = true;
printf("%d ", v);

struct AdjListNode* temp = graph->array[v].head;


while (temp != NULL) {
if (!visited[temp->dest]) {
DFSUtil(temp->dest, visited, graph);
}
temp = temp->next;
}
}

// DFS function (main)


void DFS(struct Graph* graph, int startVertex) {
bool* visited = (bool*) malloc(graph->V * sizeof(bool));
for (int i = 0; i < graph->V; i++)
visited[i] = false;

printf("DFS Traversal starting from vertex %d: ", startVertex);


DFSUtil(startVertex, visited, graph);
printf("\n");
free(visited);
}

int main() {
struct Graph* graph = createGraph(6);
addEdge(graph, 0, 1);
addEdge(graph, 0, 2);
addEdge(graph, 1, 3);
addEdge(graph, 1, 4);
addEdge(graph, 2, 4);
addEdge(graph, 3, 5);
addEdge(graph, 4, 5);

DFS(graph, 0);

return 0;
}

6.7 Applications - Tree, Graph

Trees and Graphs are incredibly powerful and versatile data structures with a vast
array of applications across various domains of computer science and beyond.

Applications of Trees:

File Systems: The hierarchical structure of directories and files in an operating


system is a classic example of a tree structure.

Database Indexing: B-trees and B+ trees are widely used in databases to


efficiently store and retrieve data.

Compilers: Parse trees (syntax trees) are used by compilers to represent the
syntactic structure of source code.

Decision Making: Decision trees are used in machine learning for classification
and regression tasks.

Network Routing: Trees can represent network topologies, and spanning trees
are used in network protocols.

XML/HTML Parsing: The Document Object Model (DOM) of an HTML or XML


document is represented as a tree.
Game Trees: In artificial intelligence, game trees are used to represent possible
moves and outcomes in games like chess or tic-tac-toe.

Heaps: Used to implement priority queues and the Heap Sort algorithm.

Data Compression: Huffman coding uses binary trees to represent variable-


length codes for characters, enabling data compression.

Applications of Graphs:

Social Networks: Representing users as vertices and their


friendships/connections as edges (e.g., Facebook, LinkedIn).

Transportation Networks: Modeling road networks, flight routes, or public


transportation systems. Used for shortest path algorithms (e.g., Google Maps).

Computer Networks: Representing connections between computers, servers,


and other devices. Used for routing and network analysis.

World Wide Web: Web pages can be considered vertices, and hyperlinks
between them are directed edges. Used by search engines for ranking pages.

Dependency Graphs: Representing dependencies between tasks in a project,


modules in a software system, or prerequisites in a course.

Circuit Design: Representing electronic circuits, where components are vertices


and wires are edges.

Recommendation Systems: Suggesting friends, products, or content based on


connections in a graph.

Biological Networks: Modeling protein-protein interaction networks, gene


regulatory networks, etc.

Image Segmentation: Graph-based algorithms are used in image processing to


segment images into meaningful regions.

Unit 7: Searching and Sorting Algorithms

We have already covered some fundamental searching and sorting algorithms in Unit 2
(Binary Search, Insertion Sort, Merge Sort, Quick Sort) as applications of arrays. This
unit will serve as a dedicated section to reinforce and potentially introduce other
important algorithms in these categories, emphasizing their principles, complexities,
and practical considerations.
7.1 Searching Algorithms

Searching algorithms are used to find the location of a target element within a data
structure. The efficiency of a search algorithm depends heavily on the organization of
the data.

Linear Search (Sequential Search)

Linear search is the simplest searching algorithm. It sequentially checks each element
of the list until a match is found or the end of the list is reached.

How it Works:

1. Start from the first element of the list.

2. Compare the current element with the target element.

3. If they match, the search is successful, and the index of the element is returned.

4. If they don't match, move to the next element.

5. Repeat until a match is found or all elements have been checked.

Time Complexity:

Worst Case: O(n) (target element is at the end or not present).

Best Case: O(1) (target element is at the beginning).

Average Case: O(n).

Space Complexity: O(1) (constant extra space).

Advantages:

Simple to implement.

Works on unsorted data.

Disadvantages:

Inefficient for large datasets.

C Implementation:
#include <stdio.h>

int linearSearch(int arr[], int n, int target) {


for (int i = 0; i < n; i++) {
if (arr[i] == target) {
return i; // Element found at index i
}
}
return -1; // Element not found
}

int main() {
int arr[] = {10, 20, 80, 30, 60, 50, 110, 100, 130, 170};
int n = sizeof(arr) / sizeof(arr[0]);
int target = 50;

int result = linearSearch(arr, n, target);

if (result != -1) {
printf("Element %d found at index %d\n", target, result);
} else {
printf("Element %d not found in the array\n", target);
}

target = 99;
result = linearSearch(arr, n, target);
if (result != -1) {
printf("Element %d found at index %d\n", target, result);
} else {
printf("Element %d not found in the array\n", target);
}

return 0;
}

Binary Search

As discussed in Unit 2, Binary Search is a highly efficient algorithm for searching in


sorted arrays. Its time complexity is O(log n), making it significantly faster than linear
search for large datasets.

(Refer back to Unit 2.2.1 for detailed explanation and implementation of Binary
Search.)

7.2 Sorting Algorithms

Sorting algorithms arrange elements in a specific order. We have already covered


Insertion Sort, Merge Sort, and Quick Sort. Let's briefly review them and consider other
important sorting algorithms.
Insertion Sort

(Refer back to Unit 2.2.2 for detailed explanation and implementation of Insertion
Sort.)

Merge Sort

(Refer back to Unit 2.2.2 for detailed explanation and implementation of Merge Sort.)

Quick Sort

(Refer back to Unit 2.2.2 for detailed explanation and implementation of Quick Sort.)

Bubble Sort

Bubble Sort is a simple sorting algorithm that repeatedly steps through the list,
compares adjacent elements and swaps them if they are in the wrong order. The pass
through the list is repeated until no swaps are needed, which indicates that the list is
sorted.

How it Works:

1. Starting from the beginning of the list, compare the first two elements.

2. If the first element is greater than the second, swap them.

3. Move to the next pair of elements and repeat the comparison and swap.

4. Continue this process until the end of the list. After the first pass, the largest
element will be at the end.

5. Repeat the entire process for the remaining unsorted part of the list, excluding
the last element (which is now sorted).

6. Continue until no swaps are needed in a pass.

Time Complexity:

Worst Case: O(n^2) (e.g., reverse sorted array).

Best Case: O(n) (already sorted array, if optimized to stop early).

Average Case: O(n^2).

Space Complexity: O(1) (in-place sorting).


Advantages:

Simple to understand and implement.

Disadvantages:

Very inefficient for large datasets.

C Implementation:

#include <stdio.h>

void swap(int* xp, int* yp) {


int temp = *xp;
*xp = *yp;
*yp = temp;
}

void bubbleSort(int arr[], int n) {


int i, j;
bool swapped;
for (i = 0; i < n - 1; i++) {
swapped = false;
for (j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(&arr[j], &arr[j + 1]);
swapped = true;
}
}
// If no two elements were swapped by inner loop, then break
if (swapped == false)
break;
}
}

void printArray(int arr[], int size) {


int i;
for (i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}

int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);

printf("Original array: ");


printArray(arr, n);

bubbleSort(arr, n);

printf("Sorted array (Bubble Sort): ");


printArray(arr, n);
return 0;
}
Selection Sort

Selection Sort is an in-place comparison sorting algorithm. It divides the input list into
two parts: a sorted sublist of items built up from left to right at the front (left) of the
list, and an unsorted sublist of the remaining items that occupy the rest of the list.

How it Works:

1. Find the minimum element in the unsorted array and place it at the beginning.

2. For the first position in the unsorted part, search the entire unsorted part for the
smallest element.

3. Swap the smallest element with the element at the current position.

4. Repeat the process for the next position in the unsorted part until the entire array
is sorted.

Time Complexity:

Worst Case: O(n^2).

Best Case: O(n^2).

Average Case: O(n^2).

Space Complexity: O(1) (in-place sorting).

Advantages:

Simple to implement.

Performs well on small lists.

Minimal number of swaps (useful when writes are expensive).

Disadvantages:

Inefficient for large datasets.

C Implementation:
#include <stdio.h>

void swap(int *xp, int *yp) {


int temp = *xp;
*xp = *yp;
*yp = temp;
}

void selectionSort(int arr[], int n) {


int i, j, min_idx;

// One by one move boundary of unsorted subarray


for (i = 0; i < n - 1; i++) {
// Find the minimum element in unsorted array
min_idx = i;
for (j = i + 1; j < n; j++)
if (arr[j] < arr[min_idx])
min_idx = j;

// Swap the found minimum element with the first element


swap(&arr[min_idx], &arr[i]);
}
}

void printArray(int arr[], int size) {


int i;
for (i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}

int main() {
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);

printf("Original array: ");


printArray(arr, n);

selectionSort(arr, n);

printf("Sorted array (Selection Sort): ");


printArray(arr, n);
return 0;
}

Heap Sort

Heap Sort is a comparison-based sorting technique based on the Binary Heap data
structure. It is an in-place sorting algorithm and is not stable. It works by first building
a max-heap (or min-heap) from the input data, and then repeatedly extracting the
maximum (or minimum) element from the heap and rebuilding the heap.

How it Works:
1. Build a Max-Heap: Convert the input array into a max-heap. In a max-heap, the
largest element is always at the root.

2. Extract Elements: Repeatedly extract the maximum element from the heap
(which is the root), and place it at the end of the sorted portion of the array. After
extraction, the heap property is restored by calling heapify on the reduced
heap.

Time Complexity: O(n log n) in all cases (worst, average, best).

Space Complexity: O(1) (in-place sorting).

Advantages:

Efficient and consistent performance.

In-place sorting.

Disadvantages:

Not stable (relative order of equal elements might change).

Can be slower in practice than Quick Sort due to less optimal cache performance.

C Implementation:
#include <stdio.h>

void swap(int *a, int *b) {


int temp = *a;
*a = *b;
*b = temp;
}

// To heapify a subtree rooted with node i which is


// an index in arr[]. n is size of heap
void heapify(int arr[], int n, int i) {
int largest = i; // Initialize largest as root
int left = 2 * i + 1; // left child
int right = 2 * i + 2; // right child

// If left child is larger than root


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

// If right child is larger than largest so far


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

// If largest is not root


if (largest != i) {
swap(&arr[i], &arr[largest]);

// Recursively heapify the affected sub-tree


heapify(arr, n, largest);
}
}

// main function to do heap sort


void heapSort(int arr[], int n) {
// Build heap (rearrange array)
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);

// One by one extract an element from heap


for (int i = n - 1; i > 0; i--) {
// Move current root to end
swap(&arr[0], &arr[i]);

// call max heapify on the reduced heap


heapify(arr, i, 0);
}
}

void printArray(int arr[], int n) {


for (int i = 0; i < n; ++i)
printf("%d ", arr[i]);
printf("\n");
}

int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);

printf("Original array: ");


printArray(arr, n);
heapSort(arr, n);

printf("Sorted array (Heap Sort): ");


printArray(arr, n);
return 0;
}

Unit 8: Finalization and Conclusion

This unit will cover the final aspects of the book, including a summary of key
takeaways, suggestions for further learning, and a concluding message.

8.1 Key Takeaways

Throughout this book, we have embarked on a journey through the fundamental


concepts of Data Structures using C. We started by understanding the basic building
blocks of data organization and algorithm analysis, then delved into various linear and
non-linear data structures, and finally explored essential searching and sorting
algorithms. Here are some key takeaways:

Data Structures are about Organization: They provide efficient ways to store,
organize, and manage data, which is crucial for building performant software.

Algorithm Analysis is Key: Understanding time and space complexity (Big O


notation) allows us to evaluate and compare the efficiency of different algorithms
and choose the most suitable one for a given problem.

Arrays are Fundamental: While simple, arrays are the basis for many other data
structures and are highly efficient for direct element access.

Linked Lists Offer Flexibility: Their dynamic nature makes them ideal for
scenarios requiring frequent insertions and deletions, overcoming the fixed-size
limitation of arrays.

Stacks and Queues Manage Order: Stacks (LIFO) and Queues (FIFO) are
essential for managing data flow in various applications, from function calls to
task scheduling.

Trees Model Hierarchy: Binary trees, BSTs, and Heaps are powerful for
representing hierarchical data and enabling efficient search and retrieval
operations.
Graphs Model Relationships: Graphs are versatile for representing complex
connections and are fundamental to solving problems in networks, social
systems, and more.

Searching and Sorting are Core Operations: Efficient algorithms for searching
(Binary Search) and sorting (Merge Sort, Quick Sort, Heap Sort) are critical for
data processing and optimization.

C Provides Low-Level Control: Implementing data structures in C gives you a


deep understanding of memory management and how these structures work at a
fundamental level.

8.2 Further Learning

The world of data structures and algorithms is vast and continuously evolving. This
book has provided a solid foundation, but there's always more to explore. Here are
some suggestions for your continued learning journey:

Practice, Practice, Practice: The best way to master data structures and
algorithms is by solving coding problems. Websites like LeetCode, HackerRank,
and GeeksforGeeks offer a plethora of problems to test your understanding.

Explore Advanced Data Structures: Delve into more complex data structures
such as AVL Trees, Red-Black Trees, Hash Tables (with different collision
resolution techniques), Tries, and Disjoint Set Unions.

Study Advanced Algorithms: Explore dynamic programming, greedy algorithms,


backtracking, and advanced graph algorithms like Dijkstra's, Floyd-Warshall, and
Minimum Spanning Tree algorithms (Prim's and Kruskal's).

Understand Different Paradigms: Learn about object-oriented programming


(OOP) and how data structures are implemented in languages like C++ or Java,
which offer higher-level abstractions.

Competitive Programming: Participate in competitive programming contests to


sharpen your problem-solving skills under time pressure.

Read More Books and Research Papers: Consult other textbooks and academic
papers to gain deeper insights and different perspectives on various topics.

Contribute to Open Source: Get involved in open-source projects to see how


data structures and algorithms are applied in real-world software development.
8.3 Conclusion

Congratulations on completing this journey through Data Structures using C! You have
now acquired a fundamental understanding of how data can be efficiently organized
and manipulated in computer programs. This knowledge is not just theoretical; it is
the bedrock upon which all efficient software is built.

As you continue your computer science education and career, you will find that the
principles and techniques learned here are indispensable. Whether you are developing
operating systems, designing databases, creating artificial intelligence applications, or
building web services, a strong grasp of data structures and algorithms will empower
you to write more efficient, scalable, and robust code.

Remember, the journey of learning is continuous. Keep exploring, keep practicing, and
keep building. The ability to choose the right data structure and algorithm for a given
problem is a hallmark of a skilled computer scientist. We hope this book has ignited
your passion for efficient programming and provided you with the tools to tackle
complex computational challenges. Happy coding!

Author: Manus AI

References:

[1] GeeksforGeeks. (n.d.). Data Structures. Retrieved from


https://www.geeksforgeeks.org/data-structures/

[2] Programiz. (n.d.). Data Structures and Algorithms. Retrieved from


https://www.programiz.com/dsa/

[3] TutorialsPoint. (n.d.). Data Structures. Retrieved from


https://www.tutorialspoint.com/data_structures/index.htm

[4] Introduction to Algorithms, Third Edition by Thomas H. Cormen, Charles E.


Leiserson, Ronald L. Rivest, Clifford Stein.

[5] Data Structures and Algorithms in C++ by Michael T. Goodrich, Roberto Tamassia,
David M. Mount.

You might also like