java八大经典排序算法

排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳所有的排序记录,在排序过程当中须要访问外存。咱们这里说说八大排序就是内部排序。git


当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。算法

8种排序之间的关系:shell


 当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短。数组

直接插入

基本思想

直接插入排序是将未排序的数据插入至已排好序序列的合适位置。数据结构

具体流程以下:ide

一、首先比较数组的前两个数据,并排序;函数

二、比较第三个元素与前两个排好序的数据,并将第三个元素放入适当的位置;性能

三、比较第四个元素与前三个排好序的数据,并将第四个元素放入适当的位置;ui

......spa

四、直至把最后一个元素放入适当的位置。

例子
{4,5,1,2,8,6,7,3,10,9} 
取无序区间的第一个,从右向左扫描有序区间比较,方括号内可视为有序区间。 
第一次:[4],5,1,2,8,6,7,3,10,9 
第二次:[4,5],1,2,8,6,7,3,10,9 
第三次:[1,4,5],2,8,6,7,3,10,9 
第四次:[1,2,4,5],8,6,7,3,10,9 
第五次:[1,2,4,5,8],6,7,3,10,9 
第六次:[1,2,4,5,6,8],7,3,10,9 
第七次:[1,2,4,5,6,7,8],3,10,9 
第八次:[1,2,3,4,5,6,7,8],10,9 
第九次:[1,2,3,4,5,6,7,8,10],9 
第十次:[1,2,3,4,5,6,7,8,9,10]

算法分析

直接插入排序算法的空间复杂度为O(1)。

最好的状况,要比较的无序序列本来就是顺序有序的,那么要比较的次数是n-1,移动了0次,时间复杂度O(n)。

最坏的状况,要比较的无序序列本来就是逆序有序的,那么要比较的次数是(n+2)(n-1)/2,移动的次数(n+4)(n-1)/2,时间复杂度O(n²)。

直接插入排序的平均复杂度为O(n²)。

直接插入排序是稳定的。

Java实现

/**

 * 直接插入排序

 * 时间复杂度:O(n^2)

 *@param data

 */

public static void insertSort(int[] data){

   int temp;

   for (int i = 1; i < data.length; i++) {

       temp = data[i];//待插入数据

       int j;

       for(j = i - 1; j >= 0; j--) {

           //判断是否大于temp,大于则后移一位

           if(data[j] > temp) {

                data[j+1] = data[j];

           }else{

                break;

           }

       }

       data[j + 1] = temp;

    }

}

希尔排序

基本思想

希尔排序严格来讲是基于插入排序的思想,又被称为缩小增量排序。

  具体流程以下:

  一、将包含n个元素的数组,分红n/2个数组序列,第一个数据和第n/2+1个数据为一对...

  二、对每对数据进行比较和交换,排好顺序;

  三、而后分红n/4个数组序列,再次排序;

  四、不断重复以上过程,随着序列减小并直至为1,排序完成。

假若有初始数据:25  11 45  26  12  78。

  一、第一轮排序,将该数组分红 6/2=3 个数组序列,第1个数据和第4个数据为一对,第2个数据和第5个数据为一对,第3个数据和第6个数据为一对,每对数据进行比较排序,排序后顺序为:[25, 11, 45, 26,12, 78]。

  二、第二轮排序,将上轮排序后的数组分红6/4=1个数组序列,此时逐个对数据比较,按照插入排序对该数组进行排序,排序后的顺序为:[11, 12, 25, 26, 45, 78]。

算法分析

对于插入排序而言,若是原数组是基本有序的,那排序效率就可大大提升。另外,对于数量较小的序列使用直接插入排序,会因须要移动的数据量少,其效率也会提升。所以,希尔排序具备较高的执行效率。

  希尔排序并不稳定,O(1)的额外空间,时间复杂度为O(n²)

Java实现

/**

 * 希尔(shell)算法

 * 时间复杂度:O(n^2)

 *@param data

 */

