排序算法汇总

冒泡排序

算法思想:比较+交换。从数组第一个元素开始,两两比较相邻元素,将较大者后移,直到最后一个元素。重复此操作,直到数组中所有元素有序。

public class BubbleSort {
    public static void sort(int[] arr) {
    //重复N-1次
        for (int i = 0; i < arr.length - 1; i++) {
        //优化方案,如果有一轮比较中没有发生交换,则表示数据已经有序,避免无用循环
            boolean swaped = false;
            //从第一个元素开始两两比较,如果前者大于后者,则交换位置
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int tmp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = tmp;
                    swaped = true;
                }
            }
            if (!swaped) {
                break;
            }
        }
    }
}

复杂度分析:最坏情况下,输入全逆序数组,需要进行N-1轮比较交换,比较交换最多为N-1次,最少1次,总比较次数为N(N-1)/2,平均比较次数为N/2,则最坏时间复杂度O(N(N-1)/2) = O(N^2)。空间复杂度:算法只需要存储临时变量tmp的固定存储空间,空间复杂度为O(1)。

选择排序

算法思路:在未排序的数组中找到最小元素,交换到未排序数组的起始位置;然后在剩余未排序的数组中找到最小元素,交换到未排序数组的起始位置,以此类推,直到数组有序。

public static void sort(int[] arr) {
        for (int i = 0; i < arr.length-1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            if (minIndex != i) {
                int tmp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = tmp;
            }
        }
    }

算法分析:外层循环共N-1次,内层比较最多比较N-1次,最少比较1次,平均比较N/2次,时间复杂度为O(N^2)。排序过程中只需要固定的存储空间存储临时变量,空间复杂度为O(1)。

插入排序

算法思想:构建有序序列,对于未排序的数据,在有序序列中从后向前查找,找到合适的位置插入(也可以边找边插入),直到输入数组有序。

for (int i = 1; i < arr.length; i++) {
            int tmp = arr[i];
            for (int j = i; j >= 1; j--) {
                if (tmp < arr[j - 1]) {
                    arr[j] = arr[j - 1];
                    arr[j - 1] = tmp;
                } else {
                    break;//已经找到合适位置,不需要继续向前比较了
                }
            }
        }

算法分析:最坏情况下,外层循环需要插入N-1个数,内层循环最多需要比较N-1次,最少需要比较1次,平均比较N/2次,时间复杂度O(N^2),空间复杂度O(1)。如果输入序列本身有序,则时间复杂度为O(N)。

希尔排序

希尔排序是最早突破O(N^2)时间复杂度的排序算法之一,是插入排序的改进版,它通过比较相距一定距离的元素来工作。
算法思想:希尔排序使用一个增量序列来每次对相隔一定距离的元素进行插入排序,随着增量值的减小,数据逐渐大部分有序,直到数据全部有序。

public static void sort(int[] arr) {
        for (int gap = arr.length / 2; gap >= 1; gap /= 2) {
        //从gap开始,后面的每一个数进行子序列插入排序
            for (int i = gap; i < arr.length; i++) {
            //按增量gap插入排序
                for (int j = i; j >= gap; j -= gap) {
                    if (arr[j] < arr[j - gap]) {
                        int tmp = arr[j];
                        arr[j] = arr[j - gap];
                        arr[j - gap] = tmp;
                    }
                }
            }
        }
    }

算法分析:定理:通过交换相邻元素进行排序的任何算法都需要O(N^2)时间复杂度。排序算法通过消除逆序得以向前工作,为了使一个排序算法以亚二次时间复杂度运行,必须执行一些比较,特别是要对相距较远的元素进行交换,这些交换必须每次消除不止一个逆序。使用hibbard增量的希尔排序最坏时间复杂度为O(N^3/2)

快速排序

算法思想:快速排序算法体现了分治的思想。选取一个枢纽元,将小于枢纽元的元素放在其左边,将大于枢纽元的放在其右边,则枢纽元本身的位置已确定。然后再对左边子序列和右子序列分别应用这种策略,直至数组有序。

第一步:选取枢纽元

枢纽元的选择通常有三种方式:

  • 选取第一个元素:这种方式在输入序列有序的情况下效率比较低,如果输入是随机的,可以选择这种方式
  • 随机选择一个元素:这种方式每次都要根据输入随机生成一个数字,生成随机数效率并不是很高
  • 选取输入的中值:一般来说输入的中值不容易计算,一般采用将输入左端、右端、和中心位置上的中值来作为枢纽元。
第二步:遍历剩余数据,将数据分割为小于枢纽元和大于枢纽元的两部分

分割策略均采用两端向中间逼近的方式。
针对选取第一个元素为枢纽元的情况,分割策略如下:

取游标i,j,i从左端开始,j从右端开始。首先从j向左遍历,遇到小于枢纽元的元素则与枢纽元交换位置,然后交换遍历方向,由i向右端遍历,遇到大于枢纽元的元素再与枢纽元交换位置…以此类推,交替遍历,我们可以看到静止的游标位置即枢纽元的位置,最终遍历到i=j时所有数据都遍历完毕,分割完成。

