面试时写不出排序算法?看这篇就够了。

本文主要详细讲述常见的八种排序算法的思想、实现以及复杂度。java

:notebook: 本文已归档到:「bloggit

:keyboard: 本文中的示例代码已归档到:「algorithm-tutorialgithub

冒泡排序

要点

冒泡排序是一种交换排序。算法

什么是交换排序呢?shell

交换排序:两两比较待排序的关键字,并交换不知足次序要求的那对数,直到整个表都知足次序要求为止。编程

算法思想

它重复地走访过要排序的数列,一次比较两个元素,若是他们的顺序错误就把他们交换过来。走访数列的工做是重复地进行直到没有再须要交换,也就是说该数列已经排序完成。数组

这个算法的名字由来是由于越小的元素会经由交换慢慢“浮”到数列的顶端,故名。编程语言

假设有一个大小为 N 的无序序列。冒泡排序就是要每趟排序过程当中经过两两比较,找到第 i 个小(大)的元素,将其往上排。ide

以上图为例,演示一下冒泡排序的实际流程:post

假设有一个无序序列 { 4. 3. 1. 2, 5 }

  • 第一趟排序:经过两两比较,找到第一小的数值 1 ,将其放在序列的第一位。
  • 第二趟排序:经过两两比较,找到第二小的数值 2 ,将其放在序列的第二位。
  • 第三趟排序:经过两两比较,找到第三小的数值 3 ,将其放在序列的第三位。

至此,全部元素已经有序,排序结束。

要将以上流程转化为代码,咱们须要像机器同样去思考,否则编译器可看不懂。

  • 假设要对一个大小为 N 的无序序列进行升序排序(即从小到大)。
    • 每趟排序过程当中须要经过比较找到第 i 个小的元素。
    • 因此,咱们须要一个外部循环,从数组首端(下标 0) 开始,一直扫描到倒数第二个元素(即下标 N - 2) ,剩下最后一个元素,必然为最大。
  • 假设是第 i 趟排序,可知,前 i-1 个元素已经有序。如今要找第 i 个元素,只需从数组末端开始,扫描到第 i 个元素,将它们两两比较便可。
    • 因此,须要一个内部循环,从数组末端开始(下标 N - 1),扫描到 (下标 i + 1)。

核心代码

public void bubbleSort(int[] list) {
    int temp = 0; // 用来交换的临时数

    // 要遍历的次数
    for (int i = 0; i < list.length - 1; i++) {
        // 从后向前依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
        for (int j = list.length - 1; j > i; j--) {
            // 比较相邻的元素,若是前面的数大于后面的数,则交换
            if (list[j - 1] > list[j]) {
                temp = list[j - 1];
                list[j - 1] = list[j];
                list[j] = temp;
            }
        }

        System.out.format("第 %d 趟:\t", i);
        printAll(list);
    }
}
复制代码

算法分析

冒泡排序算法的性能

参数 结果
排序类别 交换排序
排序方法 冒泡排序
时间复杂度平均状况 O(N2)
时间复杂度最坏状况 O(N3)
时间复杂度最好状况 O(N)
空间复杂度 O(1)
稳定性 稳定
复杂性 简单

时间复杂度

若文件的初始状态是正序的,一趟扫描便可完成排序。所需的关键字比较次数 C 和记录移动次数 M 均达到最小值:Cmin = N - 1, Mmin = 0。因此,冒泡排序最好时间复杂度为 O(N)。

若初始文件是反序的,须要进行 N -1 趟排序。每趟排序要进行 N - i 次关键字的比较(1 ≤ i ≤ N - 1),且每次比较都必须移动记录三次来达到交换记录位置。在这种状况下,比较和移动次数均达到最大值:

Cmax = N(N-1)/2 = O(N2)

Mmax = 3N(N-1)/2 = O(N2)

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

所以,冒泡排序的平均时间复杂度为 O(N2)。

总结起来,其实就是一句话:当数据越接近正序时,冒泡排序性能越好。

算法稳定性

冒泡排序就是把小的元素往前调或者把大的元素日后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。

因此相同元素的先后顺序并无改变,因此冒泡排序是一种稳定排序算法。

优化

对冒泡排序常见的改进方法是加入标志性变量 exchange,用于标志某一趟排序过程当中是否有数据交换。

若是进行某一趟排序时并无进行数据交换,则说明全部数据已经有序,可当即结束排序,避免没必要要的比较过程。

