3. 交换排序
-
基本思想:
两两比较,如果发生逆序则交换,直到所有记录都排好序为止。 -
常见的交换排序方法:
- 冒泡排序 O(n^2)
- 快速排序 O(nlogn)
3.1 冒泡排序——基于简单的交换思想
3.1.1 算法分析和思路
-
每趟不断将记录两两比较,并按**“前小后大”**规则排序。
-
Q:为什么称之为冒泡?
A:把数据元素竖着放,观察排序的整个过程就会发现,“轻”的元素往上浮,“重”的元素往下沉。就像气泡,轻的向上冒,重的向下沉。 -
每一趟增加一个有序元素,最大的元素到了最后面,前面还是无序状态,(n-1)趟后,剩下的一个元素一定有序,即最小的元素,此时所有元素都有序。
3.1.2 算法示例
- 总结:
- 元素/记录总个数:n个;
- 趟数:(n-1)趟;
- 每趟比较次数:第 i 趟需要两两比较(n-i)次。
3.1.3 算法代码
//冒泡排序
/*
元素/记录总个数:n个
趟数:(n-1)趟
每趟比较次数:第 i 趟需要两两比较(n-i)次
*/
void BubbleSort1(SqList &L){
for(int i = 1; i <= L.length-1; i++){//n-1趟;有哨兵位,索引从1开始
for(int j = 1; j <= L.length-i; j++){//第 i 趟需要两两比较(n-i)次
//发生逆序,两两交换
if(L.nums[j].key > L.nums[j+1].key){
Swap(L, j, j+1);
}
}
}
}
- 冒泡排序优化:
若某一趟比较时记录不发生交换,说明序列全部有序,即可提前结束算法。
//冒泡排序优化:某一趟未发生交换,即停止算法,后面几趟可以省略
/*
元素/记录总个数:n个
趟数:(n-1)趟
每趟比较次数:第 i 趟需要两两比较(n-i)次
*/
void BubbleSort2(SqList &L){
int flag = 1;//flag作为是否发生交换的标记。1:发生交换;0:未发生交换
for(int i = 1; (i <= L.length-1) && (flag == 1); i++){//n-1趟;有哨兵位,索引从1开始
flag = 0;
for(int j = 1; j <= L.length-i; j++){//第 i 趟需要两两比较(n-i)次
//发生逆序,两两交换
if(L.nums[j].key > L.nums[j+1].key){
Swap(L, j, j+1);
flag = 1;//发生交换,flag置为1;若本趟未发生交换,flag保持为0
}
}
}
}
3.1.4 算法性能分析
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( 1 ) O(1) O(1)
- 是一种稳定的排序方法
3.2 快速排序——改进的交换排序
3.2.1 算法分析和思路
-
基本思想:
- 任取一个元素(如:第一个元素)为中心;
- 比中心元素小的一律放前面,比它大的一律后放面,将待排序序列分割成左右两个子表;
- 对各个子表重新选择中心元素,并依此规则分别进行以上排序;(递归思想)
- 直到每个子表的元素只剩一个,整个序列有序。
-
具体实现:
- 选定一个中心点作为参考,通过一趟排序,所有元素与之比较,小的调整到左边,大的调整到右边。
- 中心点/中间数/枢轴:第一个元素、最后一个元素、最中间一个元素、任选一个元素等。
- 如何调整?
- 开辟一个存放n个元素的空间。比中心点小的元素,从左往右放;反之,从右往左放。
缺点:需要额外的空间,当元素个数比较多时,较浪费空间。 - 只需要一个额外位置,将中心元素复制到index=0的位置,即哨兵位。
low和high双指针。双指针重合,结束循环。
每一趟的左右子表的形成是采用从两头向中间交替式逼近法。
由于每趟中对各子表的操作都相似,可采用递归算法。
当子表只剩一个元素时,即双指针重合,无需排序。
(详细细节可参考代码理解)
- 开辟一个存放n个元素的空间。比中心点小的元素,从左往右放;反之,从右往左放。
3.2.2 算法示例
3.2.3 算法代码
//快速排序算法
void QSort(SqList &L, int low, int high){
int centerIndex;
if(low < high){//当子表只剩一个元素时,即low和high重合,无需排序
centerIndex = FindCenter(L, low, high);
//将序列一分为二,center为中心点排好序的位置
QSort(L, low, centerIndex-1);//对左子表递归排序
QSort(L, centerIndex+1, high);//对右子表递归排序
}
}
//获得一趟排序后中心点位置索引值
int FindCenter(SqList &L, int low, int high){
L.nums[0] = L.nums[low];//子列第一个元素(即index=low)为中心点,中心元素复制到index=0的位置
int centerValue = L.nums[low].key;//中心点元素值
while(low < high){//low和high重合,即结束循环,重合位置即为所求的中心点位置索引值
while((low < high) && (L.nums[high].key >= centerValue)) high--;
L.nums[low] = L.nums[high];
while((low < high) && (L.nums[low].key <= centerValue)) low++;
L.nums[high] = L.nums[low];
}
L.nums[low] = L.nums[0];
return low;
}
3.2.4 算法性能分析
-
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
- 实验结果表明:就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。
- 划分元素的选取是影响时间性能的关键。
-
空间复杂度: O ( l o g n ) O(logn) O(logn)
- 快速排序不是原地排序
原地排序不需要额外的辅助空间,辅助空间和元素个数无关 - 快速排序需要递归。如果要递归,必须要用到系统栈,系统栈会记录下递归时每次调用的现场、参数值、返回值等,栈的长度取决于递归调用的深度。
- 不用递归,用自己设计的栈来实现,同样需要自己设计的用户栈。
- 快速排序不是原地排序
-
是一种不稳定的排序方法
-
快速排序不适于对原本有序或基本有序的记录序列进行排序。
越乱越好,越有序反而越慢了,快速排序不是自然排序方法。(自然排序:基本有序,更快)