这道题的题面非常简单,就是简单的对一个乱序的链表进行排序。 以往做的排序问题很多,但是大多数都是在数组中进行排序的。关于链表的排序做的比较少,这里专门开一贴来记录链表中的排序算法。注意这里所说的面向链表的排序算法特指game of pointers,也就是说这里不考虑有些人把数字取到vector里再sort一下放回链表的讨巧做法。
一、插入排序 这是我首先想到的算法,而且它确实非常适合链表这种数据结构。 插入排序需要保留一个有序区,所以这里我用了[Head, Tail]这个区间来指出有序区域的范围,那么我们要插入的下一个元素,理所当然的就是Tail+1这个位置的元素了,这里我记为Present,整个插入排序的代码如下:
ListNode* sortList(ListNode* head) {
if(head == nullptr)
return head;
ListNode* Dummy = new ListNode(0, head);
ListNode* Tail = Dummy->next;
ListNode* Present = Tail->next;
while(Present)
{
if(Tail->val <= Present->val)
Tail = Tail->next;
else
{
ListNode* Search = Dummy;
while(Search->next->val <= Present->val)
Search = Search->next;
Tail->next = Present->next;
Present->next = Search->next;
Search->next = Present;
}
Present = Tail->next;
}
return Dummy->next;
}
这段代码注意几个点:
1.遇到链表问题首先申请个Dummy结点总是没错的,即便它有些情况下没什么用,但是也不会带来额外的坏处:) 2. 循环结束的标志是要插入的结点为空结点。 3. 首先考虑特殊情况,这样可以大大简化代码逻辑,比如上述代码中首先考虑如果要插入的节点的值大于尾结点的值,那这种情况下只需要将有序区向后移就可以了,并不需要额外的动作:
if(Tail->val <= Present->val)
Tail = Tail->next;
4.否则,如果不是特殊情况,那么就的确需要插入操作了。这时候就得找到合适的插入位置,注意这里Search指针每一次都用它的下一个结点去和要插入的结点Present比较大小,这是因为我们修改链表时需要记住待插入位置的前一个结点:
while(Search->next->val <= Present->val)
Search = Search->next;
还有这里我们用了while循环,这是因为我们确定在前面的有序区一定可以找到要插入的位置。 5.完成一次新结点的插入操作之后,无论如何,总要更新下一个要插入结点的指针,这个有时候容易忘。
Present = Tail->next;
以上就是面向链表的简单插入排序的代码思路,在此做个记录。 事实上,这个代码效率不高,复杂度是
O
(
n
2
)
O(n^2)
O(n2)这个量级的,结果就是:
二、归并排序 归并排序算法的时间复杂度是
O
(
n
log
?
n
)
O(n\log{n})
O(nlogn),这个算法显然高效多了,但是一旦出现
l
o
g
n
logn
logn,一定也会想到这会涉及到链表的划分。这里面根据划分的次序就可以分为自顶向下和自底向上。
首先来看看自顶向下的方法,这种方法是递归的,每次将链表从其中点分开成为等长的两个部分,这样一层层切下去直到子链表的长度为1,在回归的时候再将它们merge(归并)起来。首先给出完整代码:
class Solution {
ListNode* mergeList(ListNode* L1, ListNode* L2)
{
ListNode* Dummy = new ListNode(0, nullptr);
ListNode* Present = Dummy;
while(L1 != nullptr && L2 != nullptr)
{
if(L1->val <= L2->val)
{
Present->next = L1;
L1 = L1->next;
}
else
{
Present->next = L2;
L2 = L2->next;
}
Present = Present->next;
}
if(L1 != nullptr)
Present->next = L1;
if(L2 != nullptr)
Present->next = L2;
return Dummy->next;
}
ListNode* splitList(ListNode* Head, ListNode* Tail)
{
if(Head == nullptr)
return Head;
if(Head->next == Tail)
{
Head->next = nullptr;
return Head;
}
ListNode* Slow = Head;
ListNode* Fast = Head;
while (Fast != Tail) {
Slow = Slow->next;
Fast = Fast->next;
if (Fast != Tail) {
Fast = Fast->next;
}
}
return mergeList(splitList(Head, Slow), splitList(Slow, Tail));
}
public:
ListNode* sortList(ListNode* head)
{
return splitList(head, nullptr);
}
};
首先需要指出的是,这里使用的是左闭右开的区间,所以链表的范围是[Head, Tail),然后我们使用快慢指针的算法去找链表的中点,这些都是常规方法。我当时在代码实现时,其实是对快慢指针这里有疑问的:
ListNode* Slow = Head;
ListNode* Fast = Head;
while (Fast != Tail) {
Slow = Slow->next;
Fast = Fast->next;
if (Fast != Tail) {
Fast = Fast->next;
}
}
之前我写快慢指针时,代码是这样的,我一开始也是这样写的:
ListNode* slow = head;
ListNode* fast = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
但事实上这样子会死循环,原因得从这两种快慢指针算法的区别上来找。 事实上,本题中的快慢指针写法当遇到链表长度为偶数时,会找到中间靠后的中心元素,而之前的代码找到的是中间靠前的那个元素。
4 -> [2] -> 1 -> 3 //原始写法
4 -> 2 -> [1] -> 3 //本题写法
原始写法下,再结合左闭右开区间的约定,链表自顶向下拆分的过程会在最后两个结点处陷入死循环:
1 -> 3 // 原始写法下切分到长度为2的小链表时,算法将陷入死循环
了解了这一点,其他的思路其实就是比较简单的,先拆分(N->1),再合并(1->N),用递归可以优雅地实现这种思路,时间复杂度是
O
(
n
log
?
n
)
O(n\log{n})
O(nlogn)。
再看自底向上的方式,这种方式就是迭代法了,我个人觉得递归这种写法并不让我特别喜欢,你可以用它来炫技,展现你高超的算法设计思维(但也就仅限于此)。否则,我还是更倾向于迭代,它的设计难度相对更低,也更适用于真正的工程场景,代码可维护性高,首先给出完整代码:
class Solution {
ListNode* mergeList(ListNode* ListA, ListNode* ListB)
{
ListNode* Dummy = new ListNode(0, nullptr);
ListNode* Present = Dummy;
while(ListA && ListB)
{
if(ListA->val <= ListB->val)
{
Present->next = ListA;
ListA = ListA->next;
}
else
{
Present->next = ListB;
ListB = ListB->next;
}
Present = Present->next;
}
if(ListA)
Present->next = ListA;
if(ListB)
Present->next = ListB;
return Dummy->next;
}
int getLength(ListNode* Head)
{
int Counter = 0;
while(Head)
{
Counter += 1;
Head = Head->next;
}
return Counter;
}
public:
ListNode* sortList(ListNode* head)
{
if(head == nullptr)
return head;
int Length = getLength(head);
ListNode* Dummy = new ListNode(0, head);
int SubLength;
for(SubLength = 1 ; SubLength < Length ; SubLength <<= 1)
{
ListNode* Previous = Dummy;
ListNode* SubHead = Dummy->next;
ListNode* SubTail = SubHead;
ListNode* SubList1 = nullptr;
ListNode* SubList2 = nullptr;
while(SubHead)
{
int Counter = 1;
while(Counter < SubLength && SubTail && SubTail->next)
{
SubTail = SubTail->next;
Counter += 1;
}
SubList1 = SubHead;
SubHead = SubTail->next;
SubTail->next = nullptr;
SubTail = SubHead;
Counter = 1;
while(Counter < SubLength && SubTail && SubTail->next)
{
SubTail = SubTail->next;
Counter += 1;
}
SubList2 = SubHead;
if(SubTail)
{
SubHead = SubTail->next;
SubTail->next = nullptr;
SubTail = SubHead;
}
ListNode* SortedSubList = mergeList(SubList1, SubList2);
Previous->next = SortedSubList;
while(Previous->next)
Previous = Previous->next;
}
}
return Dummy->next;
}
};
基本思路就是按照子链表长度(SubLength)进行迭代,每一个长度下就数出来对应长度的子链表,一直数到链表结束,每相邻两个子链表归并一下,再插回到原先的链表中(Previous指针就是干这事儿的)。持续此过程,直到子链表长度为Length/2,这次归并完整个链表将完全有序,算法时间复杂度同样为
O
(
n
log
?
n
)
O(n\log{n})
O(nlogn)。
这篇文章梳理了关于链表排序的一些解法,其中使用归并法完成排序的两种方法是要掌握的重点。
|