四、排序算法详解
4.1 冒泡排序(Bubble Sorting)
4.1.1 基本思想
通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
4.1.2 优化
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)。
4.1.3 图解
小结:
- 一共进行数组的大小-1次大的循环。
- 每一趟排序的次数在逐渐的减少。
- 如果我们发现在某趟排序中,没有发生一次交换,可以提前结束冒泡排序。这个就是优化。
4.1.4 代码实现
package com.company.sort;
import java.lang.reflect.Array;
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args){
int[] arr = new int[]{3,9,-1,10,-2};
System.out.println("排序前");
System.out.println(Arrays.toString(arr));
bubbleSort(arr);
System.out.println("排序后");
System.out.println(Arrays.toString(arr));
}
public static void bubbleSort(int[] arr){
// 排序(arr.length -1)趟
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]){
// 定义一个辅助变量,用于交换数据
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
优化后:
public static void bubbleSort(int[] arr){
// 排序(arr.length -1)趟
for (int i = 0; i < arr.length -1; i++){
System.out.println(Arrays.toString(arr));
// 每一趟排序都会固定一个最大值(最小值)
Boolean flag = false; // 标识本趟排序有没有进行过交换
for (int j = 0; j < arr.length -1 - i; j++){
if(arr[j] > arr[j+1]){
// 定义一个辅助变量,用于交换数据
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
// 修改标识,表示已经进行过交换
flag = true;
}
}
if(!flag){ // 本趟排序依次交换也没有,说明数组已经是有序的了,跳出循环
break;
}
}
}
4.1.5 代码运行时间测试
增加测试冒泡排序时间的代码:
public static void main(String[] args){
// 测试冒泡排序的速度(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 date1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
String date1Str = simpleDateFormat.format(date1);
System.out.println("排序前的时间是=" + date1Str);
bubbleSort(arr);
Date date2 = new Date();
String date2Str = simpleDateFormat.format(date2);
System.out.println("排序后的时间是=" + date2Str);
}
结论:冒泡排序的时间复杂度为O(n^2)。80000随机数,冒泡排序的时间在20s左右。
4.2 选择排序(Select Sorting)
4.2.1 基本思想
第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换,第二次从arr[1]~arr[n-1]中选取最小值,与arr[1]交换,第三次从arr[2]~arr[n-1]中选取最小值,与arr[2]交换,...,第i次从arr[i-1]~arr[n-1]中选取最小值,与arr[i-1]交换,...,第n-1次从arr[n-2]~arr[n-1]中选取最小值,与arr[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。
4.2.2 图解
说明:
- 选择排序一共有(arr.length - 1)趟排序。
- 每一趟排序,又是一个循环,循环的伪代码如下:
- 先假定当前数为最小值;
- 然后用当前数与后面的每一个数进行比较,如果发现有比当前数更小的数,记录该数的下标,并重新赋值最小值;
- 当遍历完数组后,就得到本趟排序的最小值和下标;
- 交换当前值与最小值的位置。
4.2.3 优化
当某趟排序确定的最小值和当前值是一样的时候,不进行交换,省略上述步骤的第四步。
4.2.4 代码实现
package com.company.sort;
import java.util.Arrays;
public class SelectSort {
public static void main(String[] args){
int[] arr = new int[]{10,20,-10,3,18,40};
selectSort(arr);
}
public static void selectSort(int[] arr){
// 排序arr.length -1 趟
for (int i = 0; i < arr.length - 1; i++){
// 初始当前值为最小值,并与后面每一个数进行比较,找到本趟循环的最小值和下标。
int min = arr[i];
int minIndex = i;
for (int j = i + 1; j < arr.length; j++){
if(min > arr[j]){
min = arr[j];
minIndex = j;
}
}
// 交换当前值与最小值得位置
arr[minIndex] = arr[i];
arr[i] = min;
// 打印每一趟排序的结果
System.out.println("第"+(i+1)+"趟排序的结果:"+Arrays.toString(arr));
}
}
}
优化:
public static void selectSort(int[] arr){
// 排序arr.length -1 趟
for (int i = 0; i < arr.length - 1; i++){
// 初始当前值为最小值,并与后面每一个数进行比较,找到本趟循环的最小值和下标。
int min = arr[i];
int minIndex = i;
for (int j = i + 1; j < arr.length; j++){
if(min > arr[j]){
min = arr[j];
minIndex = j;
}
}
// 交换当前值与最小值得位置
// 优化,如果当前值和最小值是同一个,不交换
if(minIndex != i){
arr[minIndex] = arr[i];
arr[i] = min;
}
// 打印每一趟排序的结果
System.out.println("第"+(i+1)+"趟排序的结果:"+Arrays.toString(arr));
}
}
4.2.5 代码运行时间测试
结论:选择排序的时间复杂度为O(n^2)。80000随机数,选择排序的时间在2s左右,比冒泡快!!!。
4.3 插入排序(Insert Sorting)
4.3.1 基本思想
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的数依次与有序表元素的数进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
4.3.2 图解
4.3.3 代码实现
package com.company.sort;
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args){
// 创建一个80000个随机数据的数组
int[] arr = new int[8];
for (int i = 0; i < 8; i++){
arr[i] = (int)(Math.random() * 1000); // 随机在[0,8000000]产生数
}
insertSort(arr);
}
public static void insertSort(int[] arr){
// 对(arr.length - 1)个无序数进行插入
for (int i = 1; i < arr.length; i++){
//1.保存待插入无序数insertVal
//2.依次将待插入数与前面的有序数组进行对比
//3.当insertVal大于某值时,进行插入
int insertVal = arr[i];
int insertIndex = i ; //记录插入数的下标
//优化,确定插入的条件
if(insertVal < arr[insertIndex -1]){
// 当insertVal大于/等于指定值时,表示插入位置已经找到
while (insertIndex >= 1 && insertVal < arr[insertIndex -1]){
arr[insertIndex] = arr[insertIndex - 1]; //当insertVal小于指定值时,指定值后移一位
insertIndex--; //下标向前移动
}
// 将insertVal插入insertIndex位置
arr[insertIndex] = insertVal;
// 打印每一趟排序的结果
System.out.println("第"+i+"趟排序的结果:"+ Arrays.toString(arr));
}
}
}
}
4.3.4 代码运行时间测试
结论:选择排序的时间复杂度为O(n^2)。80000随机数,选择排序的时间在4s左右,介于冒泡和选择排序之间!!!。
4.4 希尔排序(Shell Sorting)
4.4.1 基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
4.4.2 图解
4.4.3 代码实现
在希尔排序的理解时,我们倾向于对于每一个分组,逐组进行处理,但在代码实现中,我们可以不用这么按部就班地处理完一组再调转回来处理下一组(这样还得加个for循环去处理分组)比如[5,4,3,2,1,0] ,首次增量设gap=length/2=3,则为3组[5,2] [4,1] [3,0],实现时不用循环按组处理,我们可以从第gap个元素开始,逐个跨组处理。对有序序列进行插入时,可以采用元素交换法寻找最终位置,也可以采用数组元素移动法寻觅。
交换法:
/**
* 希尔排序 针对有序序列在插入时采用交换法。
* @param arr
*/
public static void shellSort1(int[] arr){
// 1.首先对arr按照增量gap进行分组
// 初始化增量,并逐渐减小
int temp = 0;
for (int gap = arr.length / 2; gap > 0; gap /= 2){
//2. 从第gap元素开始,逐个对其所在组进行直接插入操作
for (int i = gap; i < arr.length; i++){
int j = i;
while (j-gap >=0 && arr[j] < arr[j-gap]){
// 交换元素
temp = arr[j-gap];
arr[j-gap] = arr[j];
arr[j] = temp;
j -= gap;
}
}
}
}
移动法:
/**
* 希尔排序 针对有序序列在插入时采用移动法。
* @param arr
*/
public static void shellSort2(int[] arr){
// 1.首先对arr按照增量gap进行分组
// 初始化增量,并逐渐减小
for (int gap = arr.length / 2; gap > 0; gap /= 2){
//2. 从第gap元素开始,逐个对其所在组进行直接插入操作
for (int i = gap; i < arr.length; i++){
int insertVal = arr[i];
int insertIndex = i;
if(insertVal < arr[i -gap]){
while ((insertIndex - gap >= 0) && insertVal < arr[insertIndex -gap]){
arr[insertIndex] = arr[insertIndex - gap];
insertIndex -= gap;
}
arr[insertIndex] = insertVal;
}
}
}
}
4.4.4 代码运行时间测试
希尔排序交换法的时间和普通的插入排序时间差不多,在4s左右;希尔排序移动法的速度非常快,80000万的随机数排序,时间小于1s,时间复杂度为为O(N*logN)。推荐使用移动法。
4.5 快速排序(Quick Sorting)
4.5.1 基本思想
快速排序采用了分治的思想。
- 找到一个基准值(pivot),一般选择待排序数组的第一个元素。
- 对数组进行分区,将小于等于基准数的全部放在左边,大于基准数的全部放在右边。
- 重复1,2步骤,分别对左右两个子分区进行分区,一直到各分区只有一个数为止。
4.5.2 图解
4.5.3 代码实现
package com.company.sort;
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args){
// 创建一个80000个随机数据的数组
int[] arr = new int[8];
for (int i = 0; i < 8; i++){
arr[i] = (int)(Math.random() * 1000); // 随机在[0,8000000]产生数
}
quickSort(arr,0,arr.length - 1);
}
/**
* 排序过程
* @param arr 待分区数组
* @param left 待分区数组最小下标
* @param right 待分区数组最大下标
*/
public static void quickSort(int[] arr, int left, int right){
if(isEmpty(arr)){
return;
}
if (left < right){
int temp = partition(arr,left,right);
quickSort(arr,left,temp - 1);
quickSort(arr,temp + 1,right);
}
}
public static boolean isEmpty(int[] arr){
return arr == null || arr.length == 0;
}
/**
* 分区过程
* @param arr 待排序数组
* @param left 待排序数组最小下标
* @param right 待排序数组最大下标
* @return 排好序之后基准数的位置下标,方便下次的分区
*/
public static int partition(int[] arr, int left, int right){
int pivot = arr[left]; //定义基准数,默认为数组的第一个元素
while (left < right){ //循环执行的条件
//因为默认的基准数是在最左边,所以首先从右边开始比较进入while循环的判断条件
//如果当前arr[right]比基准数大,则直接将右指针左移一位,当然还要保证left<right
while (left < right && arr[right] > pivot){
right--;
}
//跳出循环说明当前的arr[right]小于等于基准值,那么直接将当前数填充到左指针(初始为基准数所在位置)所在的位置,并且左指针向右移一位
//这时当前数(arr[right])所在的位置空出,需要从左边找一个比基准数大的数来填充。
if(left < right){
arr[left++] = arr[right];
}
System.out.println("右指针赋值"+ Arrays.toString(arr));
//下面的步骤是为了在左边找到比基准数大的数填充到right的位置。
//因为现在需要填充的位置在右边,所以左边的指针移动,如果arr[left]小于基准数,则直接将左指针右移一位
while (left < right && arr[left] < pivot){
left++;
}
//跳出上一个循环说明当前的arr[left]的值大于基准数,需要将该值填充到右边空出的位置,同时右指针左移一位。
if(left < right){
arr[right--] = arr[left];
}
System.out.println("左指针赋值"+Arrays.toString(arr));
}
//当循环结束说明左指针和右指针已经相遇。并且相遇的位置是一个空出的位置,
//这时候将基准数填入该位置,并返回该位置的下标,为分区做准备。
arr[left] = pivot;
System.out.println("最后赋值"+Arrays.toString(arr));
return left;
}
}
4.5.4 代码运行时间测试
快速排序的时间复杂度为O(N*logN),事实上,快速排序通常明显比其他O(n log n)算法更快。测试8000000数据的排序,时间不到1s中,非常快!!!
4.6 归并排序(Merge Sorting)
4.6.1 基本思想
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)
4.6.2 图解
图解1:
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
图解2:
4.5.3 代码实现
package com.company.sort;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public class MergeSort {
public static void main(String[] args){
// 测试冒泡排序的速度(O(n^2))
// 创建一个80000个随机数据的数组
int[] arr = new int[8];
for (int i = 0; i < 8; i++){
arr[i] = (int)(Math.random() * 8); // 随机在[0,8000000]产生数
}
Date date1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
String date1Str = simpleDateFormat.format(date1);
System.out.println("排序前的时间是=" + date1Str);
int[] temp = new int[arr.length];
mergeSort(arr,0,arr.length - 1,temp);
Date date2 = new Date();
String date2Str = simpleDateFormat.format(date2);
System.out.println("排序后的时间是=" + date2Str);
System.out.println("排序后的数组"+Arrays.toString(arr));
}
/**
*@Description: 归并排序
*@Param:
*@return:
*@Author: your name
*@date:
*/
public static void mergeSort(int[] arr,int left,int right,int[] temp){
if(left < right){
int mid = (left + right) / 2; // 中间索引
// 右边递归进行处理
mergeSort(arr,mid + 1,right,temp);
// 左边递归进行处理
mergeSort(arr,left,mid,temp);
// 合并
merge(arr,left,mid,right,temp);
}
}
/**
*@Description: 合并方法
*@Param:
* left 左边有序序列的初始索引
* min 中间索引
* right 右面索引
* temp 临时变量
*@return: void
*@Author: your name
*@date:
*/
public static void merge(int[] arr,int left,int mid,int right,int[] temp) {
int i = left; // 初始化i,左边有序序列的初始索引
int j = mid + 1; // 初始化j,右边有序序列的初始索引
int t = 0; // 初始化temp数组的索引
// 第一步:
// 将左右有序序列,按照规则依次放到temp数组中
// 直到左右序列,有一个处理完毕为止
while (i <= mid && j <= right) {
// 如果左边序列的当前元素,小于等于右边序列的当前元素
// 即将左侧序列的当前元素,填充到temp数组中
// 同时t++,i++
if(arr[i] <= arr[j]){
temp[t] = arr[i];
t++;
i++;
}else { // 否则将右边序列的当前元素填充到temp数组中,同时t++,j++
temp[t] = arr[j];
t++;
j++;
}
}
// 第二步:
// 如果左侧序列有剩余元素,将剩下的元素,依次填充到temp数组中
while (i <= mid){
temp[t] = arr[i];
t++;
i++;
}
// 如果右侧序列有剩余元素,将剩下的元素,依次填充到temp数组中
while (j <= right){
temp[t] = arr[j];
t++;
j++;
}
// 第三步:
// 将temp数组赋值给arr,注意索引!!!
t = 0;
int tempLeft = left;
while (tempLeft <= right){
arr[tempLeft] = temp[t];
tempLeft++;
t++;
}
}
}
参考: