一文彻底理解各种排序算法

排序算法种类繁多,每一种复杂度都不一样,差异较大,我们不禁有个疑问:我们为什么要记那么多种算法,为什么不直接使用最好的一种排序算法呢?答案是,这个要看具体的待排序数据,没有一种排序算法在任何场景下性能都是最优的。

这就需要我们真正的理解每种算法的特性,本文针对各种算法进行统一整理比对,彻底理解它们。

一. 算法横评

此图先记的大概内容,等看完下文理解了每种算法,在回过来看,就明朗了;这个图需要合理的记住:

  1. 种类:交、插、选、归、基;默念5遍;
  2. 时间复杂度:交、插、选每个大类里面都有1个算法简单的和1个算法复杂的,简单的算法时间复杂度都是O(n^2);复杂点的,时间复杂度O(nlogn);
  3. 辅助空间:不需要临时数组直接在原数组上干的,都是O(1),需要临时数组或者递归有临时变量的都大于这个。比如快排的每一次递归中有临时变量存基准数据;归并排序则小一个和原数组同样大小的临时数组;
  4. 稳定性:稳定性指的是,对于待排序数组中有相同元素值的,排序后,相同元素值得位置可能会交换;一般来说,排序过程中的比较是在相邻的两个元素间进行的排序算法是稳定的。

所以,哪一种最好?
据说快速排序 在平均时间性能方便表现最佳。但是他受原始数据影响可能达到O(n^2);
堆排序和归并倒是没有最坏情况,数据量小的时候堆快;数据量大的时候归并快,但是归并需要的辅助空间也大;
直接插入排序在数据两较小时,性能最佳;一般搭配快排和归并使用。

二. 交换排序

本节介绍冒泡排序和快速排序。
为什么属于交换排序?因为排序的过程主要动作是比较&交换

2.1 冒泡排序

冒泡算法时间复杂度比较高O(n^2),同时比较容易理解,本文就不图解了,直接回顾下增序代码:

public static void maoPaoSort(int[] a) {
    int length = a.length;
    //因为每个元素都是需要冒泡一次,因此外层循环是0到length-1
    for (int i = 0; i < length; i++) {
    	//内层从第2个元素开始,和前一个比,如果前面的大,就两者交换
    	//注意结束条件是length - i,因为每当一个元素冒到最右边,这个元素下次就不需要参与比较了
        for (int j = 1; j < length - i; j++) {
            //比较交换
            if (a[j - 1] > a[j]) {
                int temp = a[j];
                a[j] = a[j - 1];
                a[j - 1] = temp;
            }
        }
    }
}

因为冒泡整个过程都在比较交换,所以属于交换类算法。

2.2 快速排序

快速排序是在冒泡排序的基础上改进来的。冒泡每次都是从一边重新开始比较,很多在上一次已经比较过了,仍然要再次比较,简单的说就是存在很多次重复的比较,很浪费。

快速排序选择一个基准值,大于他的放一边,小于他的放在另外一边,理想情况下就是一分为二,这样下一次大家就各自在自己的小圈子比较交换,而不用每次都从原始数组的最左边开始,这样就在一定程度上节省了外层的循环次数,本来是n次,现在变为logn。

2.2.1 快排算法

  1. 初始数据

如下一组初始数据:

整体思路
a.一组无序的数据,默认使用两个指针i、j,初始位置分别在数组的最左边和最右边;然后选取一个基准数据temp,简单点选择a[0]=72;
b.然后先从j开始,往左逐个判断,如果a[j]大于等于temp则不动,继续往左遍历;如果小于temp则移动到左边i位置,然后i++;
c.然后从i开始往右遍历,如果a[i]小于等于temp则不动,继续往右遍历;如果大于temp则移动到右边j的位置,然后j–;
d.重复步骤bc,直到i=j;将temp放在a[i]的位置;完成一次分组(partition);
e.此时i的位置,左右两边分别是一个新的小数组,递归重复以上bcde的步骤;

