本文已参与「新人创作礼」活动,一起开启掘金创作之路。
归并排序介绍
在讲归并排序之前,我们先了解一下什么是归并。
我们先来看看这个题目:合并两个有序数组
这个题目是很简单的,我们只要创建一个数组ret【】,长度是两个数组长度的和。
然后创建两个变量,cur1 、cur2
这两个变量分别指向了两个数组的第一个元素,
然后比较他们所指向元素的大小,小的那个就添加到我们的ret里面,然后指向下一个地方。
就这样一直循环,直到有个变量指向了数组的最后一个。
这个时候跳出循环,看看是哪个数组没有被用完,然后弄个循环,把变量送到我们创建的那个数组里面去。
public static int[] merge(int[] arr1 , int[] arr2){
int len = arr2.length + arr1.length;
int[] ret = new int[len];
int cur1 = 0;
int cur2 = 0;
int index = 0;
while(cur1 < arr1.length || cur2 < arr2.length){
// 这边我这样子做是为了省去一个循环
int num1 = cur1 < arr1.length ? arr1[cur1] : Integer.MAX_VALUE;
int num2 = cur2 < arr2.length ? arr2[cur2] : Integer.MAX_VALUE;
//这个是判断两个元素的大小,并让较小的cur往后走一步。
if(num1 < num2){
ret[index++] = num1;
cur1++;
}else{
ret[index++] = num2;
cur2++;
}
}
return ret;
}
上面的代码就是将两个有序的数组变成一个有序的数组。
那我们解决了上面的那个问题之后,对我们解决归并算法有什么帮助呢?
那我们想想,把一个数组分开来看,是不是一个又一个的小数组组成的。
我们看这个数组
1,5,9,8
我们可以看到,这个数组可以分为{1}、{5}、{9}、{8}
那我们让他们两两合并看看
{1,5}、{8,9}
我们继续合并
{1,5,8,9}
那么这个就是归并排序。
那我们怎么能做到把元素变成一个又一个单独的小个体呢?
变成小个体之后怎么能让他们再集合起来呢?
这个当然是很简单的。我们只要用到递归就可以了。之后再介绍不用递归的方法。
那我们是怎么借用递归来让他们变成一个又一个的单独体呢?
public static void process(int[] arr, int L, int R) {
if (L == R) {
System.out.println(arr[L]);
}
int mid = L + ((R - L) / 1);
process(arr, L, mid); //递归调用自己的左边
process(arr, mid + 1, R); //递归调用自己的右边,这边就是把一个数组分为了两半,然后继续分。
}
我们这样子就可以解决了,我们有一个左边的变量,有一个右边的变量,然后让他们相互靠近,之后l == r的时候就是单独的一个了。
当我们得到单独的一个又一个的时候,我们只要在里面添加merge就可以了。
public static void process(int[] arr , int L , int R){
if(L == R){
return;
}
int mid = L + ((R-L)/2);
process(arr , L , mid);
process(arr , mid + 1 , R);
merge(arr , L , mid , R);
}
public static void merge(int[] arr , int l , int m , int r){
int[] ret = new int[r - l + 1];
int cur1 = l;
int cur2 = m + 1;
int index = 0;
while(cur1 <= m || cur2 <= r){
int num1 = cur1 <= m ? arr[cur1] : Integer.MAX_VALUE;
int num2 = cur2 <= r ? arr[cur2] : Integer.MAX_VALUE;
if(num1 <= num2){
ret[index++] = num1;
cur1++;
}else{
ret[index++] = num2;
cur2++;
}
}
for(int i = 0;i<ret.length;i++){
arr[i + l] = ret[i];
}
}
这边我把merge也顺带放出来了,其实就和最上面的差不多,那这个时候我们就可以写一个函数来调用这个process然后归并排序的非递归版本就完成了。
public static void mergeSort1(int[] arr){
if(arr == null || arr.length < 2){
return;
}
process(arr , 0 , arr.length - 1);
}
在我们实际开发的时候其实是很回避递归这个东西的,为什么呢?
因为这个会调用系统栈,在调用自己的时候,栈会单独开辟一个空间,那我们递归到最小的部分的时候,其实有可能开了好多的栈了,就导致了效率比较低,那我们现在来弄一个非递归版本
我们想想,上边的递归版本是为什么递归的,是不是因为切割最小部分的时候用到了递归,那我们能不能用循环来做到这个呢?
其实是可以的,你可以发现,我们在合并的时候其实是2的倍数。
原本都是一个单独的个体,然后两两在一起,这个时候个体集合里面有两个元素,这个时候再两两集合,这个时候一个集合里面有四个,这样持续下去就可以了,那我们也可以在循环里面弄个这样的变量,先是1,后面是2,后面是4,来控制哪些可以在一起。
我们先定义一个tep来记录应该有几个元素在一个单独的集合里面,
然后我们不能让这个tep过大,最大只能是数组长度,这个当做循环的结束条件
这个时候我们定义一个l来控制左边的内容,我们同时也要控制变量的大小,不能让数组溢出,l是一个集合的最左侧,那么我们得让中间部分不能溢出,那么我们怎么能控制右边呢?其实是有方法的,我们后面解答。
这个时候我们的l + tep是不能炒股数组的长度的,我们还要一个循环,然后在这个循环里面操作merge。
那有的人会问了,两个循环,那不是O(n^2)了吗?
其实不是的,因为我们的tep是每次乘2上升的,所以时间复杂度不是O(n^2)
我们上面已经解决了溢出的问题,那我们要确定中间的位置了,我们这边的mid = l + tep - 1,这个减一是为了遇到偶数的时候,我们选择的是上中位数。
那我们的右边怎么操作呢?我们上边只是保证了中间不会溢出,那右边怎么办呢?
咱们这样子解决
int r = Math.min(mid + tep , N - 1);
我们让r在mid + tep 和 N - 1中间选一个(N是数组的长度)。
当我们左中右都弄好的时候,我们就可以调用merge函数了。
当我们最里面的循环结束之后,tep就可以乘2
我们来看看代码是怎么样的。
public static void mergeSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int tep = 1;
int N = arr.length;
while(tep < N){
int l = 0;
while(l + tep < N){
int mid = l + tep - 1;
int r = Math.min(mid + tep , N - 1);
merge(arr , l , mid , r);
l = r + 1;
}
if(tep > N / 2){
break;
}
tep *= 2;
}
}
里面有一段代码是要解读一下的
if(tep > N / 2){
break;
}
这个代码是为了防止溢出Integer。
如果N是Integer最大限度的一半多的时候,那我们如果让tep乘2,就很有可能会溢出。