目录
1.稳定性
两个相等的数据,如果经过排序 之后,排序算法 能够保证其相对位置不发生变化,则称该算法是具备稳定性的算法。
2.分类
1.基于比较(直接把两个元素进行大小比较)的排序,也叫内部排序,将数据都在内存中操作
2.需要借助硬盘等辅助介质进行的排序操作(大数据排序,数据大到内存放不下):桶排序、基数排序、计数排序。
3.插入排序
3.1 直接插入排序
每次选择无序区间的第一个数插入到有序区间的合适位置,不断重复此流程知道数组有序。
/**
* 直接插入排序
* @param arr
*/
public static void insertSort(int[] arr){
//有序数组[0,i)
//默认第一个就是有序
for (int i = 1; i <arr.length ; i++) {
//每次都从无序区间中选择第一个元素插入到有序区间的合适位置
for (int j = i; j >0 &&arr[j-1]>arr[j] ; j--) {
swap(arr,j,j-1);
}
}
}
3.2 性能分析
时间复杂度 | 空间复杂度 | ||
最好 | 平均 | 最坏 | |
O(n) | O(n^2) | O(n^2) | O(1) |
数据有序 | 数据逆序 |
稳定性:稳定
总结:在插入排序中,初始数据越接有序,时间效率越高,因此 ,插入 排序经常作为高级排序的优化手段,在小数据规模上的性能非常好。
3.3 折半插入排序
由于插入是在有序区间进行插入,因此我们可以使用二分查找法来快速定位插入的位置(之前是通过遍历有序区间插入)。再将要插入元素的位置及其之后位置上的元素进行数据搬移。
/**
* 折半插入排序
*/
public static void bsInsertSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
int val=arr[i];
int low=0;
int high=i;
while (low<high) {
int mid = (low + high) / 2;
if (val >=arr[mid]) {//保证了稳定性
low=mid+1;
} else {
high=mid;
}
}
//数据搬移
for (int j = i; j > low; j--) {
arr[j]=arr[j-1];
}
//low就是元素插入位置
arr[low]=val;
}
}
时间复杂度O(n^2)
4.希尔排序
4.1 原理
希尔排序又称缩小增量法。基本原理是:选定一个整数gap,将待排序的数据中所有记录按gap分组。所有距离为gap的放在同一组,将组内元素进行排序。然后不断缩小gap的大小,直到gap=1,当gap=1时数组已经近乎有序,调用普通插入排序统一排序即可(直接插入排序最好)。
gap一般就是数组长度一直除以2或除以3,直到gap=1。
4.2 实现
/**
* 希尔排序
* @param arr
*/
public static void shellSort(int[] arr){
int gap=arr.length/2;
while (gap>=1){
insertionSortGap(arr,gap);
gap = gap / 2;
}
}
private static void insertionSortGap(int[] arr, int gap) {
//不断从gap开始走到数组末尾
for (int i = gap; i <arr.length ; i++) {
//最内层从gap索引开始向前看,看的元素就是距离gap长度的元素
// 不断比较当前元素和前面gap元素大小
// j - gap >= 0说明前面数组还要相同距离的元素,比较arr[j] 和 arr[j - gap]
for (int j = i; j - gap >= 0 &&arr[j] <arr[j-gap]; j=j-gap) {
swap(arr, j, j-gap);
}
}
}
为什么要从gap的位置从后往前去遍历呢 ?
因为从后往前遍历,交换j和j-gap的值,并且直到j-gap<0结束,这样就保证了能将最小值放在最前面。
4.3 性能分析
时间复杂度 | 空间复杂度 | ||
最好 | 平均 | 最坏 | |
O(n) | O(n^1.3) | O(n^2) | O(1) |
数据有序 | 比较难构造 |
5. 选择排序
5.1 直接选择排序-原理
每次从无序区间中选择最小值放在无序区间的最开始位置,每当进行一次无序循环(选择无序区间的最小值),无序区间的个数减一,有序区间的个数加一。
有序区间:[0,i) 无序区间:[i+1,n)
5.2 实现
/**
* 直接选择排序
* @param arr
*/
public static void selectSort(int[] arr) {
// 每次从待排序数组中选择最小值放在待排序数组的最前面。
// 最外层的for循环表示要执行的总次数,类似于冒泡,当剩下最后一趟时,整个数组已经有序
// 默认第一个元素就是有序的
// 已经有序的集合[0,i)
// 待排序的集合[i + 1,n)
// 每次进行一趟排序,最小值就放在了数组的最前面,已经有序的集合元素个数 + 1
// 待排序集合元素个数 - 1
for (int i = 0; i < arr.length - 1; i++) {
// min变量存储了最小值元素的下标
int min = i;
// 每次从无序区间中选择最小值
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
// 此时min就存储了最小值的元素下标,就把min对应的元素换到无序区间的最前面
swap(arr,i,min);
}
}
5.3 性能分析
时间复杂度 | 空间复杂度 |
O(n^2) | O(1) |
数据不敏感 | 数据不敏感 |
稳定性:不稳定
5.4 双向选择排序
之前的直接选择排序时是每次从无序区间中选出一个最小值放在无序区间的最前面,一趟下来只有一个元素到达最终位置。
而双向选择排序则是每次从无序区间中选取一个最大值和最小值,分别放在无序区间的最前面和最后面,一趟下来 就有两个元素到达了最终位置。
/**
* 双向选择排序,每次选出最小值放前面,最大值放后面
* @param arr
*/
public static void selectSortOP(int[] arr) {
int low = 0,high = arr.length - 1;
// 有序区间[0,low + 1)
while (low < high) {
int min = low,max = low;
for (int i = low + 1; i <= high; i++) {
if (arr[i] > arr[max]) {
max = i;
}
if (arr[i] < arr[min]) {
min = i;
}
}
// min存储了无序区间的最小值,max存储了无序区间的最大值
swap(arr,low,min);
if (max == low) {
// 当max就处在low位置,由于swap(arr,low,min),low对应的元素值修改了,修改到min对应的下标
max = min;
}
swap(arr,max,high);
low += 1;
high -= 1;
}
}
6、堆排序
6.1 原理
1.将任意数组进行堆化(升序调整为最大堆,降序调整为最小堆),再依次进行extractMax(取出堆顶元素)操作,就得到了一个排序的数组。
这种方法的时间复杂度O(logn),空间复杂度O(n),需要创建一个和原数组大小一致的临时空间。
2.原地堆排序:将任意数组进行堆化(升序调整为最大堆,降序调整为最小堆),此处假设得到一个升序集合 ,需要建大堆。此时堆顶元素就是整个数组中的最大值,排序后它应该处在数组的末尾,因此交换它和当前堆的最后一个元素,这样最大值就到了最终位置,再对交换后的堆顶元素进行下沉操作(每当有一个元素到达最终位置时,此时的下沉操作就不能再考虑这个元素),如此迭代到最后一个元素时,数组已经有序。
6.2 实现
/**
* 将任意数组进行原地堆排序
* @param arr
*/
public static void heapSort(int[] arr){
//将任意数组调整为最大堆
//从最后一个非叶子节点开始
for (int i = (arr.length-1-1)/2; i >=0 ; i--) {
siftDown(arr,i,arr.length);
}
//依次取出堆顶元素和最后位置元素交换
//最开始待排序[0,arr.length-1] 已排序[]
//第一次排序 待排序[0,arr.length-2] 已排序[arr.length-1]
//第二次排序 待排序[0,arr.length-3] 已排序[arr.length-2,arr.length-1]
for (int i = arr.length-1; i >0 ; i--) {
swap(arr,0,i);
siftDown(arr,0,i);
}
}
/**
* 元素的下沉操作
* @param arr
* @param i
* @param n 当前arr中有效的元素个数
*/
private static void siftDown(int[] arr, int i, int n) {
while((i*2)+1<n){
int j=(i*2+1);
if(j+1<n && arr[j+1]>arr[j]){
j=j+1;
}
if(arr[i]>arr[j]){
break;
}else {
swap(arr,i,j);
i=j;
}
}
}
private static void swap(int[] arr, int i, int j) {
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
}
6.3 性能分析
时间复杂度 | 空间复杂度 |
O(n*log(n)) | O(1) |
数据不敏感 | 数据不敏感 |
稳定性:不稳定
7. 冒泡排序
7.1 原理
在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程。直到数组整体有序。
7.2 实现
/**
* 冒泡排序
* @param arr
*/
public static void bubbleSort(int[] arr){
//最外层表示要比较的趟数,此处-1是因为,整个待排序数组剩一个元素时,整个数组已经有序
for (int i = 0; i < arr.length-1; i++) {
boolean isSwapped=false;
for (int j = 0; j < arr.length-1-i; j++) {
if(arr[j]>arr[j+1]){
isSwapped=true;
swap(arr,j,j+1);
}
}
if(!isSwapped){
//内层循环没有元素交换,整个数组有序
break;
}
}
}
private static void swap(int[] arr, int i, int j) {
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
7.3 性能分析
稳定性:稳定
8. 快速排序
8.1 原理
选取一个分区点(基准值),将数组分为三部分,基准值之前的数组都小于基准值,基准值之后的数组的值都大于基准值,重复这个过程,就得到了一个排序数组。
8.2 实现
/**
* 快排的基础实现
* @param arr
*/
public static void quickSort(int[] arr){
quickSortInternal(arr,0,arr.length-1);
}
/**
* 在l...r进行快排
* @param arr
* @param l
* @param r
*/
private static void quickSortInternal(int[] arr, int l, int r) {
if(r-l<=15){
insertBase(arr,l,r);
return;
}
//求基准值
int p=partition(arr,l,r);
quickSortInternal(arr,l,p-1);
quickSortInternal(arr,p+1,r);
}
private static int partition(int[] arr, int l, int r) {
//在当前数组中随机选择一个元素作为基准值
int randomIndex=random.nextInt(l,r);
swap(arr,l,randomIndex);
int v=arr[l];
int j=l;
//i是当前处理的元素下标
//arr[l+1...j]<v
//arr[j+1...i]>=v
for (int i = l+1; i <=r ; i++) {
if(arr[i]<v){
swap(arr,j+1,i);
//小于v的元素+1
j++;
}
}
swap(arr,j,l);
return j;
}
private static void swap(int[] arr, int i, int j) {
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
8.3 性能分析
稳定性:不稳定
性能衰减:1.当待排序元素接近有序时,快速排序退化为单支树,时间复杂度为O(n^2)。
2.当待排序数组中包含大量重复元素时,会导致大于等于基准值部分的元素远大于小于基准值的部分,使递归树产生倾斜。
9. 归并排序
9.1 原理
将集合排序时分为两大部分:1.拆分:将原数组不断拆分,拆分到每个小数组只剩下一个元素时,拆分过程结束。2.合并 :将拆分后的数组不断合并,直到合并到整个数组,这是整个数组有序 。
9.2 实现
/**
* 在arr上进行归并排序
* @param arr
*/
public static void mergeSort(int[] arr){
mergeSortInternal(arr,0,arr.length-1);
}
/**
* 递归
* 在arr[l...r]上进行归并排序
* @param arr
* @param l
* @param r
*/
private static void mergeSortInternal(int[] arr, int l, int r) {
if(r-l<=15){
insertBase(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..mid]和[mid+1...r]合并为一个大的有序数组
* @param arr
* @param l
* @param mid
* @param r
*/
private static void merge(int[] arr, int l, int mid, int r) {
//开辟一个大小和合并后数组大小相同的数组
int[] temp=new int[r-l+1];
//将原数组的内容拷贝到新数组
for (int i = l; i <=r; i++) {
temp[i-l]=arr[i];
}
int i=l;//左半有序数组的第一个元素
int j=mid+1;//右半有序数组第一个元素
//遍历原数组,选择左半区间和右半区间的最小值写回原数组
//k表示当前处理到原数组的哪个位置
for (int k = l; k <=r ; k++) {
if(i>mid){
//此时左半区间处理完毕,将右区间剩下的元素写回原数组
arr[k]=temp[j-l];
j++;
}else if(j>r){
//此时有右半区间处理完毕,将左区间剩下的元素写回原数组
arr[k]=temp[i-l];
i++;
}else if(temp[i-l]<temp[j-l]){
arr[k]=temp[i-l];
i++;
}else {
arr[k]=temp[j-l];
j++;
}
}
}
/**
* 归并排序的非递归写法
* @param arr
*/
public static void mergeSortNonRecursion(int[] arr){
//sz表示每次合并的个数,最开始从1个元素开始合并,以此累乘
for (int sz = 1; sz <= arr.length ; sz=sz*2) {
//merge过程,i表示每次merge开始的索引下标
for (int i = 0; i+sz <arr.length ; i+=sz*2) {
merge(arr,i,i+sz-1,Math.min(i+2*sz-1, arr.length-1));
}
}
}
9.3 性能分析
时间复杂度 | 空间复杂度 |
O(n * log(n)) | O(n) |
数据不敏感 | 数据不敏感 |
归并排序和堆排序一样,都是非常稳定的O(nlogn)的排序算法。
10. 七大排序总结
排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
希尔排序 | O(n) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(1) | 不稳定 |
快速排序 | O(n * log(n)) | O(n * log(n)) | O(n^2) | O(log(n)) ~ O(n) | 不稳定 |
归并排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(n) | 稳定 |