下面正式开始。

  1. 开始j的位置数据为85,因为85>temp(72),所以保持不动;然后j- -到48的位置,因为48<temp,所以48移动到左边i的位置,覆盖掉72,进入步骤3;

  2. 步骤2完成后,i++,进入如下状态;只要发生过移动交换,就得更换遍历方向,现在轮到从左边i往右开始遍历;此时i位置上是6,小于temp,因此保持不动,i++,进入步骤4;

  3. 此时i位置上是57,因为57小于temp,保持不动,i++,进入步骤5;

  4. 此时i位置是88,大于temp,因此需要将88移动到右边j的位置,覆盖掉48,同时j- -,进入步骤6;

  5. 现在轮到从j开始往左遍历,因为73大于temp,保持不动,j- -,进入步骤7;

  6. 此时j位置为83,大于temp,保持不动,j- -,进入步骤8;

  7. 此时j位置是42,小于temp,因此需要移动到左边i的位置,覆盖左边88,然后i++,进入步骤9;

  8. 现在轮到从i开始往右遍历,因为60小于temp,所以保持不动,i++,进入步骤10;

  9. 此时i已经等于j,不在继续遍历移动,需要将temp的值覆盖到ij的位置上,进入步骤11;

  10. 到这里,就完成了一次partition;

上述步骤完成后,i=j,此位置的两边,分别又是1个数组,两个数组的的起始位置分别如下:

左边为:0到i-1;[48,6,57,42,60];
右边为:i+1到a.length-1;[83,73,88,85]
然后递归重复以上步骤。

2.2.2 快排代码

 public static void main(String[] args) {
    int[] a = {72, 6, 57, 88, 60, 42, 83, 73, 48, 85};
    int i = 0;
    int j = a.length - 1;
    //最开始i、j分别在最两端
    quickSort(a, i, j);
    System.out.println(Arrays.toString(a));
}
   public static void quickSort(int[] a, int low, int high) {
    //递归结束条件
    if (low < high) {
        int index = partition(a, low, high);
        //上面完成了一次partition,然后递归继续partition
        quickSort(a, low, index - 1);
        quickSort(a, index + 1, high);
    }
}
public static int partition(int[] a, int low, int high) {
    int i = low;
    int j = high;
    //基准值,简单点的话就取第一个,这里有学问
    int temp = a[i];
    while (i < j) {
        //先从右边开始:如果大于基准值,则不往左边转移数据,指针往左移动就好了,注意j必须始终在i右边,也就是i<j
        while (a[j] >= temp && i < j) {
            j--;
        }
        //
        if (i < j) {
            //右边j的数据转移到左边i的位置上
            a[i] = a[j];
            i++;
        }
        //然后左边开始:
        while (a[i] <= temp && i < j) {
            i++;
        }
        if (i < j) {
            //左边i的数据转移到右边j的位置上
            a[j] = a[i];
            j--;
        }
    }
    //走到这说明i=j,直接将基准数据放到这个位置
    a[i] = temp;
    //返回中分位置
    return i;
}

2.2.3 时间空间复杂度

时间复杂度:平均O(nlogn),logn是外层比较次数,n是内层比较次数。在初始数据基本有序时,外层不能起到一分为二的效果,还是n次,整体上复杂度达到最坏的O(n^2);

空间复杂度:O(logn),因为用到了递归,每层递归要保存一个临时基准数据。

三. 插入排序

插入排序算法又多种:

  • 直接插入排序
  • 折半插入排序
  • 2-路插入排序
  • 希尔排序

前3种的时间复杂度都是O(n^2),
而希尔排序是O(n^1.5),本文详细分析下直接插入排序和希尔排序。

为什么属于插入排序?因为整体排序过程中,主要动作是重新插入

3.1 直接插入排序

3.1.1 直接插入排序算法

直接插入排序算法比较简单,整体上就是两层循环,以增序排序为例:

  1. 外层i从数组的第2(开始就是第2个与第1个比)个元素开始,i++往右遍历,元素值暂存为temp;
  2. 内层循环从j=i-1开始,j- - 往左遍历,逐个与外层数据temp比较大小,如果temp<a[j],就“交换”,然后temp继续往左与a[j-1]比较,否则结束内层循环循环;
  3. 重复以上步骤,直到外层循环结束。

注意:上述的“交换”,只是为了方便描述,实际并不是交换,只是后面的元素往右插入,但是temp元素并没有直接放在后边位置上,因为还要继续往左比较。
这也是为什么叫做插入排序,而不是属于交换排序那一类的,因为实质上并不是交换,而是左边大的元素,往右挪动插入

下面是部分图解:

3.1.2 直接插入排序代码

下面是增序的代码:

public class InsertSort {

