选择排序和冒泡排序是我们初学C语言必学的两种简单的排序算法,也是我们以后学习数据结构与算法课程中更复杂的排序算法的基础。本文用由浅入深的逻辑条理,试图将这两种排序算法讲解清楚。
本文所有的排序均是针对一维数组的,全文所用的一维数组的例子如下:
#include
int main () {
int N=5,A[N] = {2,4,1,5,3};
return 0;
}
一次选择所要完成的任务是:从数组元素A[i]开始,到最后一个元素,即A[N-1],选择其中的最小值;然后经过一些数组元素之间的交换,将这个最小值放在A[i]中,最小值以外的其它数组元素的值仍要分布在A[i+1]到A[N-1]之中,次序不作要求。
思路1:要完成这个任务,我们可以遍从A[i+1]开始,到A[N-1]为止的所有元素,如果某个A[j] < A[i],我们就将A[j]和A[i]中的值进行交换。完成这个遍历的后果就只,此时A[i]的值就是原来A[i]到A[N-1]中的最小值,原来除最小值以外的其他值,仍分布在A[i+1]到A[N-1]之中,次序在所不论。
代码1:
#include
int main () {
int N=5,A[5] = {2,4,3,5,1},i,j,tmp;
i = 2;
for (j = i+1; j < N; j++) {
if (A[j] < A[i]) {
tmp = A[i];
A[i] = A[j];
A[j] = tmp;
}
}
for (i = 0; i < N; i++) printf("%d ", A[i]);
return 0;
}
输出:
2 4 1 5 3
在上面的代码中,我们选择从下标为i=2,即A[2]开始到A[N-1]为止的所有元素中的最小值,将其赋值给A[2],其他原有的数组元素值任然分布在A[3]到A[4]之中。
设想一下,如果我们第一次选择i=0的位置为起始位置,进行"一次选择"的任务,那么整个数组中的最小值将会赋值给A[0],其他元素仍然会分布在A[1]到A[N-1]之中。第二次我们再选择i=1的位置为起始位置,再进行一次"一次选择"的任务,那么A[1]到A[N-1]中最小的值将会被赋值给A[1],其他值仍然分布A[2]到A[N-1]之中。这样A[0],A[1]元素其实分别就已经是原来数组元素的最小值和次最小值了。依此类推可知,对当我们将"一次选择"任务中选择的起始下标i从0开遍历到N-2,这样我们恰好就能完成对原数组的从小到大的排序了。根据上面的想法,我们就有了如下的选择排序算法。
代码2:
#include
int main () {
int N=5,A[5] = {2,4,3,5,1},i,j,tmp;
for (i = 0; i < N-1; i++) {
for (j = i+1; j < N; j++) {
if (A[j] < A[i]) {
tmp = A[i];
A[i] = A[j];
A[j] = tmp;
}
}
}
for (i = 0; i < N; i++) printf("%d ", A[i]);
return 0;
}
思路:在"一次选择"的算法中,也就是上述代码2的内循环中,我们可以有这样的优化办法,就是不要每次找到一个比A[i]小的A[j]就进行交换,可先把目前找到的最小值的下标缓存起来,等到遍历完以后,进行一次交换就行了。根据这个思路,我们就有了优化后的选择排序算法。
代码3:
#include
int main () {
int N=5,A[5] = {2,4,3,5,1},i,j,min,tmp;
for (i = 0; i < N-1; i++) {
min = i;
for (j = i+1; j < N; j++) {
if (A[j] < A[min]) {
min = j;
}
}
if (min > i) {
tmp = A[i];
A[i] = A[min];
A[min] = tmp;
}
}
for (i = 0; i < N; i++) printf("%d ", A[i]);
return 0;
}
先看一次"冒泡",它的思路比较固定,就是让j从0开始直到N-2,比较A[j]和它后面紧邻的元素A[j+1]的大小,如果A[j]>A[j+1],就交换A[j]和A[j+1]的值;否则就让j继续遍历。这样当遍历完以后,原本A[0]到A[N-1]中最大的元素,就被交换赋值给A[N-1]。
代码4:
#include
int main () {
int N=5,A[N] = {2,4,1,5,3},i,j,tmp;
for (j = 0; j < N-1; j++) {
if (A[j] > A[j+1]) {
tmp = A[j];
A[j] = A[j+1];
A[j+1] = tmp;
}
}
for (i = 0; i < N; i++) printf("%d ", A[i]);
return 0;
}
输出:
2 1 4 3 5
设想一下,当我们完成第1次"冒泡"任务,那么数组元素A[0]到A[N-1]中最大的就被交换到A[N-1]中了,当我们完成第2次"冒泡"任务,那么数组元素A[0]到A[N-2]中的最大的就被交换到A[N-2]中了。这里要注意,元素A[0]到A[N-2]中的最大的一定比A[N-1]中的元素小,所以没有优化过的"冒泡"任务会把元素A[0]到A[N-2]中的最大的和A[N-1]进行比较,但是比较的结果一定是A[N-1]大,所以不会进行交换。于是,当我们进行了N-1次"冒泡"任务后,整个数组就按从小到大的次序排好了。根据上面的想法,我们就有了如下的冒泡排序算法。
代码5:
#include
int main () {
int N=5,A[N] = {2,4,1,5,3},i,j,tmp;
for (i = 0; i < N-1; i++) {
for (j = 0; j < N-1; j++) {
if (A[j] > A[j+1]) {
tmp = A[j];
A[j] = A[j+1];
A[j+1] = tmp;
}
}
}
for (i = 0; i < N; i++) printf("%d ", A[i]);
}
在上面的代码里,外层循环的环变量i,只是用来控制了里层"冒泡"的迭代次数。并没有参与里层"冒泡"。
对上面的冒泡排序的代码,我们可以从两个方面进行优化。
(1)首先对里层循环,我们知道第1次"冒泡"完成后,数组最大的元素就已经被拍到最后一位了,那么第2次"冒泡"时,就没有必要在比较A[N-1]和A[N-2]了。一次类推,容易验证当外层循环为i时,里层循环只需要进行到j=N-1-i就可以了。根据上述想法,我们就有了对里层循环优化后的冒泡排序算法。
代码6:
#include
int main () {
int N=5,A[N] = {2,4,1,5,3},i,j,tmp;
for (i = 0; i < N-1; i++) {
for (j = 0; j < N-1-i; j++) {
if (A[j] > A[j+1]) {
tmp = A[j];
A[j] = A[j+1];
A[j+1] = tmp;
}
}
}
for (i = 0; i < N; i++) printf("%d ", A[i]);
}
(2)对外层循环的优化,上面的代码我们可以看到,外层循环的i总会遍历从0开始到N-1的所有整数;换句话说,里层循环作为外层循环的循环体,总会被执行N-1次。是否有这个必要呢?我们假设,当有一次执行里层循环时,里层循环遍历的每一个A[j]和A[j+1]都不发生交换,这时整个数组必然是已经按照从小到大的次序排好了,否侧不可能不发生交换。于是,我们可以增加一个变量flag,来记录里层循环是否发生了交换。如果有一次里层循环没有发生交换,那么这次里层循环执行完毕后,就没有比较再进行接下来的里层循环了;也就是说,这时外层循环可以直接跳出。根据上述想法,我们就有了对里层循环优化后的冒泡排序算法。
代码7:
#include
int main () {
int N=5,A[N] = {2,4,1,5,3},i,j,tmp,flag=1;
for (i = 0; i < N-1; i++) {
for (j = 0; j < N-1-i; j++) {
if (A[j] > A[j+1]) {
tmp = A[j];
A[j] = A[j+1];
A[j+1] = tmp;
flag = 0;
}
}
if (flag) break;
}
for (i = 0; i < N; i++) printf("%d ", A[i]);
}
最后,如果我们对函数运用的足够熟练,我们可以把上面讲的两种排序算法封装成函数,方便调用。
代码8:
#include
void sort_c (int A[], int N) { // 选择排序
int i,j,min,tmp;
for (i = 0; i < N-1; i++) {
min = i;
for (j = i+1; j < N; j++) {
if (A[j] < A[min]) min = j;
}
if (min > i) {
tmp = A[i];
A[i] = A[min];
A[min] = tmp;
}
}
}
void sort_b (int A[], int N) { // 冒泡排序
int i,j,tmp,flag = 1;
for (i = 0; i < N-1; i++) {
for (j = 0; j < N-1-i; j++) {
if (A[j] > A[j+1]) {
tmp = A[j];
A[j] = A[j+1];
A[j+1] = tmp;
flag = 0;
}
}
if (flag) break;
}
}
int main () {
int N=5,A[N] = {2,4,3,5,1},i;
sort_b(A,N); // 调用冒泡排序
// sort_c(A,N); // 调用选择排序
for (i = 0; i < N; i++) printf("%d ", A[i]);
return 0;
}