堆(优先队列)之习题分析
一、堆以及优先队列的概念
(一)、堆的概念
严格来讲,堆有不同的种类,但是我们在算法学习中,主要用的还是二叉堆,而二叉堆有最大堆和最小堆之分。
最大(最小)堆是一棵每一个节点的键值都不小于(大于)其孩子(如果存在)的键值的树。大顶堆是一棵完全二叉树,同时也是一棵最大树。小顶堆是一棵完全完全二叉树,同时也是一棵最小树。
需要注意的问题是:堆中的任一子树也还是堆,即大顶堆的子树也都是大顶堆,小顶堆同样。
图一为大顶堆,图二为小顶堆
(二)、优先队列——PriorityQueue
1、优先队列的概念
PriorityQueue类在Java1.5中引入。PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。要求使用Java Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。
2、优先队列的数据结构
优先队列底层的数据结构其实是一颗二叉堆,优先队列使用二叉堆的特点,可以使得插入的数据自动排序(升序或者是降序)
二叉堆:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-djYv4zYK-1609923407678)(https://pics3.baidu.com/feed/f2deb48f8c5494ee1523ac36af9877f899257e14.jpeg?token=43d94f8e2c6a6a839bde47f73f4dc955)]
特点:
- 二叉堆是一个完全二叉树
- 根节点总是大于左右子节点(大顶堆),或者是小于左右子节点(小顶堆)
3、优先队列的源码分析
(1)、属性
/**
* 默认初始量
*/
private static final int DEFAULT_INITIAL_CAPACITY = 11;
/**
* 维持一个队列:因为基于二叉堆来实现优先队列,queue[i]的子节点为queue[2*i+1]/queue[2*i+2]
*/
transient Object[] queue;
/**
* 优先队列中元素个数
*/
private int size = 0;
/**
* 比较器:用于降序或者是比较自定义的对象
*/
private final Comparator<? super E> comparator;
/**
* 优先队列的结构:被修改的次数
*/
transient int modCount = 0;
(2)、构造方法
/**
* 创建一个 PriorityQueue,并根据其自然顺序对元素进行排序
*/
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
/**
* 使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序
*/
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
/**
* 创建包含指定 collection 中元素的 PriorityQueue
*/
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
/**
* 使用指定的初始容量创建一个 PriorityQueue,并根据指定的比较器对元素进行排序
*/
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
/**
* 创建包含指定 collection 中元素的 PriorityQueue
*/
@SuppressWarnings("unchecked")
public PriorityQueue(Collection<? extends E> c) {
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
}
else if (c instanceof PriorityQueue<?>) {
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
}
else {
this.comparator = null;
initFromCollection(c);
}
}
/**
* 创建包含指定优先级队列元素的 PriorityQueue
*/
@SuppressWarnings("unchecked")
public PriorityQueue(PriorityQueue<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initFromPriorityQueue(c);
}
/**
* 创建包含指定有序set元素的PriorityQueue
*/
@SuppressWarnings("unchecked")
public PriorityQueue(SortedSet<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initElementsFromCollection(c);
}
(3)、常用方法
/**
* 插入一个元素
*/
public boolean add(E e) {
return offer(e);
}
/**
* 插入一个元素
*/
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
/**
* 查询队顶元素
*/
@SuppressWarnings("unchecked")
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
/**
* 删除一个元素,并返回删除的元素
*/
@SuppressWarnings("unchecked")
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
/**
* 查询对象o的索引
*/
private int indexOf(Object o) {
if (o != null) {
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
return -1;
}
/**
* 删除一个元素
*/
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
/**
* 判断是否包含该元素
*/
public boolean contains(Object o) {
return indexOf(o) != -1;
}
二、数据流的中位数
(一)、题目需求
如何得到一个数据流中的中位数?
如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
示例 1:
输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
示例 2:
输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]
限制:
最多会对 addNum、findMedian 进行 50000 次调用。
(二)、解法
PriorityQueue<Integer> maxHeap;
PriorityQueue<Integer> minHeap;
/**
* initialize your data structure here.
*/
public MedianFinder() {
maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
minHeap = new PriorityQueue<>();
}
public void addNum(int num) {
if (maxHeap.size() != minHeap.size()) {
minHeap.add(num);
maxHeap.add(minHeap.poll());
} else {
maxHeap.add(num);
minHeap.add(maxHeap.poll());
}
}
public double findMedian() {
if (maxHeap.size() != minHeap.size()) {
return minHeap.peek();
} else {
return (minHeap.peek() + maxHeap.peek()) / 2.0;
}
}
(三)、代码分析
1、定义大顶堆与小顶堆
大顶堆:存储输入数据流中排序后,前半段的元素。堆顶为前半段元素的最后一位元素
小顶堆:存储输入数据流中排序后,后半段的元素。堆顶为后半段元素的第一位元素
PriorityQueue<Integer> maxHeap;
PriorityQueue<Integer> minHeap;
2、初始化大顶堆与小顶堆
public MedianFinder() {
maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
minHeap = new PriorityQueue<>();
}
3、进行元素添加操作,并判断大顶堆与小顶堆的数量。
(1)、若大顶堆与小顶堆的数量不一致则先将其放入小顶堆中进行排序,小顶堆堆顶元素出队列,同时小顶堆重新进行排序。出队的元素进入大顶堆,同时大顶堆重新进行排序。
(2)、若大顶堆与小顶堆的数量一致则先将其放入大顶堆中进行排序,大顶堆堆顶元素出队列,同时大顶堆重新进行排序。出队的元素进入小顶堆,同时小顶堆重新进行排序。
public void addNum(int num) {
if (maxHeap.size() != minHeap.size()) {
minHeap.add(num);
maxHeap.add(minHeap.poll());
} else {
maxHeap.add(num);
minHeap.add(maxHeap.poll());
}
}
4、计算中位数
(1)、若大顶堆与小顶堆的数量不一致,则数据流数量为奇数,中位数为小顶堆的堆顶元素。
(2)、若大顶堆与小顶堆的数量不一致,则数据流数量为偶数,中位数为小顶堆的堆顶元素加上大顶堆的堆顶元素之和/2。
public double findMedian() {
if (maxHeap.size() != minHeap.size()) {
return minHeap.peek();
} else {
return (minHeap.peek() + maxHeap.peek()) / 2.0;
}
}
三、第K大元素
(一)、题目需求
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
说明:
你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。
(二)、解法
1、快速队列
public int findKthLargestQuickSort(int[] nums, int k) {
if (nums == null || nums.length == 0 || k < 1 || k > nums.length) {
return -1;
}
sort(nums, 0, nums.length - 1);
return nums[nums.length - k];
}
private void sort(int[] arr, int start, int end) {
if (start >= end) {
return;
}
int left = start;
int right = end;
int pivot = arr[start];
while (left <= right) {
while (left <= right && arr[left] < pivot) {
left++;
}
while (left <= right && arr[right] > pivot) {
right--;
}
if (left <= right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
sort(arr, start, right);
sort(arr, left, end);
}
2、快速队列改进版
public int findKthLargestQuickSort(int[] nums, int k) {
if (nums == null || nums.length == 0 || k < 1 || k > nums.length) {
return -1;
}
return partition(nums, 0, nums.length - 1, nums.length - k);
}
private int partition(int[] arr, int start, int end, int k) {
if (start >= end) {
return arr[k];
}
int left = start;
int right = end;
int mid = start + (end - start) / 2;
int pivot = arr[mid];
while (left <= right) {
while (left <= right && arr[left] < pivot) {
left++;
}
while (left <= right && arr[right] > pivot) {
right--;
}
if (left <= right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
if (k <= right) {
partition(arr, start, right, k);
}
if (k >= left) {
partition(arr, left, end, k);
}
return arr[k];
}
3、优先队列
public int findKthLargest(int[] nums, int k) {
if (nums == null || nums.length == 0 || k < 1 || k > nums.length) {
return -1;
}
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int num : nums) {
heap.add(num);
if (heap.size() > k) {
heap.poll();
}
}
return heap.peek();
}
(三)、代码分析
1、定义并初始化小顶堆优先队列进行辅助
PriorityQueue<Integer> heap = new PriorityQueue<>();
2、逐个加入队列中,并进行判断当前队列元素数量是否超过k
for (int num : nums) {
heap.add(num);
if (heap.size() > k) {
heap.poll();
}
}
3、返回第K个大的元素——堆顶元素
return heap.peek();