leetcode 23: Merge k Sorted Lists

這次的題目是合併 K 個已排序的串列,一開始先試著直接把所有元素串在一起,放進一個陣列後排序完再填進串列 結果長得像這樣:

#include <stdlib.h>

int cmp(const void* a, const void* b)
{
    return *(int*)a - *(int*)b;
}

struct ListNode* mergeKLists(struct ListNode** lists, int listsSize) {
    struct ListNode *ret = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode *tail = ret;
    int sz = 0;

    for(int i=0 ; i<listsSize ; i++)
    {
        struct ListNode *idx = lists[i];
        while(idx)
        {
            // update
            tail->next = idx;
            tail = tail->next;
            idx = idx->next;
            sz++;
        }
    }

    // special case
    if(sz == 0) return NULL;

    // remove first node cuz it is empty
    tail = ret;
    ret = ret->next;
    free(tail);

    int* arr = (int*)malloc(sizeof(int)*sz);
    tail = ret;
    sz = 0;

    // copy and sort
    while(tail)
    {
        arr[sz++] = tail->val;
        tail = tail->next;
    }
    qsort(arr, sz, sizeof(int), cmp);

    tail = ret;
    sz = 0;

    // copy and free
    while(tail)
    {
        tail->val = arr[sz++];
        tail = tail->next;
    }
    free(arr);

    return ret;
}

結果 $16ms/8MB$ ,其實還算不錯,只是 code 略長,另外也沒有用到所有串列皆已排序的性質 在這裡改用依序合併兩個串列的方式,因為皆已排序,所以只要比較首元素,將較小的接在前面,之後再遞迴得合併其餘元素就好,code 如下:

struct ListNode* mergeTwoLists(struct ListNode* a, struct ListNode* b)
{
    if(!a && !b) return NULL;
    if(a && !b) return a;
    if(b && !a) return b;

    if(a->val < b->val)
    {
        a->next = mergeTwoLists(a->next, b);
        return a;
    }
    else
    {
        b->next = mergeTwoLists(a, b->next);
        return b;
    }
}

每個元素都會掃過一遍,所以合併的複雜度是 $O(|a|+|b|)$ 接下來是要合倂所有串列的部分,依序兩兩合併的 code 非常短,長這樣:

struct ListNode* mergeKLists(struct ListNode** lists, int listsSize)
{
    if(listsSize == 0) return NULL;

    for(int i=1 ; i<listsSize ; i++)
    {
        lists[i] = mergeTwoLists(lists[i-1], lists[i]);
    }
    return lists[listsSize-1];
}

但這樣的總複雜度是 $O(Nk)$ , $N$ 是最後的串列總長, $k$ 是串列數量,因為串列越來越長,在合併時會拖慢速度 這樣子 submit 的結果是 $204ms/8.1MB$ 加速的方法是在合併時先從比較短的串列開始合併,所以將修改 code 如下:

struct ListNode* mergeKLists(struct ListNode** lists, int listsSize)
{
    if(listsSize == 0) return NULL;

    int mergedSize;
    while(listsSize != 1)
    {
        mergedSize = (listsSize + 1) / 2;
        for(int i=0 ; i<mergedSize ; i++)
        {
            if(2*i+1 >= listsSize)
                lists[i] = lists[2*i];
            else
                lists[i] = mergeTwoLists(lists[2*i], lists[2*i+1]);
        }
        listsSize = mergedSize;
    }

    return lists[0];
}

這裡的 code 更改了合併的順序,是讓相鄰兩串列兩兩合併,示意圖如下

這樣的順序,讓串列的長度比較不會那麼極端,結果確實快了不少 $(16ms/8.1MB)$ 雖然說建個 heap 保證每次都合併最短的兩個串列就能保證比較次數最少,但是會讓空間複雜度上升到 $O(k)$ ,也會產生額外的時間開銷,我想在通常情況下,上述方法應該是足夠快速了

題外話,這題用 python 也可以寫的很短:

class Solution:
    def mergeKLists(self, lists: 'List[ListNode]') -> 'ListNode':
        r = []
        for l in lists:
            while l:
                r.append(l.val)
                l = l.next
        return sorted(r)

結果 98.14%,感覺上面的時間都白費了


735 Words

2019-02-09T19:24:13