4.1 排序
排序就是将一个原本无序的序列按照某个规则重新排列成有序序列的过程。现有的排序算法主要有以下几类:
- 插入类排序:直接插入排序、折半插入排序、希尔排序等;
- 交换类排序:冒泡排序、快速排序等;
- 选择类排序:简单选择排序、堆排序等;
- 归并类排序:二路归并排序等;
- 基数类排序:基数排序等;
4.1.1 冒泡排序
冒泡排序的本质在于交换
,属于交换类排序,这类排序还有快速排序等。所谓“交换”,就是指每一趟排序,都通过一系列的交换操作,使得一个元素排到它最终应该在的位置上。
若我们想使用冒泡排序将一个乱序的数字序列从小到大排列,每趟我们都要通过交换的方式将当前剩余元素的最大值移到最右端。这个过程中,大的元素“沉”到右端,小的元素“浮”到左端,所以被形象地称为冒泡排序。
通过一个例子看一下冒泡排序的步骤:从小到大排列{3, 4, 1, 5, 2}
- 第一趟
从左到右,先比较第一、二个元素, 3 < 4 3 < 4 3<4,所以3和4的顺序不变。{3, 4, 1, 5, 2};
比较第二、三个元素, 4 > 1 4 > 1 4>1,所以将4和1的位置交换。{3, 1, 4, 5, 2};
比较第三、四个元素, 4 < 5 4 < 5 4<5,4和5的顺序不变。{3, 1, 4, 5, 2};
比较第四、五个元素, 5 > 2 5 > 2 5>2,将5和2的位置进行交换。{3, 1, 4, 2, 5};
就此,我们将最大的数值移到了最右端,接下来只要对前4个数值再进行一次冒泡排序,就可以将第二大的值移到5的前面。
- 第二趟
从左到右比较第一、二个元素, 3 > 1 3 > 1 3>1,将3和1的位置交换。{1, 3, 4, 2, 5};
比较第二、三个元素, 3 < 4 3 < 4 3<4,3和4的顺序不变。{1, 3, 4, 2, 5};
比较第三、四个元素, 4 > 2 4 > 2 4>2,将4和2的位置交换。{1, 3, 2, 4, 5};
到这里为止,我们已经将4这个第二大的数值移到它该在的位置了。不知道大家发现了没有,这个序列中有5个元素,我们第一趟排序比较了4次,第二趟比较只比较了3次,如果继续比较下去,我们一共会比较4趟,后一趟比前一趟都会少比较一次元素。所以,我们可以总结出一个规律:若序列中有n个元素,冒泡排序总共会比较n-1趟,且每一趟的比较次数会从n-1开始逐趟递减。
接下来用上面的这个列子实现一下冒泡排序的算法:
#include <cstdio>
int main(){
int a[10] = {3, 4, 1, 5, 2};
int temp;
// 冒泡排序的主要代码
// 这里5是指数组a的长度,为了方便理解特意写成5-1
// 其实也就是上文提到的n-1趟
for(int i=0; i<5-1; i++){
// 从第一趟开始,比较次数会从n-1开始递减
// 所以这里除了5-1,为了保证递减,最后要写成5-1-i
// 因为i从0开始递增
for(int j=0; j<5-1-i; j++){
// 如果前一个元素大于后一个元素
// 就交换前后元素,否则不用交换
// 这是交换的核心代码
if(a[j] > a[j+1]){
temp = a[j+1];
a[j+1] = a[j];
a[j] = temp;
}
}
}
// 从前往后输出排好序的元素
// 除了最后一个元素,元素之间要有空格
for(int i=0; i<5; i++){
printf("%d", a[i]);
if(i < 5-1){
printf(" ");
}
}
return 0;
}
时间复杂度
:冒泡排序的基本操作主要是内层循环中元素交换的操作。整个代码有两个for循环,每次外层循环对应的内层循环次数为
n
−
1
−
i
n-1-i
n−1−i。一般时间复杂度是考虑的最坏情况,假设每次比较之后都要交换元素,由此我们计算一下算法总的比较次数
1
+
2
+
3
+
⋯
+
(
n
−
2
)
+
(
n
−
1
)
=
(
1
+
(
n
−
1
)
)
∗
(
n
−
1
)
2
=
n
2
−
n
2
1 + 2 + 3 + \cdots + (n-2) + (n-1) = \frac{(1+(n-1))*(n-1)}{2} = \frac{n^2 - n}{2}
1+2+3+⋯+(n−2)+(n−1)=2(1+(n−1))∗(n−1)=2n2−n,这里的n是元素个数。综上,冒泡排序的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
空间复杂度
:空间复杂度中的
O
(
1
)
O(1)
O(1)是指算法消耗的空间不随数据规模
n
n
n的增大而增大。在冒泡排序中,只有一个额外的temp变量,因此空间复杂度为
O
(
1
)
O(1)
O(1)。
由于冒泡排序的时间复杂度较高,当使用它时,一般会设置一个变量,当某一趟排序过程中没有发生元素交换时,这个变量会记录下来并用以结束算法。为什么可以提前结束呢?因为一趟排序中没有发生元素交换,说明整个序列已经是有序状态,提前结束可以有效减少操作次数。
#include <cstdio>
int main(){
int a[10] = {3, 4, 1, 5, 2};
int flag, temp;
for(int i=0; i<5-1; i++){
// 初始化标记
flag = 0;
for(int j=0; j<5-1-i; j++){
if(a[j] > a[j+1]){
temp = a[j+1];
a[j+1] = a[j];
a[j] = temp;
// 发生元素交换,标记变化
flag = 1;
}
}
// 若没有元素进行交换,算法结束
if(flag == 0){
break;
}
}
for(int i=0; i<5; i++){
printf("%d", a[i]);
if(i < 5-1){
printf(" ");
}
}
return 0;
}