程序员必知:单链表排序(快速排序、归并排序)

简介: 程序员必知:单链表排序(快速排序、归并排序)

本题目来源于LeetCode,具体如下:

Sort a linked list in O(n log n) time using constant space complexity.

题目要求复杂度O(nlogn),因此我们很自然考虑使用快速排序或者归并排序,但是后来经过实践证明,使用快速排序总是AC超时,归并排序则可以正确AC。

分析一下原因,个人认为是与测试数据有关,因为快速排序不能保证算法复杂度一定是O(nlogn),当数据比较集中时,即使做随机选取key值,算法的复杂度也非常接近O(N^2),因此会出现超时,所以考虑使用归并排序。

下面是采用归并排序的思路已经AC代码:

主要考察3个知识点,

知识点1:归并排序的整体思想

知识点2:找到一个链表的中间节点的方法

知识点3:合并两个已排好序的链表为一个新的有序链表

归并排序的基本思想是:找到链表的middle节点,然后递归对前半部分和后半部分分别进行归并排序,最后对两个以排好序的链表进行Merge。

【cpp】 view plain copy

#include

#include

#include

#include

#include

#include

using namespace std;

struct ListNode {

int val;

ListNode next;

ListNode(int x) : val(x), next(NULL) {}

};

class Solution {

public:

ListNode mergeLists(ListNode a, ListNode b) //合并两个已经排序的链表

{

if (a == NULL) return b ;

if (b == NULL) return a ;

ListNode ret = NULL ;

ListNode tail = NULL ;

ret = new ListNode(-1) ;

tail = ret ;

while (a b)

if (a->val val)

{

tail->next = a ;

tail = tail->next ;

a = a->next ;

}

else

{

tail->next = b ;

tail = tail->next ;

b = b->next ;

}

if (a)

tail->next = a ;

if (b)

tail->next = b ;

ListNode del = ret ;

ret = ret->next ;

delete del ;

return ret ;

}

ListNode getMid(ListNode head) //得到中间节点

{

if (!head) return NULL ;

if (!head->next) return head ;

ListNode slow = head ;

ListNode fast = head->next ;

while (fast fast->next)

{

slow = slow->next ;

fast = fast->next->next ;

}

return slow ;

}

ListNode sortList(ListNode head) { //合并排序

if (!head) //代码效果参考:http://www.zidongmutanji.com/bxxx/437098.html

return NULL ;

if (!head->next) return head ;

ListNode mid = getMid(head) ;

ListNode nextPart = NULL ;

if (mid)

{

nextPart = mid->next ;

mid->next = NULL ;

}

return mergeLists(

sortList(head) ,

sortList(nextPart)

) ;

}

};

void insertBack(ListNode head, ListNode tail, ListNode n) //从尾部插入

{

if (n)

{

if (head == NULL)

{

head = n ;

tail = n ;

}

else

{

(tail)->next = n ;

tail = n ;

}

}

}

int main(int argc, char** argv)

{

ifstream in("data.txt") ;

ListNode head = NULL ;

ListNode //代码效果参考:http://www.zidongmutanji.com/zsjx/395573.html

tail = NULL ;

int val ;

Solution s ;

while(in ] val)

{

ListNodetmp = new ListNode(val) ;

insertBack(head, tail, tmp) ;

}

head = s.sortList(head) ;

while(head)

