【数据结构与算法】【算法】三种高级排序算法

举报
huahua.Dr 发表于 2021/09/16 00:56:55 2021/09/16
【摘要】 详解三种高级排序算法:归并排序、希尔排序和快速排序

一、归并排序

什么是归并排序

归并排序就是将两个有序的子序列合并到一个序列中。是分治算法的一个典型应用,什么是分治算法呢?分治算法就是利用算法将一个原问题分解成一个个子问题(子问题与原问题的性质一样),分别计算出每个子问题的最优解,即可求出原问题的解;最简单的就是问题就是二分法问题,归并排序正是分治算法的二分法问题;

分治算法可以使用递归或者动态规划等思想,将复杂问题简单化。

归并排序的使用场景

当空间内存比较充足,数据量有比较大的时候,为了更好的完成排序效率,推荐使用归并排序,其时间复杂度为O(N*logN);

归并排序的实现方式

结合递归思想,将一个需要排序的数组,反复的两半切割成子数组,直到得到的子数组只含有一个数据项(基值条件);

然后将每个切成的子数组使用归并进行合并到一个数组中,反复执行,最后得到一个有序的数组。

public class TestMain {

    public  static int[] a = {1, 4, 5, 11, 2, 7, 6, 9, 8, 12, 66, 33, 55};

    public static void main(String[] args) {

        int[] arraySpace = new int[a.length];

        recMerge(arraySpace,0,a.length-1);

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

            System.out.println(arraySpace[i]);

        }

    }

 

    private static void recMerge(int[] arraySpace, int lower, int upper) {

        // 基值条件:如果两个下标值相等,说明不能在分割了,直接返回

        if (lower == upper) {

            return;

        }

        // 采用二分法的分治思想,拆分两个子数组,最后采用归并排序

        int mid = (lower + upper) / 2;

        recMerge(arraySpace, lower,mid);

        recMerge(arraySpace,mid+1,upper);

        merge(arraySpace,lower,mid+1,upper);

    }

    private static void merge(int[] arraySpace, int lower, int mid,int upper) {

        int size = upper - lower + 1; //本次归并的个数

        int index = 0;

        int lowerIndex = lower; // 记录一开始的位置

        int midIndex = mid - 1; // 记录中间的位置

        //当两边都有值时,进行比较排序

        while (lower <= midIndex && mid <= upper) {

                if (a[lower] < a[mid]) {

                    arraySpace[index++] = a[lower++];

                }else {

                    arraySpace[index++] = a[mid++];

                }

        }

        // 当右边比较完并完成排序,只剩左边的值,则直接插入

        while (lower <= midIndex) {

            arraySpace[index++] = a[lower++];

        }

        //当左边比较完并完成排序,只剩右边的值,则直接插入

        while (mid <= upper) {

            arraySpace[index++] = a[mid++];

        }

        // 最后将归并排序完的数组,复制到原数组中,等待下一次归并排序,直到最后完整排序完

        for (int i = 0; i < size; i++) {

            a[lowerIndex++] = arraySpace[i];

        }

    }

}

 

归并排序的优缺点

归并排序的优点很明显:时间复杂度比较快:O(N*logN)。

归并排序缺点也很明显,就是浪费空间。第一是需要单独一块空间存放归并数据,第二就是每次归并完都需要将其复制到原数组中,特别浪费空间,如果空间内存不足的,请慎重使用。事实上,所说的缺点大多是由于归并排序使用的递归算法,也就是分治思想,带来的问题,虽然利用分治思想可以很好的解决我们的实际问题,但是实际中它的效率并不高;因此呢,如果我们想要追求高效率,那么我们可以考虑一下怎么消除递归等方法,常用的方法一般是利用循环或者栈来代替

注意一下:

大多数递归算法都可以用栈结构来代替,例如我们的编译器在处理方法函数的调用的时候,我们会遇到方法函数内还有存在一层层的其他方法函数的调用,就跟递归的原理一样,一层层的调用,直到最后一层,然后再一层层的返回;编译器利用了栈来实现这种思想,将方法名、参数和返回值地址一层层的压入栈中,利用先进后出的特性,最后返回到最开始调用的那个方法函数里,用栈消除递归,效率更高更可靠。

二、希尔排序

什么是希尔排序

希尔排序就是由一个人叫希尔的人基于插入排序的改进的一个算法;在插入排序算法基础上增加一个新特性(在插入排序中增加对间隔元素的排序,间隔大小从n慢慢减到1范围【该间隔范围称之为间隔序列,生成这个间隔序列的方法有很多种,常用的方式是利用递归方式,使用公式h=3h+1,其中h为间隔大小,初始为1,一直递归,当间隔大小h大于数组大小即停止,然后使用公式h=(h-1)/3推倒公式生成逆置的间隔序列】,当间隔大小为1时,最后一次排序即完成最后的排序),大大提高了插入排序的执行效率。

希尔排序的使用场景

在中等规模的数据排序中(如几千个数据项),执行效率比较良好,也比较容易实现,代码比较简单,写起来并不复杂,因此这种场景下推荐使用,其平均时间复杂度为O(N*logN^2),实际取值范围为:O(N*N^1/2)到O(N*N^1/6);因为除了一些特殊情况,希尔排序的效率很难从理论上分析,这些范围都是从实验中得出来的一个评估。

希尔排序的实现

