C++如何实现单向链表的快速排序_C++链表指针交换算法详解【面试】

2次阅读

不能直接套用数组快排的partition逻辑,因为单向链表无法随机访问和反向遍历,强行模拟会导致o(n²)时间复杂度及边界错误;正确做法是用三路划分+哑节点仅遍历一次完成归类,并严格处理断链与拼接。

C++如何实现单向链表的快速排序_C++链表指针交换算法详解【面试】

为什么不能直接套用数组快排的 partition 逻辑

数组快排依赖随机访问(arr[i]),而单向链表只能从头逐个遍历。若强行模拟“取中位数”或“双向扫描交换”,会因无法反向移动指针导致时间退化为 O(n²),且边界判断极易出错。

关键约束:只能用 next 指针前进,没有 prev;每次比较和移动都需额外遍历,必须避免重复扫描。

  • 错误做法:在 partition 中反复从 head 往后找第 k 个节点——单次 partition 就变成 O(n²)
  • 正确思路:用「三路划分」代替传统双路,只做一次遍历完成小于/等于/大于基准的归类
  • 基准选法建议固定用头节点值(head->val),避免为找中位数额外遍历

如何用三个哑节点完成一趟 partition(无内存分配)

不 new 节点、不复制数据,仅重连指针。用三个哑节点分别收集小于、等于、大于基准的节点:

ListNode dummy_lt(0), dummy_eq(0), dummy_gt(0); ListNode *lt = &dummy_lt, *eq = &dummy_eq, *gt = &dummy_gt;

遍历原链表时,把每个节点摘下后挂到对应哑节点尾部,最后拼接:lt->next = dummy_eq.next; eq->next = dummy_gt.next;

立即学习C++免费学习笔记(深入)”;

  • 注意:必须保存 next 指针再移动当前节点,否则链断裂:ListNode* next = cur->next; cur->next = nullptr;
  • 拼接前要确保各段末尾为 nullptr,否则可能成环(尤其 dummy_gt 段为空时)
  • 返回时跳过哑节点:return dummy_lt.next ? dummy_lt.next : (dummy_eq.next ? dummy_eq.next : dummy_gt.next);

递归边界与断链处理的关键细节

单向链表无法像数组那样靠索引切分,必须显式断开子链首尾。常见崩溃点是忘记置空最后一个节点的 next,导致子链仍连着后续无关节点。

  • 递归前必须分离三段:获取 lt_taileq_tailgt_tail,并设 lt_tail->next = eq_tail->next = gt_tail->next = nullptr;
  • 空链表或单节点直接返回,但要注意:单节点时 eq 段有值,ltgt 均为空,此时不能对空段递归
  • 递归调用应只传非空段的头节点:sortList(dummy_lt.next),而非 &dummy_lt

面试时最容易被追问的性能与稳定性问题

这个实现平均 O(n log n),但最坏仍是 O(n²)(已排序链表选头为基准)。稳定?否——相同值节点的相对顺序在 eq 段内保持,但 ltgt 段来自不同原始位置,整体不稳定。

  • 优化方向:随机选基准需先遍历计数再随机跳转,代价高;改用「中位数的中位数」过于复杂,面试中提一句即可
  • 空间复杂度是 O(log n)(递归),不是 O(1);若要求 O(1) 空间,得改写为迭代+模拟栈,极难且非常规
  • 真正容易翻车的是:没处理空指针解引用(如 head->next 前未判 head 是否为空)、拼接后忘记返回新头节点

链表快排的核心不在“快”,而在“如何用有限指针操作规避随机访问缺陷”。每一步断链、挂载、拼接,都得盯着 next 指针的归属和生命周期。

text=ZqhQzanResources