public static voidshellSortSmallToBig(int[] data) {

   int j = 0;

   int temp = 0;

   for (int increment = data.length / 2; increment > 0; increment /= 2){

       for (int i = increment; i < data.length; i++) {

           temp = data[i];

           for (j = i - increment; j >= 0; j -= increment) {

                if (temp < data[j]) {

                    data[j + increment] =data[j];

                } else {

                    break;

                }

           }

           data[j + increment] = temp;

       }

    }

}

简单选择

基本思想

选择排序是一种简单直观的排序算法,其基本原理以下:对于给定的一组记录,通过第一轮比较后获得最小的记录,而后将该记录的位置与第一个记录的位置交换;接着对不包括第一个记录之外的其余记录进行第二次比较,获得最小记录并与第二个位置记录交换;重复该过程,知道进行比较的记录只剩下一个为止。

算法分析

时间复杂度:假设有n个数据,数据交换的次数最多为n-1次,但程序的整体的比较次数较多。因此综合考虑有直接选择排序的时间复杂度为O(n2)(n的平方)。因此当记录占用字节数较多时,一般比直接插入排序的执行速度快些。

空间复杂度:直接选择排序的空间复杂度很好,它只须要一个附加单元用于数据交换,因此其空间复杂度为O(1)。

稳定性:因为在直接选择排序中存在着不相邻元素之间的互换,所以,直接选择排序是一种不稳定的排序方法。以数组{49,38,65,97,76,13,27,49}为例, 


Java实现

/**

 * 简单选择排序

 * 时间复杂度:O(n^2)

 *@param data

 */

public static void selectSort(int[] data) {

   for (int i = 0; i < data.length; i++) {

       int temp = data[i];

       int flag = i; //将当前下标定义为最小值下标

       for (int j = i + 1; j < data.length; j++) {

           if (data[j] < temp) { //a[j] < temp 从小到大排序;a[j] >temp 从大到小排序

                temp = data[j];

                flag = j; //若是有小于当前最小值的关键字将此关键字的下标赋值给flag

            }

       }

       if (flag != i) {

           data[flag] = data[i];

           data[i] = temp;

       }

    }

}

堆排序

基本思想

堆是一种特殊的树形数据结构,其每一个节点都有一个值,一般提到的堆都是指一颗彻底二叉树,根结点的值小于(或大于)两个子节点的值,同时,根节点的两个子树也分别是一个堆。

堆排序就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构形成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),而后将剩余的 n-1 个序列从新构形成一个堆,这样就会获得 n 个元素中次大的值。如此反复执行,便能获得一个有序序列了。

堆排序的实现须要解决的两个关键问题:

(1)将一个无序序列构成一个堆。

(2)输出堆顶元素后,调整剩余元素成为一个新堆。

大根堆排序算法的基本操做

① 初始化操做:将R[1..n]构造为初始堆;

②每一趟排序的基本操做:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,而后将新的无序区调整为堆(亦称重建堆)。

注意:

①只需作n-1趟排序,选出较大的n-1个关键字便可以使得文件递增有序。

②用小根堆排序与利用大根堆相似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任什么时候刻堆排序中无序区老是在有序区以前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。

算法分析

堆排序的运行时间主要耗费在初始构建堆和在重建堆时反复筛选上。在构建对的过程当中,由于咱们是彻底二叉树从最下层最右边的非终端节点开始构建,将它与其孩子进行比较和如有必要的互换,对每一个非终端节点来讲,其实最多进行两次比较和互换操做,所以整个构建堆的时间复杂度为O(n)。

在正式排序时,第i次取堆顶记录重建堆须要用O(logi)的时间(彻底二叉树的某个节点到根节点的距离为这里写图片描述),而且须要取n-1次堆顶记录,所以,重建堆的时间复杂度为O(nlogn)。

因此整体来讲,堆排序的时间复杂度为O(nlogn),因为堆排序对原始记录的状态并不敏感,所以它不管是最好、最坏和平均时间复杂度均为O(nlogn)。这在性能上显然要远远好过于冒泡、简单选择、直接插入的时间复杂度了。

