合并k排序列表超出时间限制(leetcode)

时间:2015-03-09 05:17:03

标签: c++ algorithm sorting

merge-k-sorted-lists

合并k个已排序的链接列表并将其作为一个排序列表返回。分析并描述其复杂性。

我的代码:

    ListNode *mergeTwoLists(ListNode *p1, ListNode *p2) {
    ListNode dummy(-1);
    ListNode *head = &dummy;
    while(p1 != nullptr && p2 != nullptr) {
        if (p1->val < p2->val) {
            head->next = p1;
            head = head->next;
            p1 = p1->next;
        } else {
            head->next = p2;
            head = head->next;
            p2 = p2->next;
        }
    }
    if (p1 != nullptr) {
        head->next = p1;
    }
    if (p2 != nullptr) {
        head->next = p2;
    }
    //head->next = nullptr;
    return dummy.next;
}
ListNode *mergeKLists(vector<ListNode *> &lists) {
    if (lists.size() == 0) return nullptr;
    if (lists.size() == 1) return lists[0];
    ListNode *p1, *p2, *p;
    while (lists.size() > 1) {
        p1 = lists.back();
        lists.pop_back();
        p2 = lists.back();
        lists.pop_back();
        p = mergeTwoLists(p1, p2);
        lists.push_back(p);
    }
    return lists[0];
}

我总是超时限时。我该如何更改程序?

2 个答案:

答案 0 :(得分:4)

您正在做的事情具有复杂性O(nk^2),其中n是每个数组的大小。您一次合并两个列表。为什么?你合并前两个列表需要2n个操作,前两个组合的大小是2n。现在将其与第三个合并,数组大小变为3n并且3n操作完成,因此操作总数为2n+3n+....kn(算术级数),即O(nk^2)。而是采用优先级队列( min heap )插入所有k列表的第一个元素。现在每次从优先级队列中取出最小元素(将其放入新列表中),将其从优先级队列中删除,然后插入该元素所属的列表的下一个元素。由于所有元素都是从优先级队列中插入和删除的,因此总共有nk个元素,复杂度为O(nklog(k))。 (删除/插入时间)优先级队列为O(log(number_of_elements_in_queue))。最多在队列中,任何时候都有k个元素。
有关更详细的说明以及代码,请查看此处:Merging k sorted lists。我认为这足以在leetcode上获得AC:)。

答案 1 :(得分:1)

您的问题是您正在进行不平衡的合并。如果每个列表都有n个要素开始,merge(a,b)表示您合并了长度为ab的列表(需要时间O(a+b)),那么您正在执行的操作是

merge(n,n)
merge(2n,n)
merge(3n,n)
merge(4n,n)
....

因此你需要付出很多代价才能在很长一段时间里重复这个问题。使用k元素进行(1/2) k^2 n工作。

您可以寻找专门的不平衡合并算法,但更简单的方法是重新组织您的工作以合并相似大小的列表。如果您开始使用k列出每个n元素,那么您可以

k/2 instances of `merge(n,n)`
k/4 instances of `merge(2n,2n)`
...
1 instance of `merge(nk/2, nk/2)`

每个步骤都需要nk次,并且有lg(k)个步骤,总费用为nk lg(k)

如果k不是2的幂,或者列表长度不一样,那么你可以做很多事情来尝试并最大限度地减少工作量,但一个非常简单的方法是要使lists成为deque而不是vector,并且对于每个合并,您弹出两个后面的列表并将结果推送到前面而不是。另一个简单的优化是首先按长度对列表进行排序。


k不是太大时,other answer可能会更好。如果k相当大,那么使用混合算法可能会更好:您选择一个合适的m并按照我的描述组织整个工作,而不是一次合并两个列表,您一次合并m个列表。

我在适当的m进行的前两次猜测是ceil(sqrt(k)),而另一个答案的算法对于m合并方式有效的最大值。

(如果出于某些奇怪的原因m仍然非常大,那么你使用混合算法进行m - 方式合并)


为什么我要做出上述预测?另一个答案是只传递一次数据,因此只要您的CPU能够有效地维护长度为k的优先级队列以及同时从k列表中读取,它肯定会更好而不是我的算法让很多人通过数据。

但是当k变得太大时,你会遇到问题:

  • 您的TLB可能没有足够的条目一次从k列表中读取
  • 您的缓存可能不够大,无法在列表的所有k中存储一两个缓存行,也不适合优先级队列

缓存未命中,特别是TLB未命中会降低性能。混合算法重新组织工作,以便您保持我的算法方法(平衡合并)的好处,而几乎所有的工作都是通过高效的m合并来完成另一个答案。