堆应用: 合并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;
  }
}

总结

  • 如何建堆,如何调整更加巧妙,是个问题.
  • 堆的重点基本操作的实现, 有时候单拎出来实现也是需要的; 重点数据结构的重点方法,有时也需要理解记忆。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章