前言
不得不说随着互联网的发展现在的笔试题也是越来越难了,不仅要求在其规定时间内实现其要求的功能,还要求将代码实现时间复杂度的降低优化。好了废话不说了,题目要求如下:
合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。
数据范围:节点总数 0≤n≤5000,每个节点的val满足∣val∣<=1000
要求:时间复杂度O(nlogn)
该说不说一点都不意外,要知道你在牛客或者是力扣上面进行练习的时候有哪些测试案例不通过他是会直接告诉你是什么测试案例,你也就清楚了自己的问题可能出在了哪里,但是笔试的时候不会给出通过不了的测试案例,他只会给你一个冰冷无情的通过率数字(我想问你们公司自己测试的时候出了bug难道测试人员也不会告诉程序员是啥样的测试案例不通过吗?)。
好了吐槽完了以下是正文:
核心代码模式
首先就是核心代码模式了,做过牛客和力扣的都知道,他们为了减轻你写代码的复杂量,专门只提供给你一个写核心功能函数的地方,什么输入啊,输出啊都不要你管,简直太良心了有木有?
初始界面
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) : val(x), next(nullptr) {}
* };
*/
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param lists ListNode类vector
* @return ListNode类
*/
ListNode* mergeKLists(vector<ListNode*>& lists) {
// write code here
}
};
首先呢,告诉你基类节点叫ListNode,然后给出你需要写入的功能函数mergeKLists,并提供相应参数,你只需要在函数的大括号内写内容就行了。
优先级队列
然后,为了实现所谓的时间复杂度的优化,我们这边建议使用C++标准库中的std::priority_queue(俗称优先级队列,不清楚的请点击链接自动跳转),这个类型需要三个东西,一个叫模板参数,指定了优先队列中存储的元素类型,一个叫底层容器,一个叫比较函数对象,确定了如何对队列中的元素进行排序和排列。
那么按照上述说法,我们首先需要定义一个比较函数:
// 自定义比较函数,用于最小堆的比较
struct Compare {
bool operator()(const ListNode* a, const ListNode* b) {
return a->val > b->val;
}
};
然后建立这个优先级队列:
std::priority_queue<ListNode*, std::vector<ListNode*>, Compare> minHeap;
提取参数
由于函数mergeKLists所提供的参数为vector<ListNode*>& lists>,而我们需要对其中数据进行处理,那么我们首先就需要对其进行数据参数的提取:
for (ListNode* list : lists) {
if (list) {
minHeap.push(list);
}
}
确定返回值
那么接下来我们就需要定义一个返回值类型ListNode*的头结点:
// 创建一个头结点作为结果链表的头
ListNode* dummy = new ListNode(0);
ListNode* current = dummy;
核心部分
接下来就是整个功能函数的最核心的部分,他叫动态规划:
// 从最小堆中取出最小的节点,将其加入结果链表,并将其下一个节点重新放入最小堆
while (!minHeap.empty()) {
ListNode* smallest = minHeap.top();
minHeap.pop();
current->next = smallest;
current = current->next;
if (smallest->next) {
minHeap.push(smallest->next);
}
}
我们进入一个循环,该循环将一直执行,直到最小堆 minHeap
为空。在循环中,我们从最小堆中取出最小的节点 smallest
,这是堆中的顶部元素,它是 k 个链表中当前值最小的节点。我们从堆中移除 smallest
,因为我们将要将它加入结果链表中。我们将 smallest
加入到结果链表的尾部,由 current
指向的位置,然后将 current
移动到新添加的节点上,这是为了在结果链表中保持正确的顺序。我们检查 smallest
是否有下一个节点(即 smallest->next
是否存在)。如果存在,我们将下一个节点重新放入最小堆 minHeap
中,以便继续比较和合并。
end
return dummy->next;
ACM模式
以上呢,就是核心代码模式的全部内容,接下来我们讲讲ACM模式。
ListNode
首先ACM模式我们不需要把这个功能函数mergeKLists写成类内函数的形式,但是我们此时就需要加入一个叫做ListNode的结构体了:
// 定义链表节点
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
然后多写一个打印输出函数:
// 辅助函数,用于打印链表
void printList(ListNode* head) {
ListNode* current = head;
while (current) {
std::cout << current->val << " -> ";
current = current->next;
}
std::cout << "nullptr" << std::endl;
}
main
最后就是主函数了,不过关于主函数我这里给出的实现较为简单,并没有写关于如何读取数据的部分(ACM最难的可能就是如何从他给的输入框里面得到全部正确的数据这一点了):
int main() {
// 创建三个升序链表
ListNode* list1 = new ListNode(1);
list1->next = new ListNode(4);
list1->next->next = new ListNode(5);
ListNode* list2 = new ListNode(1);
list2->next = new ListNode(3);
list2->next->next = new ListNode(4);
ListNode* list3 = new ListNode(2);
list3->next = new ListNode(6);
std::vector<ListNode*> lists = { list1, list2, list3 };
// 合并链表
ListNode* merged = mergeKLists(lists);
// 打印合并后的链表
printList(merged);
return 0;
}
然后关于数据从输入端读取这部分后面我会专门写篇相应的博客讲解,在本文中主要还是实现功能。
最后,写文不易,不收藏也请给个赞,谢谢亲~!
(本文仅供学习时参考,如有错误,纯属作者技术不到位,不足之处请多指教,谢谢)