    public static void main(String[] args) {
        int[] arr = {11, 10, 44, 23, 3, 56, 3, 6};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int[] arr) {
        int j;
        //外层循环从第2个开始,i++
        for (int i = 1; i < arr.length; i++) {
            //先直接和上一个元素判断,如果大于上一个元素就不用交换了
            if (arr[i] < arr[i - 1]) {
                //暂存外层i位置元素,因为前面元素可能会覆盖他
                int temp = arr[i];
                //内层从i-1开始,j--;
                for (j = i - 1; j >= 0; j--) {
                    //如果外层temp小于左面的元素,那么左边的元素往右挪动一下
                    if (temp < arr[j]) {
                        arr[j + 1] = arr[j];
                    } else {
                        //外层元素不小于,就是都大于左边元素,内层就不需要比较了,直接结束
                        break;
                    }
                }
                //内层比较、挪动完成后,此时的j+1的位置就是temp的归宿
                arr[j + 1] = temp;
            }
        }
    }
}

3.2 希尔排序

希尔排序(Shell Sort)也叫缩小增量排序,是在直接插入排序的基础上改进来的。

上面提到的直接插入排序算法,有两个特点:

  1. 算法简单,在数据量比较小的情况下,效率也是比较高的;
  2. 在待排序数组有序时,时间复杂度可以提升到O(n);

希尔排序就是利用这两点,为了缩小数据量,将整个待排序列分割成多个小的子序列,每个子序列使用直接插入排序;最后在基本有序的数组上,整体上再来一次直接插入排序即可。

3.2.1 希尔排序算法

希尔排序分割成多个小序列的分割是有讲究的。一是分割成多小的子序列,二是分割几次。
这里面套路比较深,通常简单的做法是按照如下增量数组:
{length/2,length/2/2,…,1}

也就是:

  1. 首先使用待排序数组长度/2作为第一次的增量,然后下次再除以2,直到增量等于1。这是最外层的循环;
  2. 中间循环依然是直接插入排序算法;

下面开始图解:
还是使用上文直接插入排序的数据:

上图解释下:因为数组长度是8,除以2也就是4,所以首先使用gap=4来进行分组得到{11,3}{10,56}{44,3}{23,6};然后从第一组的第2个元素开始,与自己组里的左边元素进行直接插入排序处理。
首先是{11,3},因为11大于3,交换;
然后是{10,56},因为10不大于56,所以不动;
然后是{44,3},以为44大于3,所以交换;
然后是{23,6},因为23大于6,所以交换;到这里就完成第一趟排序,结果如下:

上面第一趟使用的增量gap=4,第二次需要再除以2,也就是gap=2;第3次再除以2,也就是gap=1;因为增量gap已经等于1了,就是最后一次了,过程如下图:

以上就是希尔排序的算法过程,整体上比较容易理解,下面编码。

3.2.2 希尔排序代码

public class ShellSort {

    public static void main(String[] args) {
        int[] arr = {11, 10, 44, 23, 3, 56, 3, 6};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int[] arr) {
        //增量gap数组{length/2,length/2/2,...,1},也就是{4,2,1}
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            System.out.println("gap=" + gap);
            //此部分和上节的直接插入排序基本一致,区别是直接插入增量是1,但还是希尔需要多次排序,每次增量不一样;
            sort(arr, gap);
        }
    }

    public static void sort(int[] arr, int gap) {
        int j;
        //外层循环从第gap个开始,也就是第一组的第2个元素开始;然后注意是 i++
        //这里必须理解为什么是从gap开始,为什么还是i++而不是i=i+gap
        for (int i = gap ; i < arr.length; i++) {
            //先直接和同一个子序列的上一个元素判断,如果大于上一个元素就不用交换了
            if (arr[i] < arr[i - gap]) {
                //暂存外层i位置元素,因为前面元素可能会覆盖他
                int temp = arr[i];
                //内层从i-gap开始,j=j-gap;
                for (j = i - gap; j >= 0; j -= gap) {
                    //如果外层temp小于左面的元素,那么左边的元素往右挪动一下
                    if (temp < arr[j]) {
                        arr[j + gap] = arr[j];
                    } else {
                        //外层元素不小于,就是都大于左边元素,内层就不需要比较了,直接结束
                        break;
                    }
                }
                //内层比较、挪动完成后,此时的j+1的位置就是temp的归宿
                arr[j + gap] = temp;
            }
        }
    }
}

说明:sort(int[] arr, int gap)方法中第一层for循环,从gap开始,然后i++;

  1. 从gap开始:实际和直接插入排序一样的,都是从数组的第2个元素开始;我们开始把原始数组按照gap=4分组后,第一组元素就是{a[0],a[gap]},第二个元素的位置就是gap;
  2. 为什么是i++,不是i=i+gap ?因为我们在sort(int[] arr, int gap)的外层for循环中,是同时在处理多个子序列,每个子序列只处理一部分,就得i++继续处理下一个子序列;而不是完全排完一个子序列然后再排下一个子序列。

