An array is a contiguous block of memory that stores multiple items of the same data type together. Here, each element will be efficiently located by its index, and the size is equal to the number of elements in the array, which must be fixed.
Most programming languages follow the standard of 0-based indexing, where the first element is present at the 0th index. On the other hand, sometimes we use 1-based array indexing, where the first element is present at index 1. For example, we use 1-based indexing in implementing array-based data structures like heap, segment tree, etc.
In a one-dimensional array, elements are stored in a contiguous memory location, so the variable declared as an array is a pointer to the address of the first element of the array. This address is called the base address.
We can calculate the address of any other element using this formula: Memory Address (A[i]) = Base Address (A) + Word Length * (i - First Index).
A two-dimensional array is a tabular representation used to store elements in a rows and columns format. It is a collection of m x n elements, where m represents the number of rows and n represents the number of columns. To access any element in a 2-D array, two pieces of information are required: 1) The row index of the element and 2) The column index of the element.
We usually represent a 2-D array using a row-major representation, where array elements are stored row by row. All elements of the first row are stored in sequence, followed by the second row, then the third, fourth, and so on.
We can find the address of any element located at the ith row and the jth column using this formula: Memory Address (A[i][j])= Base Address(A)+ Word Length * (n * (i-1) + (j-1)).
A three-dimensional array is an extension of a two-dimensional array with the addition of depth. We can visualize it as a cube with rows, columns, and depth as the third dimension. To access any element in a three-dimensional array, three pieces of information are required to identify the position of an element: 1) Depth index, 2) Row index, and 3) Column index.
We use 3D arrays in:
Now we know the basic idea behind an array. Let's look at the various operations that can be performed on arrays:
In an unsorted array, we search elements using linear search.
//Pseudocode
int linearSearch(int A[], int n, int key)
{
for (int i = 0; i < n; i = i + 1)
{
if (A[i] == key)
return i
}
return -1
}
The worst-case time complexity of the linear search is O(n).
In an unsorted array, there is no order associated with the data elements. So we can directly insert elements at the end of the array. Suppose the size of the array is n and the index of the end element is endIndex.
//Pseudocode
int insert(int A[], int endIndex, int key, int n)
{
if (endIndex >= n)
{
print("Array is full. Element could not be inserted")
return -1
}
A[endIndex + 1] = key
return endIndex + 1
}
The time complexity of the insert operation at the end is O(1).
We first search for the element to be deleted using linear search. Let's assume that the linear search returns the index of the element in the array, which we store in the variable position.
//Pseudocode
int delete(int A[], int endIndex, int key)
{
int position = linearSearch(A, endIndex, key)
if (position == - 1)
{
print("Element is not present in the array")
return -1
}
for (int i = position; i < endIndex; i = i + 1)
A[i] = A[i + 1]
return endIndex - 1
}
Time complexity = Time complexity of the linear search + Time complexity of shifting elements to the left = O(n) + O(n) = O(n).
In a sorted array, we search elements using the binary search.
//Pseudocode
int binarySearch(int A[], int l, int r, int key)
{
if (l > l)
return -1
int mid = l + (r - l)/2
if (key == A[mid])
return mid
if (key > A[mid])
return binarySearch(A, mid + 1, r, key)
else
return binarySearch(A, l, mid - 1, key)
}
The worst-case time complexity of the binary search is O(logn).
In a sorted array, we first perform a search operation to find the correct position to insert the given element. For this, we can use linear search, which will take O(n) time in the worst case. But the array is sorted, so instead of linear search, we can use a modified binary search to find the position efficiently in O(logn) time.
Once we have identified the correct position, we insert the given element by shifting elements to one right.
//Pseudocode
int binarySearch(int A[], int low, int high, int element)
{
if (high <= low)
return (element > A[low]) ? (low + 1) : low
int mid = low + (high - low) / 2
if (element == A[mid])
return mid + 1
if (element > A[mid])
return binarySearch(A, mid + 1, high, element)
return binarySearch(A, low, mid - 1, element)
}
int insertInSortedArray(int A[], int endIndex, int key, int n)
{
if (endIndex >= n)
{
print("Array is full. Element could not be inserted.")
return -1
}
int position = binarySearch(A, 0, endIndex, key)
for (int i = endIndex; i >= position; i = i - 1)
A[i + 1] = A[i]
A[position] = key
return endIndex + 1
}
Time complexity = Time complexity of the binary search + Time complexity of shifting elements to the right = O(logn) + O(n) = O(n).
Note: In an unsorted array, insertion is faster as compared to a sorted array because we don’t have to care about the position at which the element is to be placed.
In the delete operation on a sorted array, we first search for the element to be deleted using binary search and then perform the delete operation by shifting the elements to the one left.
//Pseudocode
int deleteInSortedArray(int A[], int endIndex, int key)
{
int position = binarySearch(A, 0, endIndex, key)
if (position == -1)
{
print("Element is not present in the array.")
return -1
}
for (int i = position; i < endIndex; i = i + 1)
A[i] = A[i + 1]
return endIndex - 1
}
Time complexity = Time complexity of the binary search + Time complexity of shifting elements to the left = O(logn) + O(n) = O(n).
Constant time random access: The index of each element in an array maps directly to a particular memory address. So we can access the element instantly if we know the index of the given element in an array.
Optimal use of memory: Arrays consist purely of data, so no space is wasted with links or other formatting information.
Sequential Access and Memory Locality: Most programming problems require iterating through all the elements of a data structure. So arrays are good for this because successive data elements are present in sequential order. This property helps us to implement high-speed cache memory on modern computer architectures.
Simplicity: Arrays are easy to understand, implement, and use in various applications.
Compatibility with Low-Level Languages: Arrays are a fundamental data structure supported by most programming languages, including low-level languages like C and C++. This makes arrays widely usable and accessible in various programming environments.
Along with their advantages, arrays also have some disadvantages:
Fixed Size: Arrays have a fixed size and the size of an array cannot be changed dynamically. In other words, we cannot adjust the size of the array in the middle of a program’s execution. For example, our code will fail soon as we try to add the (n + 1)th data element if we only allocate space for n data elements.
Memory Wastage: Array requires allocating enough memory for the maximum number of elements upfront. This can lead to memory wastage if the allocated size is much larger than the number of elements actually stored. This inefficiency can be problematic in memory-constrained environments or when dealing with dynamic data where the size fluctuates over time.
Note: To solve the above problems, we can use the idea of the dynamic array where we can increase the array size dynamically as we need them.
Lack of Flexibility: Arrays have a fixed structure i.e. the elements must be of the same data type and size. This can be restrictive in scenarios where you need to store elements of different types or sizes.
Inefficient Insertion and Deletion: Inserting or deleting elements within an array (at the start or middle) can be inefficient. The idea is simple: We need to shift other elements, which will take O(n) time in the worst case. This inefficiency is problematic for large arrays or in the scenario of frequent modifications.
Sequential Structure: While the sequential access property of arrays can be advantageous, it can also be a limitation. If we need to search for a specific element that is not near the beginning or end of the array, we need to iterate through a significant portion of the array. This will result in slower search operations compared to other data structures like hash tables or binary search trees.
There are several array-based data structures that build upon the basic array concept to provide additional functionality and improved performance.
If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, Enjoy algorithms!