Go、C++、Java中的堆,以实现合并多个有序链表为例

402 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

引言

堆分为最小堆和最大堆,堆采用完全二叉树作为存储结构。一般使用堆进行排序以及解决海量数据Top K问题。对于掌握多门语言的同学,总是容易记混Go、C++、Java中的堆的定义及操作。本文以合并多个有序链表为例,分别介绍Go、C++、Java中的堆。

剑指 Offer II 078. 合并排序链表

image.png

解决此问题比较好的方法主要有两种:

设链表有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;
    }
}