核心代码

// 对 bubbleSort 的优化算法
public void bubbleSort_2(int[] list) {
    int temp = 0; // 用来交换的临时数
    boolean bChange = false; // 交换标志

    // 要遍历的次数
    for (int i = 0; i < list.length - 1; i++) {
        bChange = false;
        // 从后向前依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
        for (int j = list.length - 1; j > i; j--) {
            // 比较相邻的元素,若是前面的数大于后面的数,则交换
            if (list[j - 1] > list[j]) {
                temp = list[j - 1];
                list[j - 1] = list[j];
                list[j] = temp;
                bChange = true;
            }
        }

        // 若是标志为false,说明本轮遍历没有交换,已是有序数列,能够结束排序
        if (false == bChange)
            break;

        System.out.format("第 %d 趟:\t", i);
        printAll(list);
    }
}
复制代码

示例代码

个人 Github 测试例

样本包含:数组个数为奇数、偶数的状况;元素重复或不重复的状况。且样本均为随机样本,实测有效。

快速排序

要点

快速排序是一种交换排序。

快速排序由 C. A. R. Hoare 在 1962 年提出。

算法思想

它的基本思想是:

经过一趟排序将要排序的数据分割成独立的两部分:分割点左边都是比它小的数,右边都是比它大的数。

而后再按此方法对这两部分数据分别进行快速排序,整个排序过程能够递归进行,以此达到整个数据变成有序序列。

详细的图解每每比大堆的文字更有说明力,因此直接上图:

上图中,演示了快速排序的处理过程:

  1. 初始状态为一组无序的数组:二、四、五、一、3。
  2. 通过以上操做步骤后,完成了第一次的排序,获得新的数组:一、二、五、四、3。
  3. 新的数组中,以 2 为分割点,左边都是比 2 小的数,右边都是比 2 大的数。
  4. 由于 2 已经在数组中找到了合适的位置,因此不用再动。
  5. 2 左边的数组只有一个元素 1,因此显然不用再排序,位置也被肯定。(注:这种状况时,left 指针和 right 指针显然是重合的。所以在代码中,咱们能够经过设置断定条件 left 必须小于 right,若是不知足,则不用排序了)。
  6. 而对于 2 右边的数组 五、四、3,设置 left 指向 5,right 指向 3,开始继续重复图中的1、2、3、四步骤,对新的数组进行排序。

核心代码

public int division(int[] list, int left, int right) {
    // 以最左边的数(left)为基准
    int base = list[left];
    while (left < right) {
        // 从序列右端开始,向左遍历,直到找到小于base的数
        while (left < right && list[right] >= base)
            right--;
        // 找到了比base小的元素,将这个元素放到最左边的位置
        list[left] = list[right];

        // 从序列左端开始,向右遍历,直到找到大于base的数
        while (left < right && list[left] <= base)
            left++;
        // 找到了比base大的元素,将这个元素放到最右边的位置
        list[right] = list[left];
    }

    // 最后将base放到left位置。此时,left位置的左侧数值应该都比left小;
    // 而left位置的右侧数值应该都比left大。
    list[left] = base;
    return left;
}

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

    // 左下标必定小于右下标,不然就越界了
    if (left < right) {
        // 对数组进行分割,取出下次分割的基准标号
        int base = division(list, left, right);

        System.out.format("base = %d:\t", list[base]);
        printPart(list, left, right);

        // 对“基准标号“左侧的一组数值进行递归的切割,以致于将这些数值完整的排序
        quickSort(list, left, base - 1);

        // 对“基准标号“右侧的一组数值进行递归的切割,以致于将这些数值完整的排序
        quickSort(list, base + 1, right);
    }
}
复制代码

算法分析

快速排序算法的性能

参数 结果
排序类别 交换排序
排序方法 快速排序
时间复杂度平均状况 O(Nlog2N)
时间复杂度最坏状况 O(N2)
时间复杂度最好状况 O(Nlog2N)
空间复杂度 O(Nlog2N)
稳定性 不稳定
复杂性 较复杂

时间复杂度

当数据有序时,以第一个关键字为基准分为两个子序列,前一个子序列为空,此时执行效率最差。

而当数据随机分布时,以第一个关键字为基准分为两个子序列,两个子序列的元素个数接近相等,此时执行效率最好。

