摘要:归并排序是我们常用的八大排序中的一种,其排序思路中和快速排序算法一样使用到了递归的思想,同时在归并排序中还用到了一个算法,就是有序数组合并算法。配合递归与有序数组合并算法,归并排序能够高效且稳定的完成排序,归并排序的优点在于其时间复杂度低,稳定性高,但是缺点也是有的,那就是空间复杂度很高。
我们首先来详细说说归并排序的算法思路,归并排序的算法思路并不复杂,其主要是一个拆分与合并的过程,接下来我们用图解来看看归并排序究竟是如何排序的。
首先,我们得到了这样的一个数组:
之后我们将其进行一次按照中间位置一分为二的划分:
之后我们在此基础上再为这两个被划分出来的数组进行进一步划分:
只要每个数组长度大于1,那么我们就会继续划分,因此在上图中的情况下我们仍然要继续划分,如图所示:
被划分成这个状态之后,我们便不再划分,而是两两将其进行有序数组的拼接,如图所示:
在此拼接的基础上我们继续拼接,只要这个数组还是被划分为多个子数组的状态我们就会一直继续拼接,下次拼接的结果如图所示:
我们继续拼接,如下图,发现整个数组又变成一个了,拼接完成:
现在我们可以总结出归并排序的算法思路了,那就是在将整个数组进行不断划分,知道划分的每个字数组的长度为0或者为1,这是每个字数组统统都是有序数组,这是再按照有序数组的拼接算法,对每个子数组进行拼接,这样就能保证每次的拼接结果都还是有序的最终拼接成一个之后,整个数组便都是有序的,而数组的排序也宣布完成,关于这个字数组的划分,实际上是通过递归实现的逻辑上的划分,接下来我们使用动图来看看这个排序过程:
在我们了解了归并排序的基本算法之后,就要开始着手实现这个排序了。在上面的排序算法图解中,并没有着重的介绍有序数组的合并是如何实现的,因此在之后的总结中,我还会介绍如何进行合并,不过首先我们来看看对于这个排序中的拆分过程时候如何使用代码实现的:
public static void mergeSort(int[] arr,int left,int right){
if(left>=right){
return;
}
int mid = (left + right) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid+1, right);
//上边为拆分过程
}
对于拆分过程实际上就是递归,我们不断进行左右的递归,并不断减小子数组的规模,最终便会减小到每个数组的规模为1或者为0,这里需要注意的是递归出口的判断条件为:left>=right
,我们为什么不写成left==right
呢?这是因为在右递归中,我们的左边界为mid+1
,我们的mid是通过直接整除2得到的,当数组规模为1的时候,mid的运算结果为0,此时进入下一层递归时,左递归的左右边界都是0,下层左递归正常退出,但是右递归这是便出现了问题,这是右递归中的right = mid + 1,为1,如果使用left==right
,就无法判断并终止这个情况了,所有此时我们还要加入一个新情况,那就是left>right
,所以我们合写为left>=right
。
之后我们来探讨合并的过程,在合并的时候我们需要使用到一个额外空间,在合并时,我们需要先将合并结果存放在那个额外的新空间上,然后再将新空间上的结果复制回我们的当前数组位置上,我们下面用一个例子来介绍,如图所示:
在数组1,3,0,6
的分组合并过程中,1,3和0,6分别被分进了不同的组中,我们为两组分别声明i指针和j指针,同时为临时空间声明n指针,我们每次对两个数组中的指针指向数字进行比较,小的会被移动到额外的临时空间中,同时那个数组中的指针要向前移动一步,之后我们继续比较。最终我们会得到一个有序的数组,但是这个数组是被存放在临时空间中的,因此我们需要将它再复制回原数组中,这个过程非常简单,仔细看上面的图片即可理解。
接下来我们书写这个代码:
//需要注意的是整个合并过程中并没有将两个被合并的数组单独拎出来,二者始终是存在于一个数组地址上的
public static void merge(int[] arr,int left,int mid,int right){
int s1 = left;//根据拿到的左边界,我们定其为第一个数组的指针
int s2 = mid+1;//根据中间位置,让中间位置右移一个单位,那就是第二个数组的指针
int[] temp = new int[right - left+1];//根据左右边界相减我们得到这片空间的长度,以此声明额外空间
int i = 0;//定义额外空间的指针
while(s1<=mid && s2 <=right){
if(arr[s1]<=arr[s2]){//如果第一个数组的指针数值小于第二个数组的,那么其放置在临时空间上
temp[i++] = arr[s1++];
}else{//否则是第二个数组的数值放置于其上
temp[i++] = arr[s2++];
}
}
while(s1<=mid){//如果这是s1仍然没有到达其终点,那么说明它还有剩
temp[i++] = arr[s1++];//因为我们知道每个参与合并的数组都是有序数组,因此直接往后拼接即可
}
while(s2<=right){//同上
temp[i++] = arr[s2++];
}
for(int j = 0;j<temp.length;j++){//数组复制
arr[j+left] = temp[j];
}
}
之后我们将两个方法联合使用即可:
import java.util.*;
class Untitled {
public static void main(String[] args) {
int[] arr = {5,7,4,2,0,3,1,6};
mergeSort(arr, 0, arr.length-1);
System.out.print(Arrays.toString(arr));
}
public static void mergeSort(int[] arr,int left,int right){
if(left>=right){
return;
}
int mid = (left + right) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid+1, right);
merge(arr, left, mid, right);
}
//需要注意的是整个合并过程中并没有将两个被合并的数组单独拎出来,二者始终是存在于一个数组地址上的
public static void merge(int[] arr,int left,int mid,int right){
int s1 = left;//根据拿到的左边界,我们定其为第一个数组的指针
int s2 = mid+1;//根据中间位置,让中间位置右移一个单位,那就是第二个数组的指针
int[] temp = new int[right - left+1];//根据左右边界相减我们得到这片空间的长度,以此声明额外空间
int i = 0;//定义额外空间的指针
while(s1<=mid && s2 <=right){
if(arr[s1]<=arr[s2]){//如果第一个数组的指针数值小于第二个数组的,那么其放置在临时空间上
temp[i++] = arr[s1++];
}else{//否则是第二个数组的数值放置于其上
temp[i++] = arr[s2++];
}
}
while(s1<=mid){//如果这是s1仍然没有到达其终点,那么说明它还有剩
temp[i++] = arr[s1++];//因为我们知道每个参与合并的数组都是有序数组,因此直接往后拼接即可
}
while(s2<=right){//同上
temp[i++] = arr[s2++];
}
for(int j = 0;j<temp.length;j++){//数组复制
arr[j+left] = temp[j];
}
}
}
运行结果如图: