归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序是一种稳定的排序方法。
归并排序的工作原理如下描述:
- 创建一个和待排序数组登场的数组(用于临时存放数据)
- 将待排序数组对半划分,直至每个划分后的数组只有两个元素
- 将划分后的数组按大小排序
- 将划分后的数组合并
是不是很简单ヽ(゚∀゚)メ(゚∀゚)ノ (并不)
我们来一步一步的讨论如何实现这个算法
假设我们有个数组arr
,根据第一步,创建一个和它等长的数组temp
int[] temp=new int[arr.length];
然后就是对原数组进行划分了。我们不需要真的对原数组进行分割,只是在逻辑上进行划分。我们用一个指针left
来标识数组的左侧,right
来标识数组的右侧,这样我们就可以得到这个数组的中间数mid=(left+right)/2
,然后以mid
为划分,把数组划分为左右两个子数组。图中黄色方块标识的就是mid
所指向的位置
这时左侧数组可以以left
为左指针,mid
为右指针,而右侧数组可以以mid+1
为左指针,right
为右指针,然后一直这样划分下去,直到划分出来数组中只有两个成员,如图所示。同时,可以从图中看出,到最后子数组只有两个元素的时候,再分出子数组,就会使子数组的left
等于right
。所以我们可以用left
和right
的关系作为判断数组划分是否结束的标准
这样就可以写出一个递归的方法:
public void Divide(int[] arr,int left,int right,int[] temp){
if(left<right){
int mid=(left+right)/2;
Divide(arr,left,mid);
Divide(arr,mid+1,right);
//用于对子数组进行合并
Conquer(arr,left,mid,right,temp);
}
}
分完之后,就该对子数组做合并操作了,也就是上面的代码出现的Conquer()
方法。合并的操作就要用到之前定义的temp
数组了。以上图第三行的49、39两个数所在的子数组举例。用一个指针i
代替left
,在这个例子里i=0
。指针j
设置为mid+1
,在这个例子里j=1
。指针t
设置为0(作为遍历temp数组的指针)。要求i<=mid
,j<=right
:
int i=left;
int j=mid+1;
int t=0
然后遍历利用i
、j
遍历该子数组,并比较i
、j
所指向的数的的大小,把比较小的那个填入temp。在这个例子中,将38填入temp中后循环结束。
while(i<=mid && j<=right){
if(arr[i]<arr[j])
temp[t++]=arr[i++];
else if(arr[j]<arr[i])
temp[t++]=arr[j++];
}
循环结束时有可能i
还没有遍历到mid
,或者j
还没有遍历到right
,这时再剩下的数填入到temp中。在这个例子中,是前一种情况,把从i
到mid
(即0到0)的所有数填入temp(即49)
while(i<=mid)
temp[t++]=arr[i++];
while(j<=right)
temp[t++]=arr[j++];
此时temp=[38,49,0,0,0,0,0,0]
然后把t
置0,把这一段子数组的值覆盖到原始数据中:
t=0;
while(left<=right>)
arr[left++]=temp[t++];
覆盖后的原始数据arr=[38,49,65,97,23,22,76,1]
完整的代码:
public void Conquer(int[] arr,int left,int mid,int right,int[] temp){
int i=left;
int j=mid+1;
int t=0;
while(i<=mid && j<=right){
if(arr[i]<arr[j])
temp[t++]=arr[i++];
else if(arr[j]<arr[i])
temp[t++]=arr[j++];
}
while(i<=mid)
temp[t++]=arr[i++];
while(j<=right)
temp[t++]=arr[j++];
t=0;
while(left<=right)
arr[left++]=temp[t++];
}
有了上面的例子,我们可以再看看数组划分的第二行[23,22,76,1]
这个子数组该如何进行数组的合并。因为这个子数组的两个子数组已经分别做过了合并,所以实际上此时的子数组内容为[22,23,1,76]
。开始遍历这个子数组,一开始i
指向22,j
指向1。1小于22,故向temp
中插入1,right
向右移动1位,指向76;22小于76,故向temp
中插入22,left
向右移动1位,指向23;23小于76,故向temp
中插入23,left
向右移动1位,结束循环。然后把生下的76插入temp
中。此时temp=[1,22,23,76,0,0,0,0]
,因为left=4
,right=7
,所以把temp
中(7-4+1)个内容覆盖到原始数据中从4到7的位置上
整个归并排序的完整代码如下:
public class MergeSort{
public void sort(int[] arr){
int[] temp=new int[arr.length];
Divide(arr,0,arr.length-1,temp)
}
public void Divide(int[] arr,int left,int right,int[] temp){
if(left<right){
int mid=(left+right)/2;
Divide(arr,left,mid,temp);
Divide(arr,mid+1,right,temp);
Conquer(arr,left,mid,right,temp);
}
}
public void Conquer(int[] arr,int left,int mid,int right,int[] temp){
int i=left;
int j=mid+1;
int t=0;
while(i<=mid && j<=right){
if(arr[i]<arr[j])
temp[t++]=arr[i++];
else if(arr[j]<arr[i]>)
temp[t++]=arr[j++];
}
while(i<=mid)
temp[t++]=arr[i++];
while(j<=right)
temp[t++]=arr[j++];
t=0;
while(left<=right)
arr[left++]=temp[t++];
}
public static void main(String[] args){
//测试数据
int[] arr={49,38,65,97,23,22,76,1};
MergeSort ms=new MergeSort();
ms.sort(arr);
for(int i:arr)
System.out.print(i+" ");
}
}
输出结果:
1 22 23 38 49 65 76 97