Java 常用的九种排序算法与代码实现

排序问题一直是程序员工作与面试的重点,今天特意整理研究下与大家共勉!这里列出 8 种常见的经典排序,基本涵盖了所有的排序算法。

1. 直接插入排序

我们经常会到这样一类排序问题:把新的数据插入到已经排好的数据列中。将第一个数和第二个数排序,然后构成一个有序序列将第三个数插入进去,构成一个新的有序序列。对第四个数、第五个数…… 直到最后一个数,重复第二步。如题所示:

java 按字母顺序排序 java顺序排列代码_System

直接插入排序(Straight Insertion Sorting)的基本思想:在要排序的一组数中,假设前面 (n-1) [n>=2] 个数已经是排好顺序的,现在要把第 n 个数插到前面的有序数中,使得这 n 个数也是排好顺序的。如此反复循环,直到全部排好顺序。

代码实现:

首先设定插入次数,即循环次数,for(int i=1;i<length;i++),1 个数的那次不用插入。

设定插入数和得到已经排好序列的最后一个数的位数。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<array.length;i++){
            int temp=array[i];
            int j=i-1;
            while(j>=0){
                if(array[j]<temp){
                    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. 希尔排序

针对直接插入排序的下效率问题,有人对次进行了改进与升级,这就是现在的希尔排序。希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
  • 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位

如图所示:

java 按字母顺序排序 java顺序排列代码_时间复杂度_02

对于直接插入排序问题,数据量巨大时。

将数的个数设为 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<length;i++){
                //j表示每次插入排序时考虑的组的元素的右边界
                for(int j=i+length;j<a.length;j+=length){
                    //k表示前一个
                    int k=j-length;
                    //temp是要找位置往里插的元素
                    int temp=a[j];
                    /*
                    将前面的向后移动,为插入留下空子
                     */
                    while(k>=0&&temp<a[k]){
                        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<n;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<array[mid]){
                    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. 简单选择排序

常用于取序列中最大最小的几个数时。

(如果每次比较都交换,那么就是交换排序;如果每次比较完一个循环再交换,就是简单选择排序。)

遍历整个序列,将最小的数放在最前面。

遍历剩下的序列,将最小的数放在最前面。

重复第二步,直到只剩下一个数。

java 按字母顺序排序 java顺序排列代码_时间复杂度_03

代码实现:

首先确定循环次数,并且记住当前数字和当前位置。

将当前位置后面所有的数与当前数字进行对比,小数赋值给 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<length;i++){
            int min=Integer.MAX_VALUE;
            int pos=0;
            for(int  j=i;j<length;j++){
                if(array[j]<min){
                    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. 堆排序

对简单选择排序的优化。

将序列构建成大顶堆。

将根节点与最后一个节点交换,然后断开最后一个节点。

重复第一、二步,直到所有节点断开。

java 按字母顺序排序 java顺序排列代码_排序算法_04

6. 冒泡排序

很简单,用到的很少,据了解,面试的时候问的比较多!

将序列中所有元素两两比较,将最大的放在最后面。

将剩余序列中所有元素两两比较,将最大的放在最后面。

重复第二步,直到只剩下一个数。

java 按字母顺序排序 java顺序排列代码_时间复杂度_05

代码实现:

设置循环次数。

设置开始比较的位数,和结束的位数。

两两比较,将最小的放到前面去。

重复 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<length;i++){
            for(int j=0;j<length-1;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 左边和右边的数都按照第一步进行,直到不能递归。

java 按字母顺序排序 java顺序排列代码_java 按字母顺序排序_06

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. 归并排序

速度仅次于快速排序,内存少的时候使用,可以进行并行计算的时候使用。

选择相邻两个数组成一个有序序列。

选择相邻的两个有序序列组成一个有序序列。

重复第二步,直到全部组成一个有序序列。

java 按字母顺序排序 java顺序排列代码_排序算法_07

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<right){
            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<num;i++){
            max=Math.max(max,array[i]);
        }
        length=String.valueOf(max).length();
        LinkedList<LinkedList<Integer>>list=new LinkedList<LinkedList<Integer>>();
        for(int i=0;i<10;i++){
            list.add(new LinkedList<Integer>());
        }
        for(int j=0;j<length;j++){
            for(int i=0;i<num;i++){
                /*
                每一次都根据当前位放到各个桶里
                 */
                int index=(int)((array[i]%Math.pow(10,j+1))/Math.pow(10,j));
                LinkedList<Integer> integers = list.get(index);
                integers.add(array[i]);
                list.set(index,integers);
            }
            int count=0;
            for(int i=0;i<10;i++){
                LinkedList<Integer> 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. 序列初始基本有序(正序),宜用直接插入,冒泡

各算法复杂度如下:

java 按字母顺序排序 java顺序排列代码_java 按字母顺序排序_08