算法分析与设计 - 作业5
问题一
在不同应用领域,经常涉及到top-K的问题,请给出不同策略在一系列数中返回Top-K元素,并分析你的策略。
解法一
我会排序!
考虑对给定序列进行排序,取前 大即可。
使用基于比较的排序算法,时间复杂度 级别。
解法二
当 较小时,将整个序列排序有些太浪费了。
考虑进行 轮冒泡排序或选择排序,仅使 大值有序即可。
时间复杂度 级别,当 时较优。
解法三
当序列中的元素均为整数且值域较小时,可以考虑进行计数排序,并在顺序枚举值域时取枚举到的前 大即可。
若值域为 级别,则时间复杂度为 级别。
解法四
我学过快排!
考虑快排每轮进行分治时进行的操作:选择基准元素,将小于/大于基准元素的元素分别划分到基准元素的两侧。则此时根据基准元素左侧的元素数量就可以得到基准元素的排名 :
- 若 ,说明基准元素及其左侧元素即为前 大元素,停止算法。
- 若 ,说明前 大元素均小于基准元素,问题转化为求基准元素左侧的所有元素的第 大,递归进行即可。
- 若 ,说明不大于基准元素的均为前 大,可以直接加入答案中,问题转化为求基准元素右侧的所有元素的前 大值,递归进行即可。
比起分治更像是一种减治。
与快速排序复杂度分析类似地,若基于随机选择基准元素,上述算法时间复杂度期望 级别,但是时间复杂度上限为 级别。
但需要注意与解法一二不同的是,此解法仅能将前 大值选出而不能保证前 大值的内部的有序性。
该算法即为 C++ STL 中的 nth_elements
内部实现的大致思路,另外 nth_elements
在区间长度小于 3 时会转化为插入排序,很酷!
复制// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e5 + 10; //============================================================= int a[kN]; //============================================================= int kth_elements(int l_, int r_, int k_) { int p = rand() % (r_ - l_ + 1) + l_, i = l_, j = l_; std::swap(a[p], a[r_]); while (j < r_) { if (a[j] >= a[r_]) std::swap(a[i], a[j]), ++ i; ++ j; } std::swap(a[i], a[j]); if (r_ == k_ + i - 1) return a[i]; if (r_ > k_ + i - 1) return kth_elements(i + 1, r_, k_); return kth_elements(l_, i - 1, k_ - (r_ - i + 1)); } //============================================================= int main() { // freopen("1.txt", "r", stdin); // std::ios::sync_with_stdio(0), std::cin.tie(0); srand(time(0)); int n, k; std::cin >> n >> k; for (int i = 1; i <= n; ++ i) std::cin >> a[i]; int ak = kth_elements(1, n, k); for (int i = 1; i <= k; ++ i) std::cout << a[i] << " "; return 0; }
解法五
我会分治!
考虑将序列分块,每 个连续元素被分为一块,则共有 块。
对于每块套用解法四 地求出其中的前 大值,然后对所有块的有序的前 大值归并即得整体的前 大值。
归并的复杂度为 级别,总时间复杂度 级别。
解法四同阶但是常数更大了上,感觉多此一举!
但是注意到分解后的子问题可以分布式地执行后再合并,不缺算力的情况下推荐使用。
解法六
BFPRT 算法,又称中位数的中位数算法,一种对解法四的优化,可保证复杂度上限为 级别。
以发明者 Blum、Floyd、Pratt、Rivest、Tarjan 的首字母命名。怎么都是熟人、、、又见到了我最喜欢的 LCT 的 Tarjan 大神怎么哪都有你
为什么解法四会被卡到 级别?因为无法保证每次划分选择基准元素时均能选取到中位数,使基准元素两侧元素数量级不平衡,但是又会减治递归到元素数量较多一侧,从而使最坏情况下递归次数变为 级别。BFPRT 算法在算法四的基础上,对选取基准元素的过程进行了优化,使得基准元素能更加接近中位数,从而避免了上述最坏情况的出现。
具体地,在选择基准元素时,首先将整个序列每 5 个相邻元素进行分块,求得每块中的中位数,再将求得的所有中位数组成一个序列后,递归调用 BFPRT
算法求该序列的中位数即为基准元素。
可以证明求得的基准元素一定被限制在整个序列的 范围内,避免了最坏情况的发生。
在找基准元素仅仅首先遍历了整个序列,然后递归调用了 BFPRT
算法,则时间复杂度不变仍为 级别。通过递归分析可知算法总时间复杂度为 级别。
时间复杂度分析详见:BFPRT——Top k问题的终极解法 - 知乎。
代码实现时考虑了重复出现的基准元素的影响。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e5 + 10; //============================================================= std::vector <int> a; //============================================================= int bfprt(std::vector <int> &a_, int l_, int r_, int k_); int medianOfMedians(std::vector<int> &a_, int l_, int r_) { int len = r_ - l_ + 1, bnum = len / 5 + (len % 5 > 0); std::vector <int> temp1, temp2; for (int i = 1; i <= bnum; ++ i) { int bl = l_ + 5 * (i - 1), br = std::min(l_ + 5 * i - 1, r_), blen = br - bl + 1; for (int j = bl, k = 1; j <= br; ++ j, ++ k) temp1.push_back(a_[j]); std::sort(temp1.begin(), temp1.end()); temp2.push_back(temp1[blen / 2]); } std::sort(temp2.begin(), temp2.end()); return bfprt(temp2, 0, bnum - 1, bnum / 2); } std::vector<int> partition(std::vector <int> &a_, int l_, int r_, int pivot_) { int greater = l_ - 1, equal = 0, temp; for (int i = l_; i <= r_; ++ i) { if (a_[i] > pivot_) { ++ greater; temp = a_[greater], a_[greater] = a_[i]; if (equal > 0) { a_[i] = a_[greater + equal]; a_[greater + equal] = temp; } else { a_[i] = temp; } } else if (a_[i] == pivot_) { ++ equal; temp = a_[i]; a_[i] = a_[greater + equal]; a_[greater + equal] = temp; } } return std::vector<int> {greater + 1, greater + equal}; } int bfprt(std::vector <int> &a_, int l_, int r_, int k_) { if (l_ == r_) return a_[l_]; int pivot = medianOfMedians(a_, l_, r_); std::vector<int> p = partition(a_, l_, r_, pivot); if (l_ + k_ >= p[0] && l_ + r_ <= p[1]) return a_[p[0]]; if (l_ + k_ < p[0]) return bfprt(a_, l_, p[0] - 1, k_); return bfprt(a_, p[1] + 1, r_, k_ + l_ - p[1] - 1); } //============================================================= int main() { // freopen("1.txt", "r", stdin); // std::ios::sync_with_stdio(0), std::cin.tie(0); int n, k; std::cin >> n >> k; for (int i = 1; i <= n; ++ i) { int x; std::cin >> x; a.push_back(x); } int ak = bfprt(a, 0, n - 1, k); for (int i = 0; i < k; ++ i) std::cout << a[i] << " "; return 0; }
解法七
我会使用数据结构!
考虑使用支持求解最大元素的数据结构进行维护。
考虑在遍历序列元素时维护一个元素个数上限为 的小根堆,当枚举到元素 时:
- 若堆中元素个数不大于 ,则直接入堆。
- 若堆中元素个数为 ,则比较堆顶元素与 的大小关系,若 大于堆顶元素则令堆顶元素弹出,并将 入堆。
遍历完成后堆中的 个元素即为序列的前 大,且通过不断弹出直至堆空可使序列的前 大处于有序状态。
堆中元素个数上限为 ,则单次入堆/出堆操作的时间复杂度上限为 级别,则总时间复杂度 级别,在能够保证前 大有序的情况下,时间复杂度优于解法一二。
偷懒写优先队列哈哈
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e5 + 10; //============================================================= int a[kN]; //============================================================= //============================================================= int main() { // freopen("1.txt", "r", stdin); // std::ios::sync_with_stdio(0), std::cin.tie(0); int n, k; std::cin >> n >> k; std::priority_queue <int, std::vector <int>, std::greater<int> > q; for (int i = 1; i <= n; ++ i) { std::cin >> a[i]; if (i <= k) q.push(a[i]); else if (a[i] > q.top()) q.pop(), q.push(a[i]); } while (!q.empty()) { std::cout << q.top() << " "; q.pop(); } return 0; }
解法八
我会使用数据结构!
既然能用堆,那也能用二叉搜索树!
与解法六类似地,考虑维护一棵元素个数上限为 的二叉搜索树,当枚举到元素 时:
- 若树中元素个数不大于 ,则直接插入。
- 若树中元素个数为 ,则比较树中最小元素与 的大小关系,若 大于最小元素则删除最小元素,并插入 。
遍历完成后对二叉搜索树进行中序遍历即得有序的前 大值。
时间复杂度也为 级别,与解法七同阶并且结果一致,但是常数较大而且平衡树也很难写,与解法七相比没有什么竞争力。
懒得写平衡树了代码略。
问题二
调研学习排序算法 CubeSort,体会分治思想的使用。
不是我要笑死了这 b 排序、、、
考虑将待排序序列每 个连续元素分为一段(称为一个 Cube
),对每段分别调用其他排序方法排序后,再将所有段归并即得有序序列。
一般 取一个较小的值,如 8, 16
。时间复杂度依赖于调用的其他排序方法,但总时间复杂度与直接调用该排序方法同阶。
若调用的其他排序方法是稳定的,则 Cubesort 是稳定的。
Cubesort 的优势在于可以分布式地处理不同段的排序,得到结果后再归并即可,适合算力充足的情况。
并且当 较小时,可以使用在较小数据范围时近似线性的插入排序,使排序的平均复杂度远低于上限,实际使用时表现很优秀。
#include <iostream> #include <algorithm> const int CUBE_SIZE = 8; // function to sort the cube void cubeSort(int arr[], int n) { // divide the array into cubes of size CUBE_SIZE for (int i = 0; i < n; i += CUBE_SIZE) { std::sort(arr + i, arr + std::min(i + CUBE_SIZE, n)); } // merge the cubes int temp[n]; for (int i = 0; i < n; i += CUBE_SIZE) { std::merge(arr + i, arr + std::min(i + CUBE_SIZE, n), arr + std::min(i + CUBE_SIZE, n), arr + std::min(i + 2*CUBE_SIZE, n), temp + i); } // copy the result from temp[] back to arr[] for (int i = 0; i < n; i++) arr[i] = temp[i]; } // main function int main() { // input array int arr[] = {3, 6, 7, 1, 5, 2, 8, 4}; int n = sizeof(arr) / sizeof(arr[0]); // call cubeSort cubeSort(arr, n); // print the sorted array for (int i = 0; i < n; i++) std::cout << arr[i] << " "; return 0; }
写在最后
参考:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现