因此,数据越随机分布时,快速排序性能越好;数据越接近有序,快速排序性能越差。

空间复杂度

快速排序在每次分割的过程当中,须要 1 个空间存储基准值。而快速排序的大概须要 Nlog2N 次的分割处理,因此占用空间也是 Nlog2N 个。

算法稳定性

在快速排序中,相等元素可能会由于分区而交换顺序,因此它是不稳定的算法。

示例代码

个人 Github 测试例

样本包含:数组个数为奇数、偶数的状况;元素重复或不重复的状况。且样本均为随机样本,实测有效。

插入排序

要点

直接插入排序是一种最简单的插入排序

插入排序:每一趟将一个待排序的记录,按照其关键字的大小插入到有序队列的合适位置里,知道所有插入完成。

算法思想

在讲解直接插入排序以前,先让咱们脑补一下咱们打牌的过程。

  • 先拿一张 5 在手里,
  • 再摸到一张 4,比 5 小,插到 5 前面,
  • 摸到一张 6,嗯,比 5 大,插到 5 后面,
  • 摸到一张 8,比 6 大,插到 6 后面,
  • 。。。
  • 最后一看,我靠,凑到的竟然是同花顺,这下牛逼大了。

以上的过程,其实就是典型的直接插入排序,每次将一个新数据插入到有序队列中的合适位置里

很简单吧,接下来,咱们要将这个算法转化为编程语言。

假设有一组无序序列 R0, R1, ... , RN-1。

  • 咱们先将这个序列中下标为 0 的元素视为元素个数为 1 的有序序列。
  • 而后,咱们要依次把 R1, R2, ... , RN-1 插入到这个有序序列中。因此,咱们须要一个外部循环,从下标 1 扫描到 N-1 。
  • 接下来描述插入过程。假设这是要将 Ri 插入到前面有序的序列中。由前面所述,咱们可知,插入 Ri 时,前 i-1 个数确定已是有序了。

因此咱们须要将 Ri 和 R0 ~ Ri-1 进行比较,肯定要插入的合适位置。这就须要一个内部循环,咱们通常是从后往前比较,即从下标 i-1 开始向 0 进行扫描。

核心代码

public void insertSort(int[] list) {
   // 打印第一个元素
   System.out.format("i = %d:\t", 0);
   printPart(list, 0, 0);

   // 第1个数确定是有序的,从第2个数开始遍历,依次插入有序序列
   for (int i = 1; i < list.length; i++) {
       int j = 0;
       int temp = list[i]; // 取出第i个数,和前i-1个数比较后,插入合适位置

       // 由于前i-1个数都是从小到大的有序序列,因此只要当前比较的数(list[j])比temp大,就把这个数后移一位
       for (j = i - 1; j >= 0 && temp < list[j]; j--) {
           list[j + 1] = list[j];
       }
       list[j + 1] = temp;

       System.out.format("i = %d:\t", i);
       printPart(list, 0, i);
   }
}
复制代码

算法分析

直接插入排序的算法性能

参数 结果
排序类别 插入排序
排序方法 直接插入排序
时间复杂度平均状况 O(N2)
时间复杂度最坏状况 O(N2)
时间复杂度最好状况 O(N)
空间复杂度 O(1)
稳定性 稳定
复杂性 简单

时间复杂度

当数据正序时,执行效率最好,每次插入都不用移动前面的元素,时间复杂度为 O(N)

当数据反序时,执行效率最差,每次插入都要前面的元素后移,时间复杂度为 O(N2)

因此,数据越接近正序,直接插入排序的算法性能越好

空间复杂度

由直接插入排序算法可知,咱们在排序过程当中,须要一个临时变量存储要插入的值,因此空间复杂度为 1

算法稳定性

直接插入排序的过程当中,不须要改变相等数值元素的位置,因此它是稳定的算法。

示例代码

个人 Github 测试例

样本包含:数组个数为奇数、偶数的状况;元素重复或不重复的状况。且样本均为随机样本,实测有效。

希尔排序

要点

希尔(Shell)排序又称为缩小增量排序,它是一种插入排序。它是直接插入排序算法的一种威力增强版

该方法因 DL.Shell 于 1959 年提出而得名。

算法思想

希尔排序的基本思想是:

把记录按步长 gap 分组,对每组记录采用直接插入排序方法进行排序。 随着步长逐渐减少,所分红的组包含的记录愈来愈多,当步长的值减少到 1 时,整个数据合成为一组,构成一组有序记录,则完成排序。

