C++ 分形排序

2025年3月22日 | 阅读 12 分钟

分形排序 是一种非比较排序算法,它以与分形相同的方式应用分治策略。然而,分形排序的使用相对较少,与快速排序或归并排序等广为人知的算法相比,对其的讨论和分析也更少见。然而,分形排序是另一种通过细分数组并反复排序的方法,但遵循分形模式算法。与大多数传统的排序模型一样,数组被分区并分别排序,然后合并回来。然而,分形排序更进一步,将数组细分为更小、更复杂的块,并且此过程就像分形一样被复制。

在每次递归调用合并时,数组会分成许多部分,而不是像归并排序算法中那样分成中间部分。一旦数组被分区到较小的部分(每个部分包含一两个元素),合并就会开始。这种分裂和连接过程会一直持续到到达数组的最后一个元素,然后它们就会被排序。

在每一步,数组都可以根据需要进行任意精度的划分,并且递归划分可以具有不同的复杂性级别。例如,分形排序可以将数组分成一半,也可以分成四分之一或类似的部分,这样它的排序图就会比简单的分裂复杂得多,同时在剪枝解决方案时使用递归,并且通常具有 O(n log n) 的时间复杂度。

分形排序 是对非比较排序技术的一种有趣的理论探索。该算法独特的递归性质清楚地根植于其在数学结构分形中的概念基础,分形在不同尺度上表现出自相似性。分形排序虽然对于大多数计算机问题来说不是一种可行的解决方案,但它展示了通过在排序等计算问题中利用递归自相似技术可以获得的潜在发现,而这些问题最好通过分治策略来解决。

方法-1:基本递归分形排序(分治法)

基本递归分形排序受分治技术启发,并向分形结构的递归和自相似性有所偏离。与在每个递归步骤中将数组分成两个相等部分的归并排序快速排序不同,分形排序将数组分解成更小、更精细的片段。递归划分过程一直持续到子数组小到无法单独排序为止,然后它们被合并回一个已排序的数组。

程序

输出

 
Original array: 38 27 43 3 9 10 12 22 67 51 
Sorted array: 3 9 10 12 22 27 38 43 51 67   

说明

归并排序将数组分成两部分,而基本递归分形排序则使用四路划分,遵循分形类方法将结构分解成更小的单元。这是分形概念,其中结构被细分为更小的单元。它递归地将数组分成这些部分并单独排序,然后将它们合并回来。

递归划分

数组使用三个中点:mid1、mid2 和 mid3 被分成四个部分。具体来说,这些中点用于数组的第一个四分之一,以及沿数组的相同点。

举个例子:数组有 16 个元素,这意味着中点会将数组分成四个大约 4 个元素的部分。这些部分中的每一个都充当子问题,并被进一步递归细分。递归在数组段变得足够小时(1 或 0 个元素)停止,在这种情况下,我们在调用堆栈上对整个数组进行排序,递归结束。

基本情况

当子数组大小小于 2 时,满足基本情况,算法停止进行任何其他递归调用,并在递归调用堆栈展开时开始合并这些明显已排序的段。当遇到这些基本情况时,它会停止进一步调用递归调用。当递归树向下展开时,算法开始合并临时排序的段。

该算法将从小的、已排序的片段开始,并实际地将所有内容合并回来以增加大的已排序的段。这种分步合并操作由最初的小型排序段形成更大的排序部分,并且通过连接这些最优排序函数,创建了一个完全排序的数组。

合并部分

每个部分递归划分后,下一个子步骤是将其合并到更大的已排序数组中。由于数组被分成四部分,合并比传统归并排序中的两部分合并更复杂。该算法使用三阶段合并过程

  • 它首先将第一季度(mid1)与最左边的部分(left)合并。
  • 之后,它取第二季度并将其与最后一个部分(right)合并。
  • 它得到上述两个合并的结果,这导致了一个完全排序的部分。
  • 辅助函数 mergeTwoSortedArrays 通过允许我们比较两个已排序的数组并将它们以排序的方式合并来帮助我们完成所有这些操作。它会一直执行,直到所有四个部分合并为一个已排序的数组。

复制合并的数组

之后,四个部分合并为一个已排序的数组,并将合并结果写入原始数组的原始(未合并)位置。它确保在每次递归步骤后,原始数组中都反映了最新的数组排序方法。该算法表现出直观的特性;随着递归的展开,数组对于更大的部分会变得有序,并且在算法结束时,整个数组都会被排序。

复杂度分析

时间复杂度

在基本递归分形排序中,递归涉及每个级别的划分(其中每个级别需要将四个部分分开)以及在合并过程中发生的递归,具有对数递归。每个递归步骤将数组分成 4 个部分,而不是像归并排序那样分成 2 个部分。分支因子增加,但整体递归深度是对数的。如果数组有 n 个元素,我们需要 4 次以该数组为底的对数才能达到基本情况(log₄(n)),其中数组的每个部分大小为 1。

