一、基本概念:
1、完全二叉树:若二叉树的深度为h,则除第h层外,其他层的结点全部达到最大值,且第h层的所有结点都集中在左子树。
2、满二叉树:满二叉树是一种特殊的的完全二叉树,所有层的结点都是最大值。
二叉堆
二叉堆是基于完全二叉树的基础上,加以一定的条件约束的一种特殊的二叉树。
根据约束条件的不同,二叉堆又可以分为两个类型:
大顶堆和小顶堆。
大顶堆
即任何一个父节点的值,都 大于等于 它左右孩子节点的值。
小顶堆
即任何一个父节点的值,都 小于等于 它左右孩子节点的值。 二叉堆的根节点叫做 堆顶 ,它是大顶堆里面的最大值,小顶堆里的最小值。
二、堆的存储与表示
我们在二叉树章节中学习到,完全二叉树非常适合用数组来表示。由于堆正是一种完全二叉树,我们将采用
数组来存储堆。
当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。节点指针通过索引映射公式
来实现。
如图 所示,给定索引 𝑖 ,其左子节点索引为 2𝑖 + 1 ,右子节点索引为 2𝑖 + 2 ,父节点索引为 (𝑖 − 1)/2
(向下取整)。当索引越界时,表示空节点或节点不存在。
我们可以将索引映射公式封装成函数,方便后续使用。
/**
* 获取左边子节点的索引
* @param parent 父节点索引
* @return
*/
private int left(int parent) {
return 2* parent + 1;
}
/**
* 获取右边子节点的索引
* @param parent 父节点索引
* @return
*/
private int right(int parent) {
return 2* parent + 2;
}
/**
* 获取父节点索引
* @param son 子节点索引
* @return
*/
private int getParent(int son) {
return (son - 1) / 2;
}
三、访问堆顶元素
堆顶元素即为二叉树的根节点,也就是列表的首个元素。
/**
* 访问堆定元素
* @return
*/
public T peek() {
return maxHeap.get(0);
}
四. 元素入堆
给定元素 val ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能
已被破坏。因此,需要修复从插入节点到根节点的路径上的各个节点,这个操作被称为「堆化 heapify」。
考虑从入堆节点开始,从底至顶执行堆化。如图 8‑3 所示,我们比较插入节点与其父节点的值,如果插入节
点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须
交换的节点时结束。
设节点总数为 𝑛 ,则树的高度为 𝑂(log 𝑛) 。由此可知,堆化操作的循环轮数最多为 𝑂(log 𝑛) ,元素入堆操
作的时间复杂度为 𝑂(log 𝑛) 。
/**
* val 元素入队
* @param val
*/
public void push (T val) {
// 尾部添加元素
maxHeap.add(val);
// 进行上浮操作
siftUp(maxHeap.size() - 1);
}
/**
* 进行上浮操作
* @param i
*/
private void siftUp(int i) {
// 节点拥有父节点,才需要上浮
while (i > 0) {
// 获取父节点索引
int parentIndex = getParent(i);
// 如果父节点小于 子节点,进行上浮
if (maxHeap.get(parentIndex).compareTo(maxHeap.get(i)) < 0) {
// 父子,节点数据进行交换
exchangeData(parentIndex,i);
// 进行迭代,上浮
i = parentIndex;
} else {
return;
}
}
}
/**
* 交换索引处数据
* @param j 索引j
* @param i 索引i
*/
private void exchangeData(int j, int i) {
T iData = maxHeap.get(i);
T jData = maxHeap.get(j);
maxHeap.set(i,jData);
maxHeap.set(j,iData);
}
五、堆顶元素出堆
堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的
索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采用以下操
作步骤。
-
交换堆顶元素与堆底元素(即交换根节点与最右叶节点)。
-
交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素)。
-
从根节点开始,从顶至底执行堆化。
与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 𝑂(log 𝑛) 。
/**
* 元素出队处理
* @return
*/
public T pop () {
// 堆为空,返回空
if (maxHeap.isEmpty()) {
return null;
}
// 交换根节点和最后一个节点数据
exchangeData(0,getHeapSize()-1);
// 删除最后一个元素
T val = maxHeap.remove(getHeapSize() - 1);
// 从顶至底堆化
siftDown(0);
return val;
}
/***
* 进行下沉操作
* @param i
*/
private void siftDown(int i) {
while (true) {
int lIndex = left(i);
int rIndex = right(i);
// 左右节点的最大值对应索引
int maxIndex = lIndex;
// 没有左节点,不处理
if (lIndex > maxHeap.size()-1) {
return;
}
// 获取左节点值
T lData = maxHeap.get(lIndex);
// 有右节点
if (rIndex <= getHeapSize() - 1) {
T rData = maxHeap.get(rIndex);
// 右节点 > 左节点
if (rData.compareTo(lData) > 0) {
// 取右节点
maxIndex = rIndex;
}
}
// 比较两个节点
T sonData = maxHeap.get(maxIndex);
T data = maxHeap.get(i);
// 父节点小于子节点
if (data.compareTo(sonData) < 0) {
// 交换
exchangeData(maxIndex,i);
// 进行下一轮迭代
i = maxIndex;
} else {
return;
}
}
}
六、堆常见应用
-
优先队列:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 𝑂(log 𝑛),而建队操作为 𝑂(𝑛) ,这些操作都非常高效。
-
堆排序:给定一组数据,我们可以用它们建立一个堆,然后不断地执行元素出堆操作,从而得到有序数据。然而,我们通常会使用一种更优雅的方式实现堆排序,详见后续的堆排序章节。
-
获取最大的 𝑘 个元素:这是一个经典的算法问题,同时也是一种典型应用,例如选择热度前 10 的新闻作为微博热搜,选取销量前 10 的商品等。
七、完整代码
package heapP;
import java.util.ArrayList;
import java.util.List;
/**
* @author Administrator
*/
public class myHeap<T extends Comparable<T>> {
// 存储数据
private List<T> maxHeap;
/**
* 获取堆的大小
* @return
*/
public int getHeapSize() {
return maxHeap.size();
}
/**
* 构建函数
*/
public myHeap() {
this.maxHeap = new ArrayList<>();
}
/**
* 获取左边子节点的索引
* @param parent 父节点索引
* @return
*/
private int left(int parent) {
return 2* parent + 1;
}
/**
* 获取右边子节点的索引
* @param parent 父节点索引
* @return
*/
private int right(int parent) {
return 2* parent + 2;
}
/**
* 获取父节点索引
* @param son 子节点索引
* @return
*/
private int getParent(int son) {
return (son - 1) / 2;
}
/**
* 访问堆定元素
* @return
*/
public T peek() {
return maxHeap.get(0);
}
/**
* val 元素入队
* @param val
*/
public void push (T val) {
// 尾部添加元素
maxHeap.add(val);
// 进行上浮操作
siftUp(maxHeap.size() - 1);
}
/**
* 进行上浮操作
* @param i
*/
private void siftUp(int i) {
// 节点拥有父节点,才需要上浮
while (i > 0) {
// 获取父节点索引
int parentIndex = getParent(i);
// 如果父节点小于 子节点,进行上浮
if (maxHeap.get(parentIndex).compareTo(maxHeap.get(i)) < 0) {
// 父子,节点数据进行交换
exchangeData(parentIndex,i);
// 进行迭代,上浮
i = parentIndex;
} else {
return;
}
}
}
/**
* 交换索引处数据
* @param j 索引j
* @param i 索引i
*/
private void exchangeData(int j, int i) {
T iData = maxHeap.get(i);
T jData = maxHeap.get(j);
maxHeap.set(i,jData);
maxHeap.set(j,iData);
}
/**
* 元素出队处理
* @return
*/
public T pop () {
// 堆为空,返回空
if (maxHeap.isEmpty()) {
return null;
}
// 交换根节点和最后一个节点数据
exchangeData(0,getHeapSize()-1);
// 删除最后一个元素
T val = maxHeap.remove(getHeapSize() - 1);
// 从顶至底堆化
siftDown(0);
return val;
}
/***
* 进行下沉操作
* @param i
*/
private void siftDown(int i) {
while (true) {
int lIndex = left(i);
int rIndex = right(i);
// 左右节点的最大值对应索引
int maxIndex = lIndex;
// 没有左节点,不处理
if (lIndex > maxHeap.size()-1) {
return;
}
// 获取左节点值
T lData = maxHeap.get(lIndex);
// 有右节点
if (rIndex <= getHeapSize() - 1) {
T rData = maxHeap.get(rIndex);
// 右节点 > 左节点
if (rData.compareTo(lData) > 0) {
// 取右节点
maxIndex = rIndex;
}
}
// 比较两个节点
T sonData = maxHeap.get(maxIndex);
T data = maxHeap.get(i);
// 父节点小于子节点
if (data.compareTo(sonData) < 0) {
// 交换
exchangeData(maxIndex,i);
// 进行下一轮迭代
i = maxIndex;
} else {
return;
}
}
}
}
八、测试代码
package heapP;
/**
* @author Administrator
*/
public class TestDemo {
public static void main(String[] args) {
myHeap<Integer> myHeap = new myHeap<>();
for (int i = 1; i < 10;i++) {
myHeap.push(i);
}
while (myHeap.getHeapSize() > 0) {
Integer pop = myHeap.pop();
System.out.println(pop);
}
}
}
输出
输出:
9
8
7
6
5
4
3
2
1