【问题标题】:Sorting linked lists in C [closed]在C中对链表进行排序[关闭]
【发布时间】:2011-11-02 08:20:44
【问题描述】:

我被要求编写一个函数,它接受 3 个未排序的链表并返回一个组合所有三个列表的排序链表。您能想到的最佳方式是什么?

我真的没有内存限制,但是有/没有内存限制你会怎么做?

【问题讨论】:

  • 添加了作业标签。无论如何,您如何对它们进行排序?按字母顺序,从小到大..?
  • 将 3 个列表连接在一起(列表 1 的尾部 -> 头部列表 2 等),那么您只有 1 个列表并简化为一个简单的排序功能。
  • 你知道哪些排序算法?
  • 如果排序需要是链表排序,那么使用指向节点的小指针数组的自底向上合并排序是最快的。 wiki example。由于节点一次合并到内部数组中,因此可以单独处理 3 个列表,也可以在合并到内部数组之前将其连接成一个列表。然后合并内部数组以形成单个排序列表。

标签: c algorithm sorting data-structures linked-list


【解决方案1】:

链表没有有效的排序算法。 创建一个数组、排序和重新链接。

【讨论】:

  • Err...mergesort 工作得很好。唯一的技巧是弄清楚如何有效地划分列表。例如:chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html .
  • @dmckee - 合并排序仅在列表已经排序的情况下才有效——在这种情况下,3 列表最初是未排序的,所以第 1 步是对链接列表进行排序,然后合并连接它们——如果内存不是问题,那么创建一个指针数组,对指针进行排序,然后创建一个新的链表会更有效。
  • Soren:mergesort 可以未排序的列表上使用,然后合并函数/方法/工具可以用来合并它们。跨度>
  • -1:我在我的 Haskell 程序中一直使用合并排序。它以Data.List.sort 为幌子。它在 O(nlogn) 中高效运行。
  • 顺便说一句:这将是获得disciplined badge 的好时机。
【解决方案2】:

如果这 3 个列表是单独排序的,问题会很简单,但如果不是这样,那就有点棘手了。

我会写一个函数,它接受一个排序列表和一个未排序列表作为参数,遍历未排序列表的每个项目,并依次将其添加到排序列表中的正确位置,直到未排序列表中没有剩余项目列表。

然后简单地创建第四个“空”列表,该列表本质上是“已排序”的,然后使用每个未排序列表调用您的方法三次。

将列表转换为数组可能会使事情在能够使用更高级的排序技术方面更有效率,但必须考虑转换为数组的成本并与原始列表的大小进行平衡。

