本文已参与「新人创作礼」活动,一起开启掘金创作之路
引言
堆分为最小堆和最大堆,堆采用完全二叉树作为存储结构。一般使用堆进行排序以及解决海量数据Top K问题。对于掌握多门语言的同学,总是容易记混Go、C++、Java中的堆的定义及操作。本文以合并多个有序链表为例,分别介绍Go、C++、Java中的堆。
剑指 Offer II 078. 合并排序链表
解决此问题比较好的方法主要有两种:
设链表有k个,节点总个数为n
-
分治 (将k个链表合并,分成多次 两个链表合并的问题)时间复杂度为 O(n*log k)
-
最小堆 (取K个链表头部元素放入最小堆,依次弹出最小的节点,并放入最小节点指向的下一节点)时间复杂度为 O(n*log k)
本文为了记堆的使用,故下面都是用最小堆来实现
Go 堆实现
重述一下思路:
- 每个链表都是排序好的,所以k个链表头必定是所有值里最小的k个值
- 将k个链表头结点组成最小堆
- 每次取出当前的最小值节点,加入到结果链表中
- 并将这个节点下一个节点(非空)放入堆中
go中提供了 container/heap 来实现堆,堆中元素的类型需要实现 heap.Interface 这个接口。
- 补充一下heap需要的几个函数即可使用heap,详看注释
package main
import "container/heap"
// Definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}
//这里堆中存放其实不是节点值,而是k个链表中的头节点
type MyHeap []*ListNode
// 如果是存放int类型,那么定义 type IntHeap []int
//补充一下heap需要的函数
func (m MyHeap) Len() int {
return len(m)
}
//比较器,决定是最小堆还是最大堆的关键
func (m MyHeap) Less(i, j int) bool {
return m[i].Val < m[j].Val
}
func (m MyHeap) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
//注意push和pop中传的是指针
func (m *MyHeap) Push(v interface{}) {
*m = append(*m, v.(*ListNode)) /// v.(类型),比如v.(int)
}
func (m *MyHeap) Pop() interface{} {
old := *m
n := len(old)
x := old[n-1]
*m = old[:n-1]
return x
}
//合并链表函数
func mergeKLists(lists []*ListNode) *ListNode {
if len(lists) == 0 {
return nil
}
if len(lists) == 1 {
return lists[0]
}
var h = new(MyHeap) //定义最小堆h
heap.Init(h) //要初始化
for _, node := range lists { //依次将k个头节点插入
if node != nil {
heap.Push(h, node)
}
}
var ans = new(ListNode) //新建一个空节点指针
var cur = ans
for h.Len() > 0 {
var node = heap.Pop(h).(*ListNode) //取出当前的最小值节点
cur.Next = node //加入到结果链表中
node = node.Next //将这个节点下一个节点(非空)放入堆中
if node != nil {
heap.Push(h, node)
}
cur = cur.Next
}
return ans.Next //返回时 返回指向该节点的下一节点
}
C++ 堆实现
c++标准库中的堆,默认是大顶堆(max heap),包括下面几个函数:
- make_heap: 根据指定的迭代器区间以及一个可选的比较函数,来创建一个heap. O(N)
- push_heap: 把指定区间的最后一个元素插入到heap中. O(logN)
- pop_heap: 弹出heap顶元素, 将其放置于区间末尾. O(logN)
- sort_heap:堆排序算法,通常通过反复调用pop_heap来实现. N*O(logN)
C++11加入了两个新成员:
- is_heap: 判断给定区间是否是一个heap. O(N)
- is_heap_until: 找出区间中第一个不满足heap条件的位置. O(N)
因为heap以算法的形式提供,所以要使用这几个api需要包含 #include < algorithm >
PriorityQueue中的元素在逻辑上构成了一棵完全二叉树,但实际这个堆结构在物理存储上是用数组实现的。
对于已有数据类型初始化:
小根堆初始化
priority_queue<int,vector<int>,greater<int>> p
大根对初始化
priority_queue<int,vector<int>,less<int>> p
三个参数的意思:
@para p1 :该队列盛放的数据类型
@para p2 :container,需要是数组实现,一般就是vector
@para p3 :比较器,(因为涉及到了比较排序嘛
使用大根堆且不用自定义的数据结构,只要填写第一个参数即可:
priority_queue<int> p
对于自定义数据结构的堆 需要重载 < 符号 或者 写一个比较器
基本操作(和queue相似操作)
- top 访问队头元素
- empty 队列是否为空
- size 返回队列内元素个数
- push 插入元素到队尾 (并排序)
- emplace 原地构造一个元素并插入队列
- pop 弹出队头元素
- swap 交换内容
下面看合并多个有序链表的实现
class Solution {
public:
// 小根堆的回调函数(自定义比较函数)
struct cmp{ //通过结构体重载()函数
bool operator()(ListNode *a,ListNode *b){
return a->val > b->val; //如果反着来的话,就是大根堆
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
priority_queue<ListNode*, vector<ListNode*>, cmp> q; //创建一个优先队列
// 建立大小为k的小根堆
for(int i=0;i<lists.size();i++){
if(lists[i]) q.push(lists[i]);
}
// 使用哑节点
ListNode *dummy = new ListNode(0);
ListNode* tail = dummy;
// 开始出队
while(!q.empty()){ //队不空则循环
ListNode* top = q.top(); //top指针指向第一个出队的元素(最小的)
q.pop(); //出队
tail->next = top; //出队元素链接到dummy后面
tail = top; //更新p指针指向
if(top->next){ //如果出队元素的下一个不为空,就把它下一个放进去
q.push(top->next);
}
}
return dummy->next;
}
};
Java 堆实现
需要构造ListNode的比较器
class Solution {
// 自定义比较器,使得堆顶元素是堆中所有节点中值最小的节点
public static class ListNodeComparaotr implements Comparator<ListNode> {
public int compare(ListNode m, ListNode n) {
return m.val - n.val;
}
}
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) {
return null;
}
PriorityQueue<ListNode> heap = new PriorityQueue<>(new ListNodeComparaotr());
for (ListNode head : lists) {
if (head != null) {
heap.add(head);
}
}
ListNode virtualHead = new ListNode();
ListNode node = virtualHead;
// 不断的从堆中取出堆顶元素添加到结果集中,并将其下一个节点放入堆中
while (!heap.isEmpty()) {
ListNode small = heap.poll();
node.next = small;
node = node.next;
// PriorityQueue 中不能加入 null 元素,会运行失败
// 如果是 null 也不需要加入堆中
if (small.next != null) {
heap.add(small.next);
}
}
return virtualHead.next;
}
}