算法之堆的应用(优先队列)

简介

堆(Heap)是一个可以被看成近似完全二叉树的数组。树上的每一个结点对应数组的一个元素。除了最底层外,该树是完全充满的,而且是从左到右填充。—— 来自:《算法导论》

堆包括最大堆和最小堆:最大堆的每一个节点(除了根结点)的值不大于其父节点;最小堆的每一个节点(除了根结点)的值不小于其父节点。

堆常见的操作:

HEAPIFY 建堆:把一个乱序的数组变成堆结构的数组,时间复杂度为 O ( n ) O(n) O(n)
HEAPPUSH:把一个数值放进已经是堆结构的数组中,并保持堆结构,时间复杂度为 O ( l o g   n ) O(log\ n) O(log n)
HEAPPOP:从最大堆中取出最大值或从最小堆中取出最小值,并将剩余的数组保持堆结构,时间复杂度为 O ( l o g   n ) O(log\ n) O(log n)
HEAPSORT:借由 HEAPFY 建堆和 HEAPPOP 堆数组进行排序,时间复杂度为 O ( n   l o g   n ) O(n\ log\ n) O(n log n),空间复杂度为 O ( 1 ) O(1) O(1)
堆结构的一个常见应用是建立优先队列(Priority Queue)。

例题

以上题目均来自 leetcode 和 牛客网.

解析

23. 合并K个升序链表

题目描述

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

分析

  1. 分治合并。略
  2. 优先队列。创建最小堆,按照链表节点的值建立优先级,开始时将每个链表的头节点放入优先队列,每次从堆中弹出堆顶元素,合并到结果中,然后将下一个节点入堆,直到堆为空。
    时间复杂度:链表个数为k,每个链表长度为 n n n,堆大小为 k k k,所以每次插入和删除的时间复杂度为 O ( l o g   k ) O(log\ k) O(log k),一共有 k n kn kn 个节点入堆和出堆,所以总的时间复杂度为 O ( k n ∗ l o g   k ) O(kn*log\ k) O(knlog k)
    空间复杂度为 O ( k ) O(k) O(k)

代码

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        PriorityQueue<ListNode> heap = new PriorityQueue<>((x, y) -> x.val - y.val);
        for (int i = 0; i < lists.length; i++) {
            if (lists[i] != null) {
                heap.add(lists[i]);
            }
        }
        ListNode dummy = new ListNode(-1), cur = dummy, node;
        while (!heap.isEmpty()) {
            node = heap.poll();
            cur.next = node;
            cur = cur.next;
            if (node.next != null) {
                heap.add(node.next);
            }
        }
        return dummy.next;
    }
}

215. 数组中的第K个最大元素

题目描述

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

分析

  1. 优先队列。求第k个最大元素,建立小顶堆,维护堆大小为k,超过k就弹出堆顶元素。当遍历完数组之后,堆中留下的就是最大的k个元素,堆顶就是第k个最大元素。
    时间复杂度: O ( n l o g   k ) O(nlog\ k) O(nlog k)
    空间复杂度: O ( k ) O(k) O(k)
  2. 快排思想。 O ( n ) O(n) O(n)解法,略。

代码

class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        for (int i = 0; i < nums.length; i++) {
            heap.add(nums[i]);
            if (i >= k) {
                heap.poll();
            }
        }
        return heap.peek();
    }
}

239. 滑动窗口最大值

题目描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置    最大值
---------------            -----
[1 3 -1] -3 5 3 6 7    3
1 [3 -1 -3] 5 3 6 7    3
1 3 [-1 -3 5] 3 6 7    5
1 3 -1 [-3 5 3] 6 7    5
1 3 -1 -3 [5 3 6] 7    6
1 3 -1 -3 5 [3 6 7]    7

分析

  1. 优先队列。建立最大堆,堆里存储的元素包含nums的索引和值,按照值的大小建立优先级。依次遍历nums中元素,入堆,同时检查堆顶元素是否在窗口之内,窗口之外的直接弹出,碰到窗口之内的加入答案集合。
    时间复杂度:极端情况下nums升序排列,堆的大小最大为n,所以插入和删除的时间复杂度为 O ( l o g   n ) O(log\ n) O(log n),加上遍历nums每个元素的复杂度,总时间复杂度为 O ( n l o g   n ) O(nlog\ n) O(nlog n).
    空间复杂度: O ( n ) O(n) O(n).
  2. 单调队列。 O ( n ) O(n) O(n)解法,略

代码

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        if (n == 0 || k == 0) {
            return new int[0];
        }
        int[] res = new int[n - k + 1];
        PriorityQueue<int[]> heap = new PriorityQueue<>((x, y) -> y[0] - x[0]);
        
        for (int i = 0; i < n; i++) {
            heap.add(new int[]{nums[i], i});
            if (i >= k - 1) {
                while (heap.peek()[1] <= (i - k)) {
                    heap.poll();
                }
                res[i - k + 1] = heap.peek()[0];
            }
        }
        return res;
    }
}

264. 丑数 II

题目描述

给你一个整数 n ,请你找出并返回第 n 个 丑数 。

丑数 就是只包含质因数 2、3 和/或 5 的正整数。

示例 1:

输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。

分析

  1. 优先队列。维护一个最小堆,最开始放入堆的元素是1,然后每次弹出堆顶元素,剩以2,3,5后再入堆。同时维护一个HashSet过滤重复元素。这样第n次弹出的元素就是所求的第n个丑数。
    时间复杂度: O ( n   l o g   n ) O(n\ log\ n) O(n log n)
  2. 动态规划。 O ( n ) O(n) O(n) 解法,略。

代码

class Solution {
    public int nthUglyNumber(int n) {
        PriorityQueue<Long> heap = new PriorityQueue<>();
        Set<Long> seen = new HashSet<>();
        heap.add(1L);
        seen.add(1L);
        int[] factors = {2, 3, 5};

        long res = 0;
        for (int i = 0; i < n; i++) {
            res = heap.poll();
            for (int f : factors) {
                long v = f * res;
                if (!seen.contains(v)) {
                    heap.add(v);
                    seen.add(v);
                }
            }
        }
        return (int)res;
    }
}

295. 数据流的中位数

题目描述

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

  • void addNum(int num) - 从数据流中添加一个整数到数据结构中。
  • double findMedian() - 返回目前所有元素的中位数。

示例:

addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3)
findMedian() -> 2

分析

  1. 两个优先队列。维护一个大顶堆maxHeap和一个小顶堆minHeap,并且maxHeap和minHeap的大小要么相等,要么maxHeap比minHeap大一,并且大顶堆维护的是最大的数,小顶堆维护的是最小的数。这样中位数就是maxHeap的堆顶元素或者是两个堆顶元素的平均数。
    时间复杂度:addNum:O(logn),findMedian:O(1)

代码

class MedianFinder {

    PriorityQueue<Integer> minHeap;
    PriorityQueue<Integer> maxHeap;
    /** initialize your data structure here. */
    public MedianFinder() {
        minHeap = new PriorityQueue<>();
        maxHeap = new PriorityQueue<>((x, y) -> y - x);
    }

    public void addNum(int num) {
        if (minHeap.size() != maxHeap.size()) {
            maxHeap.add(num);
            minHeap.add(maxHeap.poll());
        } else {
            minHeap.add(num);
            maxHeap.add(minHeap.poll());
        }
    }

    public double findMedian() {
        if (minHeap.size() != maxHeap.size()) {
            return maxHeap.peek() + 0.0;
        } else {
            return (maxHeap.peek() + minHeap.peek()) / 2.0;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值