咱们来经过演示图,更深刻的理解一下这个过程。

在上面这幅图中:

初始时,有一个大小为 10 的无序序列。

  • 第一趟排序中,咱们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,能够分为 5 组。
    • 接下来,按照直接插入排序的方法对每一个组进行排序。
  • 在** 第二趟排序中**,咱们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,能够分为 2 组。
    • 按照直接插入排序的方法对每一个组进行排序。
  • 第三趟排序中,再次把 gap 缩小一半,即 gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组。
    • 按照直接插入排序的方法对每一个组进行排序。此时,排序已经结束

须要注意一下的是,图中有两个相等数值的元素 55 。咱们能够清楚的看到,在排序过程当中,两个元素位置交换了

因此,希尔排序是不稳定的算法。

核心代码

public void shellSort(int[] list) {
   int gap = list.length / 2;

   while (1 <= gap) {
       // 把距离为 gap 的元素编为一个组,扫描全部组
       for (int i = gap; i < list.length; i++) {
           int j = 0;
           int temp = list[i];

           // 对距离为 gap 的元素组进行排序
           for (j = i - gap; j >= 0 && temp < list[j]; j = j - gap) {
               list[j + gap] = list[j];
           }
           list[j + gap] = temp;
       }

       System.out.format("gap = %d:\t", gap);
       printAll(list);
       gap = gap / 2; // 减少增量
   }
}
复制代码

算法分析

希尔排序的算法性能

参数 结果
排序类别 插入排序
排序方法 希尔排序
时间复杂度平均状况 O(Nlog2N)
时间复杂度最坏状况 O(N1.5)
时间复杂度最好状况
空间复杂度 O(1)
稳定性 不稳定
复杂性 较复杂

时间复杂度

步长的选择是希尔排序的重要部分。只要最终步长为 1 任何步长序列均可以工做。

算法最开始以必定的步长进行排序。而后会继续以必定步长进行排序,最终算法以步长为 1 进行排序。当步长为 1 时,算法变为插入排序,这就保证了数据必定会被排序。

Donald Shell 最初建议步长选择为 N/2 而且对步长取半直到步长达到 1。虽然这样取能够比 O(N2)类的算法(插入排序)更好,但这样仍然有减小平均时间和最差时间的余地。可能希尔排序最重要的地方在于当用较小步长排序后,之前用的较大步长仍然是有序的。好比,若是一个数列以步长 5 进行了排序而后再以步长 3 进行排序,那么该数列不只是以步长 3 有序,并且是以步长 5 有序。若是不是这样,那么算法在迭代过程当中会打乱之前的顺序,那就不会以如此短的时间完成排序了。

已知的最好步长序列是由 Sedgewick 提出的(1, 5, 19, 41, 109,...),该序列的项来自这两个算式。

这项研究也代表“比较在希尔排序中是最主要的操做,而不是交换。”用这样步长序列的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,可是在涉及大量数据时希尔排序仍是比快速排序慢。

算法稳定性

由上文的希尔排序算法演示图便可知,希尔排序中相等数据可能会交换位置,因此希尔排序是不稳定的算法。

直接插入排序和希尔排序的比较

  • 直接插入排序是稳定的;而希尔排序是不稳定的。
  • 直接插入排序更适合于原始记录基本有序的集合。
  • 希尔排序的比较次数和移动次数都要比直接插入排序少,当 N 越大时,效果越明显。
  • 在希尔排序中,增量序列 gap 的取法必须知足:**最后一个步长必须是 1 。 **
  • 直接插入排序也适用于链式存储结构;希尔排序不适用于链式结构

示例代码

个人 Github 测试例

样本包含:数组个数为奇数、偶数的状况;元素重复或不重复的状况。且样本均为随机样本,实测有效。

简单选择排序

要点

简单选择排序是一种选择排序

选择排序:每趟从待排序的记录中选出关键字最小的记录,顺序放在已排序的记录序列末尾,直到所有排序结束为止。

算法思想

  1. 从待排序序列中,找到关键字最小的元素;
  2. 若是最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
  3. 从余下的 N - 1 个元素中,找出关键字最小的元素,重复 一、2 步,直到排序结束。

如图所示,每趟排序中,将当前**第 i 小的元素放在位置 i **上。

