Java 集合扩展系列(一) | 图框架
Java集合扩展系列(二) |索引堆
Java集合扩展系列(三) |并查集
Java集合扩展系列(四) |字典树
1、前言
好像Java自带的集合体中并没有原生索引堆实现, 甚至原生的普通堆都没有,而优先级队列PriorityQueue虽然可以当作堆来使用,虽然底层是用了堆的思想实现, 但是它本质还是属于队列体系。
2、什么是堆
- 特点1: 本质是
一棵完全二叉树 - 特点2:
每个节点的值比它左右孩子节点的值都小 或者 都大。- 比孩子节点值都大的叫
最大堆, 反之叫最小堆。
- 比孩子节点值都大的叫
如下图两颗完全二叉树就是属于堆
3、堆的应用场景
堆可以以O(1)时间复杂度快速的获得集合里的最大 或者 最小值。
应用场景
- 生成优先级队列
- 大数据量的计算TopN问题
- 计算动态数据集合的中位数、百分位数
- 高性能定时任务执行器
4、堆实现原理
4.1 入堆
说白就是把一个节点插入一颗完全二叉树的尾节点的过程,如下入,直接插入到最后, 而我们一般用数组实现完全二叉树, 这个过程会更加的简单, 直接加入数组尾节点即可。 但是插入后可能不满足堆的第二大特性, 所以需要从尾节点开始进行一次上浮修复操作
4.2 出堆
出堆需要把堆顶的元素移除, 即将一颗完全的二叉树的根节点删除, 在实现上一般会让根节点和尾节点进行一次数值交换,然后删除尾节点。 然后从根节点开始进行一次下浮修复操作 即可。
如下图的将根节点16先与尾节点4交换,然后再删除尾节点。
4.3 修复
修复就是当堆内的节点之间的关系不再满足于 每个节点的值比它左右孩子节点的值都小 或者 都大的特点时进行的一次修复操作。 主要分为上浮修复 和 下沉修复
4.3.1 上浮修复
上浮修复的实现就是从某一个节点开始不断与父节点进行比较交换, 假设是最大堆,如果当前节点比父节点大, 则此节点应该往上浮, 所以交换它们两个值。 依次类推, 父节点继续与爷爷节点比较交换..., 直到满足条件 或者 到达 根部。
4.3.2 下沉修复
下浮修复的实现就是从某一个节点开始不断与子节点进行比较交换, 假设是最大堆,如果当前节点比子节点小, 则此节点应该往下浮, 所以交换它们两个值。 依次类推, 子节点继续与孙子节点比较交换..., 直到满足条件 或者 到达 尾部。
下面是一个最大堆的 依次出堆顶元素 的操作, 并从堆顶节点开始进行下沉修复操作的过程
5、Java如何使用堆
-
使用优先级队列PriorityQueue替代堆的使用, 每次出队可以把优先级最高的元素出队。 元素的优先级默认按照添加的元素的自然顺序判断, 也可以创建构造函数的时候指定自定义比较器。
-
PriorityQueue是无界队列, 如果是要只保留N个元素, 需要我们自己控制堆的容量
5.1 堆排序
// 优先级队列可当 堆使用, 默认不指定时最小堆
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
//PriorityQueue<Integer> minHeap = new PriorityQueue<>((a,b)-> b-a); //最大堆
// 往堆添加元素
int[] arr = {3,5,1,2,8,4};
for (int i = 0; i < arr.length; i++) {
minHeap.add(arr[i]);
}
// 此时的堆结构为
/*
1
2 3
5 8 4
*/
// 依次出堆顶, 输出为: 1,2,3,4,5,8
while (!minHeap.isEmpty()){
Integer poll = minHeap.poll();
System.out.println(poll);
}
5.1 求解TopN问题
求最小的K个数,可以使用最大堆, 因为每次出堆会把最大的出掉,所以堆剩下的就是最小的。
求最大的K个数,可以使用最小堆,因为每次出堆会把最小的出掉, 所以堆剩下的就是最大的。
测试使用最大堆求最小的K个数
// 创建最大堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a,b) -> b-a);
//往数组添加10万个数
ArrayList<Integer> arrList = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
arrList.add(i);
}
// 将数组数据顺序打乱
Collections.shuffle(arrList);
// 设置求最小的K个数
int K = 10;
// 往堆添加元素
for (Integer value : arrList) {
if (maxHeap.size() < K){
maxHeap.add(value);
}else if (value < maxHeap.peek()){
// 找到更小的数value,则把它添加到堆
maxHeap.remove();
maxHeap.add(value);
}
}
// 依次出堆顶, 取出最小的K个数
while (!maxHeap.isEmpty()){
Integer poll = maxHeap.poll();
System.out.println(poll);
}
测试使用最小堆求最大的K个数
// 创建最大堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>();
//往数组添加10万个数
ArrayList<Integer> arrList = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
arrList.add(i);
}
// 将数组数据顺序打乱
Collections.shuffle(arrList);
// 设置求最小的K个数
int K = 10;
// 往堆添加元素
for (Integer value : arrList) {
if (maxHeap.size() < K){
maxHeap.add(value);
}else if (value > maxHeap.peek()){
// 找到更大的数value,则把它添加到堆
maxHeap.remove();
maxHeap.add(value);
}
}
// 依次出堆顶, 取出最小的K个数
while (!maxHeap.isEmpty()){
Integer poll = maxHeap.poll();
System.out.println(poll);
}
6、索引堆
- 索引堆其实也是堆
- 将数据和索引这两部分分开存储,与普通堆不同此时的每个节点存放一个索引,而这个索引指向真正的对象值。但是构建索引堆还是依赖于索引对应的对象的实际值去比较和交换
索引堆结构如下图所示
- 完全二叉树的每个节点存放的是索引Index的值, 而data里面维护了Index 和 实际值的映射关系, 然后比较交换的时候是根据data的实际值去构建堆
- 其实和普通堆实现起来只是加多了一层映射关系,核心实现几乎一样。
与普通堆对比
- 普通堆在交换操作可能耗费时间较大,索引堆
优化了交换元素的消耗。 - 普通堆建成后很难索引到它,很难去改变它,但索引堆却可以以0(1)级别索引到具体元素,
也就是说它可以支持很方便的修改堆元素的操作 - 具有
O(1)级别按索引访问的特点又有堆快速取出极值的特点。 - 甚至可以认为是
具有堆功能的Map, 不过其中堆的构建是根据索引对应的值 而不是索引。
6.1、索引堆实现代码
/** 索引堆
* @author burukeyou
* @param <KEY> 索引
* @param <VALUE> 索引对应的值
*/
public class IndexHeap<KEY, VALUE extends Comparable<VALUE>> {
// 实际的索引树 keyTree[下标i] = 索引
private ArrayList<KEY> keyTree;
// Map<索引,下标>
private Map<KEY,Integer> keyIndexMap;
// Map<索引,元素>
private Map<KEY, VALUE> keyDataMap;
// 是否最小堆
private boolean isMinHeap;
// 堆的最大容量
private Integer maxSize;
public IndexHeap(boolean isMinHeap) {
this.isMinHeap = isMinHeap;
this.keyTree = new ArrayList<>();
this.keyIndexMap = new HashMap<>();
this.keyDataMap =new HashMap<>();
}
public IndexHeap(boolean isMinHeap, Integer maxSize) {
this(isMinHeap);
this.maxSize = maxSize;
}
public int size() {
return keyTree.size();
}
public boolean isEmpty() {
return keyTree.isEmpty();
}
public VALUE get(KEY key) {
return keyDataMap.get(key);
}
public boolean containsKey(KEY key) {
return keyDataMap.containsKey(key);
}
/**
* 替换索引的值
* @param key
* @param e
*/
public void replace(KEY key, VALUE e) {
if (!keyDataMap.containsKey(key)){
return;
}
keyDataMap.put(key, e);
// 获取索引对应的节点
int j = keyIndexMap.get(key);
// 从节点j开始修复堆
moveUp(j);
moveDown(j);
}
/**
* 查看堆顶元素
*/
public Entry<KEY,VALUE> peek() {
KEY key = keyTree.get(0);
VALUE value = keyDataMap.get(key);
return new Entry<>(key,value);
}
/**
* 堆顶元素出堆
*/
public Entry<KEY,VALUE> remove() {
if (isEmpty()){
return null;
}
Entry<KEY,VALUE> ret = peek();
//1.把尾节点与堆头交换存放的索引值
swap(keyTree,0,size()-1);
// 更新
keyIndexMap.put(keyTree.get(0),0);
KEY lastKey = keyTree.get(size() - 1);
keyIndexMap.remove(lastKey);
keyDataMap.remove(lastKey);
keyTree.remove(size() - 1);
// 从堆顶节点开始下浮修复
moveDown(0);
return ret;
}
/**
* 入堆 (索引KEY不存在时)
* @param key 索引
* @param e 实际值
*/
public void add(KEY key, VALUE e) {
if (!keyDataMap.containsKey(key)) {
keyTree.add(key);
keyDataMap.put(key, e);
keyIndexMap.put(key, size() - 1);
moveUp(size() - 1);
}
}
/**
* 入堆 (索引KEY存在时进行替换)
* @param key 索引
* @param e 实际值
*/
public void addOrReplace(KEY key, VALUE e){
if (!keyDataMap.containsKey(key)){
add(key,e);
}else{
replace(key,e);
}
}
/**
* 从i开始向上浮动修复堆
* @param i 索引树的节点下标
*/
private void moveUp(int i) {
while (i > 0){
VALUE iValue = getValueByIndex(i);
VALUE parentValue = getValueByIndex(parent(i));
// 最小堆
if (isMinHeap && iValue.compareTo(parentValue) > 0){
break;
}
// 最大堆
if (!isMinHeap && iValue.compareTo(parentValue) < 0){
break;
}
int patentIndex = parent(i);
swapAndRestKeyIndexMap(keyTree,i,patentIndex);
i = parent(i);
}
}
public VALUE getValueByIndex(int index){
return keyDataMap.get(keyTree.get(index));
}
/**
* 从i开始向下浮动修复堆
* @param i 索引树的节点下标
*/
private void moveDown(int i) {
//是否有孩子节点,因为是完全二叉树,只判断左孩子即可
while(leftChildren(i) < size()) {
// 指向较大或者较小的子节点
int tempMin = leftChildren(i);
int rightIndex = tempMin + 1;
//右子节点是否存在
if (rightIndex < size()){
// 最小堆, 如果右孩子比左孩子小,则更新tempMin指向
if (isMinHeap && getValueByIndex(tempMin).compareTo(getValueByIndex(rightIndex)) > 0){
tempMin = rightIndex;
}
// 最大堆, 如果右孩子比左孩子大,则更新tempMin指向
if (!isMinHeap && getValueByIndex(tempMin).compareTo(getValueByIndex(rightIndex)) < 0){
tempMin = rightIndex;
}
}
// 与父节点比较判断是否需要交换,不需要则堆结构调整修复完毕。
if (isMinHeap && getValueByIndex(i).compareTo(getValueByIndex(tempMin)) <= 0 ){
break;
}
if (!isMinHeap && getValueByIndex(i).compareTo(getValueByIndex(tempMin)) >= 0 ){
break;
}
//
swapAndRestKeyIndexMap(keyTree,i,tempMin);
//更新父节点
i = tempMin;
}
}
// 获取父节点
private int parent(int index){
return (index-1)/2;
}
// 获取左子节点
private int leftChildren(int index){
return index * 2 + 1;
}
// 获取➡又子节点
private int rightChildren(int index){
return index * 2 + 2;
}
// 交换索引堆中的索引i和j
private void swap(ArrayList<KEY> arr, int i, int j) {
KEY tmp = arr.get(i);
arr.set(i,arr.get(j));
arr.set(j,tmp);
}
// 交换索引堆中的索引i和j 并更新keyIndexMap的关系
private void swapAndRestKeyIndexMap(ArrayList<KEY> arr, int i, int j) {
KEY tmp = arr.get(i);
arr.set(i,arr.get(j));
arr.set(j,tmp);
keyIndexMap.put(keyTree.get(i),i);
keyIndexMap.put(keyTree.get(j),j);
}
/**
* TopN计算封装
* @param key
* @param e
*/
public void addForTopN(KEY key, VALUE e){
if (maxSize == null || size() < maxSize){
// 没指定阈值, 或者没达到阈值正常添加
add(key,e);
return;
}
// 指定了堆大小 并且 堆元素大小达到最大值需要,需要出堆一个
Entry<KEY, VALUE> topEntry = peek();
if (isMinHeap && e.compareTo(topEntry.getValue()) > 0){
// 找到更大的数,添加到堆
this.remove();
this.add(key,e);
}else if (!isMinHeap && e.compareTo(topEntry.getValue()) < 0){
// 找到更小的数,添加到堆
this.remove();
this.add(key,e);
}
}
/**
* 查看堆情况
*/
public void output(){
List<Entry<KEY,VALUE>> ret = new ArrayList<>();
for (KEY key : keyTree) {
VALUE value = keyDataMap.get(key);
ret.add(new Entry<>(key,value));
}
ret.sort(Comparator.comparing(Entry::getValue));
ret.forEach(System.out::println);
}
}
/**
* @author burukeyou
*/
public class Entry<KEY,VALUE> {
private KEY key;
private VALUE value;
public Entry(KEY key, VALUE value) {
this.key = key;
this.value = value;
}
public KEY getKey() {
return key;
}
public VALUE getValue() {
return value;
}
public void setKey(KEY key) {
this.key = key;
}
public void setValue(VALUE value) {
this.value = value;
}
@Override
public String toString() {
return "{" +
"'key':" + key +
", 'value':" + value +
'}';
}
}
6.2 测试
堆排序
// 创建最小索引堆 参数: true-最小堆, false-最大堆
IndexHeap<Integer, String> indexMinHeap = new IndexHeap<>(true);
// 往索引堆添加元素
for (int i = 0; i < 10; i++) {
indexMinHeap.add(i, "机器人"+i);
}
// 修改索引堆中索引为3的元素的值
indexMinHeap.replace(3,"机器人9999999");
// 查看堆顶元素
Entry<Integer, String> peek = indexMinHeap.peek();
// 出堆
while (!indexMinHeap.isEmpty()){
Entry<Integer, String> tmp = indexMinHeap.remove();
System.out.println(tmp);
}
计算TopN
- 有一个场景, 每天都有新用户添加, 初始会给每个用户分配幸运值, 需要实时计算幸运值排行前十的用户, 并且中途收到命令, 将此刻幸运值最高的用户 获得特殊奖励积分调到888888888。
// 创建最小索引堆 设置堆容量10
IndexHeap<Integer, Integer> indexMinHeap = new IndexHeap<>(true,10);
// 1、新增一万个用户, 分配一个随机幸运值
for (int i = 0; i <= 10000; i++) {
indexMinHeap.addForTopN(i,new Random().nextInt(10000));
}
// 2、接到上级命令,将此刻幸运值最高的用户 获得特殊奖励, 幸运值调到888888888
Integer maxUserId = indexMinHeap.peek().getKey(); // 积分最高的用户的id
System.out.println("将用户: " + maxUserId + " 幸运值调到 888888888");
indexMinHeap.replace(maxUserId,888888888);
// 3、又新增了两万个用户
for (int i = 10001; i <= 20000; i++) {
indexMinHeap.addForTopN(i,new Random().nextInt(100000));
}
// 4、查询幸运值排行前十的用户
System.out.println("=======幸运值排行如下: "+ LocalDateTime.now() +"===========");
indexMinHeap.output();
// 5、又新增了1万个用户
for (int i = 20001; i <= 30000; i++) {
indexMinHeap.addForTopN(i,new Random().nextInt(100000));
}
// 6、查询幸运值排行前十的用户
System.out.println("=======幸运值排行如下: "+ LocalDateTime.now() +"===========");
indexMinHeap.output();