堆其实就是二叉树,只不过父亲节点永远比左右孩子大(或者小),成为最大堆(最小堆)。
根节点永远是最大(最小)。 注意:它不是二分搜索树(根节点比左孩子大,比右孩子小)。
虽然堆是一颗树,内部可以用一个数组来表示。 将每个节点按索引位置对应到数组中。在操作时,先根据二叉树的特性,算出索引,然后操作数组中的数据。
如下图:
上图中:给任意一个节点的索引,可以算出其父亲节点,左孩子和右孩子的索引,去操作数组中对应索引位置的数据。
下面,实现一个最大堆:
public class MaxHeap<E extends Comparable<E>> {
private Array<E> data;
public MaxHeap(int capacity) {
data = new Array<E>(capacity);
}
public MaxHeap() {
data = new Array<E>();
}
// 返回堆中的元素个数
public int size() {
return data.getSize();
}
// 返回一个布尔值, 表示堆中是否为空
public boolean isEmpty() {
return data.isEmpty();
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
private int parent(int index) {
if (index == 0) {
throw new IllegalArgumentException("index-0 doesn't have parent.");
}
return (index - 1) / 2;
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
private int leftChild(int index) {
return 2 * index + 1;
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
private int rightChild(int index) {
return 2 * index + 2;
}
// 向堆中添加元素
public void add(E e) {
data.addLast(e);
siftUp(data.getSize() - 1);
}
//元素上浮,当前节点与父亲节点相比,如果大,就和父亲节点交换
private void siftUp(int k) {
while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
data.swap(parent(k), k);
k = parent(k);
}
}
// 看堆中的最大元素
public E findMax() {
if (data.getSize() == 0) {
throw new IllegalArgumentException("Can not findMax when heap is empty.");
}
return data.getFirst();
}
// 取出堆中的最大元素,并且替换成元素e
public E replace(E e){
E ret = findMax();
data.set(0, e);
siftDown(0);
return ret;
}
// 取出堆中最大元素
public E extractMax() {
E e = findMax();
data.swap(0, data.getSize() - 1);
data.removeLast();
siftDown(0);
return e;
}
//元素下沉
//找出左孩子与右孩子中值大的位置,再和k比较,看看是否需要交换
private void siftDown(int k) {
while (leftChild(k) < data.getSize()) {
int j = leftChild(k);
//如果有右孩子,并且右孩子的值大,需要和右孩子做交换
if ((j + 1) < data.getSize() && data.get(j).compareTo(data.get(j + 1)) < 0) {
j++;
}
if (data.get(k).compareTo(data.get(j)) >= 0) {
break;
}
data.swap(k, j);
k = j;
}
}
}
有了堆的特性,现在实现一个优先队列(不是先进先出,新添加的元素不一定在队尾,而是根据优先级-重写compareTo方法,元素位置会发生变化)
public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
private MaxHeap<E> maxHeap;
public PriorityQueue(){
maxHeap = new MaxHeap<>();
}
@Override
public int getSize(){
return maxHeap.size();
}
@Override
public boolean isEmpty(){
return maxHeap.isEmpty();
}
@Override
public E getFront(){
return maxHeap.findMax();
}
@Override
public void enqueue(E e){
maxHeap.add(e);
}
@Override
public E dequeue(){
return maxHeap.extractMax();
}
}
下面来做一道关于topK的题目:
/**
* 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
* <p>
* 示例 1: 输入: nums = [1,1,1,2,2,3], k = 2 (输出数组中,出现频率前2名的元素) 输出: [1,2]
* <p>
* 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/top-k-frequent-elements 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
*/
public class Solution {
//思路: 先把数组中元素和对应的次数存入map,再遍历map将元素和次数经过比较后,存入堆中。 只存k个元素,最后打印堆中元素。
//定义一个类表示元素和出现次数,将Freq对象存入堆中
private class Freq implements Comparable<Freq> {
public int e, count;
public Freq(int e, int count) {
this.e = e;
this.count = count;
}
@Override
//重写此方法,调换大小,存入最大堆后,实则是一个最小堆。 后续和堆元素的最小值比较时,只需要比较堆顶元素。
public int compareTo(Freq another) {
if (this.count < another.count) {
return 1;
} else if (this.count > another.count) {
return -1;
} else {
return 0;
}
}
}
public List<Integer> topKFrequent(int[] nums, int k) {
TreeMap<Integer, Integer> map = new TreeMap<>();
for (int num : nums) {
if (map.containsKey(num)) {
map.put(num, map.get(num) + 1);
} else {
map.put(num, 1);
}
}
MaxHeap<Freq> maxHeap = new MaxHeap<>();
for (int key : map.keySet()) {
if (maxHeap.size() < k) {
maxHeap.add(new Freq(key, map.get(key)));
} else if (map.get(key) > maxHeap.findMax().count) {
maxHeap.extractMax();
maxHeap.add(new Freq(key, map.get(key)));
}
}
LinkedList<Integer> res = new LinkedList<>();
while (!maxHeap.isEmpty()) {
res.add(maxHeap.extractMax().e);
}
return res;
}
private static void printList(List<Integer> nums) {
for (Integer num : nums) {
System.out.print(num + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] nums = {1, 1, 1, 2, 2, 3};
int k = 2;
printList((new Solution()).topKFrequent(nums, k));
}
}