排序算法(一) -- 选择排序和冒泡排序

选择排序和冒泡排序是我们初学C语言必学的两种简单的排序算法,也是我们以后学习数据结构与算法课程中更复杂的排序算法的基础。本文用由浅入深的逻辑条理,试图将这两种排序算法讲解清楚。

本文所有的排序均是针对一维数组的,全文所用的一维数组的例子如下:

cpp 复制代码
#include <stdio.h>
int main () {
    int N=5,A[N] = {2,4,1,5,3};
    return 0;
}

一、选择排序

1.1 一次选择

一次选择所要完成的任务是:从数组元素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:

cpp 复制代码
#include <stdio.h>
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;
}

输出:

cpp 复制代码
2 4 1 5 3

在上面的代码中,我们选择从下标为i=2,即A[2]开始到A[N-1]为止的所有元素中的最小值,将其赋值给A[2],其他原有的数组元素值任然分布在A[3]到A[4]之中。

1.2 选择排序 -- 对"一次选择"的多次迭代。

设想一下,如果我们第一次选择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:

cpp 复制代码
#include <stdio.h>
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;
}

1.3 优化后的选择排序

思路:在"一次选择"的算法中,也就是上述代码2的内循环中,我们可以有这样的优化办法,就是不要每次找到一个比A[i]小的A[j]就进行交换,可先把目前找到的最小值的下标缓存起来,等到遍历完以后,进行一次交换就行了。根据这个思路,我们就有了优化后的选择排序算法。

代码3:

cpp 复制代码
#include <stdio.h>
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;
}

二、冒泡排序

2.1 一次冒泡

先看一次"冒泡",它的思路比较固定,就是让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:

cpp 复制代码
#include <stdio.h>
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; 
}

输出:

cpp 复制代码
2 1 4 3 5

2.2 冒泡排序 -- 对一次"冒泡"的多次迭代

设想一下,当我们完成第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:

cpp 复制代码
#include <stdio.h>
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,只是用来控制了里层"冒泡"的迭代次数。并没有参与里层"冒泡"。

2.3 对冒泡排序的优化

对上面的冒泡排序的代码,我们可以从两个方面进行优化。

(1)首先对里层循环,我们知道第1次"冒泡"完成后,数组最大的元素就已经被拍到最后一位了,那么第2次"冒泡"时,就没有必要在比较A[N-1]和A[N-2]了。一次类推,容易验证当外层循环为i时,里层循环只需要进行到j=N-1-i就可以了。根据上述想法,我们就有了对里层循环优化后的冒泡排序算法。

代码6:

cpp 复制代码
#include <stdio.h>
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:

cpp 复制代码
#include <stdio.h>
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:

cpp 复制代码
#include <stdio.h>
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;
}
相关推荐
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
爱敲代码的憨仔1 小时前
《线性代数的本质》
线性代数·算法·决策树
熬夜学编程的小王1 小时前
【C++篇】深度解析 C++ List 容器:底层设计与实现揭秘
开发语言·数据结构·c++·stl·list
yigan_Eins1 小时前
【数论】莫比乌斯函数及其反演
c++·经验分享·算法
阿史大杯茶1 小时前
AtCoder Beginner Contest 381(ABCDEF 题)视频讲解
数据结构·c++·算法
陌小呆^O^1 小时前
Cmakelist.txt之win-c-udp-server
c语言·开发语言·udp
Chris _data2 小时前
二叉树oj题解析
java·数据结构
დ旧言~2 小时前
【高阶数据结构】图论
算法·深度优先·广度优先·宽度优先·推荐算法
时光の尘2 小时前
C语言菜鸟入门·关键字·float以及double的用法
运维·服务器·c语言·开发语言·stm32·单片机·c
张彦峰ZYF2 小时前
投资策略规划最优决策分析
分布式·算法·金融