算法分析基础
Correctness of Algorithms
Definition
When we talk about the correctness of an algorithm, we actually mean the correctness with respect to its specification.
Specification expresses the task to be done by the algorithm, which consists of:
- (optional) name of algorithm and list of its arguments
- Precondition (or initial condition): it species what is correct input data to the
problem - Postcondition (or final condition): it species what is the desired result of the algorithm)
Example:
- name:
- input: An array of integers
- output: A permutation of that array that is sorted (monotonic).
Total correctness(完全正确性)
An algorithm is called totally correct for the given specification if and only if for any correct input data it:
- terminates
- returns correct output
Usually, while checking the correctness of an algorithm it is easier to separately:
- Check whether the algorithm stops
- Then checking the remaining part — This remaining part of correctness is called "Partial Correctness" of algorithm
Partial correctness(部分正确性)
An algorithm is called partially correct if satisfies the following condition: If the algorithm receiving correct input data stops then its result is correct.
Note: Partial correctness does not make the algorithm stop.
Total correctness:
- precondition:
- algorithm:
- postcondition:
Partial correctness:
- precondition:
- algorithm:
1
2while (true)
x := 0 - postcondition:
Neither partial nor total correctness:
- precondition:
- algorithm:
- postcondition:
The proof of total correctness
A proof of total correctness of an algorithm usually assumes 2 separate steps
- (to prove that) the algorithm always terminate for correct input data
- (to prove that) the algorithm is partially correct.
Different proof methods for them, typically
- Variants(变式) for "termination"
- Invariants(不变式) for "partial correctness"
Termination is often much easier to prove.
Example: Insertion-Sort
1 | # Procedure: Insertion-Sort(A) |
Proof the correctness of Insertion-Sort:
- The algorithm outputs correct result on every instance (partially correct).
- The algorithm terminates within finite steps on every instance (termination).
Step 1: Using loop invariant for partial correctness
General rules for loop invariant proofs:
- Initialization: It is true prior to the first iteration of the loop.
- Maintenance: If it is true before an iteration of the loop, it remains true before the next iteration.
- Termination: When the loop terminates, the invariant gives us a useful property that helps show that the algorithm is correct.
The loop invariant of the outer for
loop: By the end of -th iteration, the elements in subarray A[1:i]
are sorted order.
- Initialization: prior the first iteration(
i=2
):A[1]
is in sorted order. - Maintenance: Assume by the end of the -th iteration, the elements in subarray
A[1:i]
are in sorted order; then by the end of the -th iteration, the elements in subarrayA[1:i+1]
are in sorted order. - Termination: After the iteration
i=n
, the loop invariant states thatA
is sorted.
Let the loop invariant mentioned above as . Is there only one loop invariant? The answer is NO!
Here is another loop invariant: By the end of the -th iteration, subarray A[1:i]
retains all of the original elements in A[1:i]
in previous iteration. Let this be .
is weaker than ! Since there are more possible A[1:i]
that satisfy but not satisfy .
A good (strong) loop invariant must satisfy these three properties Initialization, Maintenance and Termination. Note that does not satisfy Termination property.
How to find the loop invariant?
Generally, the answer is: We don't know.
For simple ones, there exists effective techniques.
Step 2: Using variant for termination
Using loop variant to prove the termination:
- show that some quantity strictly decreases.
- it cannot decrease indefinitely (Bounded!)
Well-ordered set(良序集)
An ordered set is well-ordered(良序) if each and every nonempty subset has a smallest or least element.
「良序」的更多内容见离散数学笔记。
A well-ordered set has no infinite descending sequences, which can be used to ensure the termination of algorithm.
Loop variant for the inner while
loop: . For each iteration, strictly decreases. is bounded to be larger than .
Loop variant for the outer for
loop: A.length - i
. For each iteration, A.length - i
strictly decreases. A.length - i
is bounded to be larger than .
How to find the loop variant?
Again, generally, the answer is: We don't know.
However, generally speaking, it is very easy to identify. For example, the induction variable of the loop (or some linear transformation of it).
Some other strategies of correctness proof: proof by cases, proof by contrapposition, proof by contraction, etc.
When loops and/or recursions are involved: often (if not always) use mathematical induction.
Efficiency of Algorithms
Complexity
- Time complexity: how much time is needed before halting
- Space complexity: how much memory (usually excluding input) is required for successful executed
Other performance measures:
- communication bandwidth(通信带宽)
- energy consumption
- …
Time complexity is typically more important than others in analysis.
An observation: larger inputs often demands more time. Therefore, Cost of an algorithm should be a function of input size, say, .
Given an algorithm and an input, when counting the cost with respect to the RAM model:
- Each memory access takes constant time.
- Each "primitive" operation takes constant time.
- Compound operations should be decomposed.
- At last, Counting up the number of time units.
Time complexity of Insertion Sort
1 | # Cost # Times |
The total cost of the algorithm is
depends on which input of size .
The time cost of insert sort varies among inputs. How to fairly evaluate a algorithm?
Enumerate the cost of all the possible inputs? Not possible, since the input space is infinite! We can check the representative inputs, but, what are they?
Worst, Best and Average case
- Worst-case time complexity: the maximum time cost among all possible inputs of size .
- Best-case time complexity: the minimum time cost among all possible inputs of size .
- Average-case time complexity: the average time cost among all possible inputs of size .
Note: need assumption of statistics distribution of inputs.
We mainly focus on worst-case time complexity.
Exceptions: Some exponential-time algorithms are used widely in practice because the worst-case instances don't arise.
Best-case time complexity
Each time is , which means that each time the while loop condition is false at the beginning! Which means that the array is already sorted at the beginning!
Worst-case time complexity
Each time is the largest it can be, which means that each time the while loop condition is true until is equal to . A[j] > key
is true every time, which means that the array is reversely sorted at the beginning! So every time.
Average-case time complexity
What about the average case? The elements in the input array are randomly ordered.
Hint: the number of swaps equals the number of inversions!
相当于求随机列表中反序对的期望值,为
Asymptotic order of growth
Big notation
In practice, we usually don't care about the unimportant details in the counted operations.
We need one more simplifying abstraction, which can give us an intuitive feeling of the cost of an algorithm.
The abstractions is: the rate of growth, or order of growth, of the running time that really interests us, therefore, two factors are ignored:
- Constant coefficients are not that important (when is large)
- Lower-order terms are not that important (when is large).
Big $O$ notation(大 $O$ 符号)
Given a function , we denote by the following set of functions:
Asymptotic upper bounds(渐近上界): when we say is , we mean that grows no faster than a certain rate (is asymptotically at most ).
is actually a set of functions, but computer scientists often write instead of .
Consider , we have . But we do not concluded .
Therefore, the worst time complexity of Insertion Sort , which means that it is asymptotically at most .
multiple variables
is if there exists constants such that for all .
Ex. is both and , but is neither nor .
Big notation
Big $\Omega$ notation(大 $\Omega$ 符号)
Given a function , we denote by the following set of functions:
Asymptotic lower bounds(渐近下界): when we say is , we mean that grows at least as fast as a certain rate (is asymptotically at least ).
Big notation
Big $\Theta$ notation(大 $\Theta$ 符号)
Given a function , we denote by the following set of functions:
Asymptotically tight bounds(渐近紧确界): when we say is , we mean that grows precisely at a certain rate (is asymptotically equal to ).
Small and notation
Small $o$ notation(小 $o$ 符号)
Given a function , we denote by the following set of functions:
is asymptotically (strictly) smaller than .
Small $\omega$ notation(小 $\omega$ 符号)
Given a function , we denote by the following set of functions:
is asymptotically (strictly) larger than .
There is no small notation.
Some properties of asymptotic notations
- Reflexivity: ; but
- Transitivity: if and , then
- Symmetry:
- Transpose symmetry:
Asymptotic bounds and limits
If cost functions are complex, it is hard to apply the definitions to get its asymptotic bounds. In this case, it usually easier to apply limit method.
- If for some constant , then is .
- If , then is . (a.k.a. is ).
- If , then is . (a.k.a. is ).
Proof of 1.
By definition of the limits, for any , there exists such that for all .
Choose . Multiplying by yields for all .
Thus, is by definition, with and .
Asymptotic bounds for some common functions
- Polynomials: is , where is the maximum degree of .
- Logarithms: is for any .
- is for every .
- Exponentials: is for any .
- is for every .
- Factorials: is , is .
- Using Stirling's formula(斯特林公式): .
When considering brute force algorithm to solve one problem, it is usually asymptotically equal to exponential functions.
When an algorithm has a polynomial running time, we say it is efficient, and the corresponding problem is so-called easy or tractable. The algorithm has typically exposes some crucial structure of the problem.
Although, there are exceptions. Some poly-time algorithms in the wild have galactic constants and/or huge exponents. Which would you prefer: or ?