归并排序,是创建在归并操作上的一种有效的排序算法。算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。
归并排序是用分治思想,分治模式在每一层递归上有三个步骤:
● 分解(Divide):将n个元素分成个含n/2个元素的子序列。
● 解决(Conquer):用合并排序法对两个子序列递归的排序。
● 合并(Combine):合并两个已排序的子序列已得到排序结果。
递归法
① 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
② 设定两个指针,最初位置分别为两个已经排序序列的起始位置
③ 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
④ 重复步骤③直到某一指针到达序列尾
⑤ 将另一序列剩下的所有元素直接复制到合并序列尾
上图中首先把一个未排序的序列从中间分割成2部分,再把2部分分成4部分,依次分割下去,直到分割成一个一个的数据,再把这些数据两两归并到一起,使之有序,不停的归并,最后成为一个排好序的序列。
平均时间复杂度:O(nlogn)
最佳时间复杂度:O(nlogn)
最差时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定
不管元素在什么情况下都要做这些步骤,所以花销的时间是不变的,所以该算法的最优时间复杂度和最差时间复杂度及平均时间复杂度都是一样的为:O( nlogn )
归并的空间复杂度就是那个临时的数组和递归时压入栈的数据占用的空间:n + logn;所以空间复杂度为: O(n)。
归并排序算法中,归并最后到底都是相邻元素之间的比较交换,并不会发生相同元素的相对位置发生变化,故是稳定性算法。
package Sort;
import java.util.Arrays;
/**
* @BelongsProject: JAVAtest
* @BelongsPackage: Sort
* @Author:
* @CreateTime: 2023-05-01 10:18
* @Description: TODO
* @Version: 1.0
*/
public class divideAndConquer {
public static void main(String[] args) {
// 定义待排序数组
int[] arr = {14,12,15,13,11,16};
//新建一个临时数组存放
int[] tmp = new int[arr.length];
// 归并排序
mergeSort(arr, 0, arr.length - 1, tmp);
for (int i = 0; i < arr.length; i++) {
// 归并排序
System.out.print(arr[i] + " ");
}
}
public static void mergeSort(int[] arr, int low, int high, int[] tmp) {
if (low < high) {
// 求中间位置,用于将数组拆分成两部分
int mid = (low + high) / 2;
//对左边序列递归划分
mergeSort(arr, low, mid, tmp);
//对右边序列进行递归划分
mergeSort(arr, mid + 1, high, tmp);
//合并两个有序序列
merge(arr, low, mid, high, tmp);
}
}
public static void merge(int[] arr, int low, int mid, int high, int[] tmp) {
// 用于遍历 tmp 数组的指针
int i = 0;
//左边序列和右边序列起始索引
int j = low, k = mid + 1;
// 比较左右两个有序数组的元素,并按大小依次放入 tmp 数组中
while (j <= mid && k <= high) {
//左半区第一个元素小于右半区第一个元素
if (arr[j] < arr[k]) {
//接着往后继续比
tmp[i++] = arr[j++];
}
//右半区第一个元素更小,先放右半区第一个元素
else {
tmp[i++] = arr[k++];
}
// // 输出排序过程中数组 arr 的变化
System.out.println(Arrays.toString(arr));
}
//若左边序列还有剩余,则将其全部拷贝进tmp[]中
while (j <= mid) {
tmp[i++] = arr[j++];
}
while (k <= high) {
tmp[i++] = arr[k++];
}
// 将排好序的 tmp 数组复制到原数组 arr 中
for (int t = 0; t < i; t++) {
arr[low + t] = tmp[t];
}
}
}
拓展:
递归和迭代
递归和迭代都是解决问题的方法。递归是通过不断调用自身来解决问题,迭代是通过重复执行相同的操作来解决问题。
递归和迭代的主要区别在于它们的实现方式和执行过程。在递归中,程序通过调用自身来解决问题。递归函数将问题划分为子问题,并将这些子问题交给自身来解决。递归函数必须有一个终止条件,否则它将无限地调用自身,导致程序崩溃。
在迭代中,程序通过重复执行相同的操作来解决问题。迭代通常使用循环结构来实现。在每次迭代中,程序会执行相同的操作,直到达到问题的解决条件为止。迭代的优点是可以使用较少的内存,而且通常比递归更快。
递归和迭代都有各自的优点和缺点。递归通常比较容易理解和实现,但是由于每次递归都会创建新的函数调用栈,因此会消耗较多的内存。迭代通常需要较少的内存,但是实现起来可能比较困难,特别是对于复杂的算法来说。
归并排序是一种经典的分治算法,它通过将一个大的问题拆分成小的子问题并逐步解决,最后将子问题的解合并得到整体的解。该算法的时间复杂度为O(n log n),是一种稳定且高效的排序算法。
总结归并排序的步骤如下:
拆分阶段:将待排序的数组递归地一分为二,直到每个子数组只包含一个元素或为空。这些单个元素的子数组被认为是有序的。
归并阶段:将相邻的子数组两两合并,通过比较元素大小来合并它们成为一个更大的有序数组。这个过程不断重复,直到所有子数组合并成为一个完整有序数组。
归并排序的优点:
稳定性:归并排序是稳定的,意味着在排序过程中相等元素的相对位置不会改变。这对于某些应用场景很重要。
适用性:归并排序适用于各种数据结构,特别是链表结构,因为它不涉及元素的随机访问,只需要顺序访问即可。
稳定的时间复杂度:无论是最好、最坏还是平均情况,归并排序的时间复杂度都为O(n log n)。
归并排序的缺点:
额外空间消耗:归并排序需要额外的空间来存储临时的子数组和合并结果,空间复杂度为O(n)。
递归开销:在实现时通常使用递归来拆分数组,这可能会引起一定的递归开销。
总体来说,归并排序是一种高效且稳定的排序算法,尤其适用于链表等非随机访问结构。虽然它需要额外的内存空间,但在现代计算机中,内存空间通常不是主要的限制因素,所以归并排序仍然是一个值得考虑的优秀算法。
当谈到归并排序时,还有一些其他方面值得探讨的内容:
自底向上的归并排序:我们在前面提到的归并排序是自顶向下的递归版本,但也可以使用自底向上的非递归版本来实现。自底向上的归并排序从最小的子数组开始,逐步合并成越来越大的有序子数组,直到整个数组排序完成。这种实现方式避免了递归带来的函数调用开销,可以在某些情况下具有更好的性能。
逆序数与逆序对:归并排序在排序过程中可以高效地计算数组中的逆序数(或逆序对)。逆序数指的是在一个数组中,前面的元素比后面的元素大的对数。计算逆序数在某些应用中非常有用,例如计算逆序数可以衡量一个数组的有序程度。
多路归并排序:标准的归并排序是将数组一分为二,形成两个子数组,然后进行合并。但实际上,归并排序可以扩展为多路归并排序,即将数组分成多个子数组,并同时进行多个子数组的合并。这种方法在外部排序等特定情况下很有用。
优化策略:在实际应用中,归并排序可以通过一些优化策略来提高性能。例如,当子数组的大小较小时,可以切换到使用插入排序等其他排序算法,以避免归并操作的额外开销。
并行化归并排序:由于归并排序的特性,它很容易并行化实现。在现代多核处理器中,可以将归并排序任务分配给多个核心并行处理,从而加速排序过程。
总的来说,归并排序是一种非常重要且灵活的排序算法。虽然它在某些方面有一些缺点,但其稳定性、高效性和适用性使它成为许多实际应用场景中的首选排序算法。通过理解归并排序的原理和优化策略,我们可以更好地理解分治算法的思想,并在需要时进行合理地应用和改进。