【问题标题】:Replacement Selection Sort v. Selection Sort替换选择排序与选择排序
【发布时间】:2013-04-25 22:55:22
【问题描述】:

我一直在对替换选择排序进行一些研究,但我在任何地方都找不到它的任何实现或替换选择排序的良好、彻底的实现!也许我看起来不够努力,但谷歌将替换选择排序与选择排序混淆了......所以这让我想知道:

选择排序和替换选择排序之间的真正区别是什么?

在哪里可以找到替换选择排序的实现(或编写指南)?

替换选择排序的哪些特点使其比其他排序算法更受欢迎?

这个算法还有其他名字吗?

【问题讨论】:

    标签: algorithm sorting data-structures selection-sort


    【解决方案1】:

    我之前没有看过这个算法的详细描述,我的分析基于我从阅读this set of lecture notes 中收集到的内容。

    据我了解,选择排序和替换选择排序之间的主要区别在于,选择排序旨在对主内存中保存的完整序列进行排序,而替换选择排序旨在将未排序的序列转换为太大而无法放入主内存记忆成一系列可以存储在外部存储器中的排序序列的“链”。然后可以将这些外部链合并在一起以形成整个排序序列。尽管它们的名称和算法操作的一两个关键步骤相似,但它们旨在解决根本不同的问题。

    选择排序

    网上有很多关于选择排序的好教程,所以我不会花太多时间讨论它。直观地说,该算法的工作原理如下:

    • 找到最小的元素并将其交换到数组的位置 0。
    • 找到第二小的元素并将其交换到数组的位置 1。
    • 找到第三小的元素并将其交换到数组的位置 2
    • ...
    • 找到第 n 个最小的元素并将其交换到数组的第 n - 1 位。

    这假设数组可以完全保存在内存中,如果是这种情况,则该算法在 Θ(n2) 时间内运行。这不是很快,并且对于大型数据集是不可取的。

    替换选择排序

    此算法由 Donald Knuth 于 1965 年描述,因此它被设计为在与我们目前习惯的计算环境截然不同的计算环境中工作。计算机的内存很少(通常是一些固定数量的寄存器),但可以访问大型外部驱动器。构建将一些值加载到寄存器中,在那里处理它们,然后将它们直接刷新回外部存储的算法是很常见的。 (有趣的是,这类似于处理器现在的工作方式,不同之处在于使用主存储器而不是外部存储)。

    假设我们有足够的内存空间来保存两个数组:第一个数组Values,大小为 n,可以保存一堆值,第二个数组Active,大小为 n,保存布尔值。我们将尝试获取大量未排序值的输入流并尽最大努力对其进行排序,因为我们只有足够的内存空间来保存 ActiveValues 数组,以及一些额外的变量用于存储空间。

    算法背后的思想如下。首先,将包含未排序序列的外部源中的n 值直接加载到Values 数组中。然后,将所有Active 值设置为true。例如,如果n = 4,我们可能会有这样的设置:

    Values:    4    1    0    3
    Active:    Yes  Yes  Yes  Yes
    

    替换选择排序算法通过重复查找Values 数组中的最小值并将其写入输出流来工作。在这种情况下,我们首先找到 0 值并将其写入流。这给了

    Values:    4    1         3
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0
    

    现在,我们的Values 数组中有一个空白点,因此我们可以从外部源中提取另一个值。假设我们得到 2。在这种情况下,我们有这样的设置:

    Values:    4    1    2    3
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0
    

    请注意,由于 2 > 0 并且 0 是这里的最小元素,我们保证当我们将 0 写入输出时,2 不应该出现在它之前。那挺好的。因此,我们继续算法的下一步,再次在这里找到最小的元素。那是1,所以我们将它发送到输出设备:

    Values:    4         2    3
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0  1
    

    现在,从外部读取另一个值:

    Values:    4    -1   2    3
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0  1
    

    现在我们遇到了麻烦。这个新值 (-1) 低于 1,这意味着如果我们真的希望这个值按排序顺序进入输出,它应该在 1 之前。但是,我们没有足够的内存来重新读取输出设备并修复它。相反,我们将执行以下操作。现在,让我们将 -1 保留在内存中。我们将尽最大努力对剩余的元素进行排序,但是当我们这样做时,我们将进行 second 次迭代以生成排序序列,并将 -1 放入那个序列。换句话说,我们将生成两个,而不是生成一个排序序列。

    为了在内存中表明我们还不想写出 -1,我们将持有 -1 的插槽标记为非活动状态。此处显示:

    Values:    4    -1   2    3
    Active:    Yes  NO   Yes  Yes
    
    Output:    0  1
    

    从现在开始,我们就假装 -1 不在这里。

    让我们继续前进。我们现在找到内存中仍然处于活动状态的最小值 (2),并将其写入设备:

    Values:    4    -1        3
    Active:    Yes  NO   Yes  Yes
    
    Output:    0  1  2
    

    我们现在从输入设备中提取下一个值。让我们假设它是 7:

    Values:    4    -1   7    3
    Active:    Yes  NO   Yes  Yes
    
    Output:    0  1  2
    

    由于 7 > 2,它在输出中的 2 之后,所以我们什么都不做。

    在下一次迭代中,我们找到最低的活跃值 (3) 并将其写出:

    Values:    4    -1   7    
    Active:    Yes  NO   Yes  Yes
    
    Output:    0  1  2  3
    

    我们从输入设备中提取下一个值。假设它是also 3。在这种情况下,我们知道由于 3 是最小值,我们可以直接将其写入输出流,因为 3 是这里所有值中的最小值,我们可以为自己节省一次迭代:

    Values:    4    -1   7    
    Active:    Yes  NO   Yes  Yes
    
    Output:    0  1  2  3  3
    

    我们现在从输入设备中提取下一个值。假设它是 2。在这种情况下,和以前一样,我们知道 2 应该在 3 之前。就像前面的 -1 一样,这意味着我们现在需要将 2 保存在内存中;我们稍后会写出来。现在我们的设置如下所示:

    Values:    4    -1   7    2
    Active:    Yes  NO   Yes  NO
    
    Output:    0  1  2  3  3
    

    现在,我们找到最小的有效值 (4) 并将其写入输出设备:

    Values:         -1   7    2
    Active:    Yes  NO   Yes  NO
    
    Output:    0  1  2  3  3  4
    

    假设我们现在读入一个 1 作为我们的下一个输入。因此,我们将其放入Values,但将其标记为非活动:

    Values:    1    -1   7    2
    Active:    NO   NO   Yes  NO
    
    Output:    0  1  2  3  3  4
    

    只有一个有效值,即 7,所以我们把它写出来:

    Values:    1    -1        2
    Active:    NO   NO   Yes  NO
    
    Output:    0  1  2  3  3  4  7
    

    假设我们现在读取了一个 5。在这种情况下,像以前一样,我们存储它但将插槽标记为非活动:

    Values:    1    -1   5    2
    Active:    NO   NO   NO   NO
    
    Output:    0  1  2  3  3  4  7
    

    请注意,所有值现在都处于非活动状态。这意味着我们已经从内存中清除了所有可以进入当前输出运行的值。现在,我们需要写出我们稍后持有的所有值。为此,我们将所有值标记为活动,然后像以前一样重复:

    Values:    1    -1   5    2
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0  1  2  3  3  4  7
    

    -1是最小值,所以我们输出它:

    Values:    1         5    2
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0  1  2  3  3  4  7  -1
    

    假设我们读取了一个 3.-1 Values 数组中。

    Values:    1    3    5    2
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0  1  2  3  3  4  7 -1
    

    1是这里的最小值,所以我们去掉它:

    Values:         3    5    2
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0  1  2  3  3  4  7 -1  1
    

    假设我们现在没有输入值。我们会将此插槽标记为已完成:

    Values:    ---  3    5    2
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0  1  2  3  3  4  7 -1  1
    

    接下来是 2 个:

    Values:    ---  3    5    ---
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0  1  2  3  3  4  7 -1  1  2
    

    然后 3:

    Values:    ---  ---  5    ---
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0  1  2  3  3  4  7 -1  1  2  3
    

    最后,5:

    Values:    ---  ---  ---  ---
    Active:    Yes  Yes  Yes  Yes
    
    Output:    0  1  2  3  3  4  7 -1  1  2  3  5
    

    我们完成了!请注意,生成的序列 没有 排序,但它比以前好很多。它现在由按排序顺序的两条链组成。将它们合并在一起(与我们为 mergesort 进行合并的方式相同)将对结果数组进行排序。这个算法可能会产生更多的链,但由于我们的样本输入很小,我们只有两个。


    那么这有多快?好吧,循环的每次迭代总共最多进行 n 次比较(在内存中),一次读取和一次写入。因此,如果流中有 N 个总值,算法会进行 O(nN) 次比较和 O(N) 次内存操作。如果内存操作代价高昂,这也不算太糟糕,尽管最后您需要第二次通过才能将所有内容重新合并在一起。

    在伪代码中,算法如下所示:

    Make Values an array of n elements.
    Make Active an array of n booleans, all initially true.
    
    Read n values from memory into Values.
    Until no values are left to process:
        Find the smallest value that is still active.
        Write it to the output device.
        Read from the input device into the slot where the old element was.
        If it was smaller than the old element, mark the old slot inactive.
        If all slots are inactive, mark them all active.
    

    如果现在有任何理由编写此算法,我会感到震惊。几十年前,当内存非常非常小的时候,这是有道理的。如今,有更好的external sorting algorithms 可用,它们几乎肯定会胜过这个算法。

    希望这会有所帮助!

    【讨论】:

    • 谢谢!!这是迄今为止我见过的最好的解释并帮助我理解和可视化算法,在我的情况下唯一的区别是一旦你用完内存(所有寄存器都被标记为忽​​略)然后它会创建一个新的输出分区将下一个条目添加到相同的输出分区。
    • 我刚刚完成了我的硕士论文,它使用替换选择而不是快速排序来打乱 Hadoop 映射阶段的中间结果。感兴趣的朋友:inf.ufrgs.br/~pmdusso/works/…
    • 很好的解释。原来在 PostgreSQL 中使用了这个算法:wiki.postgresql.org/images/5/59/Sorting_through_the_ages.pdf
    • 猜猜我从未听说过这个算法的原因是,如果某些东西太大而无法放入你的记忆中,你就不会使用二次排序。
    【解决方案2】:

    替换排序仍然用于外部排序,因为它会产生最少数量的要合并的字符串,而合并是排序中成本最高的部分。使用的方法比提供的优秀示例 templatetypedef 稍微复杂一些,但基本上它缓冲许多记录,对它们进行排序,收集像 -1 1 等无序记录并将它们保存在缓冲区中,写出低阶部分序列,填充空的空间,再次排序,从缓冲区合并任何适合的记录,并重复直到序列不能继续,然后它转储序列,从序列记录和新的输入记录中取出缓冲,从下一个字符串开始。重复到输入结束。

    在一些应用程序中,不需要合并,因为替换排序会扫除序列记录,然后稍后将它们重新插入正确的位置。 1964 年,当霍尼韦尔和 IBM 推出这种类型时,这让许多商业客户感到惊讶。

    【讨论】:

      猜你喜欢
      • 2013-03-25
      • 2023-03-20
      • 1970-01-01
      • 2018-10-29
      • 1970-01-01
      • 1970-01-01
      • 2019-04-10
      • 2012-10-04
      • 2021-11-20
      相关资源
      最近更新 更多