3.2.2 希尔排序复杂度

时间复杂度:希尔排序的复杂度分析比较复杂,平均O(n^1.5),
空间复杂度:O(1)
稳定性:不稳定

四. 选择排序

本节介绍简单选择排序和堆排序。
为什么属于选择排序?因为每趟排序后,都要选择以一个最大值或者最小值。

4.1 简单(直接)选择排序

4.1.1 简单选择排序算法

基本思想是:每一趟选择一个最小的元素,一次从左到右放在有序序列中,最后就是一个增序的序列。

4.1.2 简单选择排序代码

 public static void selectionSort(int[] arr) {
    int minIndex, temp;
    //外层循环表示趟数
    for (int i = 0; i < arr.length; i++) {
        minIndex = i;
        //内层循环用来找最小值,并用minIndex记录最小值下标
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        //如果这一趟找到的最小值,不是初始的i,则把最小值和i位置交换一下
        if (minIndex != i) {
            temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

4.2 堆排序

堆排序也是选择排序的一种,利用了堆这种数据结构,每趟选择堆顶元素,时间复杂度比较优秀;因为使用了堆,那么需要先介绍下什么是堆。

4.2.1 相关概念

完全二叉树

完全二叉树:即每一层节点都是从上到下,从左到右逐个添加的,中间没有跳过的空的节点。

完全二叉树换为数组
完全二叉树,实际上可以使用一维数组来进行存储。
上图的完全二叉树从上到下,从左到右遍历一遍得到如下数组:
{11,10,44,23,3,56,3,6},下标分别为0,1,2,3,4,5,6,7

然后记住完全二叉树的特点:

  • left=2i+1 :假设一个节点的位置为i,那么他的左叶子节点位置为2i+1;
  • right=2i+2 :假设一个节点的位置为i,他的右叶子节点位置为2i+2;
  • parent=(i-1)/2 :如果叶子节点的位置为i,那么他的父节点的位置为(i-1)/2,下取整;
  • n/2-1 :最后一个非叶子节点的位置,下取整。比如上图就是下标3的位置,值为23.

这3个公式可以带入数据自行验证下,并牢记,写代码的时候要用。

堆是什么
堆是一种特殊的完全二叉树,分为大顶堆和小顶堆;

此图是一个大顶堆:每一个节点的值都大于等于叶子节点值;
反过来,如果每一个节点的值都小于等于叶子节点值,就是小顶堆。

大顶堆用来增序排序;
小顶堆用来降序排序

本文使用大顶堆来进行堆排序。

4.2.2 堆排序算法

有了以上基础概念后,我们来看看堆排序的算法思路,以大顶堆(增序)为例:

步骤总结

  1. 一组无序的数据,本地文demo共7个数,首先构建出大顶堆
  2. 然后后交换根节点与最后一个节点的值,这样最后一个节点就是最大值,在数组最右边;
  3. 然后进行二叉树调整,将剩余的6个数重新构建成大顶堆;
  4. 然后重复步骤2、3;

下面画图将以上步骤详细拆分,主要是步骤1和步骤3。

1 构建大顶堆
初始的无序数组:{11,10,44,23,3,56,3,6}

在这里插入图片描述
2 交换堆顶元素和末尾元素
在这里插入图片描述
3 重新调整剩余元素为大顶堆
在这里插入图片描述

可以发现,重新调整的过程,是构建大顶堆的一个子过程,代码实现的时候可以复用。

一直循环步骤2和3,直到数组剩余只有1个元素。

4.2.3 堆排序代码

public class HeapSort {

    public static void main(String[] args) {
        int[] arr = {11, 10, 44, 23, 3, 56, 3, 6};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int[] arr) {
        //1.构建大顶堆,从第一个 非叶子结点(位置:n/2-1)开始遍历,从下至上,从右至左
        for (int i = arr.length / 2 - 1; i >= 0; i--) {
            //调整过程
            adjustHeap(arr, i, arr.length);
            System.out.println("大顶堆:"+Arrays.toString(arr));
        }
        //2.从最后一个元素开始,与堆顶素交换,然后剩下的重新调整,一直到只剩下一个元素
        for (int j = arr.length - 1; j > 0; j--) {
            //将堆顶元素与末尾元素进行交换
            swap(arr, 0, j);
            //除了最后一个元素,剩余的重新对堆进行调整,从堆顶元素开始
            adjustHeap(arr, 0, j);
            System.out.println(j+":"+Arrays.toString(arr));

        }
    }

    /**
     * 调整为大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
     *
     * @param arr         待排序数组
     * @param parentIndex 当前非叶子节点位置下标
     * @param length      需要调整的元素个数,第一次是arr.length,后面每次减去1
     */
    public static void adjustHeap(int[] arr, int parentIndex, int length) {
        //取出父节点值
        int parent = arr[parentIndex];
        //左叶子节点下标
        int childIndex = 2 * parentIndex + 1;

        //因为parentIndex下面可能还有多层,这里一直比较到最低层的右叶子节点,也即是childIndex<length
        while (childIndex < length) {
            //从父结点的左叶子结点开始,也就是2parentIndex+1处开始;先找到两个叶子节点中最大的节点,让指针指向大的;
            //因为是从左叶子节点开始,所以必须childIndex + 1 < length
            if (childIndex + 1 < length && arr[childIndex] < arr[childIndex + 1]) {
                childIndex++;
            }
            //如果子节点大于父节点,将子节点值提上去,赋给父节点(不用进行交换,因为此时的父节点还要继续和下一层叶子节点比较,可能还要继续下沉)
            if (arr[childIndex] > parent) {
                arr[parentIndex] = arr[childIndex];
                //记录此时的父节点位置,需要降级变为了叶子节点的位置
                parentIndex = childIndex;
            } else {
                //如果叶子节点不大于父节点,那么本次调整直接结束
                break;
            }
            //继续从下一层的左叶子节点开始重复(如果有下一次的话)
            childIndex = 2 * childIndex + 1;
        }
        //将temp值放到最终的位置
        arr[parentIndex] = parent;
    }

    /**
     * 交换元素
     *
     * @param arr
     * @param a
     * @param b
     */
    public static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}

4.2.4 时间空间复杂度

时间复杂度:平均O(nlogn),最坏O(nlogn)
空间复杂度:O(1)

堆排序的性能,综合来说比较均衡优秀。

五. 归并排序

归并排序区别于插入、交换、选择等方式,是全新的一类排序算法。

归并算法有两种实现方式,递归方式和非递归方式实现。

5.1 递归方式

5.1.1 递归方式算法

图解如下:

上图中就是一个递归的过程。

步骤:

  1. 针对一组无序的数组,首先进行拆分,每次拆分分界线mid=(0+a.length-1)/2下取整,[0,mid]是一组,[mid+1,a.length-1]是一组,
  2. 递归拆分,一直到到最小粒度就是单个元素,也就是left=right的时候就得停止;
  3. 然后在两两进行合并,合并的同时进行排序(使用i、j 两个指针,外加一个临时数组),合并到最后整体就是一个有序的数组;

注意:递归的过程中,上图虚线中间的那一层实际是不存在的,那一层因为已经不符合递归条件,而直接进入到了下一层的合并过程中。

合并步骤
针对合并并排序的过程,也是算法中比较关键的一步,下面继续图解下这个过程。

本文就直接使用上图中合并的最后一步,两个数组{10,11,23,44}和{3,3,6,56}

5.1.2 递归方式代码

递归方式算法比较简洁,缺点是数据量很大时,栈空间太大。

public class MergingSort {
    public static void main(String[] args) {
        int[] arr = {11, 10, 44, 23, 3, 56, 3, 6};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int[] arr) {
        int left = 0;
        int right = arr.length - 1;
        //临时数组,merge要用,提前new好,
        int[] temp = new int[arr.length];
        //递归方法
        sort(arr, left, right, temp);
    }

    public static void sort(int[] arr, int left, int right, int[] temp) {
        //递归结束条件,结合上文图解,只要left还不等于right,就不能停
        //最后一次递归进来的时候,就是图解中的虚线那一次,此时left=right,已经不满足if条件
        if (left < right) {
            int mid = (left + right) / 2;
            //1.左边继续递归拆分
            sort(arr, left, mid, temp);
            //2.右边继续递归拆分
            sort(arr, mid + 1, right, temp);
            //3.merge合并;第一次到这,说明上面递归结束了,
            // 此时left=mid,mid+1=right,所以left+1=right,也就是left和right相邻,分别指向单个元素
            merge(arr, left, mid, right, temp);
        }
    }

    public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        //这里需要关注第一次merge,此时是两个单元素 合并
        //i自然在left位置开始,j在必须在mid+1的位置开始
        
        int i = left;
        int j = mid + 1;
        //临时数组temp的下标指针
        int t = 0;
        //两个子序列比较,小的放到temp中去
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[t++] = arr[i++];
            } else {
                temp[t++] = arr[j++];
            }
        }
        //走到这说明比较结束了,两个子序列可能还剩余元素没移动到temp中,下面使用两个while,把剩余元素移动到temp中去
        while (i <= mid) {
            temp[t++] = arr[i++];
        }
        while (j <= right) {
            temp[t++] = arr[j++];
        }

        //最后,完成一次merge,但是要将temp中的数据copy回arr中
        t = 0;
        while (left <= right) {
            arr[left++] = temp[t++];
        }
    }
}

