The order of an operation refers to the amount of time an operation takes to complete. As a general rule, an algorithm runs with constant time if it works on a single piece of data.
On the other hand, the order of an operation can vary greatly when dealing with collections.
1. Constant Time – O(1)
O(1) operations always take approximately the same amount of time, regardless of the size of a collection.
1.1 Array
Accessing an element in an array is a constant time operation, since the index is just an offset to the start of the array.
public int GetValueAtLocation(int[] arr, int index) { // Bounds checking if (arr == null) throw new ArgumentNullException(); if (index < 0 || index >= arr.Length) throw new ArgumentException(); // This operation runs with constant time return arr[index]; } |
1.2 Hashtable
Retrieving a value from a hashtable using a key can be done in almost linear time, depending in the implementation of the hashtable.
The time of retrieval depends on how well the hash key is implemented for the type being used as a key.
2. Log Time – O (Log(N))
Some operations take the log of the number of items to perform.
2.1 Binary Search
Binary search is used to find an item in a sorted array. With binary search, we half the number of elements we need to search during every iteration.
- /// <summary>
- /// Using binary search, find smallest int in array
- /// that has been sorted and rotated an unknown number of times.
- /// <example>
- /// arr = {93, 105, 124, 7, 23, 45, 87}
- /// returned index is 3
- /// </example>
- /// </summary>
- static int FindSortedArrayRotation(int[] arr)
- {
- if (arr == null) throw new ArgumentNullException();
- if (arr.Length == 0) throw new ArgumentException();
- int left = 0;
- int right = arr.Length - 1;
- int center = right / 2;
- if (arr[left] <= arr[right]) return 0;
- while (true)
- {
- if (left + 1 == right)
- {
- if (arr[left] < arr[right])
- {
- return left;
- }
- else
- {
- return right;
- }
- }
- else if (arr[left] > arr[center])
- {
- left = center;
- }
- else
- {
- right = center;
- }
- center = (right - left) / 2 + left;
- }
- }
3. Linear Time O(N)
The amount of time an O(N) operations takes place is proportional to the number of elements the algorithm is working on.
3.1 Search Unsorted Array
To search an unsorted array, we need to go through all the elements one at a time until we find what we are looking for.
- public int Search(int[] arr, int val)
- {
- for (int i = 0; i < arr.Length; i++)
- {
- if (arr[i] == val)
- return i;
- }
- return -1;
- }
The important point about the above method is that the ‘for’ loop goes through the array once.
3.2 Perform operation on all elements
By nature this is a linear operation.
- public void DoSomething(int[] arr)
- {
- // Do setup
- for (int i = 0; i < arr.Length; i++)
- {
- // Do something
- }
- // Do cleanup
- }
4. Non-linear Time O(N2)
With O(N2) time, we go through the array once for every element in the array (worse case).
- public void BubbleSort(int[] arr)
- {
- for (int i = 0; i < arr.Length - 1; i++)
- {
- for (int j = i + 1; j < arr.Length; j++)
- {
- if (arr[i] > arr[j])
- {
- int t = arr[i];
- arr[i] = arr[j];
- arr[j] = t;
- }
- }
- }
- }
Note: We know that it is O(N2) because we have two embedded for loops. The order of operation is just the number of embedded loops.
5. Non-linear Time O(Nm)
Here is a trick question. What is the Big O of this operation?
- public void DoSomething(int[] arr)
- {
- // Do setup
- // loop 1
- for (int i = 0; i < arr.Length; i++)
- {
- // loop 2
- for (int j = 0; j < arr.Length; j++)
- {
- DoAction1();
- }
- // loop 3
- for (int j = 0; j < arr.Length; j++)
- {
- // loop 4
- for (int j = 0; j < arr.Length; j++)
- {
- DoAction2();
- }
- }
- }
- // Do cleanup
- }
Since we have loop 1, that means we are dealing with a linear operation (at least). Loop 2 is embedded in loop 1, so now we have an O(N2) operation (at least).
The operation also has loop 3. Loop 2 and 3 are side by side, so they don’t affect the order of the operation. It is still O(N2) for now.
For loop 1,2 the order of operation is O(N2).
For loop 1,3,4 the order of operation is O(N3).
The higher order for the second one wins and the overall order of the method becomes O(N3).
Keep in mind that the order of operation for DoAction1 and DoAction2 must be taken into account. The order of operation of all embedded operations must be multiplied.
For the above DoAction1 and DoAction2, we had assumed the operation was in constant time. Now let’s assume the order for DoAction1 is O(N3) and order for DoAction2 is O(N1).
When we do this, loop 1,2 becomes 2 + 3 = 5, or O(N5).
Loop 1,2 becomes 3 + 1 = 4, or O(N4).
The first one wins now and the order of operation now becomes O(N5).