简介
堆(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)。
例题
- 23. 合并K个升序链表 困难
- 215. 数组中的第K个最大元素 中等
- 239. 滑动窗口最大值 困难
- 218. 天际线问题
- 264. 丑数 II
- 295. 数据流的中位数
以上题目均来自 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
分析
- 分治合并。略
- 优先队列。创建最小堆,按照链表节点的值建立优先级,开始时将每个链表的头节点放入优先队列,每次从堆中弹出堆顶元素,合并到结果中,然后将下一个节点入堆,直到堆为空。
时间复杂度:链表个数为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(kn∗log 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
分析
- 优先队列。求第k个最大元素,建立小顶堆,维护堆大小为k,超过k就弹出堆顶元素。当遍历完数组之后,堆中留下的就是最大的k个元素,堆顶就是第k个最大元素。
时间复杂度: O ( n l o g k ) O(nlog\ k) O(nlog k)
空间复杂度: O ( k ) O(k) O(k) - 快排思想。 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
分析
- 优先队列。建立最大堆,堆里存储的元素包含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). - 单调队列。 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,然后每次弹出堆顶元素,剩以2,3,5后再入堆。同时维护一个HashSet过滤重复元素。这样第n次弹出的元素就是所求的第n个丑数。
时间复杂度: O ( n l o g n ) O(n\ log\ n) O(n log n) - 动态规划。 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
分析
- 两个优先队列。维护一个大顶堆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;
}
}
}