{

cout [ head->val [ " " ;

head = head->next ;

}

cout [ endl ;

return 0 ;

}

下面再说一下自己AC超时的代码吧,

这里我尝试了两种实现方案:

第一种是:

在找划分点的过程中,维护连个链表Left 和Right 所有不大于key的元素都链到Left上,大于key的链到Right上,最后再将Left, key , Right三部分连接起来。

代码如下:

【cpp】 view plain copy

#include

#include

#include

#include

#include

#include

using namespace std;

struct ListNode {

int val;

ListNode next;

ListNode(int x) : val(x), next(NULL) {}

};

class Solution {

public:

inline void insertBack(ListNode head, ListNode tail, //代码效果参考:http://www.zidongmutanji.com/zsjx/213875.html

ListNode n) //从尾部插入

{

if (n)

{

if (head == NULL)

{

head = n ;

tail = n ;

}

else

{

(tail)->next = n ;

tail = n ;

}

}

}

ListNode sortList(ListNode head) {

if (!head) return NULL ;

if (head->next == NULL) return head ;

//划分

ListNode tmpNode = head ;

head = head->next ;

ListNode sleft = NULL , eleft = NULL ;

ListNode sright = NULL , eright = NULL ;

while (head)

{

ListNode insNode = head ;

head = head->next ;

insNode->next = NULL ;

if (insNode->val > tmpNode->val)

insertBack(sright, eright, insNode) ;

else

insertBack(sleft, eleft, insNode) ;

}

//递归调用

sleft = sortList(sleft) ;

sright = sortList(sright) ;

//下面三句话第一次没有加上,调试了一下午才找到原因

eleft = sleft ;

if (eleft)

{

while(eleft->next)

eleft = eleft->next ;

}

//拼接起来

if (eleft)

{

head = sleft ;

eleft->next = tmpNode ;

}

else

head = tmpNode ;

tmpNode->next = sright ; //连接起来

//返回结果

return head ;

}

};

int main(int argc, char** argv)

{

ifstream in("data.txt") ;

ListNode head = NULL ;

ListNode tail = NULL ;

int val ;

Solution s ;

while(in ] val)

{

ListNodetmp = new ListNode(val) ;

s.insertBack(head, tail, tmp) ;

}

head = s.sortList(head) ;

while(head)

{

cout [ head->val [ " " ;

head = head->next ;

}

cout [ endl ;

return 0 ;

}

第二种方案:使用快排的另一种思路来解答。我们只需要两个指针p和q,这两个指针均往next方向移动,移动的过程中保持p之前的key都小于选定的key,p和q之间的key都大于选定的key,那么当q走到末尾的时候便完成了一次划分点的寻找。如下图所示:

实现代码如下:

【cpp】 view plain copy

#include

#include

#include

#include

#include

#include

using namespace std;

struct ListNode {

int val;

ListNode next;

ListNode(int x) : val(x), next(NULL) {}

};

class Solution {

public:

ListNode getPartation(ListNode start, ListNode end)

{

if (start == end) return start ;

ListNode p1 = start ;

ListNode p2 = p1->next ;

int key = start->val ;

while(p2 != end)

{

if (p2->val < key)

{

p1 = p1->next ;

swap(p1->val, p2->val) ; //找到一个比key小的数字,与p1到p2间的数交换,

} //这之间的数都大于等于key

p2 = p2->next ;

}

swap(start->val, p1->val) ; //找到划分位置

return p1 ;

} ;

void QuickSort(ListNode start, ListNode end)

{

if (start != end)

{

ListNode pt = getPartation(start, end) ;

QuickSort(start, pt) ;

QuickSort(pt->next, end) ;

}

}

ListNode sortList(ListNode head) {

QuickSort(head, NULL) ;

return head ;

}

};

void insertBack(ListNode head, ListNode tail, ListNode n) //从尾部插入

{

if (n)

{

if (head == NULL)

{

head = n ;

*tail = n ;

}

else

{

相关文章
|
存储 缓存 算法
内存分配不再神秘:深入剖析malloc函数实现原理与机制
内存分配不再神秘:深入剖析malloc函数实现原理与机制
|
4月前
|
存储 人工智能 运维
从数据孤岛到智能洞察:构建面向未来的 Operation intelligence 体系
本文聚焦于 Operation Data 的核心特征、应用落地挑战,以及如何通过系统性方法构建真正的 Operation intelligence 能力。
302 51
|
1月前
|
人工智能 搜索推荐 持续交付
2026阿里云GPU服务器租用价格:A10、T4、V100、P100 GPU卡和L20实例
阿里云2026年最新GPU服务器(EGS)租用价格出炉,支持A10、T4、V100、P100及L20等GPU实例,适用于AI计算、模型推理、图形渲染等场景。提供按量、包月及抢占式多种计费模式,配置灵活,单卡至万卡集群均可适配,助力高效算力需求。
607 0
|
存储 算法 C语言
【C语言】深入浅出:C语言链表的全面解析
链表是一种重要的基础数据结构,适用于频繁的插入和删除操作。通过本篇详细讲解了单链表、双向链表和循环链表的概念和实现,以及各类常用操作的示例代码。掌握链表的使用对于理解更复杂的数据结构和算法具有重要意义。
3771 6
|
10月前
|
消息中间件 NoSQL 大数据
RocketMQ实战—5.消息重复+乱序+延迟的处理
本文围绕RocketMQ的使用与优化展开,分析了优惠券重复发放的原因及解决方案。首先,通过案例说明了优惠券系统因消息重复、数据库宕机或消费失败等原因导致重复发券的问题,并提出引入幂等性机制(如业务判断法、Redis状态判断法)来保证数据唯一性。其次,探讨了死信队列在处理消费失败时的作用,以及如何通过重试和死信队列解决消息处理异常。接着,分析了订单库同步中消息乱序的原因,提出了基于顺序消息机制的代码实现方案,确保消息按序处理。此外,介绍了利用Tag和属性过滤数据提升效率的方法,以及延迟消息机制优化定时退款扫描的功能。最后,总结了RocketMQ生产实践中的经验.
RocketMQ实战—5.消息重复+乱序+延迟的处理
|
设计模式 NoSQL Java
通用框架实践|Pipeline设计+幂等重试
本文讲述在闲鱼同城模块中,针对二手车和租房等业务的商业化需求,设计和实现了一个基于Pipeline模式和幂等性控制的通用框架。
|
8月前
|
存储 缓存 Java
c++的哈希表、哈希桶的介绍与实现
这一篇文章大致实现详细介绍什么是哈希,然后再介绍什么是哈希表,怎么代码实现哈希表,然后再介绍哈希桶,怎么代码实现哈希桶,最后再介绍他俩有什么细节上的差别,与代码的一些细节优化。
211 0
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
数据采集 测试技术 Swift
666条数据,训练LongWriter模型,写万字长文!模型&数据集均开源!
大模型的上下文(Context)支持越来越长的背景下,让通用的大模型遵循指令来保障长文本输出的长度,依然是一个挑战。
|
安全 Java 程序员
38-Metaspace区域是如何因为类太多而发生内存溢出的?
永久代溢出,由于到了JDK8,已经完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替,因此后续我们不再说 方法区、永久代,直接使用Metspace元空间来称呼。
1613 0
38-Metaspace区域是如何因为类太多而发生内存溢出的?