链表排序居然比数组排序还简单?——leetcode 148 (1)
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
示例 1:
输入: 4->2->1->3
输出: 1->2->3->4
示例 2:
输入: -1->5->3->4->0
输出: -1->0->3->4->5
我们之前学过了不少排序算法,其中比较常用的,而且时间复杂度是O(nlgn)的有快速排序和归并排序。但是这个题目还有一个要求,那就是只能使用常数级别的额外空间。那么这两种解法,哪种可以满足这个条件呢?我们先看归并排序。
写过归并排序的人都知道,我们必须使用额外的O(n)的空间,因为对于两个排好序的数组,想要把它们归并到一个数组里并且保持升序,需要和原来数组同样大小的空间。基于这一点,我一开始也觉得归并排序没有办法解决这次的问题。但是后来看了别人的解法,还没仔细看具体怎么实现这个算法,就看到了一句话:链表是不用额外空间的。我想了一下这句话的含义,就明白了这句话是什么意思。因为对于一个链表,我们只需要改变结点的后驱结点,也就是改变next指向的结点,就可以实现排序,而这种操作是不用额外空间的。
明白了这一点,我们就可以用归并排序解决这个问题。值得注意的是,对数组使用归并排序,是要获得这个数组的起始下标和末尾下标(或者数组长度)的。也就是说,归并排序的函数mergeSort有两个参数,而且如果想要得到排好序的数组,也是需要两个变量来表示的。
因为这一点,我一开始写链表的归并排序时,也是这么写的,mergeSort有两个参数,merge有三个参数(事实上应该是四个,但需要合并的两个排好序的数组都是相邻的,也就是说第一个数组的末尾就是第二个数组的起始,因此只需要三个参数就可以表示这两个数组),而且mergeSort要返回排好序的数组以便merge合并,所以mergeSort又要返回两个指针。而且还要考虑一大堆边界值的情况,这样写出来的代码就会异常繁琐:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution
{
public:
ListNode* sortList(ListNode* head)
{
if (!head) { return head; }
ListNode *tail = head;
while (tail) { tail = tail->next; }
pair<ListNode*, ListNode*> p = mergeSort(head, tail);
return p.first;
}
pair<ListNode*, ListNode*> mergeSort(ListNode* head, ListNode* tail)
{
if (head->next == tail) { return make_pair(head, head); }
if (head->next->next == tail)
{
if (head->val > head->next->val)
{
swap(head->val, head->next->val);
}
return make_pair(head, head->next);
}
ListNode* mid = findMid(head, tail);
pair<ListNode*, ListNode*> p1 = mergeSort(head, mid);
pair<ListNode*, ListNode*> p2 = mergeSort(mid, tail);
p1.second->next = p2.first;
pair<ListNode*, ListNode*> p =
merge(p1.first, p2.first, p2.second->next);
return p;
}
ListNode* findMid(ListNode* head, ListNode* tail)
{
ListNode* l1 = head, *l2 = head;
while (l1 != tail && l1->next != tail)
{
l1 = l1->next->next;
l2 = l2->next;
}
return l2;
}
pair<ListNode*, ListNode*> merge(ListNode* head, ListNode* mid,
ListNode* tail)
{
ListNode* p = head;
ListNode* cur = NULL, *l1 = NULL, *l2 = NULL;
if (head->val < mid->val)
{
l1 = head->next;
l2 = mid;
cur = head;
}
else
{
l1 = head;
l2 = mid->next;
head = cur = mid;
}
while (l1 != mid || l2 != tail)
{
if (l1 == mid)
{
cur->next = l2;
cur = l2;
l2 = l2->next;
}
else if (l2 == tail)
{
cur->next = l1;
cur = l1;
l1 = l1->next;
}
else
{
if (l1->val < l2->val)
{
cur->next = l1;
cur = l1;
l1 = l1->next;
}
else
{
cur->next = l2;
cur = l2;
l2 = l2->next;
}
}
}
cur->next = tail;
return make_pair(head, cur);
}
};
这段代码是能过的,但是我现在甚至不想再去具体分析这段代码。究其原因,还是因为受到了数组的思维定势的影响,对于mergeSort,非要传起始和末尾两个参数不可。事实上,我们只需要起始结点就可以了。如果只有起始结点,就有一个问题:我们怎么把链表分成两半呢?想要搞清楚这个问题,就要解决一个问题:假如我们已经知道链表的起始和末尾结点,能否找到它的中间结点?可能有人会说,先遍历一遍链表,数一下结点个数,然后再从起始结点出发,走一半的结点数,就到达了中间结点。但事实上,我们之前做过一个环形链表的题,用到了快慢指针这种解法。事实上,找中间结点依旧可以使用这种思路。只要设快指针每次向前两个单位,慢指针每次向前一个单位,同时出发,当快指针到达终点时,慢指针就到达了中间结点。
那么,现在我们不知道链表的末尾结点,还能否用快慢指针去找中间结点呢?其实在一个链表中,我们只考虑它的某一段,想找这一段的中间结点,然后进行归并排序,那么我们的确需要知道这一段到哪里结束。可是对于一段完整的链表来说,它的末尾结点自然就是NULL,这是无法改变的事实,因此也不用一个专门的指针来标记这个末尾结点。这个过程,通过findMid这个函数实现。
知道了这一点,我们就会想,能不能在排序的时候,把不参与排序的那部分链表切掉呢?也就是说,对一个链表进行归并排序,我们能不能把这个链表从中间切开,给每一段的末尾都添加一个NULL结点,然后两边各自排序,最后再连起来?
显然,这是可行的。首先,能切开吗?切开链表是完全可以的,找到mid之后直接切掉就可以了。那么,切开以后,能排序吗?之前我们已经提到,对于某段完整的链表来说,只需要知道起始结点,就可以进行归并排序了,因此切开链表之后,我们需要保留两段的起始结点。最后,切开并分别排序之后能重新接上吗?事实上,只需要知道每段的起始结点,就可以接上了。具体如何实现,请看代码的merge函数。以下,是改进后的实现代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution
{
public:
ListNode* sortList(ListNode* head)
{
if (!head) { return NULL; }
if (!head->next) { return head; }
if (!head->next->next)
{
if (head->val > head->next->val)
{
swap(head->val, head->next->val);
}
return head;
}
ListNode* mid = findMid(head), *h = mid->next;
//事实上是以mid->next为后一段链表的头结点,不过无伤大雅
mid->next = NULL;
ListNode *h1 = sortList(head), *h2 = sortList(h);
return merge(h1, h2);
}
ListNode* findMid(ListNode* head)
{
ListNode* l1 = head, *l2 = head;
while (l1 && l1->next)
{
l1 = l1->next->next;
l2 = l2->next;
}
return l2;
}
ListNode* merge(ListNode* h1, ListNode* h2)
{
/*ListNode *cur = NULL;
if (h1->val < h2->val)
{
cur = h1;
h1 = h1->next;
}
else
{
cur = h2;
h2 = h2->next;
}
ListNode *head = cur;*/
ListNode *head = new ListNode(0), *cur = head;
//新建一个起始节点,到时候再舍弃,可以省去上面的代码。
while (h1 && h2)
{
if (h1->val < h2->val)
{
cur->next = h1;
cur = h1;
h1 = h1->next;
}
else
{
cur->next = h2;
cur = h2;
h2 = h2->next;
}
}
if (!h1) { cur->next = h2; }
else { cur->next = h1; }
//return head;
return head->next;
}
};
以上就是用归并排序实现对链表排序。事实上,快速排序也是可行的,这个我们下期再说。