Module 4
Module 4
Efficient access to elements: Arrays provide direct and efficient access to any element in the collection.
Accessing an element in an array is an O(1) operation, meaning that the time required to access an element is
constant and does not depend on the size of the array.
Fast data retrieval: Arrays allow for fast data retrieval because the data is stored in contiguous memory locations.
This means that the data can be accessed quickly and efficiently without the need for complex data structures or
algorithms.
Memory efficiency: Arrays are a memory-efficient way of storing data. Because the elements of an array are
stored in contiguous memory locations, the size of the array is known at compile time. This means that memory can
be allocated for the entire array in one block, reducing memory fragmentation.
Versatility: Arrays can be used to store a wide range of data types, including integers, floating-point numbers,
characters, and even complex data structures such as objects and pointers.
Easy to implement: Arrays are easy to implement and understand, making them an ideal choice for beginners
learning computer programming.
Compatibility with hardware: The array data structure is compatible with most hardware architectures, making it
a versatile tool for programming in a wide range of environments.
Fixed size: Arrays have a fixed size that is determined at the time of creation. This means that if the size of the
array needs to be increased, a new array must be created and the data must be copied from the old array to the
new array, which can be time-consuming and memory-intensive.
Memory allocation issues: Allocating a large array can be problematic, particularly in systems with limited
memory. If the size of the array is too large, the system may run out of memory, which can cause the program to
crash.
Insertion and deletion issues: Inserting or deleting an element from an array can be inefficient and time-
consuming because all the elements after the insertion or deletion point must be shifted to accommodate the
change.
Wasted space: If an array is not fully populated, there can be wasted space in the memory allocated for the array.
This can be a concern if memory is limited.
Limited data type support: Arrays have limited support for complex data types such as objects and structures, as
the elements of an array must all be of the same data type.
Lack of flexibility: The fixed size and limited support for complex data types can make arrays inflexible compared
to other data structures such as linked lists and trees.
Coding implementation of the search operation: C Coding implementation of inserting an element at
program to implement linear search in the end: // C program to implement insert
unsorted array operation in an unsorted array.
// Inserting key
n = insertSorted(arr, n, key,
capacity);
return 0;
}
// Function call
n = deleteElement(arr, n, key);
return 0;
}
#include <math.h>
#include <stdio.h> #include <stdio.h>
// Driver code
int main()
{
int arr[] = { 10, 7, 8, 9, 1, 5 };
int N = sizeof(arr) / sizeof(arr[0]);
// Function call
quickSort(arr, 0, N - 1);
printf("Sorted array: \n");
for (int i = 0; i < N; i++)
printf("%d ", arr[i]);
return 0;
}
Time complexity of quicksort in best case, average case, and in worst case.
Time Complexity
Best Case O(n*logn)
Average O(n*logn)
Case
Worst O(n2)
Case
o Best Case Complexity - In Quicksort, the best-case occurs when the pivot element is the middle element or near to the middle
element. The best-case time complexity of quicksort is O(n*logn).
o Average Case Complexity - It occurs when the array elements are in jumbled order that is not properly ascending and not properly
descending. The average case time complexity of quicksort is O(n*logn).
o Worst Case Complexity - In quick sort, worst case occurs when the pivot element is either greatest or smallest element. Suppose, if
the pivot element is always the last element of the array, the worst case would occur when the given array is sorted already in
ascending or descending order. The worst-case time complexity of quicksort is O(n2).
#include <stdio.h>
#include <stdlib.h>
int i, j, k;
int n1 = m - l + 1;
int n2 = r - m;
i = 0;
j = 0;
k = l;
arr[k] = L[i];
i++;
else {
arr[k] = R[j];
j++;
k++;
arr[k] = L[i];
i++;
k++;
arr[k] = R[j];
j++;
k++;
if (l < r) {
int m = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
int i;
printf("\n");
// Driver code
int main()
printArray(arr, arr_size);
mergeSort(arr, 0, arr_size - 1);
printArray(arr, arr_size);
return 0;
Output
Given array is
12 11 13 5 6 7
Sorted array is
5 6 7 11 12 13
Algorithm:
MergeSort(A, p, r):
if p > r
return
q = (p+r)/2
mergeSort(A, p, q)
mergeSort(A, q+1, r)
merge(A, p, q, r)
Merge Sort is a recursive algorithm and time complexity can be expressed as following recurrence relation.
T(n) = 2T(n/2) + θ(n)
The above recurrence can be solved either using the Recurrence Tree method or the Master method. It falls in case II
of the Master Method and the solution of the recurrence is θ(Nlog(N)). The time complexity of Merge Sort isθ(Nlog(N))
in all 3 cases (worst, average, and best) as merge sort always divides the array into two halves and takes linear time to
merge two halves.
Auxiliary Space: O(N), In merge sort all elements are copied into an auxiliary array. So N auxiliary space is required
for merge sort.
// Heap Sort in C
#include <stdio.h>
*b = temp;
// n is size of heap
int largest = i;
// left = 2*i + 1
int left = 2 * i + 1;
// right = 2*i + 2
int right = 2 * i + 2;
largest = left;
// so far
largest = right;
if (largest != i) {
swap(&arr[i], &arr[largest]);
// sub-tree
heapify(arr, N, largest);
}
// Main function to do heap sort
heapify(arr, N, i);
// Heap sort
swap(&arr[0], &arr[i]);
// root again
heapify(arr, i, 0);
}
// A utility function to print array of size n
printf("\n");
// Driver's code
int main()
// Function call
heapSort(arr, N);
printArray(arr, N);
Output
Sorted array is
5 6 7 11 12 13
Complexity Analysis of Heap Sort
Time Complexity: O(N log N)
Auxiliary Space: O(log n), due to the recursive call stack. However, auxiliary space can be O(1) for iterative implementation.
Hashing:
Hashing is a technique or process of mapping keys, and values into the hash table by using a hash function. It is done for faster access to
elements. The efficiency of mapping depends on the efficiency of the hash function used.
The above technique enables us to calculate the location of a given string by using a simple hash function and rapidly find the
value that is stored in that location. Therefore the idea of hashing seems like a great way to store (key, value) pairs of th e data in a
table.
Hash function
The hash function creates a mapping between key and value, this is done through the use of mathematical formulas known as hash
functions. The result of the hash function is referred to as a hash value or hash. The hash value is a representation of the original
string of characters but usually smaller than the original.
For example: Consider an array as a Map where the key is the index and the value is the value at that index. So for an array A if we
have index i which will be treated as the key then we can find the value by simply looking at the value at A[i]. simply looking up
A[i].
Separate Chaining:
The idea behind separate chaining is to implement the array as a linked list called a chain. Separate chaining is one of the most
popular and commonly used techniques in order to handle collisions.
Let us consider a simple hash function as “key mod 7” and a sequence of keys as 50, 700, 76, 85, 92, 73, 101
Open Addressing:
Like separate chaining, open addressing is a method for handling collisions. In Open Addressing, all elements are
stored in the hash table itself. So at any point, the size of the table must be greater than or equal to the total number of
keys (Note that we can increase table size by copying old data if needed). This approach is also known as closed
hashing. This entire procedure is based upon probing.
Let us consider table Size = 7, hash function as Hash(x) = x % 7 and collision resolution strategy to be f(i) = i 2 . Insert = 22, 30, and
50.
3. Double Hashing
The intervals that lie between probes are computed by another hash function. Double hashing is a technique that reduces clust ering
in an optimized way. In this technique, the increments for the probing sequence are computed by using another hash function. We
use another hash function hash2(x) and look for the i*hash2(x) slot in the ith rotation.
In chaining, Hash table never fills up, we can always add more
2. In open addressing, table may become full.
elements to chain.
Chaining is mostly used when it is unknown how many and how Open addressing is used when the frequency and
4.
frequently keys may be inserted or deleted. number of keys is known.
Cache performance of chaining is not good as keys are stored Open addressing provides better cache performance as
5.
using linked list. everything is stored in the same table.