目录
一、排序的稳定性
二、排序的辅助类
三、选择排序
1.选择排序
双向选择排序(选择排序的优化)
2.堆排序
四、插入排序
1.直接插入排序
在近乎有序的数组下,插入排序的效率十分高效,甚至优于许多nlogN复杂度的算法。
1)插入排序和选择排序最大的不同
2)折半插入排序(直接插入的优化)
2.希尔排序
五、归并排序
1.归并排序的递归写法
⭐关于合并的细节:
⭐复杂度稳定性的分析
⭐归并排序的两点优化
⭐归并排序的核心就是merge操作 :merge操作可以很好的对物理不连续的元素集合进行排序
2.归并排序的非递归写法
六、海量数据的排序处理⭐⭐
七、快速排序
两个相等的数据,经过排序后,排序算法能保证数据相对位置不发生变化,则我们称该算法是具有稳定性的排序算法。
应用到的例子:某购物商城后台,需要按照订单金额排序,但要求排序后购买的时间的先后顺序不变,此时就需要用到稳定性的排序算法。
这几个常见的排序算法都叫内部排序:一次性将所有待排序的数据放入内存中进行的排序,基于元素之间比较的排序。除此之外,还有外部排序,是依赖硬盘(外部存储器)进行的排序算法,如如下三个排序【对于数据集合的要求非常高,只能在特定场合使用】:
写排序的代码,一定注意变量如何定义,以及未排序区间和已排序区间的定义
生成测试数组以及对排序算法进行测试
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
/**
* 排序的辅助类
* 生成测试数组以及对排序算法进行测试
* @author 美女
* @date 2022/03/16 15:18
**/
public class SortHelper {
//获取一个随机数的对象
private static final ThreadLocalRandom random=ThreadLocalRandom.current();
/**
* 1.在[left,right]上生成n个随机数
* @param n
* @param left
* @param right
* @return
*/
public static int[] generateRandomArray(int n,int left,int right){
int[] arr=new int[n];
for (int i = 0; i < arr.length; i++) {
arr[i]=random.nextInt(left,right);
}
return arr;
}
/**
* 2.生成一个大小为n的近乎有序的数组
* 思路:先生成一个有序的数组,再随机交换部分数
* @param n
* @param times 交换的次数-交换次数越小越有序
* @return
*/
public static int[] generateSortedArray(int n,int times){
int[] arr=new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i;
}
//交换部分元素,交换次数越小越有序
for (int i = 0; i < times; i++) {
//生成一个在[0,n]上的的随机数
int a=random.nextInt(n);
int b=random.nextInt(n);
int temp=arr[a];
arr[a]=arr[b];
arr[b]=temp;
}
return arr;
}
/**
* 3.生成一个arr的深拷贝数组
* 为了测试不同排序算法的性能,需要在相同数据集进行测试
*/
public static int[] arrCopy(int[] arr){
return Arrays.copyOf(arr,arr.length);
}
/**
* 4.测试性能
* 根据传入的方法名称就能调用这些方法,需要借助反射【因为这些测试方法都是static方法,都根据类名称访问,没有对象】
* 根据方法名称调用相应的排序方法对arr数组进行排序操作
*/
public static void testSort(String sortName,int[] arr){//()内是方法名称以及待排序的数组集合
//获取一个类的反射对象:Class是个对象,它的类型是SevenSort类型
Class cls=SevenSort.class;
try {
//需要调用的方法对象
Method method=cls.getDeclaredMethod(sortName,int[].class);
//排序开始终止时间
Long start=System.nanoTime();
//排序方法的调用
method.invoke(null,arr);//静态方法对象是null,在数组arr上进行排序
Long end=System.nanoTime();
if(isSorted(arr)){
//排序算法正确
System.out.println(sortName+"排序结束,共耗时:"+(end-start)/1000000.0+"ms");
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 判断数组是否有序
* @param arr
* @return
*/
public static boolean isSorted(int[] arr){
for (int i = 0; i < arr.length-1; i++) {
if(arr[i]>arr[i+1]){
System.out.println("sort error");
return false;
}
}
return true;
}
}
核心思路:
每次从无序区间中选择一个最大或最小值,存放在无序区间的最前或者最后的位置,直到所有数据都排序完为止。每经过一次排序,有序区间元素个数+1,无序区间元素个数-1。
选择排序是不稳定排序,原因分析如下图(以选择最小值为例):
代码实现:
/**
* 选择排序
*/
public static void selectionSort(int[] arr){
//最开始无序区间[0...n],有序区间[]
//arr.length-1:当无需区间只剩一个元素时,整个集合已经有序,所以可以少走一次(-1)
for (int i = 0; i < arr.length-1; i++) {
//min变量存储了当前的最小值索引,先默认第一个元素是最小值
int min=i;
//j从i+1开始,因为第一个i元素已经被选择过了,从第二个元素开始走(自己和自己不比较)
for (int j = i+1; j < arr.length; j++) {
if(arr[j]
Test测试:
public static void main(String[] args) {
int n=50000;
int[] arr=SortHelper.generateRandomArray(n,0,Integer.MAX_VALUE);
int[] arrCopy1=SortHelper.arrCopy(arr);
int[] arrCopy2=SortHelper.arrCopy(arr);
SortHelper.testSort("selectionSort",arr);
SortHelper.testSort("bubbleSort",arrCopy1);//冒泡
SortHelper.testSort("heapSort",arrCopy2);//堆排
}
结果:
核心思路:
一次排序过程中同时选出最大和最小值,放在无序区间的最后和最前。 【注意max在最前面low位置的情况】
public static void selectionSortOP(int[] arr){
int low=0;
int high= arr.length-1;
//low==high,无序区间只剩一个元素,整个区间已经有序
while (low<=high){
int min=low;
int max=low;
for (int i = low+1; i <= high; i++) {
if(arr[i]arr[max]){
max=i;
}
}
//此时min索引一定是当前无序区间的最小值索引,把这个最小值与low索引处的值交换
swap(arr,min,low);
if(max==low){
//若最大值恰好在low位置,则最小值交换后,最大值已经被换到min这个位置了,max应该=min
max=min;
}
swap(arr,max,high);
low+=1;
high-=1;
}
}
见数据结构之七大排序1—冒泡排序与堆排序
核心思路:
将待排序集合分为两个区间:(i是当前遍历的元素),已经排序的区间[0..,i);待排序区间[i...n)。每次从待排序区间中将第一个元素“插入”到已排序区间的合适位置,直到整个数组有序。(类似打扑克牌排链子)
public static void insertionSort(int[] arr){
//从第二个元素开始,默认第一个元素是已排好序的元素
for (int i = 1; i < arr.length; i++) {
//待排序区间的第一个元素:arr[i]
//从待排序区间的第一个元素向前看,找到合适插入位置
/**
* 1.
*/
// for (int j = i; j >0 ; j--) {
// if(arr[j]>=arr[j-1]){//arr[j]:待排序区间第一个元素;arr[j-1]:已排序区间最后一个元素
// //此时说明j索引对应元素比排序区间所有值都大,arr[j]已经有序了,直接进行下次循环
// //注意:相等也放在判断条件中,即相等也不进行交换,保证稳定性
// break;
// }else{
// swap(arr,j,j-1);
// }
// }
/**
* 2.=》1等价于2
*/
// for (int j = i; j >0&&arr[j]0 ; j--) {
// if(val>=arr[j-1]){
// arr[j]=val;
// break;
// }else{
// arr[j]=arr[j-1];
// }
// }
// arr[j]=val;
int j=i,val=arr[j];
for(;j>0&&val
在近乎有序的数组下再进行测试,观察插入排序性能:
当插入排序当前遍历元素>前驱元素时,此时可以提前结束内层循环。极端场景下,当集合是一个完全有序的集合,插入排序内层循环一次都不走,插入排序复杂度称为O(n)。插入排序经常用作高阶排序算法的优化手段之一。
因为插入排序中,每次都是在有序区间中选择插入位置,因此我们可以使用二分查找来定位元素的插入位置。
/**
* 折半插入排序
*/
public static void insertionSortBS(int[] arr){
//有序区间[0,i)
//无序区间[i,n)
for (int i = 1; i < arr.length; i++) {
//当前遍历的元素
int val=arr[i];
int left=0;
int right=i;//i取不到
while(left>1);
if(val=mid
left=mid+1;
}
}
//搬移left...i的元素
for (int j = i; j >left ; j--) {
arr[j]=arr[j-1];//后一个元素等于前一个元素
}
//left就是val插入的位置
arr[left]=val;
}
}
Test:
⭐缩小增量排序,先选定一个整数(gap,gap一般都选取数组长度的一半或者1/3),将待排序十足先按照gap分组,不同组之间内部使用插入排序,排序之后,再将gap/=2或gap/=3,重复上述流程,直到gap=1。
⭐当gap=1时,整个数组已经被调整为近乎有序的数组,此时就是插入排序最好的场景,最后再在整个数组上进行一次插入排序即可。
/**
* 希尔排序
*/
public static void shellSort(int[] arr){
int gap= arr.length>>1;
while(gap>1){
insertionSortByGap(arr,gap);
gap=gap>>1;
}
insertionSort(arr);
}
/**
* 按gap分组进行插入排序
* @param arr
* @param gap
*/
private static void insertionSortByGap(int[] arr,int gap) {
for (int i = gap; i < arr.length; i++) {
for (int j = i; j-gap >=0&&arr[j]
思考:希尔排序中比较gap需要不断向前看,能否从0开始不断向后看gap步?
解析:不可
归:原数组不断拆分为小数组,一直拆分到每个子数组只有一个元素。这是第一阶段,归而为一【采用递归归完开始合并】。
并:将相邻两个数组合二为一,这是第二阶段。
创建一个大小为合并后的数组大小的临时数组aux,将数组值拷贝过去。k:当前正在处理的arr的索引;i:需要合并的左侧小数组的开始的索引;j:需要合并的右侧小数组的开始的索引。
为什么合并过程要创建一个临时数组aux?
防止在合并过程中小元素要覆盖某些大元素,造成大元素的丢失。
/**
* 归并排序
*/
public static void mergeSort(int[] arr) {
mergeSortInternal(arr,0,arr.length - 1);
}
/**
* 在arr[l...r]进行归并排序,整个arr经过函数后就是一个已经有序的数组
* @param arr
* @param l
* @param r
*/
private static void mergeSortInternal(int[] arr, int l, int r) {
//归
if(l>=r){
//当前数组只剩一个元素,归过程结束
return;
}
int mid=l+((r-l)>>1);
//将原数组拆分成左右两个小区间,分别递归的进行归并排序
mergeSortInternal(arr,l,mid);
mergeSortInternal(arr,mid+1,r);
//归过程结束,此时merge开始并
merge(arr,l,mid,r);
}
/**
* 合并两个子数组arr[l...mid],arr[mid+1...r]
* 为一个大的有序数组arr[l...r]
* @param arr
* @param l
* @param mid
* @param r
*/
private static void merge(int[] arr, int l, int mid, int r) {
//先创建一个新的临时组数aux
int[] aux=new int[r-l+1];//数组长度与索引有一个1的差值
//将arr元素值拷贝到aux上
for (int i = 0; i < aux.length; i++) {
//arr的left不一定从索引0开始,它从left(l)开始,与aux的i刚好差了l个单位
//如合并15、47举例,值1的索引是4,他要放在aux[0]位置,即aux[0]=arr[0+4],l恰好是4
aux[i]=arr[i+l];
}
//此时临时数组aux的值已经拷贝好了
//k表示当前正在合并的原数组的索引下标;i是左侧小数组的开始索引,j是右侧小数组的开始索引
int i=l;
int j=mid+1;
for (int k = l; k <=r ; k++) {
if(i>mid){
//左侧区间已经处理完毕,只需要将右侧区间的值拷贝到原数组即可
arr[k]=aux[j-l];//j表示原数组索引,对应aux上的索引是有l个单位的偏移量
j++;
}else if(j>r){
//右侧区间已经被处理完毕,只需要将左侧区间的值拷贝到原数组即可
arr[k]=aux[i-l];
}else if(aux[i-l]<=aux[j-l]){
//此时左区间元素较小//等于号放在左区间,保证排序的稳定性
arr[k]=aux[i-l];
i++;
}else{
//右侧区间元素值较小
arr[k]=aux[j-l];
j++;
}
}
}
结果:
希尔排序、堆排序、归并排序的效率比较:
1.归并排序是一个稳定的nlogN级别时间复杂度的排序算法
2.此处稳定指的是两个稳定:
1)时间复杂度稳定:无论集合中元素如何变化,它的时间复杂度一直都是nlogN,不会退化为O(n^2)。【logN:递归深度就是我们拆分数组所用时间,拆了logN次,就是树的高度问题;O(n):合并两个子数组的数组遍历的过程的时间复杂度(for(int k=l,k<=r;k++))】
2)是一个稳定性的排序算法:相同元素的相对位置不发生变化
1)当左右两个子区间走完子函数后,左右两个区间已经有序了。如果此时arr[mid] mergeSortInternal(arr,l,mid);//走完这个函数之后arr[l...mid]已经有序 mergeSortInternal(arr,mid+1,r);//走完这个函数后[mid+1...r]已经有序 //只有两个子区间还有先后顺序不同时,才merge if(arr[mid]>arr[mid+1]){ //左区间最大值>右区间最小值才merge merge(arr,l,mid,r); } 2 )在小区间上,我们可以直接使用插入排序来优化,没必要元素一直拆分到1位置。从实验得出,一般r-l<=15时,使用插入排序,性能是极好的。【减少了归并的递归次数】 if(r-l<=15){ insertionSort(arr,l,r); return; } 优化后全部代码: 1.【链表】:leetCode148-排序链表 leetcode148-排序链表(应用到归并的merge方法) 2.求数组中的逆序对个数:剑指offer51 JZoffer51-数组中的逆序对(归并排序解决) 归并排序的核心:先将整个集合拆分为只有一个元素的集合(归),再将只有一个元素的集合合并为每个数组两个元素,四个元素...,直到整个数组合并(并)。 结果: 假设现在待排序的数据只有100G,但是内存只有1GB,如何排序这100GB数据呢?(本质还是归并排序【200路归并过程】) 1.先将这100G数据分别存储在200个文件中(文件在硬盘),每个文件都是0.5G。 2.分别将这200个文件一次读取到内存中,使用任何一个内部排序算法对其进行排序(快排,堆排,归并都行),此时可以得到200个有序的文件。 3.分别对这200个文件进行merge操作合并即可。 merge操作的具体思路呢?:这200个小文件已经有序,每次都取出200个文件的第一个元素放到内存中,内部排序这200个小值的最小值写回大文件,重复上述流程直到这200个文件所有内容全部写回即可得到一个排序好的大文件。 见数据结构之七大排序3—快速排序详解
/**
* 在arr[l...r]使用直接插入排序
* 归并排序中,一般r-l<=15时,使用插入排序,不必归并到1
* @param arr
* @param l
* @param r
*/
private static void insertionSort(int[] arr, int l, int r) {
for (int i = l+1; i <=r ; i++) {
for (int j = i; j >l&&arr[j]
/**
* 归并排序
*/
public static void mergeSort(int[] arr) {
mergeSortInternal(arr,0,arr.length - 1);
}
/**
* 在arr[l...r]进行归并排序,整个arr经过函数后就是一个已经有序的数组
*/
private static void mergeSortInternal(int[] arr, int l, int r) {
if(r-l<=15){
insertionSort(arr,l,r);
return;
}
int mid=l+((r-l)>>1);
mergeSortInternal(arr,l,mid);
mergeSortInternal(arr,mid+1,r);
if(arr[mid]>arr[mid+1]){
merge(arr,l,mid,r);
}
}
/**
* 在arr[l...r]使用直接插入排序
* 归并排序中,一般r-l<=15时,使用插入排序,不必归并到1
*/
private static void insertionSort(int[] arr, int l, int r) {
for (int i = l+1; i <=r ; i++) {
for (int j = i; j >l&&arr[j]
⭐归并排序的核心就是merge操作 :merge操作可以很好的对物理不连续的元素集合进行排序
2.归并排序的非递归写法
public static void mergeSortNonRecursion(int[] arr){
//最外层循环表示每次合并的子数组的元素个数
for (int sz=1;sz<= arr.length;sz+=sz){//先从1个元素开始合并
//内层循环的变量i表示每次合并的开始索引
//i+size就是右区间的开始索引,i+size
六、海量数据的排序处理⭐⭐
七、快速排序