public class TestMain {
    public  static int[] a = {1, 4, 5, 11, 2, 7, 6, 9, 8, 12, 66, 33, 55};
    public static void main(String[] args) {
        shellSort(a,a.length);
        for (int i = 0; i < a.length; i++) {
            System.out.print(" "+a[i]);
        }
    }
    private static void shellSort(int[] arraySort,int size) {
        int h = 1; // 计算出间隔序列大小值
        while (h <= size/3) { // 范围公式可以自己定义一个合适的范围
            h = h*3 +1;
        }
        int inner,outer;
        int temp;// 开辟一个临时变量空间保存插入排序的腾出空位置的元素
        while (h > 0) {
            for (outer = h; outer < size ; outer++) {
                temp = arraySort[outer];
                inner = outer;
                while (inner > h-1 && arraySort[inner - h] >= temp) {
                    arraySort[inner] = arraySort[inner - h];
                    inner -= h;
                arraySort[inner] = temp;
            }
            h = (h -1) / 3; // 逆置间隔序列
        }
    }
}

输出: 1 2 4 5 6 7 8 9 11 12 33 55 66

希尔排序的优缺点

从稳定性角度上看:由于需要进行多次插入排序,同一数据项可能都会发生位置变动,所以希尔排序是不稳定的。

从空间角度上:不需要大量的辅助空间,跟归并排序相比,归并排序需要两倍的空间完成排序;而且希尔排序实现起来与归并排序一样容易实现。

从时间角度看:希尔排序基于插入排序,引入新特性,减少复制次数,速度上得到很大改进,其时间复杂度范围从O(N*N^1/2)到O(N*N^1/6);相对于插入排序的O(N^2),效率是挺高的。

如何改进希尔排序的缺点

从上面分析可以看到,希尔排序在空间上利用的还是比较好的,关键是在于时间复杂度的问题,它是一个评估范围,也就是说会存在最坏的情况:其时间复杂度接近插入排序O(N^2)的时间复杂度;

因此我们可是从时间复杂度方面优化希尔排序的缺点:

  1. 考虑好合适的使用场景:适用于中小规模的数据量场景
  2. 尽量将时间复杂度降到最低:选择合适的生成间隔序列的公式,如:
If(h < 5) {
     h = 1;
} else {
     h = (5*h - 1) / 11;
}

上面公式利用了数字互质的约束条件:除了1以外,他们没有公约数。

 

三、快速排序算法

什么是快速排序

快速排序基于划分机制(什么是划分机制呢,就是将数据划分为两组,一组是所有数据项都大于关键字的,另一组是所有数据都小于关键字的,这个关键字就称之为枢纽或支点,叫pivot),将一组数据项划分为两组子数组,然后递归的调用快速排序方法为每个子数组进行排序来实现的。

那么怎么选择枢纽呢,可以根据一下几点出发:

  1. 选择需要排序数组中的具体某一项的值作为关键字,枢纽。
  2. 可以选择其中的任意一项,例如选择数组中最右端的数据项作为枢纽。
  3. 划分完之后,如果枢纽被插入到左右子数组之间的交界处,那么枢纽就落在排序之后的最终位置上了。

快速排序的使用场景

快速排序是最常用的、通用的、效率最快的排序算法,适用于大多数场景,其时间复杂度为O(N*logN),因此追求时间效率的,推荐使用快速排序算法。

如何实现快速排序算法

实现思想:

  1. 使用递归思想进行左右划分子数组,编写递归退出的基值条件:如果划分的左坐标大于或等于右坐标,则退出递归。
  2. 选择一个合适的枢纽值:每次划分数组的最右端数据项。
  3. 传递左右坐标值,和枢纽值,进行划分操作。
  4. 划分完,将枢纽交换到右子数组的第一个位置。并返回枢纽的位置
  5. 根据枢纽位置,进行对左子数组进行递归快速排序
  6. 根据枢纽位置,进行对右子数组进行递归快速排序
  7. 重复循环执行1-6步骤,当划分的数组只剩两个数据项的时候,划分完即为有序。
  8. 最后递归返回,整个快速排序也就完成了。
public class TestMain {

    public  static int[] a = {1, 4, 5, 11, 2, 7, 6, 9, 8, 12, 66, 33, 55};

    public static void main(String[] args) {

        quickSort(0,a.length-1,a);

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

            System.out.print(" "+a[i]);

        }

    }

    private static void quickSort (int left,int right, int[] array) {

        // 由于使用递归思想,此处为递归的基值条件
        if (left >= right) {
            return;
        }

        // 选择数组中的一个数据项作为枢纽,本次选择最右端的数据项作为枢纽
        int pivot = array[right];

        // 对数组进行左右子数组进行划分,将枢纽位置与右子数组的第一位置交换,最后返回枢纽位置
        int part = partition(left,right,pivot,array);

        // 对划分好的左子数组进行递归快速排序
        quickSort(left,part -1, array);

        // 对划分好的右子数组进行递归快速排序
        quickSort(part + 1,right,array);

    }

 

    private static int partition(int left, int right, int pivot,int[] array) {

        int leftPtr = left;

        int rightPtr = right;

        while (true) {

            while (array[leftPtr] < pivot) {
                ++leftPtr;
            }

            while (rightPtr > 0 && array[--rightPtr] > pivot){
            }

            if (leftPtr >= rightPtr) {
                break;
            }

            // 交换位置,移到枢纽的正确两边
            swap(leftPtr,rightPtr,array);
        }

        // 最后将枢纽的位置与右边子数组的第一个位置进行值交换
        swap(leftPtr,right,array);

        // 返回枢纽位置
        return leftPtr;
    }

    private static void swap(int index1,int index2,int[] array) {

        int temp = array[index1];

        array[index1] = array[index2];

        array[index2] = temp;

    }

}

输出:1 2 4 5 6 7 8 9 11 12 33 55 66

 

快速排序的优缺点

快速排序除了代码实现比较复杂一点,其他基本无缺点(相对而言),优点一大推,重点是效率高,尽管的拿去用吧,come on!!!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。