算法设计与分析
1 绪论
算法的由来
算法的定义
算法的表示
算法的分析
渐近分析
T(n)= Θ (g(n))等价于:T(n)=Ω(g(n))且T(n)=O(g(n))
渐近记号 | 名称 | 输入情况 | 情况说明 |
---|---|---|---|
Θ | 渐近紧确界 | ||
O | 渐近上界 | 最坏情况 | 确认上界,更具一般性 |
Ω | 渐近下界 | 最好情况 | 不常出现,不具普遍性 |
算法运行时间称为算法的时间复杂度,通常使用渐近记号O表示。
2 分而治之篇
分而治之:一般步骤
- 分解原问题:原问题分解成多个子问题
- 解决子问题:递归地解决各个子问题
- 合并问题解:将结果合并为原问题解
2.1 归并排序
2.2 最大子数组问题
2.3 逆序对计数问题
2.3.1 问题背景:逆序对
逆序对
- 当 i < j 时A[ i ]>A[ j ]的二元组(A[ i ],A[ j ])
例如 1 < 4 且 A[ 1 ]>A[ 4 ]
问题:数组A中有多少逆序对?
答案:五对,{(A[1],A[4]),(A[2],A[4]),(A[2],A[5]),(A[3],A[4]),(A[3],A[5])}
2.3.2 问题定义
形式化定义:逆序对计数问题
输入:
- 长度为n的数组A[1…n]
输出
- 数组A[1…n]逆序对的总数,即:
蛮力枚举
…
蛮力枚举代码:
//输入:长度为n的数组A[1...n]
//输出: 数组A[1...n]逆序对的数目sum
sum = 0;
for(i=1;i<=n;i++){
for(j=i+1;j<=n;j++){
if(A[i]>A[j])
sum++;
}
}
return sum;
时间复杂度O(n2)
分而治之
算法框架
- 把数组A二分为两个子数组A[1…n/2],A[n/2+1…n]
- 递归求解子问题
- 求解S1 :仅在A[1…n/2]中的逆序对数目
- 求解S2 :仅在A[n/2+1…n]中的逆序对数目
- 合并A[1…n/2]和A[n/2+1…n]的解
- 求解S3 :跨越子数组的逆序对数目
- S = S1 + S2 + S3
合并问题解:求解S3
策略一:直接求解
- 对每个A[ j ] ∈ A[ m+1 … n ],枚举A[ i ] ∈ A[ 1…m ]并统计逆序对数目
- 求解S3的算法运行时间:O(n2)
- 分而治之框架的算法运行时间:T(n)=2·T(n/2)+O(n2) → O(n2)
…
逆序对总数:3+4+2+1=10
直接求解S3的分而治之较蛮力枚举并未提高算法运行时间!
分析导致原因
- 运行时间受限于跨越子数组的逆序对计数方法
- 数组的有序性通常有助于提高算法的运行时间
策略二:排序求解
- 分别对数组A[1…m] 和 A[ m+1…n ]进行排序
- 对于每个A[ j ] ∈A[ m+1…n ],采用二分查找为其在[1…m] 中定位
由1 和2 可知 运行时间 n 2 \frac{n}{2} 2n log n 2 \frac{n}{2} 2n - A[ j ]在A[1…m]定位点右侧的元素均可与A[ j ]构成逆序对
…
- 求解S3的算法运行时间:O(n log n)
- 分治框架的算法运行时间:T(n)=2T(n/2)+O(n log n) → O(n log2 n)
排序求解S~3~的分而治之提高了算法运行时间,是否还有优化可能?
算法优化
- 排序和二分查找均无再优化空间
子数组排序的运行时间:O(n log n)
二分查找的运行时间:O(n log n)
致因分析
- 为将排序过程融入整个算法框架
如何将排序过程融入整个算法框架?归并排序!
策略三:归并求解
- 从左到右扫描有序子数组:A[ i ] ∈ A[ 1…m ], A[ j ] ∈ A[ m+1 …n ]
- 如果A[ i ] > A[ j ],统计逆序对,j向右移
- 如果 A[ i ] ≤ A[ j ],i向右移
- 利用归并排序框架保证合并后数组的有序性
…
S3 = 4+3+2+1=10
求解3时间复杂度降至O(n)
归并求解代码:MergeCount(A,left,mid,right)
/**
* 输入:数组A[1...n],数组下标left,mid,right
*
* 输出:跨越数组A[left..mid]和 A[mid+1...right]的逆序对数,A[left...right]
*
*/
void MergCount(int A[], int left, int mid, int right) {
int _A[left..right] = A[left..righ];
int S1 = 0, S2 = 0, S3 = 0;
int i = left, j = mid, k = 0;
while (i <= mid && j <= right) {
if (_A[i] <= _A[j]) {
A[left + k] = _A[i];
k++;
i++;
} else {
A[left + k] = _A[j];
S3 = S3 + (mid - i + 1);
k++;
j++;
}
}
if (i <= mid) {
A[k..right] = _A[i..mid]
} else
A[k..right] = _A[j..riht]
return S3,A[left..right];
}
运行实例步骤:
归并求解:CountInver( A, left, right)
/**
* 输入:数组A[1..n],数组下标left,right
* 输出:数组A[left..right]的逆序对数,递增数组A[left..right]
*/
void CountInver(int A[], int left, int right){
if(left>=right)
return A[left..right];
mid = (left+right)/2; // 分解原问题
S1 = CountInver(A,left,mid); //解决子问题
S2 = CountInver(A,mid+1,right); //解决子问题
S3 = MergCount(A,left,mid,right); //合并问题解
S = S1+S2+S3;
return S,A[left..righ];
}
小结:
算法名称 | 合并求解复杂度 | 时间复杂度 |
---|---|---|
蛮力枚举 | O(n2) | |
分而治之+直接计算 | O(n2) | O(n2) |
分而治之+排序求解 | O(n log n) | O(n log2 n) |
分而治之+归并求解 | O(n) | O(n log2 n) |
分治策略关键:合理设计合并求解算法