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

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

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

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

一、选择排序

1.1 一次选择

一次选择所要完成的任务是:从数组元素Ai开始,到最后一个元素,即AN-1,选择其中的最小值;然后经过一些数组元素之间的交换,将这个最小值放在Ai中,最小值以外的其它数组元素的值仍要分布在Ai+1到AN-1之中,次序不作要求。

思路1:要完成这个任务,我们可以遍从Ai+1开始,到AN-1为止的所有元素,如果某个Aj < Ai,我们就将Aj和Ai中的值进行交换。完成这个遍历的后果就只,此时Ai的值就是原来Ai到AN-1中的最小值,原来除最小值以外的其他值,仍分布在Ai+1到AN-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,即A2开始到AN-1为止的所有元素中的最小值,将其赋值给A2,其他原有的数组元素值任然分布在A3到A4之中。

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

设想一下,如果我们第一次选择i=0的位置为起始位置,进行"一次选择"的任务,那么整个数组中的最小值将会赋值给A0,其他元素仍然会分布在A1到AN-1之中。第二次我们再选择i=1的位置为起始位置,再进行一次"一次选择"的任务,那么A1到AN-1中最小的值将会被赋值给A1,其他值仍然分布A2到AN-1之中。这样A0,A1元素其实分别就已经是原来数组元素的最小值和次最小值了。依此类推可知,对当我们将"一次选择"任务中选择的起始下标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的内循环中,我们可以有这样的优化办法,就是不要每次找到一个比Ai小的Aj就进行交换,可先把目前找到的最小值的下标缓存起来,等到遍历完以后,进行一次交换就行了。根据这个思路,我们就有了优化后的选择排序算法。

代码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,比较Aj和它后面紧邻的元素Aj+1的大小,如果Aj>Aj+1,就交换Aj和Aj+1的值;否则就让j继续遍历。这样当遍历完以后,原本A0到AN-1中最大的元素,就被交换赋值给AN-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次"冒泡"任务,那么数组元素A0到AN-1中最大的就被交换到AN-1中了,当我们完成第2次"冒泡"任务,那么数组元素A0到AN-2中的最大的就被交换到AN-2中了。这里要注意,元素A0到AN-2中的最大的一定比AN-1中的元素小,所以没有优化过的"冒泡"任务会把元素A0到AN-2中的最大的和AN-1进行比较,但是比较的结果一定是AN-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次"冒泡"时,就没有必要在比较AN-1和AN-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次。是否有这个必要呢?我们假设,当有一次执行里层循环时,里层循环遍历的每一个Aj和Aj+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;
}
相关推荐
cfm_29141 分钟前
Redis五大基本数据结构底层了解
数据结构·数据库·redis
如竟没有火炬8 分钟前
最大矩阵——单调栈
数据结构·python·线性代数·算法·leetcode·矩阵
8Qi838 分钟前
LeetCode 1143 & 718:最长公共子序列 / 最长重复子数组
算法·leetcode·职场和发展·动态规划
绿算技术1 小时前
万卡推理集群存储选型分析:从核心架构到应用视角
大数据·科技·算法·架构
想吃火锅10052 小时前
【leetcode】1.两数之和js版
javascript·算法·leetcode
qeen872 小时前
【C++】类与对象之类的默认成员函数(二)
android·c语言·开发语言·c++·笔记·学习
net3m333 小时前
一阶软件低通滤波器算法
人工智能·算法
水木流年追梦3 小时前
大模型入门-大模型优化方法12-YaRN 长文本外推技术
人工智能·分布式·算法·正则表达式·prompt
J-Tony114 小时前
【JVM】三色标记法
java·jvm·算法