冒泡排序的基本实现及优化
一、前言
本文不是创新性文章,本文旨在对"冒泡排序"的基本逻辑、基本实现,以及已有的优化实现进行简单的学习性总结
本文所有算法均以C++实现
如有谬误、更多的想法等等,还请留言
二、基本概念
1、算法特征
O(n2) In-place 稳定
2、算法逻辑
类似于金鱼冒泡的物理现象。金鱼吐出的气泡(待排元素) 受到 上浮过程中(每次冒泡循环) 的 水压(升序/降序) 影响而逐渐变大,冒出水面时气泡大小最大升序一次循环后最大元素排在最后
具体来说:
每次冒泡循环中:相邻的元素两两比较
,
当一个元素大于
右侧相邻元素时,交换它们的位置
;
当一个元素小于或等于
右侧相邻元素时,位置不变
。
此时,最大的数字9就排到了队尾,此时称为一次冒泡。
重复冒泡过程
,进行(n -1)
次冒泡,序列必然有序
。
3、算法复杂度
3.1、时间复杂度
因为每一次冒泡过程
将遍历所有的N个元素,且将进行N-1
次冒泡过程
,所以:
平均时间复杂度:O(N2)
最好情况:O(N)
最坏情况:O(N2)
3.2、空间复杂度
因为仅仅进行了交换,所以:
空间复杂度O(1)
三、代码实现
1、一般实现
template<typename T>
void BubbleSort(vector<T> &nums) {
int length = nums.size();
for (int i = 0; i < length - 1; ++i) {
for (int j = 0; j < length - 1 - i; ++j) {
if (nums[j] > nums[j + 1]) {
T temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}//升序
}//每一次冒泡的最大值最后一定放在length - 1 - i的位置
}//冒泡过程
}
2、算法优化
2.1、提前结束算法
情景: 冒泡排序中时常会遇到未进行完N-1
次排序,序列已经有序,但算法依然继续执行,浪费了大量的时间。
解决方案: 当某次冒泡过程中没有进行交换时,就说明序列已有序
代码实现:
template<typename T>
void BubbleSort(vector<T> &nums) {
int length = nums.size();
for (int i = 0; i < length - 1; ++i) {
bool done = true;//完成标记
for (int j = 0; j < length - 1 - i; ++j) {
if (nums[j] > nums[j + 1]) {
T temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
done = false;//未完成,继续
}//升序
}//每一次冒泡的最大值最后一定放在length - 1 - i的位置
if (done) return;//排序完成,提前结束
}//冒泡过程
}
2.2、跳过有序区
情景: 序列中本身具有部分有序的片段,但算法依旧扫描过去,造成大量时间浪费。例如:[8, 0, 7, 3, 4, 5, 6
, 1, 2, 9], 其中3,4,5,6已经处于正确位置无需再遍历。
解决方案: 标记最后一次进行交换的位置,该位置之后均为有序
代码实现:
template<typename T>
void BubbleSort(vector<T> &nums) {
int length = nums.size();
int border = length - 1;//标记有序区边界
int lastExchange = 0;//记录最后一次交换的位置
for (int i = 0; i < length - 1; ++i) {
count++;
bool done = true;//完成标记
for (int j = 0; j < border; ++j) {
if (nums[j] > nums[j + 1]) {
T temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
done = false;//未完成,继续
lastExchange = j;//更新最后交换位置
}//升序
}//每一次冒泡的最大值最后一定放在length - 1 - i的位置
if (done) return;//排序完成,提前结束
border = lastExchange;//更新边界
}//冒泡过程
}
2.3、双向冒泡排序——鸡尾酒排序
情景: 序列中某些值位于最后,需要排在最前。(逆序)例如:[3, 4, 5, 6, 7, 1],一般的冒泡排序需要执行完N-1
次,如果可以逆向冒泡,则只需要1
次即可。
题外话:为什么叫做"鸡尾酒排序"? 个人理解:双向冒泡排序像是"泡泡"在来回翻滚,如同调酒师调鸡尾酒一样。可以看看鸡尾酒排序的算法可视化视频,很有意思。
空降视频04:19s
解决方案: 按正向、逆向交替的冒泡方式进行双向冒泡
代码实现:
template<typename T>
void DoubleBubbleSort(vector<T> &nums) {
int length = nums.size();
for (int i = 0; i < length / 2; ++i) {
for (int j = 0; j < length - 1 -i; ++j) {
if (nums[j] > nums[j + 1]) {
T temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}//使正序列升序
}//奇数轮,左到右
for (int j = length - 1 - i; j > i; --j) {
if (nums[j] < nums[j - 1]) {
T temp = nums[j];
nums[j] = nums[j - 1];
nums[j - 1] = temp;
}//使正序列升序
}//偶数轮,右到左
}//冒泡过程
}
2.4、优化后的鸡尾酒排序
情景: 在一般的冒泡排序中可以优化使得算法及时结束,跳过无需考虑的部分。这些优化措施在鸡尾酒排序中也有相当的优化作用。
解决方案: 将上述优化"提前结束算法"和"跳过有序区"整合入鸡尾酒排序中
代码实现:
template<typename T>
void DoubleBubbleSort(vector<T> &nums) {
int length = nums.size();
int border = length - 1;//标记有序区边界
int lastExchange = 0;//记录最后一次交换的位置
for (int i = 0; i < length / 2; ++i) {
bool done = true;//完成标记
for (int j = 0; j < border; ++j) {
if (nums[j] > nums[j + 1]) {
T temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
done = false;//未完成,继续
lastExchange = j;//更新最后交换位置
}//使正序列升序
}//奇数轮,左到右
if (done) return;//排序完成,提前结束
border = lastExchange;//更新边界
done = true;//重置标记
for (int j = border; j > i; --j) {
if (nums[j] < nums[j - 1]) {
T temp = nums[j];
nums[j] = nums[j - 1];
nums[j - 1] = temp;
done = false;//未完成,继续
lastExchange = j - 1;//更新最后交换位置
}//使正序列升序
}//偶数轮,右到左
if (done) return;//排序完成,提前结束
border = lastExchange;//更新边界
}//冒泡过程
}
四、总结
冒泡排序是最为简单的一种排序,其实用性低
,但逻辑简单
、易快速构造
。处理较少
的数据量时,可以考虑使用。
双向冒泡排序(鸡尾酒排序),是基于冒泡排序的变种版本。再附加上优化改进,虽然在使其有更高的效率、兼容性,但其平均时间复杂度依旧为O(N2)
五、参考源
算法可视化:
https://github.com/ZQPei/Sorting_Visualization
https://github.com/zamhown/sorting-visualizer
算法优化:
https://leetcode-cn.com/leetbook/read/journey-of-algorithm/5rxj8i/
https://blog.youkuaiyun.com/zgcqflqinhao/article/details/83537017
https://blog.youkuaiyun.com/lemon_tree12138/article/details/50591859
感谢阅读!
有疑问或者认为有错误请留言,谢谢!