void selectionSort(std::vector<int>& a){constint n =static_cast<int>(a.size());for(int i =0; i < n -1;++i){int k = i;for(int j = i +1; j < n;++j){if(a[j]< a[k]){ k = j;}}std::swap(a[i], a[k]);}}
void rankElements(conststd::vector<int>& a,std::vector<int>& r){constint n =static_cast<int>(a.size());std::fill(r.begin(), r.end(),0);for(int i =1; i < n;++i){for(int j =0; j < i;++j){if(a[j]<= a[i])++r[i];else++r[j];}}}
每一对元素正好比较一次,因此比较次数也是:
\[
\frac{n(n-1)}{2}
\]
Rank Sort:重排
如果排名数组 r[i] 表示 a[i] 应该去的位置,可以通过交换把元素放到正确位置。
void rearrange(std::vector<int>& a,std::vector<int>& r){constint n =static_cast<int>(a.size());for(int i =0; i < n;++i){while(r[i]!= i){int t = r[i];std::swap(a[i], a[t]);std::swap(r[i], r[t]);}}}
int sequentialSearch(conststd::vector<int>& a,int x){for(int i =0; i <static_cast<int>(a.size());++i){if(a[i]== x)return i;}return-1;}
若查找成功:
最好:目标在第 1 个位置,比较 1 次
最坏:目标在第 \(n\) 个位置,比较 \(n\) 次
平均:每个位置等概率,比较次数为:
\[
\frac{1+2+\cdots+n}{n}=\frac{n+1}{2}
\]
插入排序:代码
插入排序维护一个已排序前缀,把新元素插入到正确位置。
void insertionSort(std::vector<int>& a){constint n =static_cast<int>(a.size());for(int i =1; i < n;++i){int t = a[i];int j = i -1;while(j >=0&& t < a[j]){ a[j +1]= a[j];--j;} a[j +1]= t;}}
插入排序:最好与最坏
最好情况:数组已经有序。
每轮只比较一次,不移动元素:
\[
T_{\text{best}}(n)=\Theta(n)
\]
最坏情况:数组逆序。
第 \(i\) 轮要移动 \(i\) 个元素:
\[
1+2+\cdots+(n-1)=\frac{n(n-1)}{2}
\]
因此:
\[
T_{\text{worst}}(n)=\Theta(n^2)
\]
Step Counts:给每行计步
有时需要不只数关键操作,而是给所有语句计步。
int sum(conststd::vector<int>& a){int total =0;// 1for(int i =0; i < a.size();++i){// n+1 次条件检查 total += a[i];// n}return total;// 1}
int binarySearch(conststd::vector<int>& a,int x){int low =0;int high =static_cast<int>(a.size())-1;while(low <= high){int mid = low +(high - low)/2;if(a[mid]< x){ low = mid +1;}elseif(a[mid]> x){ high = mid -1;}else{return mid;}}return-1;}
每次循环把候选区间约减半,因此最坏时间为 \(\Theta(\log n)\)。
最大子段和问题
给定整数序列 \(a_1,a_2,\ldots,a_n\),允许存在负数,求连续子段的最大和:
\[
\max_{1\le i\le j\le n}\sum_{k=i}^{j}a_k
\]
如果所有数均为负数,这里采用约定:最大子段和为 0。
例子:
\[
[-2,\ 11,\ -4,\ 13,\ -5,\ -2]
\]
最大子段是 \([11,-4,13]\),和为 20。
最大子段和:三种思路
算法 1:三层枚举
int maxSubSum1(conststd::vector<int>& a){int maxSum =0;constint n =static_cast<int>(a.size());for(int i =0; i < n;++i){for(int j = i; j < n;++j){int thisSum =0;for(int k = i; k <= j;++k){ thisSum += a[k];} maxSum =std::max(maxSum, thisSum);}}return maxSum;}
枚举起点、终点、再求和,复杂度为 \(O(n^3)\)。
算法 2:固定起点向右累加
int maxSubSum2(conststd::vector<int>& a){int maxSum =0;constint n =static_cast<int>(a.size());for(int i =0; i < n;++i){int thisSum =0;for(int j = i; j < n;++j){ thisSum += a[j]; maxSum =std::max(maxSum, thisSum);}}return maxSum;}
对每个起点,向右扩展时复用前一次的和。
复杂度降为 \(O(n^2)\)。
算法 3:分治
把数组分成左右两半。最大子段只可能在三类位置:
完全在左半边
完全在右半边
跨过中点
递归求左右两边,再用线性扫描求跨中点最大和:
\[
T(n)=2T(n/2)+O(n)
\]
因此:
\[
T(n)=O(n\log n)
\]
分治代码:核心结构
int maxSumRec(conststd::vector<int>& a,int left,int right){if(left == right)returnstd::max(0, a[left]);int center = left +(right - left)/2;int maxLeft = maxSumRec(a, left, center);int maxRight = maxSumRec(a, center +1, right);int leftBorder =0, bestLeftBorder =0;for(int i = center; i >= left;--i){ leftBorder += a[i]; bestLeftBorder =std::max(bestLeftBorder, leftBorder);}
分治代码:合并
int rightBorder =0, bestRightBorder =0;for(int i = center +1; i <= right;++i){ rightBorder += a[i]; bestRightBorder =std::max(bestRightBorder, rightBorder);}returnstd::max({maxLeft, maxRight, bestLeftBorder + bestRightBorder});}int maxSubSum3(conststd::vector<int>& a){if(a.empty())return0;return maxSumRec(a,0,static_cast<int>(a.size())-1);}
欧几里得算法
欧几里得代码
longlong gcd(longlong m,longlong n){while(n !=0){longlong rem = m % n; m = n; n = rem;}return m;}
核心不变量:
\[
\gcd(m,n)=\gcd(n,m\bmod n)
\]
由于数字规模快速缩小,复杂度为 \(O(\log N)\)。
多维度分析表
算法
时间
空间
最好/最坏差异
关键原因
顺序查找
\(\Theta(n)\)
\(\Theta(1)\)
有
命中位置不同
二分查找
\(\Theta(\log n)\)
\(\Theta(1)\)
有
每步减半
选择排序
\(\Theta(n^2)\)
\(\Theta(1)\)
小
总要找最小值
插入排序
\(\Theta(n)\) 到 \(\Theta(n^2)\)
\(\Theta(1)\)
大
输入越有序越快
最大子段和分治
\(O(n\log n)\)
\(O(\log n)\)
小
每层线性合并
欧几里得
\(O(\log N)\)
\(\Theta(1)\)
有
余数快速缩小
分析算法的固定流程
遇到一段新代码,可以按这个顺序分析:
确定输入规模 \(n\) 或其他实例特征
选择关键操作
写出执行次数表达式
化简求和或递推式
用 \(O\)、\(\Omega\)、\(\Theta\) 表达增长阶
检查最好、最坏、平均是否不同
检查额外空间和递归栈
习题 1:selectkth
题目:分析在无序数组中找第 \(k\) 小元素的函数。
int selectKth(std::vector<int>& a,int k){for(int i =0; i < k;++i){int mini = i;for(int j = i +1; j <static_cast<int>(a.size());++j){if(a[j]< a[mini]) mini = j;}std::swap(a[i], a[mini]);}return a[k -1];}