数据结构与算法--归并排序
简介
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序
排序图解
从图中可以看出,对原数组进行拆分成两个子数组,然后对每个子数组继续拆分,直到子数组只有一个元素后,将相邻的两个子数组进行大小比较后合并,一直合并到数组有序
合并的次数 = 原数组长度 - 1
排序原理
- 尽可能的将数组拆分成两个元素相等的子数组,并对每一个子数组继续拆分,直到拆分后每个子数组的元素个数为1
- 将相邻的两个子数组进行合并成为一个有序的子数组
- 不断重复步骤2,直到最终只有一个子数组为止
代码实现
/***拆分+合并的功能*/
public void mergeSort(int[] arr,int left,int right,int[] temp){
if (left < right) {
int mid = (left + right) / 2;
//向左递归拆分
mergeSort(arr,left,mid,temp);
//向右递归拆分
mergeSort(arr,mid + 1,right,temp);
//合并拆分数据
merge(arr,left,mid,right,temp);
}
}
/**
* 合并
* @param arr 待排序的数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 临时数组
*/
public void merge(int[] arr,int left,int mid,int right,int[] temp){
//左边有序序列的初始索引
int i = left;
//右边有序序列的初始索引
int j = mid + 1;
//指向临时数组的初始索引
int t = 0;
//先把又作两边有序的数据按照规则填充到temp数组,知道左右两边的有序序列有一边处理完毕位置
while (i <= mid && j <= right) {
//如果左边的有序序列的当前元素小于等于右边的有序序列的当前元素,
//则将左边的元素拷贝到临时数组,否则将右边的元素拷贝到临时数组
if (arr[i] <= arr[j]){
temp[t] = arr[i];
t++;
i++;
} else {
temp[t] = arr[j];
t++;
j++;
}
}
//将左边剩余的数据一次填充到temp中
while (i <= mid) {
temp[t] = arr[i];
t++;
i++;
}
//将右边剩余的数据一次填充到temp中
while (j <= right) {
temp[t] = arr[j];
t++;
j++;
}
//将temp中的元素拷贝到arr中(注意不是每次都拷贝所有)
t = 0;
int leftTemp = left;
while (leftTemp <= right) {
arr[leftTemp] = temp[t];
leftTemp++;
t++;
}
}
时间复杂度分析
归并排序是分治思想的最典型的例子,上面的算法中,对a[left...right]进行排序,先将它分为a[left...mid]和a[mid+1...right]两部分,分别通过递归调用将他们单独排序,最后将有序的子数组归并为最终的排序结果。该递归的出口在于如果一个数组不能再被分为两个子数组,那么就会执行merge进行归并,在归并的时候判断元素的大小进行排序
用树状图来描述归并,如果一个数组有8个元素,那么它将每次除以2找最小的子数组,共拆log28次,值为3,所以树共有3层,那么自顶向下第k层有2k个子数组,每个数组的长度为2(3-k),归并最多需要2(3-k)次比较。因此每层的比较次数为 2k * 2(3-k)=23,那么3层总共为 3*23。
假设元素的个数为n,那么使用归并排序拆分的次数为log2n,所以共log2n层,那么使用log2n替换上面3*23中 的3这个层数,最终得出的归并排序的时间复杂度为:log2n * 2(log2n)=log2n * n,根据大O推导法则,忽略底数,最终归并排序的时间复杂度为O(nlogn)
缺点
需要申请额外的数组空间,导致空间复杂度提升,是典型的以空间换时间的操作
算法稳定性
归并排序是稳定的