【讨论】:

    【解决方案3】:

    一种选择是在所有三个链接列表上使用merge sort,然后使用最后一个合并步骤将它们合并到一个整体排序列表中。

    与大多数 O(n log n) 排序算法不同,归并排序可以在链表上高效运行。在高层次上,链表上的归并排序背后的直觉如下:

    1. 作为基本情况,如果列表包含零个或一个元素,则它已经排序。
    2. 否则:
      1. 将列表分成大小大致相等的两个列表,可能是将奇数元素移动到一个列表中,将偶数元素移动到另一个列表中。
      2. 递归地使用归并排序对这些列表进行排序。
      3. 应用merge 步骤将这些列表组合成一个排序列表。

    链表上的合并算法真的很漂亮。伪代码大致如下:

    1. 初始化一个保存结果的空链表。
    2. 只要两个列表都不为空:
      1. 如果第一个列表的第一个元素小于第二个列表的第一个元素,则将其移到结果列表的后面。
      2. 否则,将第二个列表的第一个元素移到结果列表的后面。
    3. 现在正好有一个列表是空的,将第二个列表中的所有元素移到结果列表的后面。

    这可以在O(n)时间内运行,所以归并排序的整体复杂度是O(n log n)。

    将所有三个列表单独排序后,您可以应用合并算法将三个列表合并为一个最终排序列表。或者,您可以考虑将所有三个链表连接在一起,然后使用巨大的合并排序通道同时对所有列表进行排序。没有明确的“正确方法”来做到这一点。这完全取决于你。

    上述算法运行时间为 Θ(n log n)。它也只使用 Θ(log n) 内存,因为它不分配新的链表单元,并且只需要每个堆栈帧中的空间来存储指向各个列表的指针。由于递归深度为 Θ(log n),因此内存使用量也是 Θ(log n)。


    另一个可以在链表上实现的 O(n log n) 排序是对quicksort 的修改。尽管快速排序的链表版本很快(仍然预期为 O(n log n)),但由于缺少连续存储的数组元素的局部性影响,它不如在数组上工作的就地版本快.但是,它是一种非常漂亮的算法,适用于列表。

    快速排序背后的直觉如下:

    1. 如果您有一个零元素或一元素列表,则该列表已排序。
    2. 否则:
      1. 选择列表中的某个元素用作枢轴。
      2. 将列表分为三组 - 小于基准的元素、等于基准的元素和大于基准的元素。
      3. 递归排序更小和更大的元素。
      4. 连接三个列表,依次为小、相等、大,以返回整个排序列表。

    快速排序的链表版本的优点之一是分区步骤比数组的情况要容易得多。在您选择了一个主元后(稍后会详细介绍),您可以通过为小于、等于和大于列表创建三个空列表来执行分区步骤,然后对原始链接进行线性扫描列表。然后,您可以将每个链表节点附加/前置到与原始存储桶对应的链表中。

    让这项工作发挥作用的一个挑战是选择一个好的枢轴元素。众所周知,如果枢轴的选择不好,快速排序会退化到 O(n2) 时间,但众所周知,如果随机选择一个枢轴元素,则运行时间为 O(n log n) 概率很高。在数组中这很容易(只需选择一个随机数组索引),但在链表的情况下更棘手。最简单的方法是在 0 和列表长度之间选择一个随机数,然后在 O(n) 时间内选择列表中的那个元素。或者,有一些非常酷的方法可以从链表中随机选择一个元素; one such algorithm 在这里描述。


    如果你想要一个只需要 O(1) 空间的更简单的算法,你也可以考虑使用insertion sort 对链表进行排序。虽然插入排序更容易实现,但它在最坏情况下运行时间为 O(n2)(尽管它也有 O(n) 最好情况的行为),所以它可能不是一个好的选择除非您特别想避免合并排序。

    插入排序算法背后的思想如下:

    1. 初始化一个保存结果的空链表。
    2. 对于三个链表中的每一个:
      1. 虽然该链接列表不为空:
        1. 扫描结果列表以查找此链接列表的第一个元素所属的位置。
        2. 在该位置插入元素。

    另一个可以适应链表的 O(n2) 排序算法是selection sort。使用这个算法可以很容易地实现(假设你有一个双向链表):

    1. 初始化一个保存结果的空列表。
    2. 当输入列表不为空时:
      1. 扫描整个链表,寻找最小的剩余元素。
      2. 从链表中删除该元素。
      3. 将该元素附加到结果列表中。

    这也运行在 O(n2) 时间并且只使用 O(1) 空间,但实际上它比插入排序慢;特别是,它总是在 Θ(n2) 时间内运行。


    根据链接列表的结构方式,您可能能够摆脱一些非常棒的技巧。特别是,如果给定双重链表,那么每个链表单元格中都有两个指针的空间。鉴于此,您可以重新解释这些指针的含义,以执行一些非常荒谬的排序技巧。

    作为一个简单的例子,让我们看看如何使用链表单元实现tree sort。思路如下。当链表单元存储在链表中时,next 和previous 指针具有它们原来的含义。但是,我们的目标是迭代地将链表单元从链表中拉出,然后将它们重新解释为二叉搜索树中的节点 a,其中下一个指针表示“右子树”,前一个指针表示“左子树”。如果允许这样做,这里有一个非常酷的实现树排序的方法:

    1. 创建一个指向链表单元的新指针,该指针将用作指向树根的指针。
    2. 对于双向链表的每个元素:
      1. 从链接列表中删除该单元格。
      2. 将该单元格视为 BST 节点,将该节点插入到二叉搜索树中。
    3. 按顺序遍历 BST。每当您访问一个节点时,将其从 BST 中删除并将其重新插入到双向链表中。

    这运行在最佳情况 O(n log n) 时间和最坏情况 O(n2) 时间。在内存使用方面,前两个步骤只需要 O(1) 内存,因为我们正在从旧指针中回收空间。最后一步也可以使用一些特别聪明的算法在 O(1) 空间中完成。

    您也可以考虑以这种方式实现heap sort,尽管这有点棘手。


    希望这会有所帮助!

    【讨论】:

    • 这就是我的意思,但你说得比我好得多! :O)
    • 除非我弄错了,否则当链表是双向链接时,qsort 运行良好/正常。可能想指出这一点。
    • @trinithis- 是的,这是真的。我主要是指出,在处理数组情况时,快速排序并没有从局部性获得巨大的性能提升,这导致它优于所有其他 O(n log n) 排序。
    • 你能解释一下合并步骤的时间复杂度吗?
    • @CodeYogi 合并两个排序的链表,就像合并两个排序的数组一样,可以在 O(n) 时间内完成,其中 n 是列表中元素的总数。你只需要存储尾指针。
    【解决方案4】:

    我在想你可以应用快速排序。它与归并排序几乎相同,唯一的区别是您先拆分然后合并,其中快速排序您首先“合并”然后进行拆分。如果你看起来没什么不同,那就是相反方向的合并排序快速排序

    合并排序:

    拆分->递归->合并

    快速排序:

    umnerge(与合并相反)-> 递归-> 连接(与拆分相反)

    【讨论】:

      【解决方案5】:

      @templatetypedef 在热门帖子中描述的归并排序算法在 O(n lg n) 中不起作用。因为链表不是随机访问的,所以步骤 2.1 Split the list into two lists of roughly equal size 实际上意味着 O(n^2 log n) 对链表进行排序的整体算法。稍微考虑一下。

      这是一个链接,它使用合并排序通过首先将元素读入数组来对链表进行排序 -- http://www.geekviewpoint.com/java/singly_linked_list/sort

      【讨论】:

      • 正如我在对您对另一个问题的回答的评论中提到的那样,这不是真的。进行拆分所需的线性工作不会增加渐近运行时间,因为在合并步骤中已经完成了线性工作。递归关系完全相同。从历史上看,合并排序是为了在磁带驱动器上工作而发明的(在许多方面,它的功能类似于链表)并改进了简单的 O(n^2) 排序,这就是它被使用的原因。
      猜你喜欢
      • 2010-12-09
      • 1970-01-01
      • 2012-08-02
      • 2012-10-07
      • 2022-10-04
      • 1970-01-01
      • 1970-01-01
      • 2015-10-31
      • 1970-01-01
      相关资源
      最近更新 更多