Java 常用的九种排序算法与代码实现
排序问题一直是程序员工作与面试的重点,今天特意整理研究下与大家共勉!这里列出 8 种常见的经典排序,基本涵盖了所有的排序算法。
1. 直接插入排序
我们经常会到这样一类排序问题:把新的数据插入到已经排好的数据列中。将第一个数和第二个数排序,然后构成一个有序序列将第三个数插入进去,构成一个新的有序序列。对第四个数、第五个数…… 直到最后一个数,重复第二步。如题所示:
直接插入排序(Straight Insertion Sorting)的基本思想:在要排序的一组数中,假设前面 (n-1) [n>=2] 个数已经是排好顺序的,现在要把第 n 个数插到前面的有序数中,使得这 n 个数也是排好顺序的。如此反复循环,直到全部排好顺序。
代码实现:
首先设定插入次数,即循环次数,for(int i=1;i
设定插入数和得到已经排好序列的最后一个数的位数。insertNum 和 j=i-1。
从最后一个数开始向前循环,如果插入数小于当前数,就将当前数向后移动一位。
将当前数放置到空着的位置,即 j+1。
代码如下:
package com.jiading.myRewrite;
/**
* @program: 排序算法的Java实现
* @description: 插入排序
* @author: JiaDing
* @create: 2020-03-23 00:27
* 插入排序是稳定的
* 时间复杂度是O(n^2)
* 空间复杂度是O(1)
**/
public class InsertSort {
public static void insertSort(int[]array){
//从第二个开始排
for(int i=1;i
int temp=array[i];
int j=i-1;
while(j>=0){
if(array[j]
break;
}
if(array[j]>temp){
array[j+1]=array[j];
}
j--;
}
array[j+1]=temp;
}
}
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1};
insertSort(a);
System.out.println("排序结果:");
for (int i:a
) {
System.out.println(i);
}
}
}
2. 希尔排序
针对直接插入排序的下效率问题,有人对次进行了改进与升级,这就是现在的希尔排序。希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
如图所示:
对于直接插入排序问题,数据量巨大时。
将数的个数设为 n,取奇数 k=n/2,将下标差值为 k 的数分为一组,构成有序序列。
再取 k=k/2 ,将下标差值为 k 的书分为一组,构成有序序列。
重复第二步,直到 k=1 执行简单插入排序。
代码实现:
package com.jiading.myRewrite;
/**
* @program: 排序算法的Java实现
* @description: 希尔排序
* @author: JiaDing
* @create: 2020-03-29 17:47
* 希尔排序是非稳定算法
* 希尔排序的时间复杂度是:O(nlogn)~O(n2),平均时间复杂度大致是O(n√n)
* shell排序的时间复杂度是依赖于 argument sequence 的,所以你用不同的序列,时间复杂度不同
* shell的时间复杂度分析没有完结
* 空间复杂度是O(1)
**/
public class ShellSort {
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1};
shellSort(a);
System.out.println("排序结果:");
for (int i:a
) {
System.out.println(i);
}
}
private static void shellSort(int[] a) {
/*
这个length在这里并不表示数组长度,而是表示每组元素之间的距离,也就是取元素时的步长
每次合并都将步长减半
对于按步长分开的组中的元素进行直接插入排序
*/
int length=a.length;
while(length!=0){
length=length/2;
//i表示是第几组的,因为步长是length,所以步长之内的都是不同组的
for(int i=0;i
//j表示每次插入排序时考虑的组的元素的右边界
for(int j=i+length;j
//k表示前一个
int k=j-length;
//temp是要找位置往里插的元素
int temp=a[j];
/*
将前面的向后移动,为插入留下空子
*/
while(k>=0&&temp
a[k+length]=a[k];
k-=length;
}
//应该插入到k之后的那个位置,因为第k个位置是已经比temp小的
a[k+length]=temp;
}
}
}
}
}
3. 二分排序
package com.jiading.myRewrite;
/**
* @program: 排序算法的Java实现
* @description: 二分排序
* @author: JiaDing
* @create: 2020-03-23 00:35
* 二分排序比较麻烦,细节比较多
* 二分排序是插入排序中的一种,也是稳定的
* 时间复杂度是O(n^2):这是因为如果每次都插入到第一个位置的话,需要移动n-1+n-2+···+1次
* 空间复杂度是O(1)
**/
public class BinarySort {
public static void binarysort(int[]array){
int n=array.length;
for(int i=1;i
int temp=array[i];
int left=0,right=i-1,mid=(left+right)/2;
while(left<=right){
mid=(left+right)/2;
if(temp>array[mid]){
left=mid+1;
}else if(temp
right=mid-1;
}else{
break;
}
}
for(int j=i-1;j>=left;j--){
array[j+1]=array[j];
}
//如果没有更大的,就不需要移动和插入
if(left!=i)
array[mid]=temp;
}
}
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1};
binarysort(a);
System.out.println("排序结果:");
for (int i:a
) {
System.out.println(i);
}
}
}
4. 简单选择排序
常用于取序列中最大最小的几个数时。
(如果每次比较都交换,那么就是交换排序;如果每次比较完一个循环再交换,就是简单选择排序。)
遍历整个序列,将最小的数放在最前面。
遍历剩下的序列,将最小的数放在最前面。
重复第二步,直到只剩下一个数。
代码实现:
首先确定循环次数,并且记住当前数字和当前位置。
将当前位置后面所有的数与当前数字进行对比,小数赋值给 key,并记住小数的位置。
比对完成后,将最小的值与第一个数的值交换。
重复 2、3 步。
package com.jiading.myRewrite;
/**
* @program: 排序算法的Java实现
* @description: 选择排序
* @author: JiaDing
* @create: 2020-03-23 10:04
*选择排序因为存在跨元素间的互换,所以是不稳定算法
* 时间复杂度是O(n^2)
* 空间复杂度是O(1)
**/
public class ChooseSort {
public static void chooseSort(int[]array){
int length=array.length;
for(int i=0;i
int min=Integer.MAX_VALUE;
int pos=0;
for(int j=i;j
if(array[j]
min=array[j];
pos=j;
}
}
int temp=array[pos];
array[pos]=array[i];
array[i]=temp;
}
}
public static void main(String[] args){
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1};
chooseSort(a);
System.out.println("排序结果:");
for (int i:a
) {
System.out.println(i);
}
}
}
5. 堆排序
对简单选择排序的优化。
将序列构建成大顶堆。
将根节点与最后一个节点交换,然后断开最后一个节点。
重复第一、二步,直到所有节点断开。
6. 冒泡排序
很简单,用到的很少,据了解,面试的时候问的比较多!
将序列中所有元素两两比较,将最大的放在最后面。
将剩余序列中所有元素两两比较,将最大的放在最后面。
重复第二步,直到只剩下一个数。
代码实现:
设置循环次数。
设置开始比较的位数,和结束的位数。
两两比较,将最小的放到前面去。
重复 2、3 步,直到循环次数完毕。
package com.jiading.myRewrite;
/**
* @program: 排序算法的Java实现
* @description: 冒泡排序
* @author: JiaDing
* @create: 2020-03-23 09:28
* 冒泡排序是稳定的
* 时间复杂度是O(n^2)
* 空间复杂度是O(1)
**/
public class BubbleSort {
public static void bubbleSort(int[]array){
int length=array.length;
for(int i=0;i
for(int j=0;j
if(array[j]>array[j+1]){
int temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
}
}
}
}
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1};
bubbleSort(a);
System.out.println("排序结果:");
for (int i:a
) {
System.out.println(i);
}
}
}
7. 快速排序
要求时间最快时。
选择第一个数为 p,小于 p 的数放在左边,大于 p 的数放在右边。
递归的将 p 左边和右边的数都按照第一步进行,直到不能递归。
package com.jiading.myRewrite;
import java.util.Random;
/**
* @program: 排序算法的Java实现
* @description: 快速排序
* @author: JiaDing
* @create: 2020-03-29 18:08
* 快速排序的平均时间复杂度是O(nlgn),最糟时间复杂度是O(n^2)
* 快速排序被公认在所有同数量级O(nlgn)的排序方法中平均性能最好,但如果排序序列有序时,快速排序将转为冒泡排序
* 空间复杂度是O(1)
* 快速排序算法是不稳定算法
**/
public class QuickSort {
public static void main(String[] args) {
int[] a = {49, 38, 65, 97, 76, 13, 27, 49, 78, 34, 12, 64, 1};
quickSort(a);
System.out.println("排序结果:");
for (int i : a
) {
System.out.println(i);
}
}
private static void quickSort(int[] a) {
qSort(0, a.length - 1, a);
}
private static void qSort(int left, int right, int[] a) {
if (left < right) {
//返回枢纽的位置,此时数组的元素已经根据比枢纽小还是大自动分开了
int pivotpos = Partition(left, right, a);
//再递归地处理各个部分
qSort(left, pivotpos - 1, a);
qSort(pivotpos + 1, right, a);
}
}
private static int Partition(int left, int right, int[] a) {
/*
int pivot = a[left];是把第一个元素作为枢纽,但是为了更好的平均时间复杂度,避免极端的情况,建议使用随机选择枢纽的方式
*/
Random random=new Random();
int index = random.nextInt(right - left + 1) + left;
int pivot = a[index];
//把index的元素和left的换一下,为的是这样就可以直接复用以第一个元素为枢纽的代码了
int temp=a[left];
a[left]=pivot;
a[index]=temp;
while (left < right) {//从两侧交替地向中间扫描
while (left < right && a[right] > pivot) {
right--;
}
a[left] = a[right];//将小于或者等于枢纽的元素移动到低位置
while (left < right && a[left] <= pivot) {
left++;
}
a[right] = a[left];//将大于枢纽的元素移动到低位置
}
a[left] = pivot;
return left;
}
}
8. 归并排序
速度仅次于快速排序,内存少的时候使用,可以进行并行计算的时候使用。
选择相邻两个数组成一个有序序列。
选择相邻的两个有序序列组成一个有序序列。
重复第二步,直到全部组成一个有序序列。
package com.jiading.myRewrite;
/**
* @program: 排序算法的Java实现
* @description: 归并排序
* @author: JiaDing
* @create: 2020-03-26 00:02
* 时间复杂度是O(nlogn):每次都是n,一共logn次(1合为2,2合为4···)
* 空间复杂度是O(n)
* 归并算法是稳定的
**/
public class MergeSort {
public static void main(String[] args) {
int[]array={49,38,65,97,76,13,27,49,78,34,12,64,1};
int[]temp=new int[array.length];
mergeSort(array,temp,0,array.length-1);
for (int i:array
) {
System.out.println(i);
}
}
public static void mergeSort(int[]toSort,int[]temp,int left,int right){
if(left
int mid=(left+right)/2;
mergeSort(toSort,temp,left,mid);
mergeSort(toSort,temp,mid+1,right);
//merge的时候就顺便将数据从temp拷贝回toSort
merge(toSort,temp,left,mid,right);
}
}
public static void merge(int[]toSort,int[]temp, int left,int mid,int right){
//index是temp中的下标,leftStart和rightStart都是toSort中的下标
int leftStart=left,rightStart=mid+1,index=left;
while((leftStart<=mid)&&(rightStart<=right)){
if(toSort[leftStart]<=toSort[rightStart]){
temp[index]=toSort[leftStart];
index++;
leftStart++;
}else{
temp[index]=toSort[rightStart];
index++;
rightStart++;
}
}
if(leftStart>mid){
for(int i=rightStart;i<=right;i++){
temp[index]=toSort[i];
index++;
}
}else if(rightStart>right){
for(int i=leftStart;i<=mid;i++){
temp[index]=toSort[i];
index++;
}
}
//将数据拷贝回toSort
for(int i=left;i<=right;i++){
toSort[i]=temp[i];
}
}
}
9. 基数排序
用于大量数,很长的数进行排序时。
将所有的数的个位数取出,按照个位数进行排序,构成一个序列。
将新构成的所有的数的十位数取出,按照十位数进行排序,构成一个序列。
代码实现:
package com.jiading.myRewrite;
import java.util.LinkedList;
/**
* @program: 排序算法的Java实现
* @description: 基数排序
* @author: JiaDing
* @create: 2020-03-23 09:05
* 基数排序是稳定的
* 时间复杂度是O(k*n),k为数的最高位的数量
* 空间复杂度是O(n):虽然有十个桶,但是桶中放的元素总和还是n,所以占用总空间是O(n)
**/
public class BaseSort {
public static void baseSort(int[]array){
int num=array.length;
int max=Integer.MIN_VALUE;
int length=0;
for(int i=0;i
max=Math.max(max,array[i]);
}
length=String.valueOf(max).length();
LinkedList>list=new LinkedList>();
for(int i=0;i<10;i++){
list.add(new LinkedList());
}
for(int j=0;j
for(int i=0;i
/*
每一次都根据当前位放到各个桶里
*/
int index=(int)((array[i]%Math.pow(10,j+1))/Math.pow(10,j));
LinkedList integers = list.get(index);
integers.add(array[i]);
list.set(index,integers);
}
int count=0;
for(int i=0;i<10;i++){
LinkedList integers = list.get(i);
while(!integers.isEmpty()){
//当前次从桶里放回数组
Integer integer = integers.pollFirst();
array[count]=integer;
count++;
}
}
}
}
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1};
baseSort(a);
System.out.println("排序结果:");
for (int i:a
) {
System.out.println(i);
}
}
}
10. 总结:
一、稳定性:
稳定:冒泡排序、插入排序、归并排序和基数排序
不稳定:选择排序、快速排序、希尔排序、堆排序
二、平均时间复杂度
O(n^2): 直接插入排序,简单选择排序,冒泡排序。
在数据规模较小时(9W 内),直接插入排序,简单选择排序差不多。当数据较大时,冒泡排序算法的时间代价最高。性能为 O(n^2) 的算法基本上是相邻元素进行比较,基本上都是稳定的。
O(nlogn): 快速排序,归并排序,希尔排序,堆排序。
其中,快排是最好的, 其次是归并和希尔,堆排序在数据量很大时效果明显。
三、排序算法的选择
1. 数据规模较小
(1)待排序列基本序的情况下,可以选择直接插入排序;
(2)对稳定性不作要求宜用简单选择排序,对稳定性有要求宜用插入或冒泡
2. 数据规模不是很大
(1)完全可以用内存空间,序列杂乱无序,对稳定性没有要求,快速排序,此时要付出 log(N)的额外空间。
(2)序列本身可能有序,对稳定性有要求,空间允许下,宜用归并排序
3. 数据规模很大
(1)对稳定性有求,则可考虑归并排序。
(2)对稳定性没要求,宜用堆排序
4. 序列初始基本有序(正序),宜用直接插入,冒泡
各算法复杂度如下: