堆應用: 合併k個排序鏈表

合併k個排序鏈表: 合併給定k個排序鏈表;

e.g.
2->4->nullptr
-4->4->5->nullptr
-1->nullptr
合併這3個有序的鏈表, 返回: -4->-1->2->4->4->5->nullptr

解法思路

主要思路是建堆MinHeap。從k個鏈表表頭取元素, 建堆, 然後heap.peek(), heap.remove; 持續的取每個鏈表,直到取完全部元素; 在heap.peek(), heap.remove();

注意, 對std::vector<ListNode*>記錄的list的指針, 每次取完就更新爲next, 直到next爲nullptr, 說明取完了當前的鏈表. 這時k計數應該遞減(注意不應該重複遞減);

mergeKLists函數返回值是ListNode*, 注意head,tail指針的更新 - 需要符合題意,返回鏈表, 不是vector等什麼(踩坑);

#include <iostream>
#include <vector>
#include "../heap/heap.h"

typedef struct listNode {
    int value;
    struct listNode* next;

    listNode() = default;
    listNode(int val, struct listNode* p):
        value(val), next(p) {}
    listNode(const listNode& node) {
        value = node.value;
        next = node.next;
    }
    bool operator>(const listNode& l) {
        return value > l.value;
    }
    bool operator<(const listNode& l) {
        return !(operator>(l));
    }
    bool operator==(const listNode& l) {
        return value == l.value;
    }
} ListNode;

ListNode* mergeKLists(std::vector<ListNode*>& lists, MinHeap<ListNode>& heap) {
    int k = lists.size() - 1;
    bool marked[k+1];
    ListNode* head = nullptr, *tail = nullptr;

    while (k >= 0) {
        for(unsigned int j = 0; j < lists.size(); ++j) {
            if (lists[j] != nullptr) {
                heap.insert(*(lists[j]));
                lists[j] = lists[j]->next;
            } else {
                if (!marked[j]) {
                    k--;
                    marked[j] = true;
                }
            }
        }

        ListNode* node = new ListNode(heap.peek());
        if (head == nullptr) {
            head = node;
            heap.remove();
        } else {
            tail->next = node;
            heap.remove();
        }
        tail = node;
    }

    while(heap.size() > 0) {
        ListNode* node = new ListNode(heap.peek());
        heap.remove();
        tail->next = node;
        tail = node;
    }
    return head;
}

int main()
{
    // 2->4->nullptr
    // -4->4->5->nullptr
    // -1->nullptr
    ListNode n1 = {4, nullptr};
    ListNode n2 = {2, &n1};
    ListNode m1 = {5, nullptr};
    ListNode m2 = {4, &m1};
    ListNode m3 = {-4, &m2};
    ListNode l1 = {-1, nullptr};

    std::vector<ListNode*> kk = {&n2, &m3, &l1};
    MinHeap<ListNode> heap;
    ListNode* head = mergeKLists(kk, heap);

    for (ListNode* p = head; p != nullptr; p = p->next) {
        if (p == head)
            std::cout << p->value;
        else
            std::cout << ", " << p->value;
    }
}

上述代碼, 利用了上一篇"堆的基本實現"的代碼 - MinHeap; 針對這道題解法並不是很好. 比較了七月君的林老師的答案, 總結起來有以下幾點:

  • insert建堆 vs floyd建堆; insert建堆性能差; 林老師的建堆的過程是可以優化成floyd建堆的, 還是利用shiftDown, 只是計數範圍的改變(0 - floor(n/2) - 1);
  • 林老師的解法是針對鏈表頭建堆, 堆一共有k個元素, 摘取後替換爲linklist中下一個元素,調整堆屬性; 當某一個節點出現nullptr, 說明該鏈表結束, 補充堆尾再調整; 這個方法思路較我針對每一個節點建堆對的規模較小, 在堆中shiftDown調整次數較少;
  • 林老師的解法,單獨抽取了建堆和shiftDown的邏輯,整體上較爲簡潔; 我依賴MinHeap的寫法雖然也行, 但需要注意, 此類問題可以不用完整寫出堆或別的某種數據結構的完整邏輯, 而只借助其部分邏輯的實現完成, 形式更簡潔;
  • MinHeap的實現需要struct這類用戶定義類型去重載operator<, operator>, opeartor==, 並且提供default constructor;

下面列一下林老師的解法(建堆的地方改成floyd build heap)。

#include <iostream>
#include <vector>

typedef struct listNode {                                                          
  int value;
  struct listNode* next;
} ListNode;

int shiftDown(std::vector<ListNode*>& heap, int i);
ListNode* mergeKList(std::vector<ListNode*>& lists) {
  std::vector<ListNode*> heap;
  for (auto beg = lists.begin(); beg != lists.end(); ++beg) {
    heap.push_back(*beg);
  }

  // floyd build heap;
  int n = heap.size();
  for (int j = n/2 - 1; j >= 0; --j) {
    shiftDown(heap, j);
  }

  for (unsigned int j = 0; j < heap.size(); ++j) {
    std::cout << heap[j]->value;
  }
  std::cout << std::endl;

  ListNode* head = nullptr, *tail = nullptr;
  while (heap.size() > 0) {
    ListNode* node = heap[0];
    heap[0] = heap[0]->next;
	// 當用完一個鏈, 把堆尾元素補到堆首, 刪除堆尾, 調整堆;
    if (head == nullptr) {
      heap[0] = heap[heap.size() - 1];
      heap.pop_back();
    }
    shiftDown(heap, 0);
  }
  return head;
}

void swap(std::vector<ListNode*>& heap, int i, int j) {
  ListNode* t = heap[i];
  heap[i] = heap[j];
  heap[j] = t;
}

int shiftDown(std::vector<ListNode*>& heap, int i) {
  int n = heap.size();
  int k = 2 * i + 1;
  int h = 2 * i + 2;
  
  while (k < n || h < n) {
    int left_val, right_val, idx;
    if (k < n) {
      left_val = heap[k]->value;
    }
    if (h < n) {
      right_val = heap[h]->value;
    }
    if (k < n && h < n) {
      idx = left_val < right_val ? k : h;
    } else {
      idx = k;
    }
    if (heap[i]->value > heap[idx]->value) {
      swap(heap, i, idx);
      i = idx;
      k = 2 * i + 1;
      h = 2 * i + 2;
    } else
      break;
  }
  return i;
}

int main() {
  ListNode n1 = {4, nullptr};
  ListNode n2 = {2, &n1};
  ListNode m1 = {5, nullptr};
  ListNode m2 = {4, &m1};
  ListNode m3 = {-4, &m2};
  ListNode l1 = {-1, nullptr};

  std::vector<ListNode*> kk = {&n2, &m3, &l1};
  ListNode* head = mergeKList(kk);
  for (ListNode* p = head; p != nullptr; p = p->next) {
    if (p == head)
        std::cout << p->value;
    else
        std::cout << ", " << p->value;
  }
}

總結

  • 如何建堆,如何調整更加巧妙,是個問題.
  • 堆的重點基本操作的實現, 有時候單拎出來實現也是需要的; 重點數據結構的重點方法,有時也需要理解記憶。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章