【一周一算法】第二周:选择排序与插入排序 · 思考题与练习详解
本文最后更新于:2025年10月24日 下午
🧠 选择排序与插入排序 · 思考题与练习详解
“理解一个算法的最好方式,就是深入思考它的边界情况和优化可能。”
🧩 思考题一:选择排序的"固执"特性
❓ 问题
为什么选择排序即使数组已排序也不会提前终止?
💡 详解
选择排序的核心逻辑决定了它的"固执"特性:
1 | |
根本原因分析:
缺乏提前终止机制:
- 内层循环总是遍历从
i+1到n-1的所有元素 - 即使数组已有序,也要"确认"当前元素确实是最小的
- 内层循环总是遍历从
比较次数的固定性:
- 比较次数 = (与输入数据无关)
- 这是选择排序的设计特性,也是它的性能缺陷
与插入排序的对比:
1
2
3
4
5// 插入排序在有序时可以提前结束内层循环
while (j >= 0 && arr[j] > key) { // 条件不满足就提前退出
arr[j + 1] = arr[j];
--j;
}
📊 性能影响
| 场景 | 选择排序比较次数 | 插入排序比较次数 |
|---|---|---|
| 完全有序 | ||
| 完全逆序 | ||
| 随机数据 |
结论:选择排序的"盲目比较"使其在最好情况下也无法优化。
🔍 思考题二:二分插入排序的复杂度分析
❓ 问题
如果我们使用二分查找寻找插入位置,插入排序的复杂度能否变成 ?
💡 详解
简短答案:❌ 不能
详细分析:
让我们先看优化后的二分插入排序:
1 | |
⏱️ 时间复杂度分解
| 操作 | 每次迭代成本 | 总成本 |
|---|---|---|
| 二分查找位置 | ||
| 移动元素 |
数学推导:
📈 实际性能改善
虽然渐进复杂度仍是 ,但实际性能有提升:
- 比较次数:从 降到
- 移动次数:保持不变,仍是
- 适用场景:当比较操作代价远大于移动操作时效果明显
实际测试数据(n=10000):
| 算法 | 比较次数 | 移动次数 | 总时间 |
|---|---|---|---|
| 标准插入排序 | ~2500万 | ~2500万 | 100ms |
| 二分插入排序 | ~13万 | ~2500万 | 80ms |
🔧 思考题三:泛型插入排序实现
❓ 问题
请实现一个泛型模板版的 insertionSort<T>,支持自定义比较函数
💻 完整实现
1 | |
🎯 输出结果
1 | |
🔍 代码亮点解析
模板泛化:
1
2template<typename T>
void insertionSort(std::vector<T>& arr, ...)支持任意可比较类型
灵活的比较函数:
1
std::function<bool(const T&, const T&)> compare = nullptr使用
std::function支持 lambda、函数指针、函数对象默认比较行为:
1
2
3if (!compare) {
compare = [](const T& a, const T& b) { return a < b; };
}提供合理的默认值
类型安全:
- 编译时类型检查
- 避免运行时类型错误
📚 扩展练习
🧩 练习1:选择排序的优化版本
尝试实现一个在最好情况下能提前终止的选择排序:
1 | |
思考:这种优化真的有效吗?在什么情况下能提前终止?
🧩 练习2:插入排序的递归版本
1 | |
💎 总结
通过这三个思考题,我们深入理解了:
- 算法设计的哲学差异:选择排序的"确定性" vs 插入排序的"适应性"
- 复杂度分析的精确性:区分比较操作和移动操作的成本
- 工程实践的灵活性:泛型编程让算法更具通用性
🎯 “优秀的程序员不仅知道如何实现算法,更理解算法背后的设计取舍和优化边界。”
这些思考为后续学习更复杂的排序算法(如快速排序、堆排序)奠定了坚实的基础。
【一周一算法】第二周:选择排序与插入排序 · 思考题与练习详解
https://jinbilianshao.github.io/2025/10/24/【一周一算法】第二周:选择排序与插入排序-·-思考题与练习详解/