5.2 非递归方式

5.2.1 非递归方式算法

上图中,如果直接从下半部分开始合并,就是可以不使用递归。

我们首先认为待排序数组已经分好了:

如上图单个元素就是一组,我们需要做的就是直接把他们并起来。

步骤:

  1. 先使用跨度span为1,两两merge;这个过程需要循环,是内层的循环;
  2. 然后span*2,继续进行merge;然后循环span继续翻倍;什么时候结束呢?例如我们有8个元素,跨度分别为1、2、4;总结下,就是跨度不能大于等于数组长度,即span<length。

特别注意:
上图中的demo是正好能进行两合并的,但是实际情况并不是如此,如下图:

说明:上图中,

  1. 在跨度apan=1第一次循环merge的时候,最右侧的元素66落单,则不动;
  2. 在跨度span=2的第二次循环merge时,最右侧还剩下{2,77}和{6},也是不够两组;但是,剩余3个元素,超过span,此时需要将他们合并;
  3. 在跨度span=4的第三次循环merge时,{2,66,77}这一组数据落单,因为剩余元素数为3,小于span,则不动;
  4. 最后,在跨度span=8的第4次循环merge后,完成排序;因为下一次span就等于18了,大于length,不符合外层循环条件。

5.2.2 非递归方式代码

