题目概述:
链接:点我做题
思路
一、数组储存法
这是最好想的一种方法,由于链表不支持随机访问,那么很多像堆排序、计数排序、快速牌数的Hoare方法这样依赖随机访问的算法就不能使用了,但是我们先把数据拷贝到vector容器中,然后调用STL的sort函数对vector容器进行排序,然后再遍历一遍链表把排序后的结果填入链表中。
这个方法的缺点是空间复杂度是
O
(
N
)
O(N)
O(N)。但是它的效率会很高,因为就我所知STL的sort方法集成了快速排序、小区间优化的插入排序、确认key值时的三数取中、堆排序等几种优化方法,肯定是比我们手写的速度要快很多的。
代码:
class Solution {
public:
ListNode* sortList(ListNode* head)
{
if (head == nullptr || head->next == nullptr)
{
return head;
}
vector<int> vec;
ListNode* cur = head;
while (cur)
{
vec.push_back(cur->val);
cur = cur->next;
}
sort(vec.begin(), vec.end());
cur = head;
int i = 0;
while (cur)
{
cur->val = vec[i++];
cur = cur->next;
}
return head;
}
};
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
n
)
O(n)
O(n)
二、递归归并法
本方法的思想就是合并有序数组,假设链表的起点和终点的指针分别为
l
e
f
t
left
left和
r
i
g
h
t
right
right,那么可以用快慢指针法到达链表的中间结点,设指向链表中间结点的指针是
m
i
d
mid
mid,由此可以把链表分割为
[
l
e
f
t
,
m
i
d
]
[left,mid]
[left,mid]和
[
m
i
d
,
r
i
g
h
t
]
[mid,right]
[mid,right],为了后面的归并,在分割过程中要把区间尾指针的
n
e
x
t
next
next设为null,区间中只有一个节点时,显然这个区间是有序的,然后两个单个结点的区间可以利用合并有序数组的方法进行合并,形成一个由两个结点的有序链表,然后又可以利用合并有序链表的方法,以此类推。
这个方法的缺点显然是递归会有函数栈帧的消耗,空间复杂度是
O
(
l
o
g
n
)
O(logn)
O(logn)
代码:
class Solution {
public:
ListNode* sortList(ListNode* head)
{
//控制边界
if (head == nullptr || head->next == nullptr)
{
return head;
}
//ListPartion函数会把分割做了,排序会在ListPartion函数里另外调用别的函数
return ListPartion(head, nullptr);
}
ListNode* ListPartion(ListNode* left, ListNode* right)
{
if (left == right)
{
//如果是只有一个结点的 left等于right了
//显然单个区间就是有序的 直接返回
//就直接返回
if (left != nullptr)
{
left->next = nullptr;
}
return left;
}
if (left->next == right)
{
//如果是区间中有两个元素 就把这个区间断开
left->next = nullptr;
return left;
}
//其他情况 我们用快慢指针法找到中点再断开
ListNode* fast = left;
ListNode* slow = left;
while (fast != right)
{
fast = fast->next;
slow = slow->next;
if (fast != right)
{
fast = fast->next;
}
}
ListNode* mid = slow;
//直接把归并过程和递归的分割过程写一起
return MergeSort(ListPartion(left, mid), ListPartion(mid, right));
}
ListNode* MergeSort(ListNode* l1, ListNode* l2)
{
ListNode* dummyhead = new ListNode;
ListNode* tail = dummyhead;
//合并有序链表看谁小把谁放进去就行了
while (l1 != nullptr && l2 != nullptr)
{
if (l1->val < l2->val)
{
ListNode* next = l1->next;
l1->next = tail->next;
tail->next = l1;
tail = l1;
l1 = next;
}
else
{
ListNode* next = l2->next;
l2->next = tail->next;
tail->next = l2;
tail = l2;
l2 = next;
}
}
//出来以后 l1和l2可能有一个没有到头,不过剩余部分都已有序 直接接上就行
tail->next = (l1 == nullptr) ? l2 : l1;
ListNode* ret = dummyhead->next;
delete dummyhead;
return ret;
}
};
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
l
o
g
n
)
O(logn)
O(logn)
三、非递归归并法
这个方法的思想就是我们放弃递归的想法,去这样思考:我们用size表示当前链表中的有序段的长度,也就是说链表从头开始以
s
i
z
e
size
size为长度得到的子链表是有序的,显然
s
i
z
e
=
1
size = 1
size=1时,每个长度为1的子链表当然是有序的,然后我们要把链表以
s
i
z
e
=
1
size=1
size=1长度分割,然后两两进行一个合并有序链表,经过这轮循环后,
s
i
z
e
∗
=
2
size*= 2
size∗=2,
s
i
z
e
size
size变成2,当前链表有序子链表的长度是2,然后再以长度为
s
i
z
e
=
2
size = 2
size=2进行分割,然后合并有序链表,以此类推,当size>=链表长度时,显然整个链表就有序了。
显然我们没有任何的递归调用和数组创建,所以这个方法的空间复杂度是
O
(
1
)
O(1)
O(1),时间复杂度则和归并一样是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
代码:
class Solution {
public:
ListNode* sortList(ListNode* head)
{
//边界条件
if (head == nullptr || head->next == nullptr)
{
return head;
}
int len = getListlength(head);
//设置一个哨兵头结点 这样方便插入
ListNode* dummyhead = new ListNode(0, head);
for (int size = 1; size < len; size *= 2)
{
//tail变量用来维护此轮已经size大小有序的链表
//它指向链表的尾结点
//刚进入循环 显然后没有部分有序 所以tail指向哨兵结点
ListNode* tail = dummyhead;
//cur指向当前正在处理的size大小的链表段的头结点
ListNode* cur = dummyhead->next;
while (cur)
{
//left是第一个归并的两个部分链表中第一个链表的头结点的指针
ListNode* left = cur;
//cut函数会把以起点开始size长度的链表截断(把尾指针的next改为nullptr)
//并且返回尾结点原来的下一个结点的指针
ListNode* right = cut(left, size);
//用right接到的显然就是第第二个链表的头结点的指针
cur = cut(right, size);
//以此来同时截断第二个链表并且更新cur
tail->next = ListMergeSort(left, right);
//把两个size长度的部分有序链表合成的2size有序链表接到tail后面
while (tail->next)
{
tail = tail->next;
}
//更新tail
}
}
ListNode* ret = dummyhead->next;
delete dummyhead;
return ret;
}
int getListlength(ListNode* head)
{
int ret = 0;
while (head)
{
head = head->next;
ret++;
}
return ret;
}
ListNode* cut(ListNode* head, int n)
{
//cut函数会把以起点开始size长度的链表截断
//(把尾指针的next改为nullptr)
//并且返回尾结点原来的下一个结点的指针
ListNode* cur = head;
while (--n && cur != nullptr)
//控制走n-1步 并且不要超过终点
{
cur = cur->next;
}
if (cur == nullptr)
{
//如果到达终点了
//那返回值就应该是nullptr
return nullptr;
}
ListNode* ret = cur->next;//保存原来的next值用于返回
cur->next = nullptr;//切割
return ret;
}
ListNode* ListMergeSort(ListNode* l1, ListNode* l2)
{
ListNode* dummyhead = new ListNode;
ListNode* tail = dummyhead;
while (l1 && l2)
{
if (l1->val < l2->val)
{
ListNode* next = l1->next;
l1->next = tail->next;
tail->next = l1;
tail = l1;
l1 = next;
}
else
{
ListNode* next = l2->next;
l2->next = tail->next;
tail->next = l2;
tail = l2;
l2 = next;
}
}
tail->next = l1 ? l1 : l2;
ListNode* ret = dummyhead->next;
delete dummyhead;
return ret;
}
};
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
1
)
O(1)
O(1)
四、快速排序递归版本
理论上来讲所有不依赖随机访问而是依赖于++的排序方法都可以在链表排序中使用,显然快排中的前后指针法非常适合于链表。
关于前后指针法和快速排序的思想与实现,我写过一篇博客,大家如果遗忘或者不熟悉的话可以进入我的博客学习:https://blog.csdn.net/CS_COPy/article/details/121421113?spm=1001.2014.3001.5501。
代码:
class Solution {
public:
ListNode* sortList(ListNode* head)
{
if (head == nullptr || head->next == nullptr)
{
return head;
}
QuickSortList(head, nullptr);
return head;
}
void QuickSortList(ListNode* left, ListNode* right)
{
if (left == right)
{
return;
}
ListNode* keyi = PartSort(left, right);
QuickSortList(left, keyi);
QuickSortList(keyi->next, right);
}
ListNode* PartSort(ListNode* left, ListNode* right)
{
if (left == right)
{
return left;
}
int key = left->val;
ListNode* cur = left;
ListNode* prev = left;
while (cur != right)
{
if (cur->val < key)
{
prev = prev->next;
if (prev != cur)
{
int tmp = cur->val;
cur->val = prev->val;
prev->val = tmp;
}
}
cur = cur->next;
}
left->val = prev->val;
prev->val = key;
return prev;
}
};
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
l
o
g
n
)
O(logn)
O(logn)
五、快速排序前后指针法非递归
我们用栈来模拟这个递归过程,这个方法在我的那篇博客里也有讲,其实就是把区间的左端点 l e f t left left和右端点 r i g h t right right作为栈的元素入栈,然后从栈中取出一组左右端点,然后进行一个前后指针法的单趟排序,得到一个分割点keyi(keyi这个位置的元素就已经排好了),然后把 [ l e f t , k e y i ] [left,keyi] [left,keyi](如果 l e f t ! = k e y i left != keyi left!=keyi)和 [ k e y i − > n e x t , r i g h t ] [keyi->next, right] [keyi−>next,right]入栈,循环往复,直到栈空为止。
class Solution {
public:
ListNode* sortList(ListNode* head)
{
if (head == nullptr || head->next == nullptr)
{
return head;
}
QuickSortListNOR(head, nullptr);
return head;
}
void QuickSortListNOR(ListNode* head, ListNode* tail)
{
stack<ListNode*> st;
st.push(head);
st.push(tail);
while (!st.empty())
{
ListNode* right = st.top();
st.pop();
ListNode* left = st.top();
st.pop();
ListNode* keyi = PartSort(left, right);
if (keyi != left)
{
st.push(left);
st.push(keyi);
}
if (keyi->next != right)
{
st.push(keyi->next);
st.push(right);
}
}
}
ListNode* PartSort(ListNode* left, ListNode* right)
{
int key = left->val;
ListNode* cur = left;
ListNode* prev = left;
while (cur != right)
{
if (cur->val < key)
{
prev = prev->next;
if (prev != cur)
{
int tmp = cur->val;
cur->val = prev->val;
prev->val = tmp;
}
}
cur = cur->next;
}
left->val = prev->val;
prev->val = key;
return prev;
}
};
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
1
)
O(1)
O(1)