核心代码

算法分析

简单选择排序算法的性能

参数 结果
排序类别 选择排序
排序方法 简单选择排序
时间复杂度平均状况 O(N2)
时间复杂度最坏状况 O(N2)
时间复杂度最好状况 O(N2)
空间复杂度 O(1)
稳定性 不稳定
复杂性 简单

时间复杂度

简单选择排序的比较次数与序列的初始排序无关。 假设待排序的序列有 N 个元素,则**比较次数老是 N (N - 1) / 2 **。

而移动次数与序列的初始排序有关。当序列正序时,移动次数最少,为 0.

当序列反序时,移动次数最多,为 3N (N - 1) / 2

因此,综合以上,简单排序的时间复杂度为 O(N2)

空间复杂度

简单选择排序须要占用一个临时空间,在交换数值时使用。

示例代码

个人 Github 测试例

样本包含:数组个数为奇数、偶数的状况;元素重复或不重复的状况。且样本均为随机样本,实测有效。

堆排序

要点

在介绍堆排序以前,首先须要说明一下,堆是个什么玩意儿。

是一棵顺序存储彻底二叉树

其中每一个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆。 其中每一个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆。 举例来讲,对于 n 个元素的序列 {R0, R1, ... , Rn} 当且仅当知足下列关系之一时,称之为堆:

  • Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)
  • Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)

其中 i=1,2,…,n/2 向下取整;

如上图所示,序列 R{3, 8,15, 31, 25} 是一个典型的小根堆。

堆中有两个父结点,元素 3 和元素 8。

元素 3 在数组中以 R[0] 表示,它的左孩子结点是 R[1],右孩子结点是 R[2]。

元素 8 在数组中以 R[1] 表示,它的左孩子结点是 R[3],右孩子结点是 R[4],它的父结点是 R[0]。能够看出,它们知足如下规律

设当前元素在数组中以 R[i] 表示,那么,

  • 它的左孩子结点是:R[2*i+1];
  • 它的右孩子结点是:R[2*i+2];
  • 它的父结点是:R[(i-1)/2];
  • R[i] <= R[2*i+1] 且 R[i] <= R[2i+2]。

算法思想

  • 首先,按堆的定义将数组 R[0..n]调整为堆(这个过程称为建立初始堆),交换 R[0]和 R[n];
  • 而后,将 R[0..n-1]调整为堆,交换 R[0]和 R[n-1];
  • 如此反复,直到交换了 R[0]和 R[1]为止。

以上思想可概括为两个操做:

  1. 根据初始数组去构造初始堆(构建一个彻底二叉树,保证全部的父结点都比它的孩子结点数值大)。
  2. 每次交换第一个和最后一个元素,输出最后一个元素(最大值),而后把剩下元素从新调整为大根堆。

当输出完最后一个元素后,这个数组已是按照从小到大的顺序排列了。

先经过详细的实例图来看一下,如何构建初始堆。

设有一个无序序列 { 1, 3,4, 5, 2, 6, 9, 7, 8, 0 }。

构造了初始堆后,咱们来看一下完整的堆排序处理:

仍是针对前面提到的无序序列 { 1,3, 4, 5, 2, 6, 9, 7, 8, 0 } 来加以说明。

相信,经过以上两幅图,应该能很直观的演示堆排序的操做处理。

核心代码

public void HeapAdjust(int[] array, int parent, int length) {
    int temp = array[parent]; // temp保存当前父节点
    int child = 2 * parent + 1; // 先得到左孩子

    while (child < length) {
        // 若是有右孩子结点,而且右孩子结点的值大于左孩子结点,则选取右孩子结点
        if (child + 1 < length && array[child] < array[child + 1]) {
            child++;
        }

        // 若是父结点的值已经大于孩子结点的值,则直接结束
        if (temp >= array[child])
            break;

        // 把孩子结点的值赋给父结点
        array[parent] = array[child];

        // 选取孩子结点的左孩子结点,继续向下筛选
        parent = child;
        child = 2 * child + 1;
    }

    array[parent] = temp;
}

