数据结构之二叉堆
什么是二叉堆?
二叉堆 本质上就是一颗 二叉树 ,而根据根节点数据的不同又分为:最大堆 和 最小堆。
什么是最大堆?父节点的值 永远 大于等于 两个 孩子节点 的值。
什么是最小堆?父节点的值 永远 小于等于 两个 孩子节点 的值。
二叉堆的特性
自我调整:当插入或者删除数据时,二叉堆会更改元素的位置,使父节点依然大于(小于)等于孩子节点。
完全二叉树:二叉堆是一颗完全二叉树。
二叉堆添加和删除元素
添加元素:二叉堆的元素插入,总是插入到二叉树的最后一个位置,拿最小堆举例。如果插入一个新元素0。
0和父节点5做比较,小于父节点,所以0上浮,也就是说0和父节点5交换位置。
此时依然和父节点做比较,父节点是3,0小于3,那么0继续上浮,0和3交换位置。
父节点此时为1,0依然小于父节点1,上浮至根节点位置,比较结束。
删除元素:二叉堆元素的删除,总是删除堆顶的元素,最大堆是堆中的最大值,最小的则反之。如果我们要删除堆顶元素1。
为了维持二叉堆的平衡,此时把最后一个元素10放到堆顶,现在虽然是完全二叉树,但是二叉堆的平衡已经遭到破坏,所以让元素10下沉至合适位置。
让元素10和两个孩子节点比较,取其中最小的孩子节点,交换位置,孩子节点3、2中,2最小,所以和2交换位置。
此时10的两个孩子节点又分别为7、8,所以继续下沉,和7交换位置。
10已经是叶子节点,下沉结束,此时二叉堆已经恢复平衡。
代码实现
二叉堆虽然是一颗完全二叉树,但是并不是用链式结构存储的,因完全二叉树的性质,可以存储在数组当中。
但是如何从当前元素定位到孩子节点呢?我们可以用索引来计算。
元素1存储在第一个位置,索引为0,那他的左孩子节点的下标索引为 parent * 2 + 1 = 1,右孩子节点的下标索引为 parent * 2 + 2 = 2;但是如果我们把第一个元素的下标索引定义为 1 ,那么左孩子的下标索引为 parent * 2 = 2,右孩子节点的下标索引为 parent * 2 + 1 = 3。
搞清楚这些,开始写代码
package com.zjm.binheap;
/**
* 最大堆实现
* @author Zhao JinMing
* @data 2018.9.15
* @version 0.9.0
*/
public class BinaryHeap {
private int elementCount;
private int[] elements;
private static final int DEFAULT_CAPACITY = 16;
private static final int MAXIMUM_CAPACITY = 1 << 30;
public BinaryHeap() {
this(DEFAULT_CAPACITY);
}
public BinaryHeap(int[] data) {
this();
for (int d : data) {
push(d);
}
}
public BinaryHeap(int capacity) {
elements = new int[tableSizeFor(capacity)];
elementCount = 0;
}
/**
* 入队操作
* @param data 入队数据
*/
public void push(int data) {
if(elements.length - 1 == elementCount)
resize((elementCount + 1) << 1);
elements[++elementCount] = data;
floatElement(elementCount);
}
/**
* 弹出队列
* @return 返回队列最大或者最小值
*/
public int pop() {
if(elements.length < elementCount >> 2)
resize(elementCount >> 2);
int res = elements[1];
swap(1, elementCount);
elements[elementCount--] = 0;
sinkElement();
return res;
}
/**
* 查看队首元素
* @return 返回队列第一个元素
*/
public int peek() {
return elementCount == 0 ? -1 : elements[1];
}
/**
* 元素个数
* @return 返回元素个数
*/
public int size() {
return elementCount;
}
/**
* 检验非空
* @return TRUE 队列为空
*/
public boolean isEmpty() {
return elementCount == 0;
}
/**
* 重新设置elements元素大小
* @param size 新数组的大小
*/
private void resize(int size) {
if (size > MAXIMUM_CAPACITY) {
//TODO
}
int[] newElements = new int[size];
copyArray(elements, newElements);
elements = newElements;
}
/**
* 元素上浮
* @param index 上浮元素索引位置
*/
private void floatElement(int index) {
while (index > 1 && elements[index >> 1] < elements[index]) {
swap(index >> 1, index);
index >>= 1;
}
}
/**
* 元素下沉
*/
private void sinkElement() {
int index = 1;
while (index << 1 <= elementCount &&
(elements[index] < elements[index << 1] || elements[index] < elements[(index << 1) + 1])) {
int p = index << 1;
int i = elements[p] > elements[p + 1] ? p : p + 1;
swap(index, i);
index = (i & 0x01) == 1 ? (index << 1) + 1 : index << 1;
}
}
/**
* HashMap中的方法,把传入参数变为下一个 1 << n (2的n次方大于 cap 并且 2的n-1次方小于 cap)
* @param cap 改变的参数
* @return 1 << n
*/
private static int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/**
* 数组复制
* @param source 被复制的源数组
* @param target 复制目标数组
*/
private static void copyArray(int[] source, int[] target) {
System.arraycopy(source, 0, target, 0, source.length);
}
/**
* 交换数组两元素位置
* @param indexA 元素A的索引位置
* @param indexB 元素B的索引位置
*/
private void swap(int indexA, int indexB) {
int temp = elements[indexA];
elements[indexA] = elements[indexB];
elements[indexB] = temp;
}
}
这就是一个最大堆,数组第一个位置并没有存放元素,而是从下标索引1开始,存放最大值。
运行测试数据
public static void main(String[] args) {
int length = 20;
Random random = new Random();
int[] elements = new int[length];
for (int i = 0; i < length; i++) {
int ele = random.nextInt(80);
elements[i] = ele;
System.out.print(ele + " ");
}
BinaryHeap maxHeap = new BinaryHeap(elements);
System.out.println();
while (!maxHeap.isEmpty()) {
System.out.print(maxHeap.pop() + " ");
}
}
结果输出
77 74 56 27 13 23 44 38 35 46 43 51 76 67 78 20 77 12 60 62
78 77 77 76 74 67 62 60 56 51 46 44 43 38 35 27 23 20 13 12
一个最大堆完成。