图解十大排序
1.1引言
在我们当下的生活中,排序随处可见,当每一次考试,老师都会按照成绩多少来排名,排队的时候,同学们又会按照学号顺序进行排队
在编程的世界中,应用到排序的场景也比比皆 是。例如当开发一个学生管理系统时,需要按照学号 从小到大进行排序;当开发一个电商平台时,需要把 同类商品按价格从低到高进行排序;当开发一款游戏 时,需要按照游戏得分从多到少进行排序,排名第一 的玩家就是本场比赛的MVP,等等
排序虽然很简单,它的背后却隐藏着多种多样的算法和思想。那么常用的排序算法都有哪些呢?我们又应该如何选择呢?接下来让我们来介绍主流的排序算法。
1.1.1 根据时间复杂度的不同,主流的排序算法可以分为3大类:
- 时间复杂度为O(n 2)的排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 时间复杂度为O(nlogn)的排序算法
- 希尔排序
- 归并排序
- 快速排序
- 堆排序
- 时间复杂度为O(n + k)的排序算法
- 计数排序
- 桶排序
- 基数排序
1.1.2 根据稳定性, 划分为稳定排序和不稳定排序
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
1.1.3 根据是否需要额外的存储空间来存放数据,划分为内部排序和外部排序
内排序 :所有排序操作都在内存中完成;
外排序 :由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
每一种排序算法的情况,下面这张表已经很好的概括出来,这张表会贯穿在我们所有排序算法的文章中,简单看一看,就让我们正式开始学习吧!
1.2 冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,在数列中,我们要把相邻的元素两两比较,当一个元素大于右侧相邻元素时,交换它们的位置;当一个元素小于或等于右侧相邻元素时,位置不变。详细过程如下:
那么冒泡排序的代码如何实现呢?
public static void sort(int [] arr) {
for (int i = 0; i < arr.length - 1; i++) { // 第一层指循环 length - 1次
for (int j = 0; j < arr.length - 1 - i; j++) { // 第二层指循环每次需要比较的次数
int tmp = 0;
if (arr[j] > arr[j + 1]) {
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
public static void main(String[] args) {
int [] arr = new int[] {3, 5, 2, 6, 4, 8};
sort(arr);
System.out.println(Arrays.toString(arr)); // Arrays.toString打印数组的内容
}
算法分析:冒泡排序是一种稳定排序,值相等的元素并不会打乱原本的顺序。由于该排序算法的每一轮都要遍历 所有元素,总共遍历(元素数量-1)轮,所以平均时间复杂度是O(n 2)。
- 最佳情况:T(n) = O(n)
- 最差情况:T(n) = O(n2)
- 平均情况:T(n) = O(n2)
1.2.1 冒泡排序的优化
小伙伴们,思考一下,冒泡排序可以如何优化呢?
让我们来看一个例子,当排序算 法分别执行到第6、第7轮时,数列状态如下:
很明显可以看出,经过第6轮排序后,整个数列已 经是有序的了。
在这种情况下,如果能判断出数列已经有序,并 做出标记,那么剩下的几轮排序就不必执行了,可以 提前结束工作。
二代冒泡排序代码实现:
public static void sort(int [] arr) {
// 第一层指循环 length - 1次
for (int i = 0; i < arr.length - 1; i++) {
// 判断该数组是否已经有序
boolean isSorted = true;
// 第二层指循环每次需要比较的次数
for (int j = 0; j < arr.length - 1 - i; j++) {
int tmp = 0;
if (arr[j] > arr[j + 1]) {
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
//有元素交换, 就证明该数组无序
isSorted = false;
}
}
// 如果已经有序, 跳出最外层for循环
if (isSorted)
break;
}
}
public static void main(String[] args) {
int [] arr = new int[] {3, 5, 2, 6, 4, 8};
sort(arr);
System.out.println(Arrays.toString(arr)); // Arrays.toString打印数组的内容
}
1.2.2 冒泡排序又优化
以为这就结束啦?同学们再想一想如何能再进一步提升它的性能呢?
同样来看一个例子
元素4和5比较,发现4小于5,所以位置不变。
元素5和6比较,发现5小于6,所以位置不变。
元素6和7比较,发现6小于7,所以位置不变。
元素7和8比较,发现7小于8,所以位置不变。
大家可以清楚的发现
当数组中右边许多元素已经是有序的时候,但每一轮还是需要浪费时间进行比较,那我们如何进行优化呢?让我们一起来分析分析:
从现有的逻辑来看,数组右边有序的长度和第一轮for循环排序的次数是相等的。例如第1轮排序过后的有序区长度是1,第2轮 排序过后的有序区长度是2……
实际上,数组真正的有序区可能会大于这个长度,因此后面的多次元素比较 是没有意义的。那么,该如何避免这种情况呢?我们可以在每一 轮排序后,记录下来最后一次元素交换的位置,该位置往后的数组就是有序的了
三代冒泡排序代码实现:
public static void sort(int [] arr) {
// 记录最后一次交换的位置
int lastExchange = 0;
//边界点
int sortBorder = arr.length - 1;
// 第一层指循环 length - 1次
for (int i = 0; i < arr.length - 1; i++) {
// 判断该数组是否已经有序
boolean isSorted = true;
// 第二层指循环每次需要比较的次数
for (int j = 0; j < sortBorder; j++) {
int tmp = 0;
if (arr[j] > arr[j + 1]) {
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
//有元素交换, 就证明该数组无序
isSorted = false;
lastExchange = j;
//不能直接把边界j赋值给sortBorder,这样改变了for循环里sortborder
}
}
sortBorder = lastExchange;
if (isSorted)
break;
}
}
public static void main(String[] args) {
int [] arr = new int[] {3, 5, 2, 6, 4, 8};
sort(arr);
System.out.println(Arrays.toString(arr)); // Arrays.toString打印数组的内容
}
1.2.3 冒泡排序双优化(选看)
~啊… 终于优化好了~,你们以为这就结束啦,哈哈哈,还有一种排序算法叫作鸡尾酒排序,是基于冒泡排序的一种升级排序法。
鸡尾酒排序的元素比较和交换过程是双向的。
同样来看一个例子:[2, 3, 4, 5, 6, 7, 1, 8]
按照之前的冒泡排序算法,我们需要进行7轮排序,是不是感觉像大冤种,呵呵,这时候就该鸡尾酒排序上场表演了。
第1轮(和冒泡排序一样,8和1交换)
第2轮 此时开始不一样了,我们反过来从右往左比较并进行交换。
第3轮(虽然实际上已经有序,但是流程并没有 结束)
重新从左向右进行比较并进行交换,排序过程就像是钟摆一样,今日我做主,为它改名叫钟摆排序
四代冒泡排序代码实现
public static void sort(int [] arr) {
int tmp = 0;
for (int i = 0; i < arr.length / 2; i++) {
// 有序标记, 每一轮的初始值都是true
boolean isSorted = true;
// 奇数轮(从左向右)
for (int j = i; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
//有元素交换, 就证明该数组无序
isSorted = false;
}
}
if (isSorted)
break;
isSorted = true;
//偶数轮(从右向左)
for (int j = arr.length - 1 - i; j > i; j--) {
if (arr[j - 1] > arr[j]) {
tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
isSorted = false;
}
}
if (isSorted)
break;
}
}
public static void main(String[] args) {
int [] arr = new int[] {3, 5, 2, 6, 4, 8};
sort(arr);
System.out.println(Arrays.toString(arr)); // Arrays.toString打印数组的内容
}
很明显,摆钟排序 鸡尾酒排序可以减少排序的回合数,但是代码量却增加了一倍,今天的冒泡排序我们就介绍到这里啦,都到这里啦,小伙伴该做什么应该都懂了吧
下篇预告 :图解十大排序篇二 选择排序和插入排序