算法題解:合併k個已排序的鏈表

算法題解:合併k個已排序的鏈表

題目

leetcode題目連接node

爲了簡化分析,咱們設共有k個鏈表,每一個鏈表的最大長度爲n。算法

題解1

不斷取出值最小的那個Node(由於每一個list已經排序,因此這一步只須要找出最小的head Node),添加到已排序鏈表的尾部,直到全部lists的全部Node都取完。函數

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution
{
  ListNode *_mergeKLists(ListNode *head, ListNode *tail, vector<ListNode *> &lists)
  {
    int smallest_node_index = find_smallest_head(lists);
    // 結束遞歸的狀況
    if (smallest_node_index == -1)
    {
      tail->next = NULL;
      return head;
    }

    tail->next = lists[smallest_node_index];
    lists[smallest_node_index] = lists[smallest_node_index]->next;
    // 尾遞歸
    return _mergeKLists(head, tail->next, lists);
  }

  // 從全部鏈表中找到val最小的 headNode
  int find_smallest_head(vector<ListNode *> &lists)
  {
    int smallest_index = -1;
    for (int i = 0; i < lists.size(); i++)
    {
      if (lists[i] == NULL)
      {
        lists.erase(lists.begin() + i);
        // 刪除第i項之後,下輪循環體還要訪問第i項
        i--;
        continue;
      }
      if (smallest_index == -1 || lists[i]->val < lists[smallest_index]->val)
      {
        smallest_index = i;
      }
    }
    return smallest_index;
  }

public:
  ListNode *mergeKLists(vector<ListNode *> &lists)
  {
    // 經過 dummyHead ,避免對 「head== NULL && tail == NULL」狀況進行額外判斷處理
    ListNode *dummyHead = new ListNode(-1);
    ListNode *res = _mergeKLists(dummyHead, dummyHead, lists)->next;
    delete dummyHead;
    return res;
  }
};

每一次遞歸調用_mergeKLists僅僅是將問題的規模減少1,而不是將問題分解爲多個問題。所以,這能夠被稱爲「減治法」,比「分治法」要慢一些。
這種解法雖然使用了尾遞歸,可是速度依然很慢。緣由有2:code

  1. 每次調用_mergeKLists的效果僅僅是解決了1個Node的順序,對問題的簡化程度過小,致使_mergeKLists須要調用O(nk)次。
  2. 每次調用_mergeKLists的時間開銷較高。其時間複雜度爲調用find_smallest_head的時間複雜度,o(k),並且調用vector::erase的耗時較多。

綜上所述,這種解法的時間複雜度爲O(nk)*O(k)=O(nk^2)。實際耗時359 ms,僅僅超過9.73%的提交。不是一個好的算法。排序

題解2

分治思想:先將lists中的鏈表兩兩合併,而後問題就簡化成了「合併k/2個已排序的鏈表」。遞歸

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution
{
  // 加入 merged_head 和 merged_tail 參數,是爲了可以使用尾遞歸
  ListNode *merge2Lists(ListNode *list1_head, ListNode *list2_head,
                        ListNode *merged_head, ListNode *merged_tail)
  {
    // 兩種結束遞歸的狀況
    if (list1_head == NULL)
    {
      merged_tail->next = list2_head;
      return merged_head;
    }
    else if (list2_head == NULL)
    {
      merged_tail->next = list1_head;
      return merged_head;
    }

    // 須要繼續遞歸的狀況
    else if (list1_head->val <= list2_head->val)
    {
      merged_tail->next = list1_head;
      // 尾遞歸
      return merge2Lists(list1_head->next, list2_head, merged_head, merged_tail->next);
    }
    else
    {
      merged_tail->next = list2_head;
      // 尾遞歸
      return merge2Lists(list1_head, list2_head->next, merged_head, merged_tail->next);
    }
  }

public:
  ListNode *mergeKLists(vector<ListNode *> &lists)
  {
    int lists_num = lists.size();
    if (lists_num == 0)
      return NULL;

    ListNode *dummyHead = new ListNode(-1);
    while (lists_num > 1)
    {
      for (int i = 0; i < lists_num / 2; i++)
      {
        // 經過 dummyHead ,避免對 「merged_head == NULL && merged_tail == NULL」狀況進行額外判斷處理
        dummyHead->next = NULL;
        lists[i] = merge2Lists(lists[i], lists[lists_num - 1 - i], dummyHead, dummyHead)->next;
      }
      // 「簡化問題」的過程,就是不斷減半lists_num的過程
      lists_num = (lists_num + 1) / 2;
    }
    delete dummyHead;
    // 通過log_2(k)次減半,lists 中只剩下一個sortedList
    return lists[0];
  }
};
  1. 每執行一次while的循環體,須要合併的鏈表數就減半,所以while循環體執行次數爲logk。
  2. 每次執行循環體的時間開銷:第一次執行循環體:k/2次合併2個長度爲n的鏈表,時間複雜度爲O(nk);第二次執行循環體:k/4次合併2個長度爲2n的鏈表,時間複雜度依然爲O(nk)。以此類推,每次執行循環體的時間開銷都爲O(nk)。

綜上所述,此解法的時間複雜度爲O(nklogk)。實際耗時26 ms,超過80.34%的提交。相比前面一個解法有巨大提高。ip

爲何題解2更加高效?

在題解1中,find_smallest_head的時間複雜度等於每次要合併的鏈表數(k),而鏈表數在題解1的算法執行過程當中是基本不變的。所以這個函數的執行時間始終很高,而調用一次這個函數僅僅能幫助咱們選出一個最小的節點(每次選擇的代價高,收益低)。算法的大部分時間都花在這個函數上了。leetcode

而題解2將【合併k個鏈表】分紅k/2個獨立的子問題:合併2個鏈表。獨立的意義是:當我在合併2個鏈表的時候,徹底不須要管其餘的鏈表。這種獨立性使得子問題可以很是高效的解決:每次只須要對比2個節點,就能選出一個節點。雖然每次選出的節點「質量比較差」(這個節點不太多是k個鏈表中最小的那個節點,它僅僅是2個鏈表中最小的那個節點),可是它勝在選擇的成本很是低(僅僅執行一次大小比較)。所以每次的選擇收益相比題解1要低(須要作更屢次的選擇),但代價比題解1要低得多。綜合起來,題解2總的工做量更少。get

其實題解2的思路是與歸併排序是徹底相同的。你能夠仔細對比一下。
相關文章
相關標籤/搜索