本文介绍了归并排序,这是一种时间复杂度为 o(n log n) 的分治算法。该算法非常适合对大型数据集进行排序,因为它具有稳定性,并且能够处理因尺寸过大而无法放入内存的数据。它还涵盖了合并排序的优点,包括它对链表和并行实现的适用性,同时强调了一些缺点,例如增加的空间复杂性和递归开销。
在计算机科学中,归并排序被归类为时间复杂度为 o(n log n) 的“分而治之”算法,通常使用递归来对数据进行排序。它最适合大型数据集,特别是当稳定性很重要时,以及数据无法一次全部装入内存的场景,例如外部排序或排序链表。
分治算法可以定义为具有三个步骤:
除法:如果输入大小小于某个阈值(例如一个或两个元素),则使用简单的方法直接解决问题并返回如此获得的解决方案。否则,将输入数据划分为两个或多个不相交的子集。
征服:递归地解决与子集相关的子问题。
合并:将子问题的解合并为原问题的解。
(goodrich 等人,2023,第 12.1 节归并排序)
合并排序采用分而治之的方法,将数据顺序划分为两个大致相等的分区,并递归地将左侧分区排序为较小的已排序子分区,直到达到单个元素。然后它对所有子分区进行组合和排序以构造最终排序的左分区。对未排序的右分区重复相同的过程。最后,将左右分区组合并排序以产生最终的排序输出。请参阅图 1 了解合并排序的划分和组合步骤。
图1
合并排序
在计算机科学中,归并排序被归类为时间复杂度为 o(n log n) 的“分而治之”算法,通常使用递归来对数据进行排序。它最适合大型数据集,特别是当稳定性很重要时,以及数据无法一次全部装入内存的场景,例如外部排序或排序链表。
分治算法可以定义为具有三个步骤:
除法:如果输入大小小于某个阈值(例如,一个或两个元素),则使用简单的方法直接解决问题并返回如此获得的解决方案。否则,将输入数据划分为两个或多个不相交的子集。
征服:递归地解决与子集相关的子问题。
合并:将子问题的解合并为原问题的解。
(goodrich 等人,2023,第 12.1 节归并排序)
合并排序采用分而治之的方法,将数据顺序划分为两个大致相等的分区,并递归地将左侧分区排序为较小的已排序子分区,直到达到单个元素。然后它对所有子分区进行组合和排序以构造最终排序的左分区。对未排序的右分区重复相同的过程。最后,将左右分区组合并排序以产生最终的排序输出。请参阅图 1 了解合并排序的划分和组合步骤。
图1
合并排序
在计算机科学中,归并排序被归类为时间复杂度为 o(n log n) 的“分而治之”算法,通常使用递归来对数据进行排序。它最适合大型数据集,特别是当稳定性很重要时,以及数据无法一次全部装入内存的场景,例如外部排序或排序链表。
分治算法可以定义为具有三个步骤:
除法:如果输入大小小于某个阈值(例如一个或两个元素),则使用简单的方法直接解决问题并返回如此获得的解决方案。否则,将输入数据划分为两个或多个不相交的子集。
征服:递归地解决与子集相关的子问题。
合并:将子问题的解合并为原问题的解。
(goodrich 等人,2023,第 12.1 节归并排序)
合并排序采用分而治之的方法,将数据顺序划分为两个大致相等的分区,并递归地将左侧分区排序为较小的已排序子分区,直到达到单个元素。然后它对所有子分区进行组合和排序以构造最终排序的左分区。对未排序的右分区重复相同的过程。最后,将左右分区组合并排序以产生最终的排序输出。请参阅图 1 了解合并排序的划分和组合步骤。
图1
合并排序
注意:图中没有显示所有步骤细节,但足以理解整个过程。来自“第 12 章:算法:排序和选择。 goodrich 等人的《数据结构和算法》。 (2023)。修改。
归并排序的优点
归并排序有几个优点。首先,它是一种稳定的排序算法,这意味着它保持未排序数据条目顺序的相对顺序。例如,具有相同值的两个数据元素将保留其原始输入顺序。这对于必须保持输入顺序的应用程序至关重要,例如按字母等级对学生的课程成绩进行排序时,学生在同一班级中可能有多个 a 或 b。另一个例子是在多键排序中,数据按一个标准排序,然后按另一个标准排序,稳定性对于确保保留具有相同值的数据元素的条目顺序至关重要。
此外,归并排序算法由于其顺序访问模式而特别适合链表(khandelwal,2023)。链表不支持像数组那样的索引访问,这允许随机访问数据元素。在链表中,要访问特定索引处的元素,通常需要从头节点开始遍历链表,因此不能随机访问数据。然而,使用合并排序,列表被分成两半并递归排序,而不需要索引数据。数据是按顺序访问的,这自然与链表的访问模式一致,使得合并排序非常适合对链表中存储的数据进行排序。
该算法的另一个优点是它保证了 o(n log n) 时间复杂度,使其对于大型数据集排序既可靠又高效。它的时间复杂度是可靠的,因为它在所有情况场景下都保持一致:
- 最坏情况:o(n log n) — 当数组被重复分成两半直到到达各个元素,然后进行合并过程时,就会发生这种情况。
- 平均情况:o(n log n) — 与最坏情况类似,该算法始终将数组分成两半,然后合并它们。
- 最佳情况:o(n log n) — 即使数组已部分排序,归并排序也会将其分成两半并合并它们,从而获得与最坏情况和平均情况相同的时间复杂度。
(khandelwal,2023,第 1 页)
对于可预测且稳定的时间复杂度至关重要的环境,其可靠的时间复杂度是比快速排序更好的选择。尽管快速排序的时间复杂度也是 o(n log n),但在数据可能部分排序的环境中,它可能会降级为 o(n2),这使得 marge-sort 成为此类情况的更好选择。
此外,由于合并过程中使用的临时数组需要额外的空间,其空间复杂度为 o(n);然而,它是稳定且可预测的,使得该算法非常适合对资源可用性需要可预测的大型数据集进行排序。
与基数排序算法相比,虽然其时间复杂度为 o(n k),优于归并排序的时间复杂度,但归并排序更适合数据多样化且随机的情况。这是因为基数排序最适用于数据类型,例如整数或固定长度字符串。相比之下,归并排序适用于所有类型的数据,并且不受固定或有限值范围需求的限制。而 radix-sorts 的性能取决于输入数据的范围,如果范围很大,性能可能会下降。此外,它的递归和分区性质(向左或向右排序)可以同时或并行实现。这极大地提高了合并排序算法在支持并行处理和多线程的现代计算系统中处理大规模数据集时的效率。
合并排序的缺点
另一方面,归并排序的一些缺点包括处理递归调用造成的额外开销以及深度递归情况下堆栈溢出的潜在风险。与插入排序等更简单的算法相比,这些算法在对小数据集进行排序时效率较低。
此外,它比插入排序或选择排序等更简单的算法实现起来更复杂,并且具有更高的空间复杂度,后者需要更少的内存,并且在某些应用程序中实现起来可能更简单。
Java 代码示例如前所述,merge-sort 递归分区排序可以并行或并发实现。第一个代码片段是经典的归并排序实现,第二个示例是使用并行性的归并排序实现。
java 中的基本归并排序:
import java.util.arrays; import java.util.comparator; public class mergesort { public static void mergesort(t[] array, comparator<? super t> comp) { int n = array.length; // --- base case --- // if the array has 1 or 0 elements, it is already sorted, so return. if (n < 2) { return; } // --- divide step --- // find the midpoint to divide the array into two halves int mid = n / 2; // create two subarrays: one from the left half and one from the right half // left subarray from index 0 to mid-1 t[] left = arrays.copyofrange(array, 0, mid); // right subarray from index mid to n-1 t[] right = arrays.copyofrange(array, mid, n); // --- conquer step --- // recursive call - sort the left (first) and right halves of the array mergesort(left, comp); // sort the left half mergesort(right, comp); // sort the right half // --- combine step --- // after both halves are sorted, merge them back into a single sorted array merge(left, right, array, comp); // merge sorted halves into original array } // method to merge two sorted subarrays (left and right) into the original // result array private static void merge(t[] left, t[] right, t[] result, comparator<? super t> comp){ // i, j track position in left and right arrays; k tracks result int i = 0, j = 0, k = 0; // --- merging process --- // compare elements from the left and right arrays and place the smaller // one into result. while (i < left.length && j < right.length) { // compare elements and merge them in sorted order if (comp.compare(left[i], right[j]) <= 0) { // copy from left array and move the index i result[k++] = left[i++]; } else { // copy from right array and move the index j result[k++] = right[j++]; } } // --- copy remaining elements --- // if there are any remaining elements in the left array, copy them into // result while (i < left.length) { result[k++] = left[i++]; } // if there are any remaining elements in the right array, copy them into // result while (j < right.length) { result[k++] = right[j++]; } } public static void main(string[] args) { integer[] array = { 3, 5, 1, 6, 4, 7, 2 }; mergesort(array, comparator.naturalorder()); system.out.println(arrays.tostring(array)); } }
java 中的并行归并排序:
import java.util.arrays; import java.util.comparator; import java.util.concurrent.recursiveaction; import java.util.concurrent.forkjoinpool; // parallelmergesort class class parallelmergesort extends recursiveaction { private final t[] array; private final comparator<? super t> comp; // constructor to initialize the array and comparator public parallelmergesort(t[] array, comparator<? super t> comp) { this.array = array; this.comp = comp; } // the compute method defines the parallel sorting process @override protected void compute() { int n = array.length; // --- base case --- // if the array has 1 or 0 elements, it is already sorted, so return if (n < 2) { return; } // --- divide step --- // divide the array into two halves: left (first half) // and right (second half) int mid = n / 2; // create two subarrays: left subarray from index 0 to mid-1 t[] left = arrays.copyofrange(array, 0, mid); // right subarray from index mid to n-1 t[] right = arrays.copyofrange(array, mid, n); // --- conquer step --- // create two tasks to sort the left and right halves in parallel parallelmergesort lefttask = new parallelmergesort<>(left, comp); parallelmergesort righttask = new parallelmergesort<>(right, comp); // invoke the tasks concurrently, allowing them to run in parallel invokeall(lefttask, righttask); // --- combine step --- // after both halves are sorted, merge them back into the original array merge(left, right, array, comp); } // method to merge two sorted subarrays (left and right) into the original // result array private static void merge(t[] left, t[] right, t[] result, comparator<? super t> comp) { int i = 0, j = 0, k = 0; // i, j track position in left and right arrays; k // tracks result // --- merging process --- // compare elements from the left and right arrays and place the smaller one // into result while (i < left.length && j < right.length) { if (comp.compare(left[i], right[j]) <= 0) { result[k++] = left[i++]; // take element from left array and move // the index i } else { result[k++] = right[j++]; // take element from right array and move // the index j } } // --- copy remaining elements --- // if there are any remaining elements in the left array, copy them into // result while (i < left.length) { result[k++] = left[i++]; } // if there are any remaining elements in the right array, copy them into // result while (j < right.length) { result[k++] = right[j++]; } } // initializes parallel merge sort using forkjoinpool public static void parallelmergesort(t[] array, comparator<? super t> comp) { // create a forkjoinpool for parallel execution forkjoinpool pool = new forkjoinpool(); // start the parallel sorting task by invoking the main parallelmergesort // task pool.invoke(new parallelmergesort<>(array, comp)); } }
public class Main { public static void main(String[] args) { Integer[] array = {3, 5, 1, 6, 4, 7, 2}; System.out.println("Unsorted array: " + Arrays.toString(array)); ParallelMergeSort.parallelMergeSort(array, Comparator.naturalOrder()); System.out.println("Sorted array: " + Arrays.toString(array)); } }
现实生活中的使用示例
合并排序通常在需要获取大型数据集并将其存储在磁盘上的情况下实现,例如在数据中心中,此过程通常称为外部排序。在线零售商就是一个很好的例子,例如亚马逊或 ebay,其中数以百万计的客户订单需要根据时间戳进行排序。由于数据集太大而无法放入内存,因此合并排序非常适合此任务。数据可以分块加载,在内存中并行排序以保持数据的稳定性,并在磁盘上合并。
总而言之,归并排序可靠、稳定,能够处理大型数据集,其递归分区可以并行实现,非常适合大规模在线商店排序应用。然而,在实现算法时应考虑其额外的空间和递归开销,尤其是在资源可能有限的环境中。
参考文献:
goodrich t, m.、tamassia, r. 和 goldwasser h.m.(2023 年 6 月)。第 12 章:算法:排序和选择。数据结构和算法。 zybook isbn: 979–8–203–40813–6.
khandelwal, v.(2023 年,10 月 25 日)。什么是合并排序算法:它是如何工作的,等等。简单学习。 https://www.simplilearn.com/tutorials/data-structure-tutorial/merge-sort-algorithm
最初于 2024 年 10 月 2 日发表于 alex.omegapy – medium。