前一段时间,我做了一个梦
那是一个伸手不见五指的深夜,身后是日伪军紧追不舍的脚步声。慌不择路间,一座巍峨古刹赫然耸立。前有古刹幽深,后有追兵逼近,我把心一横,咬牙翻墙而入。
刚进院子,便觉头顶阴风阵阵。抬头一看,夜空中密密麻麻的黑点正盘旋而下——是蝙蝠。起初看着不过麻雀大小,谁知随着高度降低,它们竟迎风暴涨,落地时已有人高,双翼收拢,俨然几分人形。
直觉警铃大作。我紧贴着大柱子,死死以此掩护,根本不敢与那些庞然大物对视(脑海里疯狂闪回《Discovery 探索频道》的警告:直视野兽眼睛等于挑衅)。我屏住呼吸,硬着头皮挪步,心里疯狂默念:“佛祖保佑!佛门净地,不杀生,不吃人……”
挪了一阵,我发现前方竟有一群巨型蝙蝠,正井然有序地排队,似乎在领取某种补给。
“施主莫慌。”
突然,一只手搭上了我的肩膀!我魂飞魄散地回头,却见一位面容清秀的小沙弥。他微微一笑,云淡风轻:“施主面生,想必初来乍到。这些巨蝠乃敝寺护法,性情温顺。追兵已被它们吓退。此刻它们排队,不过是为了领赏最爱的零食——‘尸净’。”
说着,他掏出一块通体晶莹、色彩斑斓的人形胶状体。
还没等我反应过来,他便热情地邀我参观后厨。一进屋,我的世界观崩塌了……所谓的炊具,竟是一口马桶造型的巨锅!锅里咕嘟冒泡,熬着一锅五颜六色的不明杂烩。
小沙弥自豪地介绍:“这可是纯天然工艺,无需食材,只需注入‘净尸水’——也就是洗干净尸体的水,熬足七七四十九小时即可成胶。”(胃里瞬间一阵翻江倒海……)
怕什么来什么,说话间,小沙弥舀了满满一大勺,热情地递到我嘴边。为了不吃这碗“孟婆汤”级别的料理,我在生死存亡之际灵机一动,强行转移话题:
“大师且慢!我看蝙蝠领餐虽有序,但食量大小不一,这就导致发餐效率低下。为何不按食量预先排序呢?”
小沙弥愣了一下,随即眼睛一亮:“施主所言极是!但这排序有个难处——时间复杂度必须是 ,空间复杂度必须是常量 。”
话音未落,我猛然惊醒。冷汗未干,但本着对梦中小沙弥负责的态度,我意识到:这分明就是 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};
}
};