在了解堆这种数据结构之前,我们需要先了解 树以及二叉树 的概念 . 因为堆是
目录
堆的定义
堆作为一种数据结构,它是一棵根节点的值总是大于或者小于左右子树的节点的值的完全二叉树.同时,在堆中,不存在两个值相等的元素.以上对堆的定义里,说明了堆的两个重要特点(红色加粗的文字).
- 根节点的值总是大于或者小于左右子树的节点的值
- 堆是一棵完全二叉树(堆可以按照层序的规则将节点逐一保存在数组中,可以高效的利用数组的空间,如果采用非完全二叉树,就会造成数组空间的浪费,图解如下)
堆的分类
- 大根堆:根节点的值总是大于左右子树的节点的值.
- 小根堆:根节点的值总是小于左右子树的节点的值.
堆的实现
public class Heap {
public int[] elem;
public int userSize;//记录元素个数
public Heap() {
elem = new int[10];
}
//对堆的数组进行初始化
public void initElem(int[] array) {
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
userSize++;
}
}
}
向下调整建立大根堆
假设给定数组{27,15,19,18,28,34,65,49,25,37},对其进行向下调整建堆
说明: child记录子树的孩子节点 parent记录子树的父节点
步骤:从最后一棵子树开始向上调整,如果左右子树的节点大于根节点,那么根节点就和子树节点最大值就进行交换,按相同步骤对其他子树进行操作
步骤图详解
中间省略了亿点点步骤......最后结果如下
代码实现
/**
*时间复杂度 O(N)
*建立大根堆创建大根堆
*/
public void createHeap() {
for (int parent = (userSize - 1 - 1) / 2;parent >= 0;parent-- ) {
int child = parent * 2 + 1;
shiftDown(parent,userSize);
}
}
/**
* 向下调整
* @param parent 父亲下标
* @param end 每棵树的结束下标 -- 不超过的 -- 最小下标
*/
private void shiftDown(int parent, int end) {
int child = parent * 2 + 1;
while (child < end) {
if (child + 1 < end && elem[child] < elem[child + 1]) {
child++;
}
//child一定记录了左右孩子的最大值下标
if (elem[child] > elem[parent]) {
swap(elem,child,parent);
parent = child;
child = parent * 2 + 1;
} else {
return;//此处孩子节点小于父节点,那么后面没有比父节点更大的值,所以可以直接结束
}
}
}
//交换函数
private void swap(int[] elem, int child, int parent) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
}
解释参数end:在向下调整时,我们需要一个结束条件,如果我们的child大于结束条件,那么就说明我们以后交换到最下面一层了,可以结束了.
我们以数组的元素个数作为结束条件,因为一旦交换到最下面一层,那么child一定是大于元素个数
堆的插入
步骤
堆的插入是将插入元素放在数组最后一个元素后面,然后再对该元素进行向上调整
步骤图详解
代码实现
/**
* 插入
* 时间复杂度 O(Log N)
* @param val
*/
public void offer (int val) {
if (isFull()) {
//扩容
elem = Arrays.copyOf(elem,elem.length * 2);
}
elem[userSize] = val;
int child = userSize;
userSize++;
shiftUp(child);
}
/**
* 向上调整 时间复杂度 O(log2N)
* @param child
*/
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while (child > 0) {
if (elem[child] > elem[parent]) {
swap(elem,child,parent);
child = parent;
parent = (child - 1) / 2;
} else {
return;
}
}
}
//判满函数
private boolean isFull() {
return userSize == elem.length;
}
堆的删除
堆的删除使用的是替换法
步骤
- 将最后一个节点的值和堆顶元素的值交换,那么删除堆顶元素就变成了删除最后一个元素,我们只需要将usedSize--就行.
- 对堆顶元素进行向下调整,调整成大根堆.向下调整和建堆的步骤一样,找到孩子节点最大值,进行交换,然后继续向下调整
步骤图详解
代码实现
/**
* 删除
* 时间复杂度 O(Log N)
*/
public void remove () {
if (isEmpty()) {
return ;
}
//删除堆顶元素
swap(elem,0,userSize-1);//将堆顶和最后一个元素交换
userSize--;
shiftDown(0,userSize);//向下调整
}
//判空函数
public boolean isEmpty() {
return userSize == 0;
}
/**
* 向下调整
* @param parent 父亲下标
* @param end 每棵树的结束下标 -- 不超过的 -- 最小下标
*/
private void shiftDown(int parent, int end) {
int child = parent * 2 + 1;
while (child < end) {
if (child + 1 < end && elem[child] < elem[child + 1]) {
child++;
}
//child一定记录了左右孩子的最大值下标
if (elem[child] > elem[parent]) {
swap(elem,child,parent);
parent = child;
child = parent * 2 + 1;
} else {
return;//此处孩子节点小于父节点,那么后面没有比父节点更大的值,所以可以直接结束
}
}
}
查看堆顶元素
步骤
查看堆顶元素只需要返回数组第一个元素即可
代码实现
public int peek() {
if (isEmpty()) {
return -1;//默认为空时返回-1,也可抛异常
}
return elem[0];
}
时间复杂度分析
向下建堆
向上建堆