一种选择是在所有三个链接列表上使用merge sort,然后使用最后一个合并步骤将它们合并到一个整体排序列表中。
与大多数 O(n log n) 排序算法不同,归并排序可以在链表上高效运行。在高层次上,链表上的归并排序背后的直觉如下:
- 作为基本情况,如果列表包含零个或一个元素,则它已经排序。
- 否则:
- 将列表分成大小大致相等的两个列表,可能是将奇数元素移动到一个列表中,将偶数元素移动到另一个列表中。
- 递归地使用归并排序对这些列表进行排序。
- 应用merge 步骤将这些列表组合成一个排序列表。
链表上的合并算法真的很漂亮。伪代码大致如下:
- 初始化一个保存结果的空链表。
- 只要两个列表都不为空:
- 如果第一个列表的第一个元素小于第二个列表的第一个元素,则将其移到结果列表的后面。
- 否则,将第二个列表的第一个元素移到结果列表的后面。
- 现在正好有一个列表是空的,将第二个列表中的所有元素移到结果列表的后面。
这可以在O(n)时间内运行,所以归并排序的整体复杂度是O(n log n)。
将所有三个列表单独排序后,您可以应用合并算法将三个列表合并为一个最终排序列表。或者,您可以考虑将所有三个链表连接在一起,然后使用巨大的合并排序通道同时对所有列表进行排序。没有明确的“正确方法”来做到这一点。这完全取决于你。
上述算法运行时间为 Θ(n log n)。它也只使用 Θ(log n) 内存,因为它不分配新的链表单元,并且只需要每个堆栈帧中的空间来存储指向各个列表的指针。由于递归深度为 Θ(log n),因此内存使用量也是 Θ(log n)。
另一个可以在链表上实现的 O(n log n) 排序是对quicksort 的修改。尽管快速排序的链表版本很快(仍然预期为 O(n log n)),但由于缺少连续存储的数组元素的局部性影响,它不如在数组上工作的就地版本快.但是,它是一种非常漂亮的算法,适用于列表。
快速排序背后的直觉如下:
- 如果您有一个零元素或一元素列表,则该列表已排序。
- 否则:
- 选择列表中的某个元素用作枢轴。
- 将列表分为三组 - 小于基准的元素、等于基准的元素和大于基准的元素。
- 递归排序更小和更大的元素。
- 连接三个列表,依次为小、相等、大,以返回整个排序列表。
快速排序的链表版本的优点之一是分区步骤比数组的情况要容易得多。在您选择了一个主元后(稍后会详细介绍),您可以通过为小于、等于和大于列表创建三个空列表来执行分区步骤,然后对原始链接进行线性扫描列表。然后,您可以将每个链表节点附加/前置到与原始存储桶对应的链表中。
让这项工作发挥作用的一个挑战是选择一个好的枢轴元素。众所周知,如果枢轴的选择不好,快速排序会退化到 O(n2) 时间,但众所周知,如果随机选择一个枢轴元素,则运行时间为 O(n log n) 概率很高。在数组中这很容易(只需选择一个随机数组索引),但在链表的情况下更棘手。最简单的方法是在 0 和列表长度之间选择一个随机数,然后在 O(n) 时间内选择列表中的那个元素。或者,有一些非常酷的方法可以从链表中随机选择一个元素; one such algorithm 在这里描述。
如果你想要一个只需要 O(1) 空间的更简单的算法,你也可以考虑使用insertion sort 对链表进行排序。虽然插入排序更容易实现,但它在最坏情况下运行时间为 O(n2)(尽管它也有 O(n) 最好情况的行为),所以它可能不是一个好的选择除非您特别想避免合并排序。
插入排序算法背后的思想如下:
- 初始化一个保存结果的空链表。
- 对于三个链表中的每一个:
- 虽然该链接列表不为空:
- 扫描结果列表以查找此链接列表的第一个元素所属的位置。
- 在该位置插入元素。
另一个可以适应链表的 O(n2) 排序算法是selection sort。使用这个算法可以很容易地实现(假设你有一个双向链表):
- 初始化一个保存结果的空列表。
- 当输入列表不为空时:
- 扫描整个链表,寻找最小的剩余元素。
- 从链表中删除该元素。
- 将该元素附加到结果列表中。
这也运行在 O(n2) 时间并且只使用 O(1) 空间,但实际上它比插入排序慢;特别是,它总是在 Θ(n2) 时间内运行。
根据链接列表的结构方式,您可能能够摆脱一些非常棒的技巧。特别是,如果给定双重链表,那么每个链表单元格中都有两个指针的空间。鉴于此,您可以重新解释这些指针的含义,以执行一些非常荒谬的排序技巧。
作为一个简单的例子,让我们看看如何使用链表单元实现tree sort。思路如下。当链表单元存储在链表中时,next 和previous 指针具有它们原来的含义。但是,我们的目标是迭代地将链表单元从链表中拉出,然后将它们重新解释为二叉搜索树中的节点 a,其中下一个指针表示“右子树”,前一个指针表示“左子树”。如果允许这样做,这里有一个非常酷的实现树排序的方法:
- 创建一个指向链表单元的新指针,该指针将用作指向树根的指针。
- 对于双向链表的每个元素:
- 从链接列表中删除该单元格。
- 将该单元格视为 BST 节点,将该节点插入到二叉搜索树中。
- 按顺序遍历 BST。每当您访问一个节点时,将其从 BST 中删除并将其重新插入到双向链表中。
这运行在最佳情况 O(n log n) 时间和最坏情况 O(n2) 时间。在内存使用方面,前两个步骤只需要 O(1) 内存,因为我们正在从旧指针中回收空间。最后一步也可以使用一些特别聪明的算法在 O(1) 空间中完成。
您也可以考虑以这种方式实现heap sort,尽管这有点棘手。
希望这会有所帮助!