【DS】八大排序思路及其优化

一. 排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

【DS】八大排序思路及其优化_第1张图片

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

【DS】八大排序思路及其优化_第2张图片

二. 插入排序

1. 直接插入排序

  • 基本思想:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。

【DS】八大排序思路及其优化_第3张图片
  • 实现过程:

  1. 把第一个元素当为有序的,从第二个元素开始,往前面的序列中插入

  1. 先将arrar[i]中的值拿出来

  1. 找到前面序列中第一个比tmp小的位置,然后插入

  1. 比tmp大的元素向后挪动一个位置

  1. 此时j位置元素比tmp小,j+1就是要插入的位置

【DS】八大排序思路及其优化_第4张图片
  • 性能分析:

  • 元素集合越接近有序,直接插入排序算法的时间效率越高

  • 时间复杂度:无序:O(N^2) 有序:(O(N))

  • 空间复杂度:O(1),它是一种稳定的排序算法

  • 稳定性:稳定(稳定的可以变为不稳定的,不稳定的不能变为稳定的)

  • 代码实现:

public static void insertSort(int[] array){
        for (int i = 1; i < array.length; i++) {
            int j = i - 1;
            int tmp = array[i];
            for (; j >= 0; j--) {
                if(array[j] > tmp){ //条件加上 = 就变成了不稳定的排序了
                    array[j+1] = array[j];
                }else{
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

2. 希尔排序

  • 基本思想:

希尔排序法又称缩小增量法, 希尔排序是直接插入排序的改进,希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

【DS】八大排序思路及其优化_第5张图片
  • 实现过程:

  1. 把待排序文件中所有记录分成gap个组,距离为gap的记录分在同一组内,并对每一组内的记录进行插入排序。

  1. 希尔排序gap先大后小, gap大于1的状态,称为预排序 ; 这个过程是让数组不断接近有序, 直到最终gap等于1 , 此时数组已经接近有序的了, 所有数据在统一在一组内排序就会很快。

  1. gap递减的规则不唯一 , 可以循环除3加1,也可以除2; 下面的代码中用的是除2实现。

  1. 假如有10个元素需要排序,首先将这10个元素为5组,对每组分别进行直接插入排序,再分为2组,并对每组分别进行直接插入排序,最后分为1组,进行直接插入排序;

【DS】八大排序思路及其优化_第6张图片
  • 性能分析:

  • 时间复杂度:O(2^1.3)

  • 空间复杂度:O(1)

  • 稳定性:不稳定的

  • 代码实现:

 public static void shellSort(int[] array){
        int gap = array.length;
        //将数组中的元素分为gap组,每组元素之间间隔gap个元素,
        //将每组中的元素进行插入排序
        while (gap > 1){
            gap /= 2;
            shell(array,gap);
        }
    }

    private static void shell(int[] array,int gap){
        for (int i = gap; i < array.length; i++) { // i+=gap 最后的gap一定为1 都会进行整体插入排序
            int j = i - gap;
            int tmp = array[i];
            for (; j >= 0; j-=gap) {
                if(array[j] > tmp){ //条件加上 = 就变成了不稳定的排序了
                    array[j+gap] = array[j];
                }else{
                    break;
                }
            }
            array[j+gap] = tmp;
        }
    }

希尔排序的特性总结:

1. 希尔排序是对直接插入排序的优化。

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很

快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排

序的时间复杂度都不固定:

《数据结构(C语言版)》--- 严蔚敏

【DS】八大排序思路及其优化_第7张图片

三. 选择排序

1. 直接选择排序

  • 基本思想:

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完

  • 实现过程:

  1. 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素

  1. 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换

  1. 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

【DS】八大排序思路及其优化_第8张图片
  • 性能分析:

  • 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用

  • 时间复杂度:O(N^2)

  • 空间复杂度:O(1)

  • 稳定性:不稳定

  • 代码实现:

public static void selectSort(int[] array){
        for (int i = 0; i < array.length; i++) { //
            int min = i;
            for (int j = i+1; j < array.length; j++) {
                if(array[j] < array[min]){
                    min = j;
                }
            }
            if(i != min){
                sort(array,i,min);
            }
        }
    }
private static void sort(int[] array , int i , int j){
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

2. 堆排序

  • 基本思想:

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。

需要注意的是排升序要建大堆,排降序建小堆。

【DS】八大排序思路及其优化_第9张图片
  • 实现过程:

升序排序步骤:

  1. 将数组转换成大根堆,使用索引end标记最后一个元素。

  1. 将堆顶元素与end处元素交换,end–。

  1. 对堆顶结点进行向下调整,调整的结点下标不大于end。

  1. 重复2,3步骤,直到end<=0。

  1. 大根堆堆顶存放最大元素,每次将最大元素与end出元素交换,再调整end-1部分的堆,这样就能依次把最大的元素放在后面 ; 同样的降序排列使用小根堆即可。

  • 性能分析:

  • 堆排序使用堆来选数,效率就高了很多。

  • 时间复杂度:O(N*logN)

  • 空间复杂度:O(1)

  • 稳定性:不稳定

  • 代码实现:

public static void heapSort(int[] array) {
    //排升序,先将数组改成一个大根堆
    createBigHeat(array);
    int end = array.length-1;
    //end=0时,只剩最后一个元素,位置也就自动确定了
    while(end > 0) {
        //将堆顶元素(也就是在确定最大元素)与数组最后一个位置元素交换
        swap(array, 0, end);
        //向下调整,注意此时不能把end位置处的元素算在调整范围内了
        shiftDown(array, 0, end);
        end--;
    }
}
private static void createBigHeat(int[] array) {
    //从最后一个根节点开始向上调整
    for (int parent = (array.length-1-1)/2; parent >= 0 ; parent--) {
        shiftDown(array, parent, array.length);
    }
}
private static void shiftDown(int[] array, int parent, int len) {
    int child = parent*2+1;
    //判断有左孩子
    while(child < len) {
        //判断是否有右孩子,并比较左右孩子的大小
        if(child+1 < len && array[child] < array[child+1]) {
            child++;
        }
        if(array[child] > array[parent]) {
            swap(array, child, parent);
            parent = child;
            child = parent*2+1;
        }else{
            break;
        }
    }
}
private static void swap(int[] array, int i, int j) {
    int tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
}

四. 交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

1. 冒泡排序

  • 基本思想:

从第二个元素开始,将该元素与前一个元素比较,如果前一个元素比较大,则交换。直到最后一个元素为最大元素,这一过程称为一趟冒泡 ; 每进行一趟冒泡排序,下次排序比较的范围就缩小一个位置,因为每进行一趟冒泡排序就会确定一个元素的最终位置。

  • 实现过程:

如果数组在冒泡前就有序了呢 , 这时候不需要再进行冒泡比较了,所以基于上面的思想可以进行优化,基本思路就是当遇到一趟排序时并没有发生元素交换,这时候就说明数组已经有序了,下一趟就不用排了,所以代码中可以使用一个标记,如果在冒泡过程中发生了交换 , 就改变标记的状态 , 这样就可以在下次冒泡前根据这个标记来确定之后是否需要继续冒泡排序了。

【DS】八大排序思路及其优化_第10张图片
  • 性能分析:

  • 冒泡排序是一种非常容易理解的排序

  • 时间复杂度:O(N^2)

  • 空间复杂度:O(1)

  • 稳定性:稳定

  • 代码实现:

public static void bubbleSort(int[] array) {
    boolean flag = true;
    for (int i = 0; i < array.length-1; i++) {
        for (int j = 0; j < array.length-1-i; j++) {
            if(array[j] > array[j+1]) {
                swap(array, j, j+1);
                flag = false;
            }
        }
        if(flag) {
            break;
        }
    }
}
private static void swap(int[] array, int i, int j) {
    int tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
}

2. 快速排序

基本思想:

从数组中找一个基准值 ,然后以该基准值,将比基准值小的元素排在基准值左边,比基准值大的元素排在基准值右边,此时基准值在数组中的排序位置已经确定了,再对基准值左边和右边的序列以相同的方式重新找基准值并将比基准值小的排左边,大的排右边,直到只剩下一个元素,此时所有的元素排序位置都确定了,排序完成。

快速排序递归实现的主框架与二叉树前序遍历规则非常像,在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。

将区间按照基准值划分为左右两半部分的常见方式有:

  • Hoare版

  • 挖坑法

  • 前后指针

实现快速排序有三步,第一找基准值, 第二确定基准值位置,第三对基准值左右序列进行相同的找基准操作,最终会使所有的元素有序。

2.1 Hoare法

实现过程:

  1. 以范围内第一个元素为基准值。

  1. 先让right从右边界开始向前遍历,遇到比基准值小的元素为止 ; 再让left从左边界开始向后遍历,遇到比基准值大的元素时停止。

  1. 然后交换这两个元素,继续2过程。

  1. 直到left与right相遇为止,将相遇位置(比基准值小的元素)与基准值原位置交换,此时基准值的位置确定,基准左边元素比基准值小,右边元素比基准值大。

  1. 分别对基准值左右序列进行上述相同的操作:取左边界元素为基准值,通过Hoare法确定基准值排序位置; 直到左右序列只有一个元素或没有元素为止。

注意:一定要先让right找比基准值小的元素,再让left找比基准值大的元素,这样保证最后相遇时的元素一定是比基准值小的元素。

【DS】八大排序思路及其优化_第11张图片

代码实现:

 /**
 * Hoare快速排序
 * 时间:N*logN
 * 空间:O(logN)
 * 稳定性:不稳定
 *
 * 当数据是有序的时候,这个快排的时间复杂度是O(n^2)
 * 空间复杂度:O(n)
 */
public static void quickSortHoare(int[] array) {
    quickHoare(array, 0, array.length-1);
}
private static void quickHoare(int[] array, int start, int end) {
    if(start >= end) {//这里必须写上>号,预防没有左树或者右树的情况
        return;    //比如: 1 2 3 4 5 或者 5 4 3 2 1
    }
    int pivot = partitionHoare(array, start, end);
    quickHoare(array, start, pivot-1);
    quickHoare(array, pivot+1, end);
}
private static int partitionHoare(int[] array, int left, int right) {
    int i = left;
    //把范围内的第一个元素当作基准
    int pivot = array[left];
    while(left < right) {
        //先从右往左边找比pivot小的元素
        //注意这里必须是先找右边较小的数字,保证在最后相遇位置的元素比基准小
        while(left < right && array[right] >= pivot) {//这里的等号要理解,如果没有的话可能会出现死循环的情况,比如开头和结尾的元素相同
            right--;//left
2.2 挖坑法

实现过程:

  1. 挖坑法与Hoare 法是很相似的,Hoare 法是交换,挖坑法不过是将交换改为了填坑:

  1. 以范围内第一个元素为基准值。

  1. 先让right从右边界开始向前遍历,遇到比基准值小的元素为止 ; 初始以基准值处为坑 , 将right位置的元素放到基准值处(填坑) , 此时right处就是新的坑位(挖坑) ; 然后left找比基准值大的元素 , 将left位置的元素放到right处(填坑) ; 此时left处就是新的坑位(挖坑) … 直到left与right相遇为止 , 此时再将基准值放到left位置(填最后一个坑)。

  1. 分别对基准值左右序列进行上述相同的操作:取左边界元素为基准值,通过挖坑法确定基准值排序位置。直到左右序列只有一个元素或没有元素为止。

注意:一定要先让right找比基准值小的元素。

代码实现:

/**
 *挖坑法快排
 *和Hoare版的相同,只是将交换 换成了 填坑
 */
public static void quickSortDigging(int[] array) {
    quickDigging(array, 0, array.length-1);
}
private static void quickDigging(int[] array, int start, int end) {
    if(start >= end) {
        return;
    }
    int pivot = partitionDigging(array, start, end);
    quickDigging(array, start, pivot-1);
    quickDigging(array, pivot+1, end);
}
private static int partitionDigging(int[] array, int left, int right) {
    int pivot = array[left];//将第一个元素作为基准保存起来,此时第一个位置就是一个坑位
    while(left < right) {

        while(left < right && array[right] >= pivot) {
            right--;
        }
        //在右边找到比基准小的元素,然后赋值给左边的坑位,此时右边就空出一个坑位
        array[left] = array[right];
        while(left < right && array[left] <= pivot) {
            left++;
        }
        //在左边找到比基准大的元素,然后赋值给右边的坑位,此时左边又空出一个坑位
        array[right] = array[left];
    }
    //最后一次循环是left往右边走和right坑位相遇,将pivot放到最后一个坑位即可
    array[left] = pivot;
    return left;
}
2.3 前后指针法

实现过程:

  1. 以范围内第一个元素为基准值。

  1. prev指向初始位置,cur指向prev的下一个位置。

  1. prev先走 , cur后走 , 每次都是移动一个位置 , cur找比基准小的元素, 找到了prev移动一个位置并将prev和cur位置处的元素交换 。

  1. 直到cur>right,再交换基准值位置和prev的值即可

  1. 分别对基准值左右序列进行上述相同的操作:取左边界元素为基准值,通过前后指针法确定基准值排序位置。直到左右序列只有一个元素或没有元素为止。

看下面的动图看起来就像是prev和cur之间夹着比基准大的元素向后走。

【DS】八大排序思路及其优化_第12张图片

上面的思路其实是可以优化的 , 由于prev先走 , cur后走 , 在cur碰到比基准大的元素前cur和prev总会指向同一个位置, 此时的交换是没有什么意义的, 所以可以加一个判断 , 当cur和prev不在同一个位置时再进行交换。

实现代码:

/**
 *前后指针快速排序
 *时间和空间: 和Hoare版的相同,只是找基准的方式不同
 */
public static void quickSortPointer(int[] array) {
    quickHoare(array, 0, array.length-1);
}
private static void quickPointer(int[] array, int start, int end) {
    if(start >= end) {
        return;
    }
    int pivot = partitionPointer1(array, start, end);
    quickPointer(array, start, pivot-1);
    quickPointer(array, pivot+1, end);
}
//写法一:
private static int partitionPointer1(int[] array, int left, int right) {
    int d = left + 1;
    int pivot = array[left];
    for (int i = left + 1; i <= right; i++) {
        //一开始i和d是在同步移动的,当i位置的元素大于基准时,此时d就不在移动,
        //当d下一次移动时,d指向的就是比基准大的元素,然后交换
        if (array[i] < pivot) {
            if(i != d) {
                swap(array, i, d);
            }
            d++;
        }
    }
    swap(array, d - 1, left);
    return d - 1;
}
//写法二:
private static int partitionPointer2(int[] array, int left, int right) {
    int prev = left;
    int cur = left+1;
    while(cur < right) {
        //当cur指向的元素小于基准且前一个元素比基准大,此时++prev使prev指向的是比基准大的元素,然后交换
        //cur和prev之间会夹着比基准大的元素不断向后
        if(array[cur] < array[left] && array[++prev] != array[cur]) {
            swap(array,cur,prev);
        }
        cur++;
    }
    //循环结束之后指向的是比基准小的元素,将其和array[left](基准)交换,
    swap(array, left,prev);
    return prev;
}
private static void swap(int[] array, int i, int j) {
    int tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
}
2.4 性能分析及快速排序优化

假设数组中有N个待排序的元素,上述三种方法实现快速排序过程其实类似于二叉树的前序遍历,递归每层需要遍历数组元素的总个数为N ,递归的高度为logN,所以快速排序时间复杂度为O(N*logN) ,空间复杂度为O(logN),但此时是比较好的情况下的复杂度 , 递归结构类似于一棵完全二叉树。

【DS】八大排序思路及其优化_第13张图片

上面的情况是我们直接选取第一个元素为基准值,但这个基准值可能是最大或最小的元素,如果基准值是数组中最大或最小的元素,此时如果还按上面的方式实现 , 递归下来就是一棵单分支型树,极端情况下,数组是有序或逆序情况下,每次找到的基准值都是最大或最小值,这时候时间复杂度为O(N^2),空间复杂度为O(N),为了解决这里的问题 , 我们就需要对上面的思路进行优化了。

上面三种方法的基本思路是不变的 , 主要需要优化的找基准值的方法 , 让基准值尽量向范围内的中位数靠拢 , 这里主要介绍下面的优化方法:

  • 三数取中法

找到范围中最左边位置,最右边位置 , 中间位置中的三个元素 , 选择选择其中一个居中的数字作为基准值。

我们可以先判断左右边界值的大小 , 然后再与中间位置的元素比较 , 以此来确定出这三个元素的中间值为基准值 。

进行了这个优化就不会有单分支情况的出现了 , 时间复杂度为O(N*logN)。

  • 递归到小的子区间时,可以考虑使用插入排序(减少递归次数)

快速排序的时间主要是在递归上 , 看上面分析复杂度的图 , 其实其实递归主要主要集中在最后几层 , 而最后几层 , 每次递归的范围内 , 所要排序的元素其实是很少的 , 而且由于元素少 , 所以这些元素是接近有序的 , 通过对上文中插入排序的分析 , 序列中元素少且接近有序的情况下使用插入排序是非常快的 , 我们可以自己去设置这个较小范围 , 当递归元素等于和小于这个范围时就采用插入排序来完成这个序列中的排序。

实现代码:

在挖坑法基础上实现

/**
 *        快速排序优化(三数取中法和在指定区间进行插入排序)
 * 数据有序的时候,使用三数取中方法,树呈完全二叉的状态,降低了时间和空间复杂度,
 * 不加插入优化: 时间,空间: 和Hoare版不考虑有序相同
 * 递归的次数主要集中在树的后面几层,后面几层的每个查找区间都很小,数据接近于有序
 * 在这些小区间中使用插排效率会很高
 */
public static void quickSortOptimize(int[] array) {
    quickHoare(array, 0, array.length-1);
}
private static void quickOptimize(int[] array, int start, int end) {
    if(start >= end) {
        return;
    }
    //指定较小区间范围使用插排
    if(end - start+1 <= 10) {
        insertSort(array, start, end);
        return;
    }
    //在找基准前尽量解决划分不均匀的问题
    //找出第一个,最后一个,中间元素的的中间值,将中间值作为基准
    int midIndex = findMidValOfIndex(array, start, end);
    swap(array, start, midIndex);
    int pivot = partitionOptimize(array, start, end);
    quickOptimize(array, start, pivot-1);
    quickOptimize(array, pivot+1, end);
}
private static void insertSort(int[] array, int left, int right) {
    for (int i = left+1; i < right; i++) {
        int tmp = array[i];
        int j = 0;
        for (j = i-1; j >= left; j--) {
            if (array[j] > tmp) {
                array[j+1] = array[j];
            }else {
                break;
            }
        }
        array[j+1] = tmp;
    }
}
private static int findMidValOfIndex(int[] array, int start, int end) {
    int midindex = (end+start) / 2;
    if(array[start] < array[end]) {
        if(array[midindex] < array[start]) {
            return start;
        }else if(array[midindex] > array[end]) {
            return end;
        }else{
            return midindex;
        }
    }else {
        if(array[midindex] > array[start]) {
            return start;
        }else if(array[midindex] < array[end]) {
            return end;
        }else{
            return midindex;
        }
    }
}
private static int partitionOptimize(int[] array, int left, int right) {
    int pivot = array[left];//将第一个元素作为基准保存起来,此时第一个位置就是一个坑位
    while(left < right) {

        while(left < right && array[right] >= pivot) {
            right--;
        }
        //在右边找到比基准小的元素,然后赋值给左边的坑位,此时右边就空出一个坑位
        array[left] = array[right];
        while(left < right && array[left] <= pivot) {
            left++;
        }
        //在左边找到比基准大的元素,然后赋值给右边的坑位,此时左边又空出一个坑位
        array[right] = array[left];
    }
    //最后一次循环是left往右边走和right坑位相遇,将pivot放到最后一个坑位即可
    array[left] = pivot;
    return left;
}

没优化的版本:

三数取中法优化优化的版本:

五. 归并排序

基本思想:

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并 。

【DS】八大排序思路及其优化_第14张图片

1. 递归实现的归并排序

递归实现归并排序时,递过程分解数组,归过程合并数组 , 用解决子问题的思考方式 , 要整个数组有序, 那么就将将这个数组均分成左右两个部分 , 先让左边有序 , 再让右边有序 , 最终再合并两个有序序列 , 而此时的左右序列同样如此解决。

每次递归将范围内的数据均分成左右两个部分,最后范围内只有一个数据 , 递归结束。

递归结束返回后就是归并的过程 , 其实就是在合并两个有序数组 , 最终将所有范围的序列合并完成后 , 排序完成。

【DS】八大排序思路及其优化_第15张图片
/**
 * 归并排序
 * 时间:O(n*logN)
 * 空间:O(n)
 * 稳定性:稳定
 */
public static void mergeSort(int[] array) {
    mergeSortChild(array, 0, array.length-1);
}
private static void mergeSortChild(int[] array, int left, int right) {
    if(left == right) {
        return;
    }
    int mid = (left+right) / 2;
    mergeSortChild(array, left, mid);
    mergeSortChild(array, mid+1, right);

    merge(array, left, mid, right);
}
public static void merge(int[] array, int left, int mid, int right) {
    int s1 = left;
    int e1 = mid;
    int s2 = mid+1;
    int e2 = right;
    int[] tmp = new int[right-left+1];
    int k = 0;
    //相当于是在合并两个有序数组
    while(s1<=e1 && s2<=e2) {
        if(array[s1] < array[s2]) {
            tmp[k++] = array[s1++];
        }else {
            tmp[k++] = array[s2++];
        }
    }
    while(s1 <= e1) {
        tmp[k++] = array[s1++];
    }
    while(s2 <= e2) {
        tmp[k++] = array[s2++];
    }
    for (int i = 0; i < tmp.length; i++) {
        array[i+left] = tmp[i];
    }
}

2. 非递归实现归并排序

非递归实现归并排序只需要完成归并操作即可 , 一一归并···两两归并···四四归并···直至整体有序 , 排序完成。

对数组进行分组,每组元素为gap,初始时为1,按照2组为一个单位进行有序合并,合并后每组元素为gap=2*gap,直到每组元素个数大于或等于排序数组元素个数len为止

合并过程中需要保证调整后的mid与right不能越界,如果越界需要调整为待排序序列的最后一个元素下标

/**
 * 非递归实现归并排序
 */
public static void mergeSortNonRecursive(int[] array) {
    int gap = 1;
    while(gap < array.length) {
        for (int i = 0; i < array.length; i += 2*gap) {
            int left = i;
            int mid = left+gap-1;
            int right = mid+gap;
            if(mid > array.length-1) {
                mid = array.length-1;
            }
            if(right >= array.length-1) {
                right = array.length-1;
            }
            merge(array, left, mid, right);
        }
        gap *= 2;
    }
}
public static void merge(int[] array, int left, int mid, int right) {
    int s1 = left;
    int e1 = mid;
    int s2 = mid+1;
    int e2 = right;
    int[] tmp = new int[right-left+1];
    int k = 0;
    //相当于是在合并两个有序数组
    while(s1<=e1 && s2<=e2) {
        if(array[s1] < array[s2]) {
            tmp[k++] = array[s1++];
        }else {
            tmp[k++] = array[s2++];
        }
    }
    while(s1 <= e1) {
        tmp[k++] = array[s1++];
    }
    while(s2 <= e2) {
        tmp[k++] = array[s2++];
    }
    for (int i = 0; i < tmp.length; i++) {
        array[i+left] = tmp[i];
    }
}

3. 性能分析

  • 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

  • 时间复杂度:O(N*logN)

  • 空间复杂度:O(N)

  • 稳定性:稳定

4. 海量数据的排序问题

外部排序:排序过程需要在磁盘等外部存储进行的排序

前提:内存只有 1G,需要排序的数据有 100G

因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序

  1. 先把文件切分成 200 份,每个 512 M

  1. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以

  1. 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了

六. 常见排序算法性能分析总结

【DS】八大排序思路及其优化_第16张图片
【DS】八大排序思路及其优化_第17张图片

七. 非基于比较排序

1.计数排序

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。

  1. 找出序列中最大值和最小值,开辟maxVal-minVal+1的辅助空间

  1. 最小的数对应下标为0的位置,遇到一个数就给对应下标处的值+1,。

  1. 遍历一遍辅助空间,就可以得到有序的一组序列。

【DS】八大排序思路及其优化_第18张图片

代码实现:

/**
 * 计数排序
 * 时间:O(范围+n)
 * 空间:O(范围)
 * 稳定性:不稳定
 * 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
 */
public static void countSort(int[] array){
        //遍历数组 找最大最小值  确定  区间  计数数组大小
        int maxVal = array[0];
        int minVal = array[0];
        for (int i = 0; i < array.length; i++) {
            if(array[i] > maxVal){
                maxVal = array[i];
            }
            if(array[i] < minVal){
                minVal = array[i];
            }
        }
        //确定长度
        int len = maxVal - minVal + 1;
        int[] countArr = new int[len];
        //开始遍历当前数组  统计每个数字出现次数
        for (int i = 0; i < array.length; i++) {
            int val = array[i];
            countArr[val - minVal]++;
        }
        int index = 0;
        //遍历计数数组  每个下标值时几 就打印几个
        for (int i = 0; i < countArr.length; i++) {
            while (countArr[i] > 0){
                array[index] = i + minVal;
                index++;
                countArr[i]--;
            }
        }
    }

2.基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  1. 基数排序:根据键值的每位数字来分配桶;

  1. 计数排序:每个桶只存储单一键值;

  1. 桶排序:每个桶存储一定范围的数值;

3.桶排序

一句话总结:划分多个范围相同的区间,每个子区间自排序,最后合并。

桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。

桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效。

【DS】八大排序思路及其优化_第19张图片

八.小试牛刀(选择题)

1.习题

1. 快速排序算法是基于()的一个排序算法。
A:分治法 B:贪心法 C:递归法 D:动态规划法

2.对记录(54,38,96,23,15,72,60,45,83)进行从小到大的直接插入排序时,当把第8个记录45插入到有序表时,为找到插入位置需比较()次?(采用从后往前比较)
A: 3 B: 4 C: 5 D: 6

3.以下排序方式中占用O(n)辅助存储空间的是()
A: 简单排序 B: 快速排序 C: 堆排序 D: 归并排序

4.下列排序算法中稳定且时间复杂度为O(n2)的是()
A: 快速排序 B: 冒泡排序 C: 直接选择排序 D: 归并排序

5.关于排序,下面说法不正确的是()
A: 快排时间复杂度为O(N*logN),空间复杂度为O(logN)
B: 归并排序是一种稳定的排序,堆排序和快排均不稳定
C: 序列基本有序时,快排退化成冒泡排序,直接插入排序最快
D: 归并排序空间复杂度为O(N), 堆排序空间复杂度的为O(logN)

6.下列排序法中,最坏情况下时间复杂度最小的是( )
A: 堆排序 B: 快速排序 C: 希尔排序 D: 冒泡排序

7.设一组初始记录关键字序列为(65,56,72,99,86,25,34,66),则以第一个关键字65为基准而得到的一趟快速排序结果是()
A: 34,56,25,65,86,99,72,66 B: 25,34,56,65,99,86,72,66
C: 34,56,25,65,66,99,86,72 D: 34,56,25,65,99,86,72,66

2.参考答案

1.A 2.C 3.D 4.B 5.D 6.A 7.A

你可能感兴趣的:(Java,-,数据结构,数据结构,链表,java,算法)