【不三不四的脑洞】一个梦所引发关于排序算法的思考

453 阅读5分钟

在这里插入图片描述

前一段时间,我做了一个梦

那是一个伸手不见五指的深夜,身后是日伪军紧追不舍的脚步声。慌不择路间,一座巍峨古刹赫然耸立。前有古刹幽深,后有追兵逼近,我把心一横,咬牙翻墙而入。

在这里插入图片描述 刚进院子,便觉头顶阴风阵阵。抬头一看,夜空中密密麻麻的黑点正盘旋而下——是蝙蝠。起初看着不过麻雀大小,谁知随着高度降低,它们竟迎风暴涨,落地时已有人高,双翼收拢,俨然几分人形。

在这里插入图片描述 直觉警铃大作。我紧贴着大柱子,死死以此掩护,根本不敢与那些庞然大物对视(脑海里疯狂闪回《Discovery 探索频道》的警告:直视野兽眼睛等于挑衅)。我屏住呼吸,硬着头皮挪步,心里疯狂默念:“佛祖保佑!佛门净地,不杀生,不吃人……”

挪了一阵,我发现前方竟有一群巨型蝙蝠,正井然有序地排队,似乎在领取某种补给。

在这里插入图片描述

“施主莫慌。”

突然,一只手搭上了我的肩膀!我魂飞魄散地回头,却见一位面容清秀的小沙弥。他微微一笑,云淡风轻:“施主面生,想必初来乍到。这些巨蝠乃敝寺护法,性情温顺。追兵已被它们吓退。此刻它们排队,不过是为了领赏最爱的零食——‘尸净’。”

说着,他掏出一块通体晶莹、色彩斑斓的人形胶状体。

还没等我反应过来,他便热情地邀我参观后厨。一进屋,我的世界观崩塌了……所谓的炊具,竟是一口马桶造型的巨锅!锅里咕嘟冒泡,熬着一锅五颜六色的不明杂烩。

在这里插入图片描述 小沙弥自豪地介绍:“这可是纯天然工艺,无需食材,只需注入‘净尸水’——也就是洗干净尸体的水,熬足七七四十九小时即可成胶。”(胃里瞬间一阵翻江倒海……)

怕什么来什么,说话间,小沙弥舀了满满一大勺,热情地递到我嘴边。为了不吃这碗“孟婆汤”级别的料理,我在生死存亡之际灵机一动,强行转移话题:

在这里插入图片描述

“大师且慢!我看蝙蝠领餐虽有序,但食量大小不一,这就导致发餐效率低下。为何不按食量预先排序呢?”

小沙弥愣了一下,随即眼睛一亮:“施主所言极是!但这排序有个难处——时间复杂度必须是 O(nlogn)O(n \log n),空间复杂度必须是常量 O(1)O(1)。”

话音未落,我猛然惊醒。冷汗未干,但本着对梦中小沙弥负责的态度,我意识到:这分明就是 LeetCode 148:排序链表! 听到这里,我突然醒了,但是本着对小沙弥负责的态 度,我还是敏感地发觉这是一个 LeetCode 148 相关的排序问题

在这里插入图片描述 所以

如何才能满足上述要求对蝙蝠进行排序?

在这里插入图片描述

首先,通过查阅相关资料,常见的几种排序算法的性能如下

方法容器时间复杂度空间复杂度
插入排序数组/链表O(n^2)O(1)
堆排序数组/链表/链表O(nlogn)/O(nlogn)/O(n^2logn)O(1)/O(n)/O(1)
快速排序数组/链表O(nlogn)~O(n^2)O(logn)~O(n)
归并排序(自上而下)数组/链表O(nlogn)/O(nlogn)O(n+logn)/O(logn)
归并排序(自下而上)数组/链表O(nlogn)/O(nlogn) O(n)/O(1)

从上表可知,堆排序在 数组 情况下、归并排序(自下而上)在 链表 情况下都满足以上条件。我这里着重介绍一下 归并排序(自下而上)

在这里插入图片描述

  • 代码和详细注释如下
/// @note 代码原作者: Huahua
/// 详细注释:ShaderJoy

/// @note 一只蝙蝠结点
struct ListNode{
    int val;        ///< 蝙蝠食量
    ListNode *next; ///< 后一只蝙蝠
    ListNode(int x): val(x), next(NULL){}
};

class Solution {
public:
  ListNode* sortList(ListNode* head) {
    /// @note 完成状态,只剩 0 或者 1 只蝙蝠
    if (!head || !head->next) return head;
    
    int len = 1;
    ListNode* cur = head;
    while (cur = cur->next) ++len; ///< 首先统计队列中有多少只蝙蝠
    
    ///@note 工具结点
    ListNode dummy(0); 
    dummy.next = head; ///< 它的后面保存的是头蝙蝠
    ListNode* l;
    ListNode* r;
    ListNode* tail;
    
    /// @note n 表示每个分组的蝙蝠个数
    /// 每次迭代后 n 都会翻倍
    /// 保证执行 log(len) 次
    for (int n = 1; n < len; n <<= 1) {      
      cur = dummy.next; ///< 当前处理的队头蝙蝠(已经部分排序的头)
      tail = &dummy; ///< 队尾
      while (cur) {
        l = cur;
        r = split(l, n);            ///< 这两行代码将当前列表分割两次,结果是 l 和 r 都是 n 个蝙蝠
        cur = split(r, n);          ///< 而此时 cur 指向了 l 和 r 的后面
        auto merged = merge(l, r);  ///< 将两个列表进行合并(并从小到大排序)
        tail->next = merged.first;  ///< 合并且有序的 head 接到 tail 的后面
        tail = merged.second;       ///< 合并且有序的 tail 成为新的 tail
      }
    }      
    return dummy.next;
  }
private:
  /// @note 将列表分成两部分,前 n 个元素和其余元素。
  /// 返回其余部分的头。
  ListNode* split(ListNode* head, int n) {    
    /// @note 往后走 n 步
    while (--n && head)
      head = head->next;
    /// @note 把 rest 保存此时的 head 后方(如果 head 存在的话)
    ListNode* rest = head ? head->next : nullptr;
    /// @note 断开 head 后方
    if (head) head->next = nullptr;
    return rest;
  }
  
  /// @note 合并两个列表,返回合并后列表的头和尾。
  pair<ListNode*, ListNode*> merge(ListNode* l1, ListNode* l2) {
    ListNode dummy(0);
    ListNode* tail = &dummy;
    while (l1 && l2) {
      /// @note 将小的放在 l1,大的放在 l2 (所以 while 循环中始终只要操作 l1 和 tail)
      if (l1->val > l2->val) swap(l1, l2);
      tail->next = l1;   ///< 保存 l1 为 tail 的下一位
      l1 = l1->next;     ///< l1 后移一位(继续和 l2 比较)
      tail = tail->next; ///< tail 也后移一位
    }
    tail->next = l1 ? l1 : l2; ///< 善后 l1, l2
    while (tail->next) tail = tail->next; ///< 并让 tail 指向合并后队列的尾部
    return {dummy.next, tail};
  }
};