堆
前言
1.没啥的,一定要记博客上,不然像上次那样把东西乱删哭死你
2.以下所学心得来自于哔站黑马数据结构课程
一、堆介绍
了解堆之前,我们要知道什么是完全二叉树
- 完全二叉树:
- 从根节点往下,除最后一层外,其余层节点全满
- 最后一层节点都向左靠拢
- 比如这样(图解):
在计算机科学中,就有一类数据结构就近似完全二叉树性质,我们把它称为堆。
而堆的实现方式,通常使用的是数组,也就是本节我们要用数组来实现一个完全二叉树,也就是堆
二、数组实现堆方式
在使用链表实现的树上,还可以说是节点,而下面开始,我都将称为结点
- 具体实现方法:
- 将二叉树的结点以原本层级顺序放入到数组索引上
- 比如根节点放在数组索引
1
的位置上,理所应当,它的左子节点就放在索引2
的位置上,右子节点就放在索引3
的位置上,当然这是有规律的 - 数组索引
0
的位置上放空,放空的原因是比较好理解好操作
- 比如根节点放在数组索引
- 具体规律:
- 看下图,比如有一个在数组索引
K
位置的结点,那么它左子结点的位置一定是位于2K
的位置,右子结点就是2K+1
- 树不是有层级的划分吗?所以根据以上规律,也可得出一个
K
位置的结点,它的子结点(下一层)也一定是位于数组索引2K
或者2K+1
的位置,而上一层结点的在数组索引位置就是k / 2
- 当然还没完,继续往下看
- 看下图,比如有一个在数组索引
- 将二叉树的结点以原本层级顺序放入到数组索引上
(图解):
同时堆的实现也有很多种,我们本节将学习大顶堆的实现方式,以下简称堆
- 大顶堆特性:
- 堆的每一个结点放置形式不再与二叉搜索树一样,堆的每个结点的key都大于等于自身的两个子结点
- 所以堆的第一个结点,也就是数组的第一个有效数据位的Key值肯定是最大的
- 堆是一颗完全二叉树,应用到数组中,那么元素的存放必然要是连续的,如果中间有元素删除了,必须补上去
了解完大顶堆如何实现,和结点存放特性,就可以来设计一个堆了
1、设计一个堆
一、堆的API设计如下
- API介绍
- 类设计,使用泛型,需要传入一个实现Comparable接口的类,因为结点元素的存放需要比较
- 上浮、下沉算法作用:当我们添加了结点或者删除了结点,可能就会破坏堆的特性,所以要对某部分结点进行重排
核心就是通过上浮下沉保持堆性质
2、构建堆并实现插入
-
精髓:
- 实现堆中插入一个元素,与所处的位置息息相关,根据堆特性下一层的数据的Key值总是得比上一层的Key值小
- 所以在进行插入元素方法,需要依次比较每一层进行交换数据
- 那么就需要用到上浮算法
- 上浮算法就是一种让当前结点和父结点进行交换的算法,交换罢了
-
思考点:
-
来看看这张图:
-
1.当Key值
S
上浮到P
的父节点后,那它还需要跟N
比较吗?- 答案:理所应当是不用的,因为原本结点
P
就比N
大,既然S
又大于P
,那么S
也必然大于N
- 答案:理所应当是不用的,因为原本结点
-
2.想想还有啥 …ing
-
package datastructure.heap;
public class Heap<T extends Comparable<T>> {
private T[] items;
private int count; //指向着最后一位结点,同时也表示着结点个数
public Heap(int capacity) {
// 本次实现舍弃了索引0,也就是从索引1开始为有效数据
// 那么:
this.items = (T[])new Comparable[capacity + 1];
}
public void insert (T item) {
// 根据堆存放性质直接在后面添加元素
items[++count] = item;
// 存放完毕,看看是否需要上浮
swim(count);
}
// 上浮: 使当前索引位置K的元素上浮到正确位置
private void swim (int k) {
for (k = count; k > 1; k /= 2) {
if (less(k / 2, k)) {
swap(k / 2, k);
}
}
}
// 比较两个索引位置的元素
private boolean less (int i, int j) {
return items[i].compareTo(items[j]) < 0;
}
// 交换两个索引位置上的元素
private void swap (int i, int j) {
T temp = items[i];
items[i] = items[j];
items[j] = temp;
}
}
3、实现删除
- 删除实现一共包括三个部分:执行删除入口、获取待删除元素的索引位置、执行下沉算法
- 总体思路是:
- 得到待删除节点的所在索引位置,然后与最后一个结点位置进行交换,把最后一个索引位置置为空,那么就删除了该数据
- 之后就对原本为最后一个结点的元素执行下沉
// 删除指定元素
public void remove (T item) {
int k = indexOf(item);
if (k != 0)
sink(k);
}
二、获取待删除元素的所在索引位置
- 思路是:用待删除元素的Key,然后与倒数第二层比较,如果是小的,那么只可能在倒数第一层
- 注意只可能,因为完全二叉树的结点存放性质,比如下图
-
倒数第二层谁一定有子结点啊,那就是count的父节点
-
比如要查找结点
3
,我们首先就取倒数第二层的节点与之比较,也就是取count
的父节点8
,那么此时虽然比8
小,但是结点3
,并不在倒数第一层节点
/**
* 传入一个元素,获取所在位置
* @param k
* @return 不存在返回0
*/
public int indexOf (T k) {
for (int i = count; i >= 1; i /= 2) {
// 且由于舍弃了索引0,所以:
if (k.compareTo(items[i / 2 == 0 ? 1 : i / 2]) <= 0) {
// 遍历这一层: 从右往左找
for (int j = i; j > i / 2; j--) {
if (k == items[j]) return j;
}
}
}
return 0;
}
三、下沉算法
- 根据总体思路:让待删除位置与最后一个位置交换元素,接着就可把最后一个索引位,置为空,删掉该元素
- 下沉算法思路:
- 交换后
- 待删除结点变为待下沉结点,每下沉一次,就重新改变待下沉结点位置,一直判断到最后一层
- so:层数的话,我们直接判断到最后一个节点的父节点即可
- 要注意的是:当判断到倒数第二层时,那么最后一层不一定是满的,所以待下沉结点的右子结点可能会为空,左子结点不可能为空,因为我们是判断拿到最后一个节点的父节点结束
// 下浮
public T sink (int k) {
// 与最后一个结点交换数据,然后进行下移这个结点
T t = items[k];
swap(k,count);
items[count] = null;
count--;
// 此时这个K表示的位置为需要下浮,
// 判断左右子结点,谁比较大谁浮上来,然后K位置沉下去
// 判断到最后一个节点的父节点,Over
for (int i = k; i <= count / 2;) {
if (items[2*i+1] != null) {
if (less(2 * i, 2 * i + 1)) {
i = 2 * i + 1;
} else {
i = 2 * i;
}
} else i = 2 * i;
if (less(k,i))
swap(k,i);
k = i; // 重新改变待下沉位置
}
return t;
}
3.1、删除顶部元素
- 这时就比较简单了,直接这样既可
// 删除最大元素
public T delMax () {
return sink(1);
}
测试类:
Heap<String> heap = new Heap<>(10);
heap.insert("A");
heap.insert("B");
heap.insert("C");
heap.insert("D");
heap.insert("E");
heap.insert("F");
heap.insert("G");
String result;
while ((result = heap.delMax()) != null) {
System.out.print(result + " ");
}
输出:
堆排序
学会了堆,重要的一点就是可以实现数据排序,那么接下来跳转到我的堆排序