空间复杂度上,它只有一个用来交换的暂存单元,也很是的不错。不过因为记录的比较与交换是跳跃式进行的,所以堆排序也是一种不稳定的排序方法。

另外,因为出事构建堆所须要的比较次数比较多,所以,他并不适合待排序序列个数较少的状况。

Java实现

/**

 * 构建大顶堆

 */

public static void adjustHeap(int[] data,int i, int len) {

    inttemp, j;

   temp = data[i];

   for (j = 2 * i; j < len; j *= 2) { //沿关键字较大的孩子结点向下筛选

       if (j < len && data[j] < data[j + 1]) {

           ++j; //j为关键字中较大记录的下标

       }

       if (temp >= data[j]) {

           break;

       }

       data[i] = data[j];

       i = j;

    }

   data[i] = temp;

}

/**

 * 堆排序

 * 时间复杂度:O(n^2)

 *@param data

 */

public static void heapSort(int[] data) {

   int i;

   for (i = data.length / 2 - 1; i >= 0; i--) { //构建一个大顶堆

       adjustHeap(data, i, data.length - 1);

    }

   for (i = data.length - 1; i >= 0; i--) { //将堆顶记录和当前未经排序子序列的最后一个记录交换

       int temp = data[0];

       data[0] = data[i];

       data[i] = temp;

       adjustHeap(data, 0, i - 1); //将a中前i-1个记录从新调整为大顶堆

    }

}

冒泡排序

基本思想

设排序表长为n,从后向前或者从前向后两两比较相邻元素的值,若是二者的相对次序不对(A[i-1]> A[i]),则交换它们,其结果是将最小的元素交换到待排序序列的第一个位置,咱们称它为一趟冒泡。下一趟冒泡时,前一趟肯定的最小元素再也不参与比较,待排序序列减小一个元素,每趟冒泡的结果把序列中最小的元素放到了序列的”最前面”。

算法分析

冒泡排序的时间复杂度为O(n^2),空间复杂度为O(1),它是一种稳定的排序算法。

1..若是咱们的数据正序,只须要走一趟便可完成排序。所需的比较次数C和记录移动次数M均达到最小值,即:Cmin=n-1;Mmin=0;因此,冒泡排序最好的时间复杂度为O(n)。

2.若是很不幸咱们的数据是反序的,则须要进行n-1趟排序。每趟排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种状况下,比较和移动次数均达到最大值:


冒泡排序的最坏时间复杂度为:O(n2) 。

综上所述:冒泡排序总的平均时间复杂度为:O(n2) 。

Java实现

/**

 * 冒泡排序

 * 时间复杂度:O(n^2)

 *@param data

 */

public static void bubbleSort(int[] data){

   int j , k;

   int flag = data.length ;//flag来记录最后交换的位置,也就是排序的尾边界

   while (flag > 0){//排序未结束标志

       k = flag; //k 来记录遍历的尾边界

       flag = 0;

       for(j=1; j<k; j++){

           if(data[j-1] > data[j]){//前面的数字大于后面的数字就交换

                //交换a[j-1]和a[j]

                int temp;

                temp = data[j-1];

                data[j-1] = data[j];

                data[j]=temp;

                //表示交换过数据

                flag = j;//记录最新的尾边界

           }

       }

    }

}

快速排序

基本思想

快速排序是对冒泡排序的一种改进。首先在数组中选择一个基准点,而后分别从数组的两端扫描数组,设两个指示标志(lo指向起始位置,hi指向末尾),首先从后半部分开始,若是发现有元素比该基准点的值小,就交换lohi位置的值,而后从前半部分开始扫秒,发现有元素大于基准点的值,就交换lohi位置的值,如此往复循环,直到lo>=hi,而后把基准点的值放到hi这个位置。一次排序就完成了。之后采用递归的方式分别对前半部分和后半部分排序,当前半部分和后半部分均有序时该数组就天然有序了。

   假设要排序的数组是A[1]……A[N],首先任意选取一个数据(一般选用第一个数据)做为关键数据,而后将全部比它的数都放到它前面,全部比它大的数都放到它后面,这个过程称为一躺快速排序。一趟快速排序的算法是:

  1)、设置两个变量I、J,排序开始的时候I:=1,J:=N;

  2)以第一个数组元素做为关键数据,赋值给X,即X:=A[1];

  3)、从J开始向前搜索,即由后开始向前搜索(J:=J-1),找到第一个小于X的值,二者交换;

  4)、从I开始向后搜索,即由前开始向后搜索(I:=I+1),找到第一个大于X的值,二者交换;

  5)、重复第三、4步,直到I=J。