public static void sort(int[] arr, int start, int end) {

        if (start >= end) {
            return;
        }

        int pivot = arr[start];

        int i = start;
        int j = end;
        while (i < j) {
            while (i < j && arr[j] >= pivot) {
                j--;
            }
            swap(arr, i, j);
            while (i < j && arr[i] <= pivot) {
                i++;
            }
            swap(arr, i, j);
        }
        sort(arr, start, i - 1);
        sort(arr, i + 1, end);

    }

针对选取中值作为枢纽元的情况,分割策略稍微复杂一些(假设数据互异):
第一步:选取枢纽元,我们采用冒泡排序先为三个元素排序,再选取中间值。注意此时对于选取的中值来说,数组的第一个和最后一个元素已经是分割完成了的。
第二步:对剩余未分割的元素进行分割。首先,对于数组[left…right],先将枢纽元放在right-1的位置,取游标i,j,i从[left+1]从左向右遍历,j从[right-2]从右向左遍历,i碰到大于枢纽元的元素时停止,j碰到小于枢纽元的元素时停止,当两者都停止时且i小于j时,交换i和j的元素,然后继续相向遍历….直到i>=j时所有数据都遍历完毕,此时j停在一个小于枢纽元的元素上,i停在一个大于枢纽元的元素上,将枢纽元和i交换则分割完毕。

对于相等的元素,我们选择让两个游标都停下来进行交换,而不是继续前进,以便最大可能获取均衡的的分割。

    public static void sort(int[] arr, int start, int end) {

        if (start >= end) {
            return;
        }
        int piovt = getPivotIndex(arr, start, end);

        int i = start, j = end - 1;
        for (;;) {

            while (arr[++i] < arr[piovt]) {
            }
            //j的下限
            while (j > start && arr[--j] > arr[piovt]) {
            }
            //关于游标这里有一个错误写法:
            /*while(arr[i]<arr[piovt]) i++;
            while(j>start&&arr[j]>arr[piovt]) j--;*/
            //这个写法在arr[i] = arr[j] = arr[piovt]时,游标将无法前进,陷入无限循环
            if (i < j) {
                swap(arr, i, j);
            } else {
                break;
            }
        }
        //i的上限
        if (i < end) {
            swap(arr, i, piovt);
        }
        sort(arr, start, i - 1);
        sort(arr, i + 1, end);
    }
算法分析
  • 最坏情况:枢纽元始终为最小或者最大元素的情况。此时有递推关系T(N)= T(N-1)+cN,cN为遍历时间,时间复杂度为O(N^2)
  • 最好情况:枢纽元始终平分输入数组。T(N) = 2T(N/2)+cN,时间复杂度为O(NlogN)
  • 平均情况:时间复杂度O(NlogN)
算法优化

快速排序在数组长度较小时性能较差,不如插入排序、冒泡排序等,原因在于当数组长度较小时,插入和冒泡的比较和交换会越来越少,而快速排序不论数组多小,选取枢纽元时的比较与交换,都是一样多的,而且选完之后的比较与交换次数也不少,算法可以在数组长度较小时换做插入等排序方式。

quickSort(int[] arr){
    if(arr.length<10){
        insertSort(arr);
    }else{
        quickSort(sub arr);
    }
}

归并排序

算法思想:归并排序体现了分治的思想。先递归地将输入数组一层层分为两个部分,然后从最底层将两个小部分排好序,归并在一起,再与同一层的另外的部分继续归并,直至数据全部有序。归并排序与快速排序均使用了分治的思想,不过归并排序是“先分再治”,而快速排序是“先治再分”。

public static void sort(int[] tmp, int[] arr, int start, int end) {
        // 基准条件
        if (start >= end) {
            return;
        }

        int mid = (start + end) / 2;
        int start0 = start;
        int start1 = mid + 1;

        sort(tmp, arr, start, mid);
        sort(tmp, arr, start1, end);

        int i = 0;
        for (;;) {

            if (start > mid && start1 > end) {
                break;
            }

            if (start > mid) {
                tmp[i++] = arr[start1++];
                continue;
            }

            if (start1 > end) {
                tmp[i++] = arr[start++];
                continue;
            }

            if (arr[start] <= arr[start1]) {
                tmp[i++] = arr[start++];
            } else {
                tmp[i++] = arr[start1++];
            }
        }

        for (int j = 0; j < i; j++) {
            arr[start0++] = tmp[j];
        }
    }

算法分析:归并排序共需要log2N次归并,归并操作的最少归并2个,最多归并N个,时间复杂度为O(N),则归并排序的时间复杂度为O(NlogN)。空间上需要O(N)的辅助存储空间。

堆排序

算法思想:根据堆序性质,首先将输入数据构建一个堆,然后依次出堆,输出序列就是有序状态。构建堆的时间复杂度为O(N),每次出堆的时间复杂度为O(logN),N个元素则堆排序的时间复杂度为O(NlogN)+O(N) = O(NlogN)。

总结

算法名称平均情况最坏情况最好情况辅助空间稳定性
冒泡排序O(N^2)O(N^2)O(N)O(1)稳定
选择排序O(N^2)O(N^2)O(N^2)O(1)不稳定
插入排序O(N^2)O(N^2)O(N)O(1)稳定
希尔排序O(N^3/2)O(1)不稳定
快速排序O(NlogN)O(N^2)O(NlogN)O(1)不稳定
归并排序O(NlogN)O(NlogN)O(NlogN)O(N)稳定
堆排序O(NlogN)O(NlogN)O(NlogN)O(1)不稳定
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值