前言
堆是一种比较奇特的数据结构,它描述的是一个完全二叉树,但是保存的方式却是一个数组,堆有两种形式:大顶堆和小顶堆。大顶堆满足每个根节点都大于其子节点,小顶堆满足每个根节点都小于其子节点,所以大顶堆的顶就是整个堆里最大的元素,而小顶堆的顶就是整个堆里最小的元素。
如图是个大顶堆
堆的特征
堆的元素和以往的树的存储方式不同,堆中的每一个元素都存储在数组中,比如上述的大顶堆在数组里的表现形式就是:
int[] heap = {9,6,5,2,4,1,3};
这个数组是如何计算出来的呢?
堆的基本公式
在堆中有以下几个核心公式会贯穿整个堆:
left_child = 2*parent + 1 //左孩子下标
right_child = 2*parent +2 //右孩子下标
parent = (index - 1) / 2 //父节点下标
我们不妨拿上面的示例数组来做个演示:
以0下标为根节点:
左孩子下标 = 0 * 2 + 1 = 1;
右孩子下标 = 0 * 2 + 2 = 2;
以2下标为根节点:
左孩子下标 = 2 * 2 + 1 = 5;
右孩子下标 = 2 * 2 + 2 = 6;
以2下标为孩子节点:
父节点下标 = ( 2-1 ) / 2 = 1 / 2 = 0
以3下标为孩子节点:
父节点下标 = ( 3-1 ) / 2 = 2 / 2 = 1
以4下标为孩子节点:
父节点下标 = ( 4-1 ) / 2 = 3 / 2 = 1
根据上述公式,编写出如下方法:
private int parent(int index){ // 找到父节点
return (index-1)/2;
}
private int leftChild(int index){ // 找到左孩子
return index*2 +1;
}
private int rightChild(int index){ // 找到右孩子
return index*2 +2;
}
将一个普通的数组转换为堆
我们随便创建一个数组int[] heap = {3,4,5,6,7,8,9}
,发现这和大顶堆相去甚远,每个节点都不满足大顶堆的需要,那么我们如何将一个数组调整成为大顶堆呢?
图解转换流程
初始状态
找到最后一个父节点,从堆的最后一处开始调整
在两个子节点中找到一个更大的,交换位置
依次交换
把交换下来的节点再一次进行验证,是否交换后破坏了顺序
转换完成
所以我们可以总结出来以下步骤来让一个数组堆化:
- 找到数组的最后一个父节点,即 parent(array.length - 1)
- 比较当前节点是否满足堆的性质,即当前节点是否大于两个子节点
2.1 如果满足堆的性质,则将当前下标减一,继续判断
2.2 如果不满足堆的性质,判断两个孩子中哪一个更大,将更大的孩子和当前节点的位置,交换完毕之后再对交换后的下标位置的值,进行递归判断是否满足堆的性质,重复步骤2 - 判断到根节点,满足堆的性质,则堆化结束
代码具体实现如下:
// 一次堆化
private void onceHeapify(int index){
int largest = index;
int left = leftChild(index);
int right = rightChild(index);
// 找最大的子节点,在判断的时候要防止下标越界
if(element.length-1 >= left && element[left].compareTo(element[index]) >0 ){
largest = left;
}
if(element.length-1 >= right && element[right].compareTo(element[largest]) >0){
largest = right;
}
// 如果最大值有变化
if(largest!=index){
T temp = element[index];
element[index] = element[largest];
element[largest] = temp;
// 交换后的节点可能不满足大顶堆性质,所以继续调用
onceHeapify(largest);
}
}
// 全部堆化
private void totalHeapify(){
int len = element.length;
/**
* 从最后一个父节点开始,从下到上调整整个堆
*/
for(int i=parent(len-1);i>=0;i--){
onceHeapify(i);
}
}
堆中插入元素
我们现在已经学会了如何创建一个初始的堆,整个堆中的每一个节点都满足堆的性质,现在要在堆中插入一个元素,应该怎么做呢?可能有人已经想到了可以在结尾插入一个元素,然后直接调用全部堆化方法,将整个堆重新创建起来,这种方法可行,但是没有用到堆已经有序的性质,为了降低时间复杂度,所以我们插入元素到最后一个位置,再进行上滤,只考虑插入的元素这一边的分支,就只影响了一个分支,而不对所有分支进行检查是否满足顺序。
图解插入元素
现在有个堆int[] heap = {9,7,8,6,4,3}
,我们要往堆中插入一个12
发现12这个节点的父节点要比12小,于是我们交换12和8
发现12这个节点的父元素还是比12小,于是我们交换12和9
发现12已经是根节点了,交换结束
上述过程中只判断了根节点的一个分支,而不去影响另一个分支。
具体代码实现如下:
public void insertVal(T val){
int len = element.length;
// 创建一个新数组
element = Arrays.copyOf(element, len + 1);
// 插入到最后一个下标
element[len] = val;
// 从最后一个数组下标开始检索是否要和父节点交换
for(int i=len;i>0;i = parent(i)){
// 如果比父节点大,则交换
if(element[i].compareTo(element[parent(i)]) >0 ){
T temp = element[i];
element[i] = element[parent(i)];
element[parent(i)] = temp;
}else{
break;
}
}
}
堆中删除元素
堆提供一种删除元素的方法就是删除根节点,为了维护堆整体的顺序,要在删除之后继续调整堆,因为删除了一个节点,最后一个节点的位置是肯定需要调整的,所以我们不妨把最后一个节点放到现在的即将被删除的根节点的位置上,调用一次OnceHeapify
方法就可以从上到下的调整好整个堆的顺序
图解删除元素
现在有一个堆int[] heap = {12,7,9,6,4,3,8}
我们要调用删除方法删除根节点。
将12移出堆,将8交换到根节点位置,调用OnceHeapify方法,检查左右节点是否有比自身值大的
发现9大于自己,所以交换9和8
判断交换后的节点是否满足堆的性质,满足,交换结束
具体代码实现如下:
public T poll(){
T res = element[0];
// 交换根节点和最后节点
element[0] = element[element.length-1];
// 截取数组
element= Arrays.copyOfRange(element, 0, element.length-1);
onceHeapify(0);
return res;
}
TopK问题
TopK问题是从一串数字中选出最大的K个,其实就是维护一个小顶堆,把最小的数字放在堆顶,每次读取数字的时候判断是否比堆顶的数字小,如果小就插入,我实现了一个粗糙且简单的优先队列来实现:
具体代码实现如下:
import java.util.Arrays;
import java.util.PriorityQueue;
public class MinHeap<T extends Comparable> {
private Object[] element;
private int size = 0;
private int capacity = 0;
public MinHeap(T[] element) {
this.element = element;
size = element.length;
totalHeapify();
}
public MinHeap(int capacity){
this.element = new Object[capacity];
this.capacity = capacity;
}
public MinHeap() {
}
private int parent(int index){
return (index-1)/2;
}
private int leftChild(int index){
return index*2 +1;
}
private int rightChild(int index){
return index*2 +2;
}
// 一次堆化
private void onceHeapify(int index){
int largest = index;
int left = leftChild(index);
int right = rightChild(index);
// 找最小的子节点,在判断的时候要防止下标越界
if(size-1 >= left && ((Comparable)element[left]).compareTo(element[index]) < 0 ){
largest = left;
}
if(size-1 >= right && ((Comparable)element[right]).compareTo(element[largest]) < 0){
largest = right;
}
// 如果最小值有变化
if(largest!=index){
T temp = (T)element[index];
element[index] = element[largest];
element[largest] = temp;
// 交换后的节点可能不满足小顶堆性质,所以继续调用
onceHeapify(largest);
}
}
// 全部堆化
private void totalHeapify(){
int len = element.length;
/**
* 从最后一个父节点开始,从下到上调整整个堆
*/
for(int i=parent(len-1);i>=0;i--){
onceHeapify(i);
}
}
public T peek(){
return (T)element[0];
}
public void capacityInsert(T val){
if(size == capacity){
if(peek().compareTo(val) < 0){
poll();
insertVal(val);
}
}else{
insertVal(val);
}
}
public void insertVal(T val){
// 创建一个新数组
if(size == capacity){
element = Arrays.copyOf(element, element.length >> 1);
}
// 插入到最后一个下标
element[size++] = val;
// 从最后一个数组下标开始检索是否要和父节点交换
for(int i=size-1;i>0;i = parent(i)){
// 如果比父节点小,则交换
if(((Comparable)element[i]).compareTo(element[parent(i)]) < 0 ){
T temp = (T)element[i];
element[i] = element[parent(i)];
element[parent(i)] = temp;
}else{
break;
}
}
}
public T poll(){
T res = (T)element[0];
// 交换根节点和最后节点
element[0] = element[size-1];
// 截取数组
element[size-1] = null;
size--;
onceHeapify(0);
return res;
}
@Override
public String toString() {
return "Heap{" +
"element=" + Arrays.toString(element) +
'}';
}
public static int[] topK(Integer[] origin,int k){
MinHeap<Integer> heap = new MinHeap<>(null);
for(int i=0;i<k;i++){
heap.insertVal(origin[i]);
}
System.out.println(heap);
return null;
}
public static void main(String[] args) {
Integer[] element = {5,6,7,8,9,4,2,1,45,123,74,56,48,31,21,654,612,31,546};
MinHeap<Integer> minHeap = new MinHeap<>(5);
for(int i=0;i<element.length;i++){
minHeap.capacityInsert(element[i]);
}
System.out.println(minHeap);
}
}
结果如图: