"Beware of bugs in the above code; I have only proved it correct, not tried it." -- Donald Knuth
Beginning Analysis
The point of analyzing algorithms is to be able to say that one algorithm is better than the other. // Assume a declaration like: int a[SIZE];
int n = 0;
while (x != a[n])
n++;
To find the position of x in the array:
Suppose the array was sorted. Now how would you answer these questions:
Example 2: Assume that we have a linked list of random integers and that we wish to find out where x is in the list. When the while loop ends, n will be the position of x in the list.
To find the position of x in the list:
Assume this struct Search code struct Node { int data; Node *next; }; // Assume the list has SIZE nodes int n = 0; Node *node = head; // head of the linked list while (node) { n++; if (node->data == x) break; node = node->next; }
Number of Linear Binary elements search search ------------------------------- 10 10 4 100 100 7 1,000 1,000 10 10,000 10,000 14 100,000 100,000 17 1,000,000 1,000,000 20
N 5 N2 microsecs 50 N lg N microsecs ------------------------------------------------------------- 10 0.0005 sec 0.002 sec 100 0.05 sec 0.03 sec 1,000 5 sec 0.5 sec 10,000 500 sec = 8 min 6.6 sec 100,000 50,000 sec = 14 hr 83 sec 1,000,000 5,000,000 sec = 58 days 997 sec = 17 min
f(x) = 5x2
g(x) = 50x(lg x)
The algorithms are equal at about x = 58.77. BTW, this graph was generated by gnuplot, a free cross-platform tool for generating graphs of any type plus a lot more. Check out some demos.
Suppose we find an algorithm whose runtime is described by: f(n) = n2 + 2n + 100
Values of the dominant term:
n | f(n) n2 n2-term as % of total ---------+------------------------------------------------------------ 10 | 220 100 45.455 100 | 10,300 10,000 97.087 1,000 | 1,002,100 1,000,000 99.790 10,000 | 100,020,100 100,000,000 99.980 100,000 | 10,000,200,100 10,000,000,000 99.999
Values of the dominant term:
n | f(n) n3 n3-term as % of total ---------+------------------------------------------------------------------------- 10 | 1,220 1,000 81.967213 100 | 1,010,300 1,000,000 98.980501 1,000 | 1,001,002,100 1,000,000,000 99.899890 10,000 | 1,000,100,020,100 1,000,000,000,000 99.989999 100,000 | 1,000,010,000,200,100 1,000,000,000,000,000 99.999000
Values of the dominant term:
It quickly becomes apparent why we are only concerned with the dominant term. The lesser terms are so insignifcant that they can be completely ignored. This greatly simplifies the comparisons among different functions.n | f(n) 2n 2n-term as % of total ---------+------------------------------------------------------------------------- 10 | 2,244 1,024 45.632799 20 | 1,057,116 1,048,576 99.192142 30 | 1,073,769,884 1,073,741,824 99.997387 40 | 1,099,511,693,556 1,099,511,627,776 99.999994
The growth rate for some common formulas as a function of the size of the problem, N (logarithmic scale)O(k) Constant, (not affected by the size of the data, sometimes written as O(1) or O(c)) O(lg N) Logarithmic (usually base 2) O(N) Linear (directly proportional to N) O(N lg N) No formal name, but just spoken as "N log N" O(N2) Quadratic (proportional to square of N) O(N3) Cubic (proportional to cube of N) O(Nk) Polynomial (proportional to N to the power k) O(2N) Exponential (proportional to 2 to power of N)
Larger picture.
lgN (lgN)2 N1/2 N NlgN N(lgN)2 N3/2 N2 --------------------------------------------------------------------------------- 3 9 3 10 30 90 30 100 6 36 10 100 600 3,600 1,000 10,000 9 81 31 1,000 9,000 81,000 31,000 1,000,000 13 169 100 10,000 1,300,000 1,690,000 1,000,000 100,000,000 16 256 316 100,000 1,600,000 25,600,000 31,600,000 10 billion 19 361 1,000 1,000,000 19,000,000 361,000,000 1 billion 1 trillion
f(n) = (dominant term) + (lesser terms)
O(f(n)) = O(dominant term + lesser terms) = O(dominant term)
Relationship of growth rates:
Sometimes you 'll see it written this way:O(1) < O(lg N) < O(N) < O(N lg N) < O(N2) < O(N3) < O(2N)
The equals sign "=" does not mean equality, but rather should be read as "is contained in". I prefer to use the "greater than" and "less than" symbols to avoid confusion.O(1) = O(lg N) = O(N) = O(N lg N) = O(N2) = O(N3) = O(2N)
Given these rules:
O(a) = O(b) (a and b are both constants) O(N + c) = O(N) + O(c) = O(N) (c is a contant) O(N) + c = O(N) (c is a constant) O(cN) = cO(N) = O(N) (c is a constant)
Self-check Replace the "?" with <, >, or =
1. = O(6) ? O(12) 2. = O(N + 5) ? O(N + 100) 3. < O(lg N) ? O(N) 4. = O(N/2) ? O(2N) 5. < O(N2 + N) ? O(N3) 6. < O(N lg N + N3) ? O(2N) 7. = O(N) + O(lg 2N) ? O(N) 8. = O(lg N) ? O(log10 N) 9. O(N/4) ? O(1/N) What is different about this one?
Note on #8: Changing the base of the logarithm is a constant factor, so it is discarded in big-O notation.
Some concrete values: Assuming 1 microsecond (1/1,000,000 sec) per element
Time to solve very large problems (from Sedgewick page 40)Number of elements in the data set Time ---------------------------------- complexity N=10 N=20 N=30 ------------------------------------------------------------ N 0.00001 sec 0.00002 sec 0.00003 N2 0.0001 sec 0.0004 sec 0.0009 sec N3 0.001 sec 0.008 sec 0.027 sec N5 0.1 sec 3.2 sec 24.3 sec 2N 0.001 sec 1.0 sec 17.9 min 3N 0.59 sec 58 min 6.5 years
operations problem size 1 million problem size 1 billion per ------------------------------------------------------- second N N lg N N2 N N lg N N2 -------------------------------------------------------------------- 106 seconds seconds weeks hours hours never 109 instant instant hours seconds seconds decades 1012 instant instant seconds instant instant weeks --------------------------------------------------------------------
Self-check Sort the complexities from "fastest" to "slowest" as N gets very large.
O(N2) O(N lg N) O(N) O(N3) O(1) O(2N) O(lg N)
|
|
Profiling results will show more details.
Instruction timing examples. Look at pages 10-12 and compare MOV, ADD, SUB, MUL, DIV instructions and the Math section on page 15.
Estimating the growth rate of an algorithm is affected by:
Consider this loop. Look carefully at the counters. What is the time complexity of these loops?
for (int i = 0; i < N; i++)
{
for (int j = 0; j < i; j++)
{
[ do something in O(1) ]
}
}
for (int i = 0; i < N; i++)
{
for (int j = i; j < N; j++)
{
[ do something in O(1) ]
}
}
The sum of the integers from 1 to N can be written as:
This leads to a time complexity of O(N2). You should become familiar with these equations as we will see them pop up in several places.N(N + 1) N(N - 1) 1 + 2 + 3 + 4 ... + N = --------- or similarly 0 + 1 + 2 + 3 + 4 + ... + N - 1 = --------- 2 2
Self-check To test your analysis of arrays and linked lists, fill in the table below.
Compare the complexity of performing the following operations on Arrays and Linked lists. What search method would you use? What's the best/worst case? What is the average number of accesses? Assume each value has equal probability of being in the array/list. Also, after the operation, the container must still be a valid array/list type.
Operation | Unsorted array | Sorted array | Unsorted linked list | Sorted linked list |
---|---|---|---|---|
Finding a particular item (by value) | ||||
Finding the ith element | ||||
Finding the maximum item | ||||
Adding an item | ||||
Deleting a particular item (by value) |
Self-check To test your analysis of the sort algorithms, fill in the table below.
Sort | Least Comparisons | Least Exchanges | Most Comparisons | Most Exchanges |
---|---|---|---|---|
Bubble sort | ||||
Insertion sort | ||||
Selection sort |
Self-check Suppose you are using a sort algorithm that requires O(N2) compares and O(N2) swaps/exchanges and you are given an unsorted array of 1,000 integers. What is the minimum number of linear searches you would need to perform before deciding it was cheaper to sort the array first, and then perform the same number of binary searches? Assume that compares/swaps/exchanges all take the same time.
Let X be the number of searches to perform. We have one equation and one unknown.
For the unsorted array, the number of comparisons is 1000 · X.
For the sorted array, we have 10 · X + (the cost of the sort). The cost of the sort is N2 compares + N2 exchanges/shifts/swaps which is 2,000,000. (Reminder: lg 1000 is 10)
This gives us this equation:
N · X = lg N · X + 2N2plugging in 1000 for N:
1000 · X = 10 · X + 2,000,000Solving for X gives us 2,020.20. This means that if we are going to search at least 2,021 times, we should sort the array first. A fewer number of searches will be faster without sorting.
Plugging in a couple of values for X we get:
X Unsorted Sorted 2020 2,020,000* 2,020,200 2021 2,021,000 2,020,210*
The lower the number (*) the faster the searches. The values represent how much work/effort is required and you can see that more than 2020 searches will be faster if you first sort the array.
Given what you now know about algorithms and their associated complexities at this point, what conclusions can you draw about the performances of the two algorithms in the graph below?
Stated another way:
An algorithm's complexity is O(g(n)) if there exists a constant factor K and
some input size n0 such that the time required by the algorithm with inputs
greater than n0 is always less than K(g(n)).
Also look at Figure 2.5 on page 48 of the Sedgewick book.
Visually stated:
Given the function: f(n) = 2x2 + 8x lg x + 100, we have some candidate functions for g(n): If g(n) = x2 then:
The take-away here is that all of the functions above grow at the same rate and are in the same category. (N2 algorithms)
Notes
3n2 is O(5n2 + 7n + 30)
3n2 is O(n2)
Self-check: During the execution of this function, how many statements are evaluated at runtime (f(n), where n is the size of the array)? What is the worst-case time complexity of this function (O(g(n)))?
void SelectionSort(int array[], int size)
{
1. for (int i = 0; i < size; i++)
{
2. int pos = i;
3. for (int j = i + 1; j < size; j++)
4. if (array[j] < array[pos])
5. pos = j;
6. int temp = array[pos];
7. array[pos] = array[i];
8. array[i] = temp;
}
}