排序算法种类繁多,每一种复杂度都不一样,差异较大,我们不禁有个疑问:我们为什么要记那么多种算法,为什么不直接使用最好的一种排序算法呢?答案是,这个要看具体的待排序数据,没有一种排序算法在任何场景下性能都是最优的。
这就需要我们真正的理解每种算法的特性,本文针对各种算法进行统一整理比对,彻底理解它们。
一. 算法横评
![](https://i-blog.csdnimg.cn/blog_migrate/3256c2c3aea63cd29d2f23fb711a628a.png)
此图先记的大概内容,等看完下文理解了每种算法,在回过来看,就明朗了;这个图需要合理的记住:
- 种类:交、插、选、归、基;默念5遍;
- 时间复杂度:交、插、选每个大类里面都有1个算法简单的和1个算法复杂的,简单的算法时间复杂度都是O(n^2);复杂点的,时间复杂度O(nlogn);
- 辅助空间:不需要临时数组直接在原数组上干的,都是O(1),需要临时数组或者递归有临时变量的都大于这个。比如快排的每一次递归中有临时变量存基准数据;归并排序则小一个和原数组同样大小的临时数组;
- 稳定性:稳定性指的是,对于待排序数组中有相同元素值的,排序后,相同元素值得位置可能会交换;一般来说,排序过程中的比较是在相邻的两个元素间进行的排序算法是稳定的。
所以,哪一种最好?
据说快速排序 在平均时间性能方便表现最佳。但是他受原始数据影响可能达到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 快排算法
- 初始数据
如下一组初始数据:
整体思路:
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的步骤;
下面正式开始。
-
开始j的位置数据为85,因为85>temp(72),所以保持不动;然后j- -到48的位置,因为48<temp,所以48移动到左边i的位置,覆盖掉72,进入步骤3;
-
步骤2完成后,i++,进入如下状态;只要发生过移动交换,就得更换遍历方向,现在轮到从左边i往右开始遍历;此时i位置上是6,小于temp,因此保持不动,i++,进入步骤4;
-
此时i位置上是57,因为57小于temp,保持不动,i++,进入步骤5;
-
此时i位置是88,大于temp,因此需要将88移动到右边j的位置,覆盖掉48,同时j- -,进入步骤6;
-
现在轮到从j开始往左遍历,因为73大于temp,保持不动,j- -,进入步骤7;
-
此时j位置为83,大于temp,保持不动,j- -,进入步骤8;
-
此时j位置是42,小于temp,因此需要移动到左边i的位置,覆盖左边88,然后i++,进入步骤9;
-
现在轮到从i开始往右遍历,因为60小于temp,所以保持不动,i++,进入步骤10;
-
此时i已经等于j,不在继续遍历移动,需要将temp的值覆盖到ij的位置上,进入步骤11;
-
到这里,就完成了一次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 直接插入排序算法
直接插入排序算法比较简单,整体上就是两层循环,以增序排序为例:
- 外层i从数组的第2(开始就是第2个与第1个比)个元素开始,i++往右遍历,元素值暂存为temp;
- 内层循环从j=i-1开始,j- - 往左遍历,逐个与外层数据temp比较大小,如果temp<a[j],就“交换”,然后temp继续往左与a[j-1]比较,否则结束内层循环循环;
- 重复以上步骤,直到外层循环结束。
注意:上述的“交换”,只是为了方便描述,实际并不是交换,只是后面的元素往右插入,但是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)也叫缩小增量排序,是在直接插入排序的基础上改进来的。
上面提到的直接插入排序算法,有两个特点:
- 算法简单,在数据量比较小的情况下,效率也是比较高的;
- 在待排序数组有序时,时间复杂度可以提升到O(n);
希尔排序就是利用这两点,为了缩小数据量,将整个待排序列分割成多个小的子序列,每个子序列使用直接插入排序;最后在基本有序的数组上,整体上再来一次直接插入排序即可。
3.2.1 希尔排序算法
希尔排序分割成多个小序列的分割是有讲究的。一是分割成多小的子序列,二是分割几次。
这里面套路比较深,通常简单的做法是按照如下增量数组:
{length/2,length/2/2,…,1}
也就是:
- 首先使用待排序数组长度/2作为第一次的增量,然后下次再除以2,直到增量等于1。这是最外层的循环;
- 中间循环依然是直接插入排序算法;
下面开始图解:
还是使用上文直接插入排序的数据:
上图解释下:因为数组长度是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++;
- 从gap开始:实际和直接插入排序一样的,都是从数组的第2个元素开始;我们开始把原始数组按照gap=4分组后,第一组元素就是{a[0],a[gap]},第二个元素的位置就是gap;
- 为什么是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 堆排序算法
有了以上基础概念后,我们来看看堆排序的算法思路,以大顶堆(增序)为例:
步骤总结
- 一组无序的数据,本地文demo共7个数,首先构建出大顶堆;
- 然后后交换根节点与最后一个节点的值,这样最后一个节点就是最大值,在数组最右边;
- 然后进行二叉树调整,将剩余的6个数重新构建成大顶堆;
- 然后重复步骤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 递归方式算法
图解如下:
上图中就是一个递归的过程。
步骤:
- 针对一组无序的数组,首先进行拆分,每次拆分分界线mid=(0+a.length-1)/2下取整,[0,mid]是一组,[mid+1,a.length-1]是一组,
- 递归拆分,一直到到最小粒度就是单个元素,也就是left=right的时候就得停止;
- 然后在两两进行合并,合并的同时进行排序(使用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 非递归方式算法
上图中,如果直接从下半部分开始合并,就是可以不使用递归。
我们首先认为待排序数组已经分好了:
如上图单个元素就是一组,我们需要做的就是直接把他们并起来。
步骤:
- 先使用跨度span为1,两两merge;这个过程需要循环,是内层的循环;
- 然后span*2,继续进行merge;然后循环span继续翻倍;什么时候结束呢?例如我们有8个元素,跨度分别为1、2、4;总结下,就是跨度不能大于等于数组长度,即span<length。
特别注意:
上图中的demo是正好能进行两合并的,但是实际情况并不是如此,如下图:
说明:上图中,
- 在跨度apan=1第一次循环merge的时候,最右侧的元素66落单,则不动;
- 在跨度span=2的第二次循环merge时,最右侧还剩下{2,77}和{6},也是不够两组;但是,剩余3个元素,超过span,此时需要将他们合并;
- 在跨度span=4的第三次循环merge时,{2,66,77}这一组数据落单,因为剩余元素数为3,小于span,则不动;
- 最后,在跨度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条件:
- 外层while循环,跨度span必须小于length;等于也不行;
- 内层while循环,右边界指right,不能超过原数组边界;
- if条件:在内层while结束后,剩余的元素数量,只要还大于span,就还能在merge一次,否则就不动。
5.3 归并算法复杂度
时间复杂度:O(nlogn),最坏O(nlogn)
空间复杂度:O(n),因为需要一个temp数组;
稳定性:稳定。这里需要解释下,第一节中提到,一般只有相邻两个元素间的比较才是稳定的,但是归并排序可不止是相邻元素间的比较。在merge的时候,两个子序列之间的比较不是相邻的,为什么还是稳定的呢?看代码里,我们在merge的是比较a[i]<=a[j],并且是从左到右,这样的结果就是两个相同的元素,左边的始终会在左边,因此不会出现相同元素位置不固定的现象,因此是稳定的。