一、冒泡排序(Bubble Sorting)
基本思想是:重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
演示冒泡过程:
以第一趟为例,有5个位置。
(1)先比较1,2位置上的元素:3<9,符合我们设定的从小到大的顺序,不交换;
(2)再比较2,3位置上的元素:-1<9,不交换;
(3)再比较3,4位置上的元素:9<10,不交换;
(4)再比较4,5位置上的元素:10<20,不交换;
由上:考虑使用一个外层循环控制趟数,一个内层循环控制交换哪两个位置上的元素。外层是在递增的,而内层在递减。所以,内层循环的控制变量可以使用外层循环的控制变量来表示出来。
上面5个数字,需要进行4次比较。仔细看看会发现,从第三趟排序开始,就没有发生过交换了,因为顺序已经符合我们从小到大的规定了。这时候,再往下执行是浪费资源的,所以考虑终止当前循环。怎么终止呢?考虑设置一个标识位,在每一趟执行完交换后,都检查一遍是否发生过交换。若在这一趟中发生了交换,则程序继续执行;若没有,则退出循环。
小结:
1)一共进行 数组的大小-1次 循环。
2)每一趟排序的次数在逐渐的减少。
3)优化:如果在某趟排序中,没有发生一次交换,可以提前结束冒泡排序。
小知识:格式化时间
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main{
public static void main(String[] args){
Date data1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(data1);
System.out.println("排序前的时间是:" + date1Str);
}
}
输出:
核心代码:
public static void bubbleSort(int[] arr) {
// 冒泡排序 的时间复杂度 O(n^2)
int temp = 0; // 临时变量
boolean flag = false; // 标识变量,表示是否进行过交换
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
// 如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
// System.out.println("第" + (i + 1) + "趟排序后的数组");
// System.out.println(Arrays.toString(arr));
if (!flag) {
// 在一趟排序中,一次交换都没有发生过
break;
} else {
flag = false; // 重置flag!!!, 进行下次判断
}
}
}
完整代码:
package com.huey.sort;
import java.text.SimpleDateFormat;
import java.util.Date;
public class BubbleSort {
public static void main(String[] args) {
// int arr[] = {3, 9, -1, 10, 20};
//
// System.out.println("排序前");
// System.out.println(Arrays.toString(arr));
// 测试一下冒泡排序的速度O(n^2)
// 创建要给80000个的随机的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
}
// 格式化时间
Date data1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(data1);
System.out.println("排序前的时间是:" + date1Str);
// 测试冒泡排序
bubbleSort(arr);
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序后的时间是=" + date2Str);
// System.out.println("排序后");
// System.out.println(Arrays.toString(arr));
// 为了容易理解,把冒泡排序的部分演变过程在下面写出
/*
*
* // 第二趟排序,就是将第二大的数排在倒数第二位
*
* for (int j = 0; j < arr.length - 1 - 1 ; j++) { // 如果前面的数比后面的数大,则交换 if
* (arr[j] > arr[j + 1]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] =
* temp; } }
*
* System.out.println("第二趟排序后的数组"); System.out.println(Arrays.toString(arr));
*
*
* // 第三趟排序,就是将第三大的数排在倒数第三位
*
* for (int j = 0; j < arr.length - 1 - 2; j++) { // 如果前面的数比后面的数大,则交换 if (arr[j]
* > arr[j + 1]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } }
*
* System.out.println("第三趟排序后的数组"); System.out.println(Arrays.toString(arr));
*
* // 第四趟排序,就是将第4大的数排在倒数第4位
*
* for (int j = 0; j < arr.length - 1 - 3; j++) { // 如果前面的数比后面的数大,则交换 if (arr[j]
* > arr[j + 1]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } }
*
* System.out.println("第四趟排序后的数组"); System.out.println(Arrays.toString(arr));
*/
}
// 将前面的冒泡排序算法,封装成一个方法
public static void bubbleSort(int[] arr) {
// 冒泡排序 的时间复杂度 O(n^2)
int temp = 0; // 临时变量
boolean flag = false; // 标识变量,表示是否进行过交换
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
// 如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
// System.out.println("第" + (i + 1) + "趟排序后的数组");
// System.out.println(Arrays.toString(arr));
if (!flag) {
// 在一趟排序中,一次交换都没有发生过
break;
} else {
flag = false; // 重置flag!!!进行下次判断
}
}
}
}
输出结果:
经多次测试,耗费时间大概在7~8s.(因电脑性能而异)
二、快速排序(Quick sort)
1、介绍
快速排序( Quick sort)是对冒泡排序的一种改进。
基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
2、联系:升级版冒泡
为何快速排序不稳定?
答:因为关键字的比较和交换是跳跃进行的。
韩顺平老师采用的以中间数为基准的方法较难理解,代码就直接放在文末。
法一:头基准
下面详解以第一个元素为基准的方法。
第一次比较,以下标为0的元素6为基准(key),从后往前找比key 小的数字,从下标5、4,一直找到3。下标3的数字3比基准小,所以进行位置交换。
开始第二次比较,这里寻找比key 大的数,需要从前往后找,递增变量i 发现下标1的数据是第一个比大的,所以用下标1的数据12和j 指向的下标3的数据进行交换,结果如下:
上面的逻辑中,左右各执行一次比较后,i 左边的数据都是比6小的了,j 右边的数据都是比6大的了,那么中间的还不确定,怎么办?
再将 j 从后向前移动,找比6小的,再将i 从前向后移动,找比6大的。那么,何时能结束?
当i 和j 碰头了,就意味着不能再继续分组了,一轮就结束了!!!
下面分步骤逐步写代码,并进行优化。
步骤1:左滑、右滑
// arr 为待排序数组,需要对arr[low] ~ arr[high] 段排序【low、high是变化的(递归)】
public static void quickSort(int[] arr, int low, int high) {
int i = low;// 只表示起始位置为low,后面还要向右边滑
int j = high;// 只表示起始位置为high,后面还要向左边滑
int key = arr[i];//基准数值
//★★★
while(arr[j] >= key) {
j--;//向左滑,直到找到比基准数小的数才退出循环
}
int t;
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
while(arr[i]<=key) {
i++;//往右滑,直到找到比基准数大的数才退出循环
}
t = arr[i];
arr[i] = arr[j];
arr[j] = t;
//第一次执行到这,j滑了一次,i也滑了一次。
//★★★
}
写到这,代码中两个★★★之间的的代码块,已经进行了一次左滑和一次右滑,i 和 j 碰面了吗?没有!说明还没结束这一轮。我需要的结果是:比key小的数,key,比key大的数 这样的一个结果。
所以左滑、右滑需要重复执行。
用while(true)一直执行两个★★★之间的的代码块吗?不行,会滑过头,当i,j,共同指向下标2的数字7时,while(arr[j] >= key) 条件仍然成立,此时,j - -,就滑过头了。
所以只能用while(i < j)来作为条件,让它俩往中间走。
步骤2:执行完一轮(i,j碰头)
public static void quickSort(int[] arr, int low, int high) {
int i = low;// 只表示起始位置为low,后面还要向右边滑
int j = high;// 只表示起始位置为high,后面还要向左边滑
int key = arr[i];//基准数值
//★★★
while(i<j) {
while(arr[j] >= key) {
j--;//向左滑,直到找到比基准数小的数才退出循环
}
int t;
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
while(arr[i]<=key) {
i++;//往右滑,直到找到比基准数大的数才退出循环
}
t = arr[i];
arr[i] = arr[j];
arr[j] = t;
//第一次执行到这,j滑了一次,i也滑了一次。
}
//★★★
}
此时,我已经得到目标排列:比key小的数,key,比key大的数
步骤3:递归处理
下面,需要再对key 两边的数组进行排序,规则同上,所以,这里使用递归的方式。
public static void quickSort(int[] arr, int low, int high) {
int i = low;// 只表示起始位置为low,后面还要向右边滑
int j = high;// 只表示起始位置为high,后面还要向左边滑
int key = arr[i];// 基准数值
while (i < j) {
while (arr[j] >= key) {
j--;// 向左滑,直到找到比基准数小的数才退出循环
}
int t;
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
while (arr[i] <= key) {
i++;// 往右滑,直到找到比基准数大的数才退出循环
}
t = arr[i];
arr[i] = arr[j];
arr[j] = t;
// 第一次执行到这,j滑了一次,i也滑了一次。
}
//★★★
// 对基准左侧的集合重复操作
quickSort(arr, low, i - 1);// key对应的下标是i
// 对基准右侧的集合重复操作
quickSort(arr, i + 1, high);
//★★★
}
步骤4:考虑细节,即极限位置。
例1:3, 4,5,6,7,8
一开始 i 指向3,j 指向8。
开始执行:j 一直往左滑,滑到了指向4的位置,4比3大,还得滑。i,j 就重合指向3了。
由于满足:while (arr[j] >= key) 继续执行j - -,就滑过头了。所以需要加条件!
while (arr[j] >= key && i < j)
例2:1,2,3,4,5,8
同例一原理,改进如下:
while (arr[i] <= key && i < j)
例3:递归到key 左边只有一个数3,右边一堆数。
low、high都指向3
由于没有加结束递归的条件,它会一直在调,没有止境。所以我们得加上结束条件:
public static void quickSort(int[] arr, int low, int high) {
if (low >= high) {
// 逆向思维,啥时候不停?low小于high的时候鸭!
return;
}
......
}
例4:下标为0上数字2,下标为1上数字3.
其中, i 指向2,j指向3.
满足:while (arr[j] >= key && i < j)
执行:j - -; 此时i , j 重了,都等于2。
往下执行,交换结果还是2。
往下执行,while (arr[i] <= key && i < j),不满足,往下执行交换,交换结果还是等于2。
例5:
不满足:while (arr[j] >= key && i < j)
往下执行,交换结果是2,3。
往下执行,while (arr[i] <= key && i < j),满足,执行 i + +,i 和 j 重了,都指向3.
往下执行交换,3跟3交换,结果还是3。
由例4、例5:不加任何条件是可以的。但加上有好处,里面的交换就不执行了,简化了复杂度。
if (i < j) {
int t;
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
最终代码:
// arr 为待排序数组,需要对arr[low] ~ arr[high] 段排序【low、high是变化的(递归)】
public static void quickSort(int[] arr, int low, int high) {
if (low >= high) {
// 逆向思维,啥时候不停?low小于high的时候鸭!
return;
}
int i = low;// 只表示起始位置为low,后面还要向右边滑
int j = high;// 只表示起始位置为high,后面还要向左边滑
int key = arr[i];// 基准数值
while (i < j) {
while (arr[j] >= key && i < j) {
j--;// 向左滑,直到找到比基准数小的数才退出循环
}
if (i < j) {
int t;
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
while (arr[i] <= key && i < j) {
i++;// 往右滑,直到找到比基准数大的数才退出循环
}
if (i < j) {
int t;
t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// 第一次执行到这,j滑了一次,i也滑了一次。
}
// 对基准左侧的集合重复操作
quickSort(arr, low, i - 1);// key对应的下标是i
// 对基准右侧的集合重复操作
quickSort(arr, i + 1, high);
}
速度测试:
八千万个数!
好家伙,果然够快。
基本都在7~8s.
前面的冒泡,才八万个数,也是这速度,可以想见快排确实不戳。
法二:中基准
以中间为基准:
package com.huey.sort;
import java.util.Arrays;
/**
* @author Huey
*
* 2021年2月18日 下午5:04:44
*/
public class QuickSort {
public static void main(String[] args) {
int[] arr = {
5, 2, 8, 8, 1, 7 };
quickSort(arr, 0, arr.length - 1);//
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr, int left, int right) {
int l = left;// 左下标
int r = right;// 右下标
int pivot = arr[(left + right) / 2];// pivot 中轴
int temp = 0;// 临时变量,交换时使用
// while循环的目的:让比pivot 小的放到左边,大的放右边
while (l < r) {
// 在pivot 左边一直找,直到找到大于等于pivot 的值,才退出
while (arr[l] < pivot) {
l += 1;
}
// 在pivot 右边一直找,直到找到小于等于pivot 的值,才退出
while (arr[r] > pivot) {
r -= 1;
}
// 如果成立,说明pivot 的左右两边的值,已经为
// 左边全部是小于等于pivot 的值;右边全部是大于等于pivot 的值
if (l >= r) {
// 条件成立的话,说明没有找到可交换的值,直接break
break;
}
// 交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
// 如果交换完后发现arr[l] 和 pivot 值相等 r--,前移一下
if (arr[l] == pivot) {
r -= 1;
}
// 如果交换完后发现arr[r] 和 pivot 值相等 l++,后移一下
if (arr[r] == pivot) {
l += 1;
}
// 写这两步,相当于手动干预一个特殊情形。若有一个值和中值pivot相同(例如:5,2,8,8,1,7)
// 会发生死循环交换,为防止死循环,在交换后就移动。
} // 退出时,已经完成了一次分割。
// 如果 l == r ,必须l++,r--否则会出现 栈溢出
if (l == r) {
l += 1;
r -= 1;
} // l+=1;r-=1相当于把基准值去掉之后进行递归
// 向左递归
if (left < r) {
quickSort(arr, left, r);
}
// 向右递归
if (right > l) {
quickSort(arr, l, right);
}
}
}