非递归方式主要是合并,合并这一部分的代码和递归方式是一致的,区别在于在层循环,我们要控制好两两合并结束的边界条件。

public static void sortNoDiGui(int[] arr) {
    int span = 1;
    int length = arr.length;
    int[] temp = new int[length];
    while (span < arr.length) {
        int left = 0;
        //right始终在合并数组的最右边
        int right = 2 * span - 1 + left;
        int mid = (left + right) / 2;
        //这里判断条件使用right,right只要不超出原始数组右边界即可
        while (right <= length - 1) {
            merge(arr, left, mid, right, temp);
            System.out.println(Arrays.toString(arr));
            // 更新下一组 left、right、mid的位置
            left = right + 1;
            right = 2 * span - 1 + left;
            mid = (left + right) / 2;
        }

        //由于待排序数组的个数不定,上面while不一定就能正好两两合并;不满足的需要特殊处理
        if (left + span < length) {
            //如果剩余的元素个数还超过一组,也就是>span,比如span=2,还剩余3个元素,那么就前俩1组,第三个1组进行合并
            //比如 12 34 45 78 910 11 ,最后9 10 11这三个元素,让910 和11合并
            merge(arr, left, (left + right) / 2, length - 1, temp);
        } 
        //继续下一个跨度合并
        span *= 2;
        System.out.println("---------------------------------------");
    }
}

说明:特别注意两个while循环的结束条件,以及外层循环中的if条件:

  1. 外层while循环,跨度span必须小于length;等于也不行;
  2. 内层while循环,右边界指right,不能超过原数组边界;
  3. if条件:在内层while结束后,剩余的元素数量,只要还大于span,就还能在merge一次,否则就不动。

5.3 归并算法复杂度

时间复杂度:O(nlogn),最坏O(nlogn)
空间复杂度:O(n),因为需要一个temp数组;
稳定性:稳定。这里需要解释下,第一节中提到,一般只有相邻两个元素间的比较才是稳定的,但是归并排序可不止是相邻元素间的比较。在merge的时候,两个子序列之间的比较不是相邻的,为什么还是稳定的呢?看代码里,我们在merge的是比较a[i]<=a[j],并且是从左到右,这样的结果就是两个相同的元素,左边的始终会在左边,因此不会出现相同元素位置不固定的现象,因此是稳定的。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值