合并在每个递归级别需要线性时间。将子数组加在一起分成四个部分似乎在每个步骤仍然是线性的合并,从而得到与归并排序相同的 O(nlogn) 时间,但会有不同的常数因子。它看起来与归并排序相似,但常数因子不同,因为它们是四部分划分。

空间复杂度

分形排序的空间复杂度是由于合并过程中使用的辅助空间。正在合并的四个部分在每个级别都存储在临时数组中。这些辅助数组的大小等于输入数组的大小,它们使用额外的内存。由于在不同级别递归地进行合并,因此有必要允许 O(n) 的额外空间来存储中间数组。这意味着总体空间复杂度为 O(n)。

方法-2:原地分形排序

原地分形排序是一种新颖的排序算法,它试图通过在原始数组中原地执行合并来减少额外的内存使用。与许多使用额外数组合并到另一个数组中的排序算法不同,原地分形排序充当一种算法,直接在原始数组内重新组织元素,以达到已排序数组的结果。在内存受限的环境中或当最大化可用空间的使用很重要时,此方法特别有利。

它应用分形排序概念,即将数组递归地分成四个段并进行排序。对段进行排序后,我们借助原地合并技术将其排序回原始数组。原地合并涉及对索引的仔细应用以进行比较和重新锁定,注意不要干扰元素的排序顺序。

原地分形排序利用递归划分和原地合并方法,将时间复杂度保持为 O(n log n),并将辅助空间复杂度保持为 O(1),这与其他高效排序算法相当或可比。这种独特的策略组合使其非常适合各种应用,并且是内存效率至关重要的排序任务的稳健解决方案。

程序

输出

 
Original array: 38 27 43 3 9 10 12 22 67 51 
Sorted array: 3 9 10 12 22 27 38 43 51 67   

说明

在原地分形排序的实现中,我们使用递归划分和原地合并来有效地排序给定的数组,同时使用最少的空间。当内存节约至关重要时,它特别有用。

核心职能

原地合并:此过程旨在在原地有效地合并数组的两个已排序部分,通过避免额外的存储需求来优化内存使用和性能。合并接收数组的两个片段:第一个从左到右,第二个从右到右,它在不分配任何额外空间的情况下合并它们。函数开始设置两个指针:我们有一个 leftIndex 和 rightIndex,一个用于左段,另一个用于右段。

它遍历这两个段并比较这两个指针处的元素。如果左段的值小于或等于右段,则增加左段的指针。但是,如果右元素的大小较小,则将其放置在左段的右位置。为了实现这一点,我们将左侧元素推到右侧,以便我们可以适应右元素。

保持排序顺序的关键是原地合并,这意味着它不使用额外的数组来实现 O(1) 的辅助空间复杂度。

递归划分:数组的递归划分由 inPlaceFractalSortUtil 函数处理。它使用三个计算出的中点:mid1、mid2 和 mid3 将输入数组分成四个段。然后,该函数递归地排序这四个段中的每一个,直到达到基本情况,即段只包含一个或零个元素,表明它们不再需要排序。

之后,在所有段都递归排序完成后,该函数使用全局定义的 inPlaceMerge 函数将所有段合并回来。这是一个特殊的顺序,以确保我们以一种排序的顺序正确地合并所有段。

主函数:排序操作通过公共接口 inPlaceFractalSort 定义。首先,它检查输入数组是否为空。如果不为空,则调用递归实用函数开始排序。此外,printArray 函数是一个实用函数,用于在排序之前和之后打印数组的内容以确认排序结果。

复杂度分析

时间复杂度

原地分形排序的时间复杂度为 O(n log n),这与一些高效方法(如归并排序和快速排序)相同。

递归划分:最后,每当发生这种情况时,数组都会被递归地分成四个段,直到每个段包含一个元素或没有元素。我们需要检查数组的大小是否为零,以确定是否需要将其分成更小的部分,但整个过程相对于数组的大小(log n)具有对数深度。

合并过程:递归级别的顺序是混乱的,因此每个递归级别都必须将排序的段合并回来。合并过程是线性时间 O(n),因为它在过程本身中比较和重新排列段内的元素。

整个问题的总时间复杂度仍然是 O(n log n),因为这两个阶段都在对数级别的数量(log n)上执行。

空间复杂度

原地分形排序的辅助空间复杂度。这是该算法的一个显著优势

  • 传统的归并排序使用额外的数组或数据结构来执行合并,而排序过程除了合并之外不需要任何其他数组或数据结构。
  • 合并直接在原始数组上执行,因此占用最少的内存来完成所有操作。