adding lecture notes for big o
This commit is contained in:
272
lectures/07_order_notation_recursion/README.md
Normal file
272
lectures/07_order_notation_recursion/README.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# Lecture 7 --- Order Notation & Basic Recursion
|
||||||
|
|
||||||
|
- Algorithm Analysis, Formal Definition of Order Notation
|
||||||
|
- Simple recursion, Visualization of recursion, Iteration vs. Recursion
|
||||||
|
- “Rules” for writing recursive functions, Lots of examples
|
||||||
|
|
||||||
|
## 7.1 Algorithm Analysis
|
||||||
|
|
||||||
|
Why should we bother?
|
||||||
|
- We want to do better than just implementing and testing every idea we have.
|
||||||
|
- We want to know why one algorithm is better than another.
|
||||||
|
- We want to know the best we can do. (This is often quite hard.)
|
||||||
|
How do we do it? There are several options, including:
|
||||||
|
- Don’t do any analysis; just use the first algorithm you can think of that works.
|
||||||
|
- Implement and time algorithms to choose the best.
|
||||||
|
- Analyze algorithms by counting operations while assigning different weights to different types of operations
|
||||||
|
based on how long each takes.
|
||||||
|
- Analyze algorithms by assuming each operation requires the same amount of time. Count the total number of
|
||||||
|
operations, and then multiply this count by the average cost of an operation.
|
||||||
|
|
||||||
|
## 7.2 Exercise: Counting Example
|
||||||
|
|
||||||
|
- Suppose arr is an array of n doubles. Here is a simple fragment of code to sum of the values in the array:
|
||||||
|
```cpp
|
||||||
|
double sum = 0;
|
||||||
|
for (int i=0; i<n; ++i)
|
||||||
|
sum += arr[i];
|
||||||
|
```
|
||||||
|
- What is the total number of operations performed in executing this fragment? Come up with a function describing the number of operations in terms of n.
|
||||||
|
|
||||||
|
## 7.3 Exercise: Which Algorithm is Best?
|
||||||
|
|
||||||
|
A venture capitalist is trying to decide which of 3 startup companies to invest in and has asked for your help. Here’s
|
||||||
|
the timing data for their prototype software on some different size test cases:
|
||||||
|
|
||||||
|
```console
|
||||||
|
n foo-a foo-b foo-c
|
||||||
|
10 10 u-sec 5 u-sec 1 u-sec
|
||||||
|
20 13 u-sec 10 u-sec 8 u-sec
|
||||||
|
30 15 u-sec 15 u-sec 27 u-sec
|
||||||
|
100 20 u-sec 50 u-sec 1000 u-sec
|
||||||
|
1000 ? ? ?
|
||||||
|
```
|
||||||
|
|
||||||
|
Which company has the “best” algorithm?
|
||||||
|
|
||||||
|
## 7.4 Order Notation Definition
|
||||||
|
|
||||||
|
In this course we will focus on the intuition of order notation. This topic will be covered again, in more depth, in
|
||||||
|
later computer science courses.
|
||||||
|
- Definition: Algorithm A is order f(n) — denoted O(f(n)) — if constants k and n0 exist such that A requires
|
||||||
|
no more than k ∗ f(n) time units (operations) to solve a problem of size n ≥ n0.
|
||||||
|
- For example, algorithms requiring 3n + 2, 5n − 3, and 14 + 17n operations are all O(n).
|
||||||
|
This is because we can select values for k and n0 such that the definition above holds. (What values?)
|
||||||
|
Likewise, algorithms requiring n 2/10 + 15n − 3 and 10000 + 35n
|
||||||
|
2 are all O(n
|
||||||
|
2
|
||||||
|
).
|
||||||
|
- Intuitively, we determine the order by finding the asymptotically dominant term (function of n) and throwing
|
||||||
|
out the leading constant. This term could involve logarithmic or exponential functions of n. Implications for
|
||||||
|
analysis:
|
||||||
|
– We don’t need to quibble about small differences in the numbers of operations.
|
||||||
|
– We also do not need to worry about the different costs of different types of operations.
|
||||||
|
– We don’t produce an actual time. We just obtain a rough count of the number of operations. This count is used for comparison purposes.
|
||||||
|
- In practice, this makes analysis relatively simple, quick and (sometimes unfortunately) rough.
|
||||||
|
|
||||||
|
## 7.5 Common Orders of Magnitude
|
||||||
|
|
||||||
|
- O(1), a.k.a. CONSTANT: The number of operations is independent of the size of the problem. e.g., compute
|
||||||
|
quadratic root.
|
||||||
|
- O(log n), a.k.a. LOGARITHMIC. e.g., dictionary lookup, binary search.
|
||||||
|
- O(n), a.k.a. LINEAR. e.g., sum up a list.
|
||||||
|
- O(n log n), e.g., sorting.
|
||||||
|
- O(n2), O(n3), O(nk), a.k.a. POLYNOMIAL. e.g., find closest pair of points.
|
||||||
|
- O(2n), O(kn), a.k.a. EXPONENTIAL. e.g., Fibonacci, playing chess.
|
||||||
|
|
||||||
|
- Play this [animation](https://jidongxiao.github.io/CSCI1200-DataStructures/animations/dynamic_memory/two_d_array/index.html) to see what exactly the above code snippet does.
|
||||||
|
|
||||||
|
## 7.6 Exercise: A Slightly Harder Example
|
||||||
|
|
||||||
|
Here’s an algorithm to determine if the value stored in variable x is also in an array called foo. Can you analyze
|
||||||
|
it? What did you do about the if statement? What did you assume about where the value stored in x occurs
|
||||||
|
in the array (if at all)?
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int loc=0;
|
||||||
|
bool found = false;
|
||||||
|
while (!found && loc < n) {
|
||||||
|
if (x == foo[loc]) found = true;
|
||||||
|
else loc++;
|
||||||
|
}
|
||||||
|
if (found) cout << "It is there!\n";
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7.7 Best-Case, Average-Case and Worst-Case Analysis
|
||||||
|
|
||||||
|
- For a given fixed size array, we might want to know:
|
||||||
|
– The fewest number of operations (best case) that might occur.
|
||||||
|
– The average number of operations (average case) that will occur.
|
||||||
|
– The maximum number of operations (worst case) that can occur.
|
||||||
|
- The last is the most common. The first is rarely used.
|
||||||
|
- On the previous algorithm, the best case is O(1), but the average case and worst case are both O(n).
|
||||||
|
|
||||||
|
## 7.8 Approaching An Analysis Problem
|
||||||
|
|
||||||
|
- Decide the important variable (or variables) that determine the “size” of the problem. For arrays and other
|
||||||
|
“container classes” this will generally be the number of values stored.
|
||||||
|
- Decide what to count. The order notation helps us here.
|
||||||
|
- If each loop iteration does a fixed (or bounded) amount of work, then we only need to count the number
|
||||||
|
of loop iterations.
|
||||||
|
- We might also count specific operations. For example, in the previous exercise, we could count the number
|
||||||
|
of comparisons.
|
||||||
|
- Do the count and use order notation to describe the result.
|
||||||
|
|
||||||
|
## 7.9 Exercise: Order Notation
|
||||||
|
|
||||||
|
For each version below, give an order notation estimate of the number of operations as a function of n:
|
||||||
|
|
||||||
|
1
|
||||||
|
```cpp
|
||||||
|
int count=0;
|
||||||
|
for (int i=0; i<n; ++i)
|
||||||
|
for (int j=0; j<n; ++j)
|
||||||
|
++count;
|
||||||
|
```
|
||||||
|
|
||||||
|
2.
|
||||||
|
```cpp
|
||||||
|
int count=0;
|
||||||
|
for (int i=0; i<n; ++i)
|
||||||
|
++count;
|
||||||
|
for (int j=0; j<n; ++j)
|
||||||
|
++count;
|
||||||
|
```
|
||||||
|
|
||||||
|
3.
|
||||||
|
```cpp
|
||||||
|
int count=0;
|
||||||
|
for (int i=0; i<n; ++i)
|
||||||
|
for (int j=i; j<n; ++j)
|
||||||
|
++count;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7.10 Recursive Definitions of Factorials and Integer Exponentiation
|
||||||
|
|
||||||
|
Factorial is defined for non-negative integers as:
|
||||||
|
n! = (
|
||||||
|
n · (n − 1)! n > 0
|
||||||
|
1 n == 0
|
||||||
|
Computing integer powers is defined as:
|
||||||
|
n
|
||||||
|
p =
|
||||||
|
(
|
||||||
|
n · n
|
||||||
|
p−1 p > 0
|
||||||
|
1 p == 0
|
||||||
|
These are both examples of recursive definitions
|
||||||
|
|
||||||
|
## 7.11 Recursive C++ Functions
|
||||||
|
|
||||||
|
C++, like other modern programming languages, allows functions to call themselves. This gives a direct method of
|
||||||
|
implementing recursive functions. Here are the recursive implementations of factorial and integer power:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int fact(int n) {
|
||||||
|
if (n == 0) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
int result = fact(n-1);
|
||||||
|
return n * result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int intpow(int n, int p) {
|
||||||
|
if (p == 0) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return n * intpow( n, p-1 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7.12 The Mechanism of Recursive Function Calls
|
||||||
|
|
||||||
|
- For each recursive call (or any function call), a program creates an activation record to keep track of:
|
||||||
|
– Completely separate instances of the parameters and local variables for the newly-called function.
|
||||||
|
– The location in the calling function code to return to when the newly-called function is complete. (Who
|
||||||
|
asked for this function to be called? Who wants the answer?)
|
||||||
|
– Which activation record to return to when the function is done. For recursive functions this can be
|
||||||
|
confusing since there are multiple activation records waiting for an answer from the same function.
|
||||||
|
- This is illustrated in the following diagram of the call fact(4). Each box is an activation record, the solid lines
|
||||||
|
indicate the function calls, and the dashed lines indicate the returns. Inside of each box we list the parameters
|
||||||
|
and local variables and make notes about the computation.
|
||||||
|
- This chain of activation records is stored in a special part of program memory called the stack
|
||||||
|
|
||||||
|
## 7.13 Iteration vs. Recursion
|
||||||
|
|
||||||
|
- Each of the above functions could also have been written using a for or while loop, i.e. iteratively. For example, here is an iterative
|
||||||
|
version of factorial:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int ifact(int n) {
|
||||||
|
int result = 1;
|
||||||
|
for (int i=1; i<=n; ++i)
|
||||||
|
result = result * i;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Often writing recursive functions is more natural than writing iterative functions, especially for a first draft of
|
||||||
|
a problem implementation.
|
||||||
|
- You should learn how to recognize whether an implementation is recursive or iterative, and practice rewriting
|
||||||
|
one version as the other. Note: We’ll see that not all recursive functions can be easily rewritten in iterative
|
||||||
|
form!
|
||||||
|
- Note: The order notation for the number of operations for the recursive and iterative versions of an algorithm
|
||||||
|
is usually the same. However in C, C++, Java, and some other languages, iterative functions are generally
|
||||||
|
faster than their corresponding recursive functions. This is due to the overhead of the function call mechanism.
|
||||||
|
Compiler optimizations will sometimes (but not always!) reduce the performance hit by automatically eliminating
|
||||||
|
the recursive function calls. This is called tail call optimization.
|
||||||
|
|
||||||
|
## 7.14 Exercises
|
||||||
|
|
||||||
|
1. Draw a picture to illustrate the activation records for the function call
|
||||||
|
cout << intpow(4, 4) << endl;
|
||||||
|
2. Write an iterative version of intpow.
|
||||||
|
3. What is the order notation for the two versions of intpow?
|
||||||
|
|
||||||
|
## 7.15 Rules for Writing Recursive Functions
|
||||||
|
|
||||||
|
Here is an outline of five steps that are useful in writing and debugging recursive functions. Note: You don’t have
|
||||||
|
to do them in exactly this order...
|
||||||
|
1. Handle the base case(s).
|
||||||
|
2. Define the problem solution in terms of smaller instances of the problem. Use wishful thinking, i.e., if someone
|
||||||
|
else solves the problem of fact(4) I can extend that solution to solve fact(5). This defines the necessary
|
||||||
|
recursive calls. It is also the hardest part!
|
||||||
|
3. Figure out what work needs to be done before making the recursive call(s).
|
||||||
|
4. Figure out what work needs to be done after the recursive call(s) complete(s) to finish the computation. (What
|
||||||
|
are you going to do with the result of the recursive call?)
|
||||||
|
5. Assume the recursive calls work correctly, but make sure they are progressing toward the base case(s)!
|
||||||
|
|
||||||
|
## 7.16 Location of the Recursive Call — Example: Printing the Contents of a Vector
|
||||||
|
|
||||||
|
Here is a function to print the contents of a vector. Actually, it’s two functions: a driver function, and a true
|
||||||
|
recursive function. It is common to have a driver function that just initializes the first recursive function call.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void print_vec(std::vector<int>& v, unsigned int i) {
|
||||||
|
if (i < v.size()) {
|
||||||
|
cout << i << ": " << v[i] << endl;
|
||||||
|
print_vec(v, i+1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void print_vec(std::vector<int>& v) {
|
||||||
|
print_vec(v, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What will this print when called in the following code?
|
||||||
|
```cpp
|
||||||
|
int main() {
|
||||||
|
std::vector<int> a;
|
||||||
|
a.push_back(3); a.push_back(5); a.push_back(11); a.push_back(17);
|
||||||
|
print_vec(a);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
How can you change the second print vec function as little as possible so that this code prints the contents
|
||||||
|
of the vector in reverse order?
|
||||||
Reference in New Issue
Block a user