public void heapSort(int[] list) {
    // 循环创建初始堆
    for (int i = list.length / 2; i >= 0; i--) {
        HeapAdjust(list, i, list.length);
    }

    // 进行n-1次循环,完成排序
    for (int i = list.length - 1; i > 0; i--) {
        // 最后一个元素和第一元素进行交换
        int temp = list[i];
        list[i] = list[0];
        list[0] = temp;

        // 筛选 R[0] 结点,获得i-1个结点的堆
        HeapAdjust(list, 0, i);
        System.out.format("第 %d 趟: \t", list.length - i);
        printPart(list, 0, list.length - 1);
    }
}
复制代码

算法分析

堆排序算法的整体状况

参数 结果
排序类别 选择排序
排序方法 堆排序
时间复杂度平均状况 O(nlog2n)
时间复杂度最坏状况 O(nlog2n)
时间复杂度最好状况 O(nlog2n)
空间复杂度 O(1)
稳定性 不稳定
复杂性 较复杂

时间复杂度

堆的存储表示是顺序的。由于堆所对应的二叉树为彻底二叉树,而彻底二叉树一般采用顺序存储方式。

当想获得一个序列中第 k 个最小的元素以前的部分排序序列,最好采用堆排序。

由于堆排序的时间复杂度是 O(n+klog2n),若 k ≤ n/log2n,则可获得的时间复杂度为 O(n)

算法稳定性

堆排序是一种不稳定的排序方法。

由于在堆的调整过程当中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,

所以对于相同的关键字就可能出现排在后面的关键字被交换到前面来的状况。

示例代码

个人 Github 测试例

样本包含:数组个数为奇数、偶数的状况;元素重复或不重复的状况。且样本均为随机样本,实测有效。

归并排序

要点

归并排序是创建在归并操做上的一种有效的排序算法,该算法是采用**分治法(Divide and Conquer)**的一个很是典型的应用。

将已有序的子序列合并,获得彻底有序的序列;即先使每一个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

算法思想

将待排序序列 R[0...n-1] 当作是 n 个长度为 1 的有序序列,将相邻的有序表成对归并,获得 n/2 个长度为 2 的有序表;将这些有序序列再次归并,获得 n/4 个长度为 4 的有序序列;如此反复进行下去,最后获得一个长度为 n 的有序序列。

综上可知:

归并排序其实要作两件事:

  • “分解”——将序列每次折半划分
  • “合并”——将划分后的序列段两两合并后排序

咱们先来考虑第二步,如何合并

在每次合并过程当中,都是对两个有序的序列段进行合并,而后排序。

这两个有序序列段分别为 R[low, mid] 和 R[mid+1, high]。

先将他们合并到一个局部的暂存数组R2 中,带合并完成后再将 R2 复制回 R 中。

为了方便描述,咱们称 R[low, mid] 第一段,R[mid+1, high] 为第二段。

每次从两个段中取出一个记录进行关键字的比较,将较小者放入 R2 中。最后将各段中余下的部分直接复制到 R2 中。

通过这样的过程,R2 已是一个有序的序列,再将其复制回 R 中,一次合并排序就完成了。

核心代码

public void Merge(int[] array, int low, int mid, int high) {
    int i = low; // i是第一段序列的下标
    int j = mid + 1; // j是第二段序列的下标
    int k = 0; // k是临时存放合并序列的下标
    int[] array2 = new int[high - low + 1]; // array2是临时合并序列

    // 扫描第一段和第二段序列,直到有一个扫描结束
    while (i <= mid && j <= high) {
        // 判断第一段和第二段取出的数哪一个更小,将其存入合并序列,并继续向下扫描
        if (array[i] <= array[j]) {
            array2[k] = array[i];
            i++;
            k++;
        } else {
            array2[k] = array[j];
            j++;
            k++;
        }
    }

    // 若第一段序列还没扫描完,将其所有复制到合并序列
    while (i <= mid) {
        array2[k] = array[i];
        i++;
        k++;
    }

    // 若第二段序列还没扫描完,将其所有复制到合并序列
    while (j <= high) {
        array2[k] = array[j];
        j++;
        k++;
    }

    // 将合并序列复制到原始序列中
    for (k = 0, i = low; i <= high; i++, k++) {
        array[i] = array2[k];
    }
}
复制代码

掌握了合并的方法,接下来,让咱们来了解如何分解

在某趟归并中,设各子表的长度为 gap,则归并前 R[0...n-1] 中共有 n/gap 个有序的子表:R[0...gap-1], R[gap...2*gap-1], ... , R[(n/gap)*gap ... n-1]

调用 Merge 将相邻的子表归并时,必须对表的特殊状况进行特殊处理。

