题目描述
给定链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
示例 2:
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]
示例 3:
输入:head = []
输出:[]
解题方法
暴力排序法
1、解题思路
最简单的想法就是,每次从待排序的链表中取下一个节点,插入到之前已经排好序的链表中。实现该方法,需要三个主要的辅助指针,指针q和qre用来遍历已经排好序的链表,寻找节点插入位置,指针p用来指向下一个待排序的节点。
注意:在排序前以头节点作为单独的链表断开,指针p从第二个节点开始遍历排序。
2、代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* sortList(ListNode* head) {
if (head == nullptr) {
return head;
}
struct ListNode* h = new ListNode(); // 辅助头结点
struct ListNode *p, *q, *qre, *t;
h->next = head;
p = head->next;
h->next->next = nullptr; // 断开待排序的链表
while (p != nullptr) {
qre = h;
q = qre->next;
while (q != nullptr && q->val < p->val) {
qre = q;
q = q->next;
}
qre->next = p;
t = p->next;
p->next = q;
p = t;
}
return h->next;
}
};
3、复杂度分析
(1)时间复杂度:在最坏情况下,每次寻找插入位置时,都要遍历一次已排好序的链表,并且遍历链表的长度和当前排序的节点序号有关,第一个节点最多遍历0次,第二个节点最多遍历1次,通过递推可得,时间复杂度T(n)=O(n^2)。
(2)空间复杂度:此处只是使用了一些辅助变量,因此空间复杂度S(n)=O(1)。
归并排序法
1、解题思路
类比于数组的归并排序,先使用分治法将链表切分为最小归并单元(只有一个节点),然后再将已排好序的链表两两归并。那么在分治过程中就会出现一个问题,数组可以通过数组下标进行两两分治,但是链表不能按下标访问,只能按顺序访问,因此如何查找一个给定头节点链表的中间节点,就是最先要解决的问题。
首先想到的是蛮力法,先遍历一次链表得到链表的长度,再遍历一次链表获取到中间节点,则查找中间节点的时间复杂度就为O(n),但遍历两次链表的操作较为冗杂,因此可以使用快慢指针来替代两次遍历。
快指针一次性遍历两个节点,慢指针一次只遍历一个节点,当快指针不能再继续遍历时,慢指针指向的节点就是中间节点。(其操作就相当于是链表长度除以2取整得到的结果),在实现快慢指针时,有以下注意点:
(1)快指针必须能够一次性走两步后,慢指针才能走一步。
如果是快指针走一步,慢指针跟着走一步的话,当链表只有两个节点时,返回的中间节点是第二个节点(快慢指针都从头节点开始),而根据数组分治的策略,每次分治的区间是[head,mid]以及[mid+1,end],那么按照上述情况分治得到的区间将一直会是[head,mid]。
(2)在找到链表的中间节点时,要先记下第mid+1个节点的指针,再断开为两个链表,其实现代码如下所示:
ListNode* node2 = mid->next;
mid->next = nullptr;
在通过中间节点实现分治后,先调用自身递归,将左右链表排序后,再合并两个链表,那么接下来的问题就是如何合并两个已排好序的链表。
在给定两个头节点后,创建一个新的头节点来存储排好序的链表,利用双指针分别遍历两个链表,以双指针均不为空为循环结束条件,一旦有一个为空了,则不需要进行比较排序,直接将剩下的链表接在后面即可。
2、代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* sortList(ListNode* head) {
// 当链表为空或者只有一个节点时,不需要进行排序
if (head == nullptr || head->next == nullptr) {
return head;
}
// 进行分治归并排序
return merge(head);
}
ListNode* merge(ListNode* node) {
if (node->next == nullptr) {
return node;// 当只有一个节点时不需要进行分治也不需要进行排序
}
// 找中间结点
ListNode* mid = findMid(node);
ListNode* node2 = mid->next;
// 进行分割
mid->next = nullptr;
// 分别对子问题进行求解
ListNode* list1 = merge(node);
ListNode* list2 = merge(node2);
// 合并两个链表
return mergeTwoList(list1, list2);
}
ListNode* findMid(ListNode* node) {
ListNode* slow = node;
ListNode* fast = node;
while (fast->next != nullptr && fast->next->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
ListNode* mergeTwoList(ListNode* node1, ListNode* node2) {
ListNode *node, *p, *q, *t;
node = new ListNode();
node->next = nullptr;
t = node;
p = node1;
q = node2;
while (p != nullptr && q != nullptr) {
if (p->val <= q->val) {
t->next = p;
t = t->next;
p = p->next;
} else {
t->next = q;
t = t->next;
q = q->next;
}
}
if (p) {
t->next = p;
} else if (q) {
t->next = q;
}
return node->next;
}
};
3、复杂度分析
(1)时间复杂度:merge函数的递归深度为logn,findMid函数时间复杂度为O(n),mergeTwoList时间复杂度为O(m+n),则merge函数每一层的时间复杂度都为O(n),总的时间复杂度T(n)=O(logn)*O(n)=O(nlogn)
(2)空间复杂度:上述函数只使用了一些辅助变量,则空间复杂度为O(1)。