简介
堆是一颗完全二叉树,分为两种,最大堆和最小堆,两者的区别在于排序方式上。最大(小)堆是值在一颗完全二叉树中,根节点的值不小于(不大于)树中其他节点的值。这里要注意的是,堆中的任一子树也是堆,但不要求一颗子树中的父节点的值不小于(不大于)另一颗子树中节点值。
最大堆
在本小节中,我们创建一个最大堆,包含建堆、增加、删除、替换4中操作。最小堆与最大堆类似,本节不再讲述最小堆。在文章最后会附上最小堆的代码实现。
建堆
在这里,我们采用数组来实现完全二叉树,并提供一个构造函数,可以将一个数组转换为最大堆。
我们先看下如何用一颗存储一颗完全二叉树。从下面的数组表示可以看出,如果父节点的索引为n,那么左子节点的索引值为2n+1,右子节点的索引值为2n+2。如果子节点的索引值为x,那么父节点的索引值为x/2。不懂的话,建议去看下树的层序遍历。
基本定义
public class MaxHeap<E extends Comparable<E>> {
//使用数组来存储数据
private E[] data;
//堆中元素的个数
private int size;
//堆的容量
private int capacity;
public MaxHeap(int capacity) {
this.capacity = capacity;
this.data = (E[]) new Object[capacity];
this.size = 0;
}
public MaxHeap(E[] elements) {
//将一个数组构成最大堆
if (elements == null || elements.length == 0) {
throw new IllegalArgumentException("elements is empty.");
}
this.capacity = elements.length;
this.size = elements.length;
this.data = Arrays.copyOf(elements, size);
//heapify:从非叶子节点不断的执行shift down操作
for (int i = data.length - 1; i >= 0; i--) {
shiftDown(getParent(i));
}
}
private int getParent(int child) {
//以数组下标0开始存储元素,父亲节点的下标是(child - 1) / 2
//以数组下标1开始存储元素,父亲节点的下标是child / 2
return (child - 1) / 2;
}
private int getLeftChild(int parent) {
//以数组下标0开始存储元素,左孩子节点的下标是2 * parent + 1
//以数组下标1开始存储元素,左孩子节点的下标是2 * parent
return 2 * parent + 1;
}
}
shiftDown
在上面的构造函数的代码中,提到shiftDown(下沉)操作。所谓的shiftDown操作就是不断的同子节点进行比较交互,最后找到元素位置的操作。如下图所示,不断对值为6的节点进行shiftDown操作,直到节点满足最大堆的性质(不小于左右子节点的值)。
因此,在上述的构造函数中,只要循环对树中的非叶子节点进行shiftDown操作,就可以完成建堆。
private void shiftDown(int index) {
int leftChildIdx = getLeftChild(index);
//不必判断有右子节点是否存在,因为完全二叉树若左子节点不存在,右子节点就一定不存在,一定是叶子节点
if (index != leftChildIdx && leftChildIdx < size) {
//如果有左右子节点,则选择值最大的节点进行比较
int maxIndex = leftChildIdx + 1 < size && data[leftChildIdx + 1].compareTo(data[leftChildIdx]) > 0
? leftChildIdx + 1 : leftChildIdx;
if (data[index].compareTo(data[maxIndex]) < 0) {
swap(index, maxIndex);
shiftDown(maxIndex);
}
}
}
删除
在堆中,只允许删除根节点的元素。因为在最大(小)堆中,删除的值是堆中最大(小)值。
删除操作,只要将堆中的根节点和最后一个节点交换位置,再将最后一个节点指向null。将根节点不断的执行shiftDown操作即可。如下图所示,我们删除一个值为10的根节点。
public E remove() {
if (isEmpty()) {
return null;
}
E maxValue = data[0];
//将第一元素与最后一个交换位置,然后不断的将跟节点执行shift down操作
swap(0, --size);
data[size] = null;
shiftDown(0);
return maxValue;
}
添加
当我们向堆中添加节点后,要求新堆也要满足最大堆的性质(节点值不大于父节点值),也就是说,新添加的节点在整个堆中的位置是固定的。我们可以将新节点添加到堆的最后,然后不断的通过shiftUp(上浮)操作,来交换父子节点。
public void add(E e) {
if (e == null) {
throw new IllegalArgumentException("element is null..");
}
if (size + 1 == capacity) {
throw new ArrayIndexOutOfBoundsException("add error. array is full.");
}
//添加到元素末尾
data[size++] = e;
//将元素添加到堆的最后一个元素,不断的将最后一个节点执行shift up操作
shiftUp(size - 1);
}
private void shiftUp(int index) {
int parent = getParent(index);
if (index != parent && data[parent].compareTo(data[index]) < 0) {
//子节点比父节点大,交换元素位置
swap(index, parent);
//交换元素后,再次与父节点进行比较,直到比父节点的值小
shiftUp(parent);
}
}
替换
替换元素比较简单,这里不在过多描述了,代码如下。
public E replace(E e) {
E old = data[0];
data[0] = e;
shiftDown(0);
return old;
}
实现
最小堆
public class MaxHeap<E extends Comparable<E>> {
//使用数组来存储数据
private E[] data;
//堆中元素的个数
private int size;
//堆的容量
private int capacity;
public MaxHeap(int capacity) {
this.capacity = capacity;
this.data = (E[]) new Object[capacity];
this.size = 0;
}
public MaxHeap(E[] elements) {
//将一个数组构成最大堆
if (elements == null || elements.length == 0) {
throw new IllegalArgumentException("elements is empty.");
}
this.capacity = elements.length;
this.size = elements.length;
this.data = Arrays.copyOf(elements, size);
//heapify:从非叶子节点不断的执行shift down操作
for (int i = data.length - 1; i >= 0; i--) {
shiftDown(getParent(i));
}
}
public boolean isEmpty() {
return size == 0;
}
public int getSize() {
return size;
}
public E remove() {
if (isEmpty()) {
return null;
}
E maxValue = data[0];
//将第一元素与最后一个交换位置,然后不断的将跟节点执行shift down操作
swap(0, --size);
data[size] = null;
shiftDown(0);
return maxValue;
}
public void add(E e) {
if (e == null) {
throw new IllegalArgumentException("element is null..");
}
if (size + 1 == capacity) {
throw new ArrayIndexOutOfBoundsException("add error. array is full.");
}
//添加到元素末尾
data[size++] = e;
//将元素添加到堆的最后一个元素,不断的将最后一个节点执行shift up操作
shiftUp(size - 1);
}
public E replace(E e) {
E old = data[0];
data[0] = e;
shiftDown(0);
return old;
}
private void shiftDown(int index) {
int leftChildIdx = getLeftChild(index);
//不必判断有右子节点是否存在,因为完全二叉树若左子节点不存在,右子节点就一定不存在,一定是叶子节点
if (index != leftChildIdx && leftChildIdx < size) {
//如果有左右子节点,则选择值最大的节点进行比较
int maxIndex = leftChildIdx + 1 < size && data[leftChildIdx + 1].compareTo(data[leftChildIdx]) > 0
? leftChildIdx + 1 : leftChildIdx;
if (data[index].compareTo(data[maxIndex]) < 0) {
swap(index, maxIndex);
shiftDown(maxIndex);
}
}
}
private void shiftUp(int index) {
int parent = getParent(index);
if (index != parent && data[parent].compareTo(data[index]) < 0) {
//子节点比父节点大,交换元素位置
swap(index, parent);
//交换元素后,再次与父节点进行比较,直到比父节点的值小
shiftUp(parent);
}
}
private int getParent(int child) {
//以数组下标0开始存储元素,父亲节点的下标是(child - 1) / 2
//以数组下标1开始存储元素,父亲节点的下标是child / 2
return (child - 1) / 2;
}
private int getLeftChild(int parent) {
//以数组下标0开始存储元素,左孩子节点的下标是2 * parent + 1
//以数组下标1开始存储元素,左孩子节点的下标是2 * parent
return 2 * parent + 1;
}
private void swap(int a, int b) {
E temp = data[a];
data[a] = data[b];
data[b] = temp;
}
}
最小堆
public class MinHeap<E extends Comparable<E>> {
//使用数组来存储数据
private E[] data;
//堆中元素的个数
private int size;
//堆的容量
private int capacity;
public MinHeap(int capacity) {
this.capacity = capacity;
this.data = (E[]) new Object[capacity];
this.size = 0;
}
public MinHeap(E[] elements) {
//将一个数组构成最大堆
if (elements == null || elements.length == 0) {
throw new IllegalArgumentException("elements is empty.");
}
this.capacity = elements.length;
this.size = elements.length;
this.data = Arrays.copyOf(elements, size);
//heapify:从非叶子节点不断的执行shift down操作
for (int i = data.length - 1; i >= 0; i--) {
shiftDown(getParent(i));
}
}
public boolean isEmpty() {
return size == 0;
}
public int getSize() {
return size;
}
public E remove() {
if (isEmpty()) {
return null;
}
E minValue = data[0];
//将第一元素与最后一个交换位置,然后不断的将跟节点执行shift down操作
swap(0, --size);
data[size] = null;
shiftDown(0);
return minValue;
}
public void add(E e) {
if (e == null) {
throw new IllegalArgumentException("element is null.");
}
if (size + 1 == capacity) {
throw new ArrayIndexOutOfBoundsException("add error. array is full.");
}
//添加到元素末尾
data[size++] = e;
//将元素添加到堆的最后一个元素,不断的将最后一个节点执行shift up操作
shiftUp(size - 1);
}
public E replace(E e) {
E old = data[0];
data[0] = e;
shiftDown(0);
return old;
}
private void shiftDown(int index) {
int leftChildIdx = getLeftChild(index);
//非叶子节点
if (index != leftChildIdx && leftChildIdx < size) {
//如果有左右子节点,则选择值最小的节点进行比较
int minIndex = leftChildIdx + 1 < size && data[leftChildIdx + 1].compareTo(data[leftChildIdx]) < 0
? leftChildIdx + 1 : leftChildIdx;
if (data[index].compareTo(data[minIndex]) > 0) {
swap(index, minIndex);
shiftDown(minIndex);
}
}
}
private void shiftUp(int index) {
int parent = getParent(index);
if (index != parent && data[parent].compareTo(data[index]) > 0) {
//子节点比父节点大,交换元素位置
swap(index, parent);
//交换元素后,再次与父节点进行比较,直到比父节点的值小
shiftUp(parent);
}
}
private int getParent(int child) {
//以数组下标0开始存储元素,父亲节点的下标是(child - 1) / 2
//以数组下标1开始存储元素,父亲节点的下标是child / 2
return (child - 1) / 2;
}
private int getLeftChild(int parent) {
//以数组下标0开始存储元素,左孩子节点的下标是2 * parent + 1
//以数组下标1开始存储元素,左孩子节点的下标是2 * parent
return 2 * parent + 1;
}
private void swap(int a, int b) {
E temp = data[a];
data[a] = data[b];
data[b] = temp;
}
}