若子表个数为奇数,则最后一个子表无须和其余子表归并(即本趟处理轮空):若子表个数为偶数,则要注意到最后一对子表中后一个子表区间的上限为 n-1。

核心代码

public void MergePass(int[] array, int gap, int length) {
    int i = 0;

    // 归并gap长度的两个相邻子表
    for (i = 0; i + 2 * gap - 1 < length; i = i + 2 * gap) {
        Merge(array, i, i + gap - 1, i + 2 * gap - 1);
    }

    // 余下两个子表,后者长度小于gap
    if (i + gap - 1 < length) {
        Merge(array, i, i + gap - 1, length - 1);
    }
}

public int[] sort(int[] list) {
    for (int gap = 1; gap < list.length; gap = 2 * gap) {
        MergePass(list, gap, list.length);
        System.out.print("gap = " + gap + ":\t");
        this.printAll(list);
    }
    return list;
}
复制代码

算法分析

归并排序算法的性能

参数 结果
排序类别 归并排序
排序方法 归并排序
时间复杂度平均状况 O(nlog2n)
时间复杂度最坏状况 O(nlog2n)
时间复杂度最好状况 O(nlog2n)
空间复杂度 O(n)
稳定性 稳定
复杂性 较复杂

时间复杂度

归并排序的形式就是一棵二叉树,它须要遍历的次数就是二叉树的深度,而根据彻底二叉树的能够得出它的时间复杂度是 O(n*log2n)

空间复杂度

由前面的算法说明可知,算法处理过程当中,须要一个大小为 n 的临时存储空间用以保存合并序列。

算法稳定性

在归并排序中,相等的元素的顺序不会改变,因此它是稳定的算法。

归并排序和堆排序、快速排序的比较

若从空间复杂度来考虑:首选堆排序,其次是快速排序,最后是归并排序。

若从稳定性来考虑,应选取归并排序,由于堆排序和快速排序都是不稳定的。

若从平均状况下的排序速度考虑,应该选择快速排序。

示例代码

个人 Github 测试例

样本包含:数组个数为奇数、偶数的状况;元素重复或不重复的状况。且样本均为随机样本,实测有效。

基数排序

要点

基数排序与本系列前面讲解的七种排序方法都不一样,它不须要比较关键字的大小

它是根据关键字中各位的值,经过对排序的 N 个元素进行若干趟“分配”与“收集”来实现排序的。

不妨经过一个具体的实例来展现一下,基数排序是如何进行的。

设有一个初始序列为: R {50, 123, 543, 187, 49, 30,0, 2, 11, 100}。

咱们知道,任何一个阿拉伯数,它的各个位数上的基数都是以 0~9 来表示的。

因此咱们不妨把 0~9 视为 10 个桶。

咱们先根据序列的个位数的数字来进行分类,将其分到指定的桶中。例如:R[0] = 50,个位数上是 0,将这个数存入编号为 0 的桶中。

分类后,咱们在从各个桶中,将这些数按照从编号 0 到编号 9 的顺序依次将全部数取出来。

这时,获得的序列就是个位数上呈递增趋势的序列。

按照个位数排序: {50, 30, 0, 100, 11, 2, 123,543, 187, 49}。

接下来,能够对十位数、百位数也按照这种方法进行排序,最后就能获得排序完成的序列。

算法分析

基数排序的性能

参数 结果
排序类别 基数排序
排序方法 基数排序
时间复杂度平均状况 O(d(n+r))
时间复杂度最坏状况 O(d(n+r))
时间复杂度最好状况 O(d(n+r))
空间复杂度 O(n+r)
稳定性 稳定
复杂性 较复杂

时间复杂度

经过上文可知,假设在基数排序中,r 为基数,d 为位数。则基数排序的时间复杂度为 O(d(n+r))

咱们能够看出,基数排序的效率和初始序列是否有序没有关联。

空间复杂度

在基数排序过程当中,对于任何位数上的基数进行“装桶”操做时,都须要 n+r 个临时空间。

算法稳定性

在基数排序过程当中,每次都是将当前位数上相同数值的元素统一“装桶”,并不须要交换位置。因此基数排序是稳定的算法。

示例代码

个人 Github 测试例

样本包含:数组个数为奇数、偶数的状况;元素重复或不重复的状况。且样本均为随机样本,实测有效。

相关文章
相关标签/搜索