1.归并排序算法
将两个有序数列合并成一个有序数列称为归并......
归并排序算法:将无序数组不断的等分成两份,直到分割成单位元素之后,再利用归并的思想,从下往上不断合并成有序数组的过程。主要分为两种:自底向上的归并排序和自上向下的归并排序。
注意:归并排序过程中,需要开辟一个相同大小的临时空间来辅助归并排序的进行。对于两个有序数组是怎么合并到一起的呢?我们来看下面的过程:
原始数组: 2 3 6 8 1 4 5 7 -- 使用索引 i 表示排好序后插入的元素位置的后一位,每次插入都需要维护
临时空间: 2 3 6 8 1 4 5 7 -- 使用索引 m n 分别表示子数组最左侧的待比较元素,每次比较都需要维护其中一个
第一轮:两个有序数组的第一个元素进行比较,较小的元素放到原始数组的第一位;
原始数组: 1 3 6 8 1 4 5 7 -- 索引 i 加1
临时空间: 2 3 6 8 1 4 5 7 -- 后一个子数组索引值+1
第二轮:继续比较剩余元素的最左侧元素
原始数组: 1 2 6 8 1 4 5 7 -- 索引 i 加1
临时空间: 2 3 6 8 1 4 5 7 -- 前一个子数组索引值+1
依次类推,直到归并排序完成。
1)自上向下的归并过程如下:递归 + 合并
public class MergeSort02 {
public static void main(String[] args) {
int[] arr = {2,5,6,3,8,4,9,1};
mergeSortUp2Down(arr,0,arr.length-1);
for (int i : arr) {
System.out.print(i + ",");
}
}
/**
* 编写有序数组的归并逻辑
* @param arr 包含两个有序区间的数组
* @param start 第1个有序区间的起始地址
* @param mid 第1个有序区间的结束地址。也是第2个有序区间的起始地址
* @param end 第2个有序区间的结束地址。
*/
public static void merge(int[] arr,int start,int mid,int end) {
//开辟临时区间,用作汇总两个有序区间
int[] temp = new int[end-start+1];
//合并两个有序区间需要维护三个变量
int i = start;
int j = mid + 1;
int k = 0; //临时区域的索引
//自定义两个有序数组的合并逻辑 + 排序逻辑
while(i<=mid && j<=end) {
if(arr[i] <= arr[j])
temp[k++] = arr[i++]; //将较小的元素插入到临时空间中,同时维护变量
else
temp[k++] = arr[j++];
}
//当一部分有序数组提前结束的话,则将剩下的值一次赋给temp[]
while(i <= mid)
temp[k++] = arr[i++];
while(j <= end)
temp[k++] = arr[j++];
//将排序好的数组,全部整合到数组a中
for(i=0 ; i<k ; i++) {
arr[start + i] = temp[i];
}
//清空临时空间
temp = null;
}
/**
* 自上向下的归并排序
* @param arr 待排序的数组
* @param start 数组的起始索引
* @param end 数组的结束索引
*/
public static void mergeSortUp2Down(int[] arr,int start,int end) {
//定义递归的终止条件
if(arr==null || start>=end) {
return;
}
int mid = (start+end)/2;
//此时r=mid,数组开始二分至最小单位。只有递归到底时,才会继续往下执行
mergeSortUp2Down(arr, start, mid);
mergeSortUp2Down(arr, mid+1, end);
//从单个元素开始,不断向上将两个有序数组合并成一个有序空间
merge(arr,start,mid,end);
}
}
2)自底向上的归并过程如下:迭代 + 合并
public class MergerSort01 {
public static void main(String[] args) {
int[] arr = {2,5,6,3,8,4,9,1,10};
mergeSortDown2Up(arr,arr.length);
for (int i : arr) {
System.out.print(i + ",");
}
}
/**
* 具体的合并逻辑
* @param arr 包含两个有序区间的数组
* @param start 第1个有序区间的起始地址
* @param mid 第1个有序区间的结束地址。也是第2个有序区间的起始地址
* @param end 第2个有序区间的结束地址。
*/
public static void merge(int[] arr,int start,int mid,int end) {
//开辟临时区间,用作汇总两个有序区间
int[] temp = new int[end-start+1];
//合并两个有序区间需要维护三个变量
int i = start;
int j = mid + 1;
int k = 0; //临时区域的索引
//自定义两个有序数组的合并逻辑 + 排序逻辑
while(i<=mid && j<=end) {
if(arr[i] <= arr[j])
temp[k++] = arr[i++]; //将较小的元素插入到临时空间中,同时维护变量
else
temp[k++] = arr[j++];
}
//当一部分有序数组提前结束的话,则将剩下的值一次赋给temp[]
while(i <= mid)
temp[k++] = arr[i++];
while(j <= end)
temp[k++] = arr[j++];
//将排序好的数组,全部整合到数组a中
for(i=0 ; i<k ; i++) {
arr[start + i] = temp[i];
}
//清空临时空间
temp = null;
}
/**
* 对长度为 n 的数组进行合并操作
* @param arr 待排序数组
* @param len 数组的长度
* @param n 子数组的长度
*/
public static void merge_Group(int[] arr,int len,int n) {
int i; //定义全局变量,记录排序数组的索引位置
int len2 = 2*n; //两个相邻子数组的长度
//将两个相邻的子数组合并到一起
for(i=0 ; i+len2-1<len ; i+=len2) {
merge(arr, i, i+n-1, i+len2-1);
}
//若 i+gap-1 < len-1,则剩余一个子数组没有配对。则将该数组合并到已排序的数组中
if(i+n-1 < len-1) {
merge(arr, i, i+n-1, len-1);
}
}
/**
* 归并排序:自底向上
* @param arr 待排序的数组
* @param len 数组的长度
*/
public static void mergeSortDown2Up(int[] arr,int len) {
int n = 0;
//判断数据是否合法
if(arr==null || len<=0) {
return;
}
//循环迭代,进行递归排序。初始数组长度为1
for(n=1 ; n<len ; n*=2) {
merge_Group(arr,len,n);
}
}
}
注:自底向上的归并排序由于没有使用数组索引的特性,因此更加适合于链表的归并排序。
2.算法优化分析
1)对于近乎有序的数组,arr[mid] <= arr[mid+1] 的情况更容易出现,在进行 if 判断后,可以省去 merge 操作。同时 if 判断本身也会损耗时间,但对于这种情况利大于弊;
if(arr[mid] > arr[mid+1]) {
merge(arr,start,mid,end);
}
2)几乎对于所有的高级排序算法,都存在一种优化。即在递归将要结束时,如果递归的范围较小,则可以使用插入排序的方式代替归并排序。
插入排序 O(N^2) a*N^2+C
归并排序 O(NlogN) b*NlogN+C
已知 a<b,当 N 小到一定范围时,会出现 a*N^2 < b*NlogN 的情况
3.时间复杂度分析
归并排序每次递归都将数组均匀等分,由 2^m=N 可知,归并排序的深度为 m=lgN。由于每层都需要 O(N) 的时间复杂度。因此归并排序的时间复杂度为 O(NlgN) 级别的。
4.空间复杂度分析
自上向下的归并排序需要存储排序数组的临时空间,因此需要O(N)的空间复杂度。