堆在逻辑上一棵完全二叉树,所以可以通过数组进行数据存储,而其余的树大多采用链式结构进行数据存储
- 堆分类:
- 大顶堆:大顶堆就是无论在任何一棵(子)树中,父节点都是最大的
- 小顶堆:小顶堆就是无论在任何一棵(子)树中,父节点都是最小的
- 堆的两种操作:
- 上浮:一般用于向堆中添加新元素后的堆平衡
- 下沉:一般用于取出堆顶并将堆尾换至堆顶后的堆平衡
- 堆排序:利用大顶堆和小顶堆的特性,不断取出堆顶,取出的元素就是堆中元素的最值,然后再使堆平衡
下面的文章以大顶堆为例,拿Java实现堆的各种操作。
1.MaxHeap
大顶堆:对于任一个(子)堆,堆顶最大
// 这里Comparable保证所有结点可比,是成堆基础
public class MaxHeap <E extends Comparable<E>> {
// 完全二叉树,排列整齐(相当于一层一层放入),可以用数组存储
private ArrayList<E> data;
public MaxHeap(int capcity) {
data = new ArrayList<>(capcity);
}
public MaxHeap() {
data = new ArrayList<>();
}
// 堆是否为空
public boolean isEmpty() {
return data.isEmpty();
}
// 堆中元素个数
public int size() {
return this.data.size();
}
//......
}
2.操作一:获取父/子节点
parent()
// 返回idx位置元素的父节点
// 注:这里从0放起(parent = (i - 1)/2 ),若是从1放起(parent = i / 2)
private int parent(int idx) {
if (idx == 0) {
throw new IllegalArgumentException("index-0 doesn't have parent");
}
return (idx - 1) / 2;
}
leftChild() / rightChild()
// 返回idx位置元素的孩子节点
// 注:这里从0放起
private int leftChild(int idx) {
// 若从1放起,leftChild = idx * 2
return idx * 2 + 1;
}
private int rightChild(int idx) {
// 若从1放起,leftChild = idx * 2
return idx * 2 + 2;
}
2.操作二:添加元素
add()
- 将元素放到堆尾,即数组最后一个元素
- 加入到最后了,上浮
public void add(E e) {
data.add(e);
// 传入需要上浮的索引
siftUp(data.size() - 1);
}
siftUp()
- 上浮:子节点与(小于自己的)父节点交换
- Time:O(logn),获取parent是不断二分(/2) 的
private void siftUp(int k) {
// 只要是父节点(data[parent]) 比 子节点(data[k])小,就进行交换
while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
// 1.交换数组中位置
// 注:这里是
Collections.swap(k, parent(k));
// 2.更新子节点,进行下一轮上浮
// 注:也可以data.set进行三步走
k = parent(k);
}
}
3.操作三:取出堆顶(最大元素)
extractMax()
取出堆顶
- 拿到堆顶元素
- 删除堆顶
- 将堆顶(0)与堆尾(size-1)交换,因为要产生新堆顶
- 删除堆尾
- 堆顶下沉
public E extractMax() {
// 1.获取堆顶元素
E ret = findMax() ;
// 2.1 将堆顶换到堆尾
Collections.swap(0, data.size() - 1);
// 2.2 删除堆尾
data.remove(data.size() - 1);
// 2.3 下沉堆顶
siftDown(0):
return ret;
}
findMax()
获取堆中最大元素
public E findMax() {
if (data.size() == 0) {
throw new IllegalArgumentException("Can not findMax when heap is empty");
}
// 堆顶最大 = 数组第一个元素(0)
return data.get(0);
}
siftDown()
- 堆顶下沉:与左右子节点较大值(且大于自己的)节点进行交换
- Time:O(logn),获取Child是不断二分(/2) 的
private void siftDown(int k) {
// 1.判断当前节点是否有子节点
// 注:因为leftChild索引肯定比rightChild小,所以只要有leftChild就有子节点
while (leftChild(k) < data.size()) {
// 2.拿到leftChild与rightChild的大值
int j = leftChild(k);
if (j + 1 < data.size() && data.get(j + 1).compareTo(data.get(j)) > 0) {
j = rightChild(k);
}
// 3.判断子节较大值是否大于自己(父节点)
if (data.get(k).compareTo(data.get(j)) >= 0) {
break;
}
// 4.若大于,交换数组中两节点位置
Collections.swap(k, j);
// 更新父节点,进行下一轮下沉
k = j;
}
}
=> 堆排序
堆排序原理:简而言之,就是利用大顶堆和小顶堆的特性,不断取出堆顶,取出的元素就是堆中元素的最值,然后再使堆平衡。下面放一个示例代码:
public class Test {
public static void main(String[] args) {
int n = 10000000;
MaxHeap<Integer> heap = new MaxHeap<>();
Random random = new Random();
// 一百万个随机数
// 注:这里用的random类!!!
for (int i = 0; i < n; i++)
heap.add(random.nextInt(Integer.MAX_VALUE));
// 不断从最大堆中取出堆顶 --> 从大到小
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = heap.extractMax();
}
// 验证取出的元素是否按照从大到小排列
for (int i = 0; i < n - 1; i++)
if (arr[i] < arr[i + 1])
throw new IllegalArgumentException("Error");
System.out.println("Test MaxHeap Success!");
}
}
4.操作四:取出堆顶,再加入一个元素
- 思路一:取出堆顶(extractMax),然后再加入一元素(add) ====> 2 * O(logn)
- 思路二:将堆顶直接修改,然后下沉 ====> O(logn)
replace()
public E replace(E e) {
// 1.获取堆顶元素
E ret = findMax();
// 2.修改堆顶,即数组0位置
data.set(0, e);
// 3.下沉
siftDown(0);
return ret;
}
5.操作五:数组堆化
heapify()
- 思路一:将已知数组一个一个加入到堆中 ====> Time = O(n*logn)
- 思路二:从第倒数一个非叶子节点开始下沉 ====> Time = O(n)
public MaxHeap(E[] arr) {
// 注:这里数组不能直接作为ArrayList参数,要先包装成List
data = new ArrayList<>(Arrays.asList(arr));
// 确定倒数第一个非叶子结点:最后一个叶子(length - 1)的父节点 ((i - 1)/ 2)
for (int i = parent(arr.length - 1); i >= 0; i--) {
// 逐个下沉
siftDown(i);
}
}
完整代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
public class MaxHeap <E extends Comparable<E>> {
private ArrayList<E> data;
public MaxHeap(int capcity) {
data = new ArrayList<>(capcity);
}
public MaxHeap() {
data = new ArrayList<>();
}
public MaxHeap(E[] arr) {
data = new ArrayList<>(Arrays.asList(arr));
for (int i = parent(arr.length - 1); i >= 0; i--) {
siftUp(i);
}
}
// 堆是否为空
public boolean isEmpty() {
return data.isEmpty();
}
// 堆中元素个数
public int size() {
return this.data.size();
}
// 返回idx位置元素的父节点
// 注:这里从0放起(parent = (i - 1)/2 ),若是从1放起(parent = i / 2)
private int parent(int idx) {
if (idx == 0) {
throw new IllegalArgumentException("index-0 doesn't have parent");
}
return (idx - 1) / 2;
}
// 返回idx位置元素的孩子节点
// 注:这里从0放起
private int leftChild(int idx) {
// 若从1放起,leftChild = idx * 2
return idx * 2 + 1;
}
private int rightChild(int idx) {
// 若从1放起,leftChild = idx * 2
return idx * 2 + 2;
}
// 添加元素
public void add(E e) {
data.add(e);
siftUp(data.size() - 1);
}
private void siftUp(int k) {
while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
Collections.swap(data, k, parent(k));
k = parent(k);
}
}
// 获取堆中最大元素
public E findMax() {
if (data.size() == 0) {
throw new IllegalArgumentException("Can not findMax when heap is empty");
}
// 堆顶最大,
return data.get(0);
}
public E extractMax() {
E ret = findMax() ;
Collections.swap(data, 0, data.size() - 1);
data.remove(data.size() - 1);
siftDown(0);
return ret;
}
private void siftDown(int k) {
while (leftChild(k) < data.size()) {
int j = leftChild(k);
if (j + 1 < data.size() && data.get(j + 1).compareTo(data.get(j)) > 0) {
j = rightChild(k);
}
if (data.get(k).compareTo(data.get(j)) >= 0) {
break;
}
Collections.swap(data, k, j);
k = j;
}
}
public E replace(E e) {
E ret = findMax();
data.set(0, e);
siftDown(0);
return ret;
}
}
最后,对堆在Java中的应用感兴趣的同学看看 PriorityQueue 的源码,它就是通过小顶堆实现的,这里放个传送门 【Java集合源码】PriorityQueue源码分析。