3.6 归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。(归并排序 属于 高级排序的一种
)
-
需求:
排序前:{8,4,5,7,1,3,6,2}
排序后:{1,2,3,4,5,6,7,8] -
排序原理︰
- 尽可能的将一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止。
- 将相邻的两个子组进行合并成一个有序的大组;
- 不断的重复步骤2,直到最终只有一个组为止。
- 归并排序API设计:
归并原理
- 必须知道 排序是在哪里进行的,又是怎么进行的。排序是在合并两个子组的时候进行的! 而合并两个子组的时候又是怎么进行的呢? 只做两个子组每个对应元素的大小比较,比如A组1元素和B组1元素大小比较,如果A组1元素小,那么就把这个A组1元素放在 assist辅助数组的1元素位置!
辅助元素作用
是用来存储 A组和B组两个 子组进行 各个对应元素比较后,筛选的正确元素的位置 然后用 辅助数组存储进去,呈现的过程!- 需要注意的是,我们要设定三个int变量 来代替所谓的指针思想。期初 p1指针指向A组lo位置,p2指向B组mid+1元素,i指针指向 辅助数组的 lo位置 元素,当子组中的元素知道自己在哪个正确的位置后,该组的指针就应该往后移动一位,而另一个组的指针不要动。 这样才能继续进行下一次的比较和筛选。
- 在合并子组的过程中,我们可能会遇到一些特殊的情况,比如说 p1指针已经走到了mid 处,即 p1 的尽头,但是 辅助数组里面还缺少B组的元素,这个时候 我们知道 B组本身就是有序的呀!!!而且造成这个的原因就是B组的数据比较大,所以直接 遍历 从 p2 指针开始 B 组的所有元素,填入到 辅助数组里即可。
- 在合并子组的过程中,我们可能会遇到一些特殊的情况,比如说 p2指针已经走到了hi 处,即 p2 的尽头,但是 辅助数组里面还缺少A组的元素,这个时候 我们知道 B组本身就是有序的呀!!!而且造成这个的原因就是A组的数据比较大,所以直接 遍历 从 p1 指针开始 A 组的所有元素,填入到 辅助数组里即可。
归并排序 代码
package com.muquanyu.algorithm.sort;
public class Merge {
//归并所需要的辅助数组
private static Comparable[] assist;
/*
比较 元素 v 是否 小于 元素 w
*/
private static boolean less(Comparable v,Comparable w){
return v.compareTo(w) < 0;
}
/*
数组元素 i 和 j 交换位置
*/
private static void exch(Comparable[] a,int i,int j)
{
Comparable temp = a[i];
a[i] = a[j];
a[j] = temp;
}
/*
对数组 a 中的元素 进行排序
*/
public static void sort(Comparable[] a)
{
//1.初始化 辅助数组 assist
assist = new Comparable[a.length];
//2.定义一个 lo 变量 和 hi 变量,分别记录数组中 最小的索引和最大的索引
int lo = 0;
int hi = a.length - 1;
//3.调用 sort 的重载方法 完成 数据 a 中,从索引 lo 到 索引 hi 的元素排序
sort(a,lo,hi);
}
/*
对数组 a 中 从 lo 到 hi 的元素 进行排序
*/
private static void sort(Comparable[] a,int lo,int hi)
{
//1.做安全性的校验
if(hi <= lo)
{
return;
}
//2.对lo 到 hi 之间的数据 分为两个组
int mid = lo + (hi - lo)/2;//5 + (9 - 5)/2 = 7;
//3.分别对每一组的数据 进行排序
sort(a,lo,mid);
sort(a,mid+1,hi);
//4.再把两个组中的数据进行 归并
merge(a,lo,mid,hi);
}
/*
对数组中 从 lo 到 mid 为一组,从 mid + 1 到 hi 为一组,对着两组数据进行归并
*/
private static void merge(Comparable[] a,int lo,int mid,int hi)
{
int p1 = lo;
int p2 = mid+1;
int i = lo;
while(p2 <= hi && p1 <= mid)
{
if(less(a[p1],a[p2]))
{
assist[i++] = a[p1++];
}else{
assist[i++] = a[p2++];
}
}
//遍历 如果 p1 的指针没有 走完,那么 顺序移动 p1 指针,把对应的元素都放在辅助数组对应索引的后面即可
while(p1 <= mid)
{
assist[i++] = a[p1++];
}
//同理 如果 p2 也没走完,那也要 顺序添进去 即可。
while(p2 <= hi)
{
assist[i++] = a[p2++];
}
//把辅助数组拷贝到原数组中
for(int index=lo;index <= hi;index++)
{
a[index] = assist[index];
}
}
}
归并排序 算法测试
package com.muquanyu.algorithm.test;
import com.muquanyu.algorithm.sort.Merge;
import java.util.Arrays;
public class MergeTest {
public static void main(String[] args) {
Integer[] arr = {8,4,5,7,1,3,6,2};
Merge.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
正常的归并排序,是必须要有 辅助数组 来 协助操作的。这样也方便 理解。而不要 直接 在 原数组里面 进行 操作!这样很容易 混乱还不说,反而效率 不一定 有 带有辅助数组的高,写起来也不是那么的快!!
归并一词的意思是什么 ?
答:“归” 指的是 在 压入的那些函数,跳栈的时候(即归的时候),我们进行 “子组的合并”。子组的合并 也被 简称为 “并” 这个字。而 由于 递归的时候,每次压栈我们都 将 子组不断的 二分!所以 最小的 A组和B组 各自仅仅只有 一个 元素,那么在进行 合并的时候,自然而然 就相当于 完成了排序的工作。 这是很奇妙的一种写法!!!在最小子组持续到最大子组的过程中 不断的进行合并,最后居然也完成了 “排序” 这个 重大的工程!
3.6.1 归并排序 时间复杂度分析
归并排序是分治思想的最典型的例子,上面的算法中,对a[lo…hi]进行排序,先将它分为a[lo…mid]和a[mid+1…hi]两部分,分别通过递归调用将他们单独排序,最后将有序的子数组归并为最终的排序结果。该递归的出口在于如果一个数组不能再被分为两个子数组,那么就会执行merge进行归并,在归并的时候判断元素的大小进行排序(而这里的排序是很奇妙的,它用一种最简单的方式实现了排序。也就是我们合并的过程,就实现了排序的过程)。
用树状图来描述归并,如果一个数组有8个元素,那么它将每次除以2找最小的子数组,共拆log8次,值为3,所以树共有3层,那么自顶向下第k层有2*k个子数组,每个数组的长度为2(3-k)*,归并最多需要2(3-k)次比较。因此 每层的I较次数为 2k * 2(3-K) = 243, 那么 3层总共为 3 * 23。
假设元素的个数为n,那么使用归并排序拆分的次数为 log2(n)所以共log2(n)层,那么使用log2(n)替换上面3*23中的 3 这个层数,最终得出的归并排序的时间复杂度为:== log2(n) * 2(log2n)=log2(n) * n ==,根据大O推导法则,忽略底数,最终归并排序的时间复杂度为 (nlogn);
nlogn 是 非常接近于 n 的 时间复杂度,所以我们说 nlogn 已经很快了!!
对于 高级排序算法,归并 算法 的代码难度和理解难度 是继 “希尔排序” 之后 第二个最简单的了。
归并排序的缺点
需要申请额外的数组空间 还有额外的多个函数开辟的空间,还要申请额外的指针变量空间,导致空间复杂度提升,是典型的 "以空间换时间的算法"
这是我们第一个 接触的 空间换时间的 算法。