算法分析

快速排序最“快”的地方在于左右两边可以快速同时递归排序下去,因此最优的状况是基准值恰好取在无序区的中间,这样可以最大效率地让两边排序,同时最大地减小递归划分的次数。此时的时间复杂度仅为O(NlogN)

快速排序也有存在不足的状况,当每次划分基准值时,获得的基准值老是当前无序区域里最大或最小的那个元素,这种状况下基准值的一边为空,另外一边则依然存在着不少元素(仅仅比排序前少了一个),此时时间复杂度为:O(n2)

Java实现

/**

 * 将数组的某一段元素进行划分,小的在左边,大的在右边

 * @param data

 * @param start

 * @param end

 * @return

 */

public static intdivide(int[] data, int start, int end){

    //每次都以最右边的元素做为基准值

    int base = data[end];

    //start一旦等于end,就说明左右两个指针合并到了同一位置,能够结束此轮循环。

    while(start < end){

        while(start < end &&data[start] <= base) {

            //从左边开始遍历,若是比基准值小,就继续向右走

            start++;

        }

        //上面的while循环结束时,就说明当前的a[start]的值比基准值大,应与基准值进行交换

        if(start < end){

            //交换

            int temp = data[start];

            data[start] = data[end];

            data[end] = temp;

            //交换后,此时的那个被调换的值也同时调到了正确的位置(基准值右边),所以右边也要同时向前移动一位

            end--;

        }

        while(start < end &&data[end] >= base) {

            //从右边开始遍历,若是比基准值大,就继续向左走

            end--;

        }

        //上面的while循环结束时,就说明当前的a[end]的值比基准值小,应与基准值进行交换

        if(start < end){

            //交换

            int temp = data[start];

            data[start] = data[end];

            data[end] = temp;

            //交换后,此时的那个被调换的值也同时调到了正确的位置(基准值左边),所以左边也要同时向后移动一位

            start++;

        }

    }

    //这里返回start或者end皆可,此时的start和end都为基准值所在的位置

    return end;

}

/**

 * 快速排序

 * 时间复杂度:O(n^2)

 * @param data

 * @param start

 * @param end

 */

public static voidquickSort(int[] data, int start, int end){

    if(start > end){

        //若是只有一个元素,就不用再排下去了

        return;

    }else{

        //若是不止一个元素,继续划分两边递归排序下去

        int partition = divide(data, start,end);

        quickSort(data, start, partition-1);

        quickSort(data, partition+1, end);

    }

}

归并排序

基本思想

归并排序就是利用归并的思想实现的排序方法。并且充分利用了彻底二叉树的深度是的特性,所以效率比较高。其基本原理以下:对于给定的一组记录,利用递归与分治技术将数据序列划分红为愈来愈小的半子表,在对半子表排序,最后再用递归方法将排好序的半子表合并成为愈来愈大的有序序列。 

通过第一轮比较后获得最小的记录,而后将该记录的位置与第一个记录的位置交换;接着对不包括第一个记录之外的其余记录进行第二次比较,获得最小记录并与第二个位置记录交换;重复该过程,知道进行比较的记录只剩下一个为止。

工做原理:

(1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列 

(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置 

(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置 

(4)重复步骤3直到某一指针达到序列尾 

(5)将另外一序列剩下的全部元素直接复制到合并序列尾

算法分析

一趟归并须要将数组 a[]中相邻的长度为h的有序序列进行两两归并.并将结果放到temp[]中,这须要将待排序列中的全部记录扫描一遍,所以耗费O(n),而又彻底二叉树的深度可知,整个归并排序须要进行()次,所以总的时间复杂度为O(nlogn),并且这是归并排序算法中最好、最坏、平均的时间性能。

因为归并排序在归并过程当中须要与原始序列一样数量的存储空间存放归并结果以及递归时深度为的栈空间,所以空间复杂度为O(n+logn).

另外,对代码进行仔细研究,发现merge函数中有if (a[i] < a[j]) 的语句,说明它须要两两比较,不存在跳跃,所以归并排序是一种稳定的排序算法。

也就是说,归并排序是一种比较占内存,但却效率高且稳定的算法

Java实现

/**

 * 归并排序

 * @param data

 * @param low

 * @param mid

 * @param high

 */

public static voidmerge(int[] data, int low, int mid, int high) {

    int[] temp = new int[high - low + 1];

    int i= low;// 左指针

    int j = mid + 1;// 右指针

    int k = 0;

    // 把较小的数先移到新数组中

    while (i <= mid && j <= high){

        if (data[i] < data[j]) {

            temp[k++] = data[i++];

        } else {

            temp[k++] = data[j++];

        }

    }

    // 把左边剩余的数移入数组

    while (i <= mid) {

        temp[k++] = data[i++];

    }

    // 把右边边剩余的数移入数组

    while (j <= high) {

        temp[k++] = data[j++];

    }

    // 把新数组中的数覆盖nums数组

    for (int k2 = 0; k2 < temp.length; k2++){

        data[k2 + low] = temp[k2];

    }

}

/**

 * 归并排序

 * 时间复杂度:O(nlog2n)

 * @param data

 * @param low

 * @param high

 */

public static voidmergeSort(int[] data, int low, int high) {

    int mid = (low + high) / 2;

    if (low < high) {

        mergeSort(data, low, mid);//左边

        mergeSort(data, mid + 1, high);//右边

        merge(data, low, mid, high);//左右归并

    }

}

基数排序

基本思想

像选择排序、插入排序、快速排序等都是基于两个元素的比较进行排序的。而基数排序无需进行元素比较,基于队列处理就可以达到排序的目的。

基数排序不是基于排序关键字来比较排序项,而是基于排序关键字的结构。对于排序关键字中的每个数字或字符的每一种可能取值,都会建立一个单独的队列。队列的数目就称为基数。

例如:要排序所有由小写字母组成的字符串,则基数就是26,就会用到26个单独的队列。若是对十进制数进行排序,则基数应该是10。

为何不是全部的排序都使用基数排序算法呢?

1.基数排序算法要根据给定问题特别设计;

2.若是排序关键字中的数字数目与列表中元素的数目接近,那么算法的时间复杂度接近O(n平方);

3.基数影响空间复杂度。

算法分析

在基数排序中,没有任何元素的比较和交换,元素只是在每一轮中从一个队列移动到另外一个队列。对于给定的基数,遍历数据的轮次是一个常数,它与排序关键字的数目无关,因而,基数排序算法的时间复杂度为O(n)

Java实现

/**

 * 基数排序

 * 时间复杂度:O(nlog2n)

 * @param data

 */

public static voidradixSort(int[] data){

    String temp;

    int numObj;

    int digit,num;

    Queue<Integer>[] digitQueue =(LinkedList<Integer>[])(new LinkedList[10]);

    for(int digitVal = 0; digitVal <= 9;digitVal++){

        digitQueue[digitVal] =(Queue<Integer>)(new LinkedList<Integer>());

    }

    //sort

    for(int pos = 0; pos <= 3; pos++){

        for(int scan = 0; scan <data.length; scan++){

            temp = String.valueOf(data[scan]);

            digit =Character.digit(temp.charAt((3 - pos)), 10);

            digitQueue[digit].add(newInteger(data[scan]));

        }

        num = 0;

        for(int digitVal = 0; digitVal <= 9;digitVal++){

           while(!(digitQueue[digitVal]).isEmpty()){

                numObj =digitQueue[digitVal].remove();

                data[num] = numObj;

                num++;

            }

        }

    }

}