一、堆
1.堆定义:
如果这棵完全二叉树的任意结点的关键字小于或等于其左孩子和右孩子的关键字,则称之为小根堆,反之则为大根堆。如:
实际上,堆在存储的时候并不是使用树形结构存储的,而是使用数组的形式。怎么理解呢?
简单的讲,堆在逻辑上表现为一棵完全二叉树,在物理上表现为一个数组。如:
树和数组之间有一个关联:树中双亲(parent)、左孩子(left)、右孩子(right)在数组中的下标满足关系:(1)已知双亲在数组中的下标 parent,则左孩子和右孩子在数组中对应的下标:
left = 2 * parent + 1;
right = 2 * parent + 2;
(2)已知孩子的下标 child(无论左孩子还是右孩子) 则:parent = (child - 1)/ 2;
该数组也可以看做是以上完全二叉树的层序遍历。
2.堆的操作
2.1 重建堆:问题:当堆顶记录改变时,如何重建堆?我们以小堆为例:
【算法思想】首先将与堆相应的完全二叉树根结点中的记录移出,该记录称为待调整记录。
此时,原来那个关键字较大的子结点相当于空结点,从空结点的左、右子树中选出一个关键字较小的记录,如果该记录的关键字仍小于待调整记录的关键字,则将该记录上移至空结点中。
重复上述移动过程,直到空结点左、右子树的关键字均小于待调整记录的关键字。此时,将
待调整记录放入空结点即可。
上述调整方法相当于把待调整记录逐步向下“筛”的过程,所以一般称其为“筛选”法或“向下调整”。
我们使用代码来解释:
public static void shiftDown(long[] array, int size, int index) {
// index代表要调整的位置。size为堆的大小。
//
while (true) {
// 1. 判断 index 所在位置是不是叶子
// 逻辑上,没有左孩子一定就是叶子了(因为完全二叉树这个前提)
int left = 2 * index + 1;
if (left >= size) {
// 越界 -> 没有左孩子 -> 是叶子 -> 调整结束
return; // 循环的出口一:走到的叶子的位置
}
// 2. 找到两个孩子中的最值【最小值 via 小堆】
// 先判断有没有右孩子
int right = left + 1; // right = 2 * index + 2
int min = left; // 假设最小值就是左孩子,所以 min 保存的最小值孩子所在的下标
if (right < size && array[right] < array[left]) {
// right < size 必须在 array[right] < array[left] 之前,不能交换顺序
// 因为先得确定有右孩子,才有比较左右孩子的意义
// 有右孩子为前提的情况下,然后右孩子的值 < 左孩子的值
min = right; // min 应该是右孩子所在的下标
}
// 3. 将最值和当前要调整的位置进行比较,判断是否满足堆的性质
if (array[index] <= array[min]) {
// 当前要调整的结点的值 <= 最小的孩子值;说明这里也满足堆的性质了,所以,调整结束
return; // 循环的出口一:循环期间,已经满足堆的性质了
}
// 4. 交换两个值,物理上对应的就是数组的元素交换 min 下标的值、index 下标的值
long t = array[index];
array[index] = array[min];
array[min] = t;
// 5. 再对 min 位置重新进行同样的操作(对 min 位置进行向下调整操作)
index = min;
}
}
过程如下:
2.2 建初堆:问题:如何由一个任意序列建初堆?
【算法思想】将一个任意序列看成是对应的完全二叉树,由于叶结点可以视为单元素的
堆,因而可以反复利用上述调整堆算法(“筛选”法),自底向上逐层把所有子树调整为堆,直到将
整个完全二叉树调整为堆。
可以证明,上述完全二叉树中,最后一个非叶结点位于第Ln/2J个位置,n为二叉树结点数
目。因此,“筛选”需从第Ln/2J个结点开始,逐层向上倒退,直到根结点。
public static void buildHeap (int[] array) {
//我们假定传入的数组是经过处理的,即数组内的元素个数就是堆的元素个数。
//通过二叉树可以观察到只需要从最后一个节点的双亲结点开始从底向上进行向下调整
for (int i = (array.length-2)/2; i >=0 ; i--) {
shiftDown(array,array.length,i);
}
}
private static void shiftDown(int[] array, int size, int index) {
//index 为当前需要调整的位置
while (index * 2 + 1 < size){
int left = index * 2 + 1;
int right = left + 1;
//找出最小孩子的下标
int min = left;
if (right < size && array[min] > array[right]){
min = right;
}
//如果当前结点满足堆的性质则结束。
if (array[index] < array[min]){
return;
}
//交换当前结点与最小孩子的值
swap(array,min,index);
//继续向下调整
index = min;
}
}
private static void swap(int[] array, int min, int index) {
int t = array[index];
array[index] = array[min];
array[min]= t;
}
二、优先队列(priority queue)
1.定义:优先队列中的元素可以按照任意顺序插入,但会按照有序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先队列中的最小的元素。不过,优先队列并没有对所有元素进行排序。如果迭代处理这些元素,并不需要对它们进行排序优先队列使用了一个精巧且高效的数据结构,称为堆(heap)。堆是一个可以自组织的二叉树,其添加(ad)和(remove)操作可以让最小的元素移动到根,而不必花费时间对元素进行排序。
与 TreeSet 一样,优先队列既可以保存实现了 Ccomparable 接口的类对象,也可以保存构造
器中提供的 Comparator 对象。
优先队列的典型用法是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队
列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除(由于习惯上将1设
为“最高”优先级,所以 remove 操作会将最小的元素删除)。
实现优先队列:
// 直接使用 long 类型作为我们的元素类型,不考虑泛型了
public class MyPriorityQueue {
// 很重要的属性:堆 = 数组 + 有效元素个数
private long[] array;
private int size;
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
// 由于我们的元素类型是 long 类型,不需要考虑 Comparable 和 Comparator 的问题
// 所以我们只需要一个构造方法即可
public MyPriorityQueue() {
array = new long[16];
size = 0;
}
public void offer(long e) {
// 放入我们的优先级队列中,放入之后,保证堆的性质仍然是满足的
ensureCapacity();
array[size] = e;
size++;
// [size - 1] 就是刚刚插入的元素的位置
shiftUp(array, size - 1);
}
// 前提:size > 0
public long peek() {
// 返回堆顶元素
if (size < 0) {
throw new RuntimeException("队列是空的");
}
return array[0];
}
public long poll() {
// 返回并删除堆顶元素
if (size < 0) {
throw new RuntimeException("队列是空的");
}
long e = array[0];
// 用最后一个位置替代堆顶元素,删除最后一个位置
array[0] = array[size - 1];
array[size - 1] = 0; // 0 代表这个位置被删除了,不是必须要写的
size--;
// 针对堆顶位置,做向下调整
shiftDown(array, size, 0);
return e;
}
// 检查我们的优先级队列对象是否正确
// 1. 0 <= size && size <= array.length
// 2. 满足小堆的特性(任取结点(除开叶子结点),其值 <= 它的两个孩子的值(如果存在的话)
public void check() {
if (size < 0 || size > array.length) {
throw new RuntimeException("size 约束出错");
}
// 如果每个结点都没问题,说明小堆成立
for (int i = 0; i < size; i++) {
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left >= size) {
// 说明是叶子,跳过
continue;
}
// 左孩子破坏了规则
if (array[i] > array[left]) {
throw new RuntimeException(String.format("[%d] 位置的值大于其左孩子的值了", i));
}
// 右孩子破坏了规则
if (right < size && array[i] > array[right]) {
throw new RuntimeException(String.format("[%d] 位置的值大于其右孩子的值了", i));
}
}
}
private void shiftDown(long[] array, int size, int index) {
while (2 * index + 1 < size) {
// 说明 index 一定有左孩子的
int min = 2 * index + 1;
int right = min + 1;
if (right < size && array[right] < array[min]) {
min = right;
}
if (array[index] <= array[min]) {
return;
}
swap(array, index, min);
index = min;
}
}
private void swap(long[] array, int i, int j) {
long t= array[i];
array[i] = array[j];
array[j] = t;
}
private void ensureCapacity() {
if (size < array.length) {
return;
}
array = Arrays.copyOf(array, array.length * 2);
}
// 向上调整期间,不需要 size
private void shiftUp(long[] array, int index) {
while (index != 0) {
int parent = (index - 1) / 2;
if (array[parent] <= array[index]) {
return;
}
swap(array, index, parent);
index = parent;
}
}
}
2.Java中实现的priority queue:
一个基于优先级堆的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator
进行排序,具体取决于所使用的构造方法。优先级队列不允许使用 null
元素。依靠自然顺序的优先级队列还不允许插入不可比较的对象(这样做可能导致
ClassCastException
)。
此队列的头 是按指定排序方式确定的最小 元素。如果多个元素都是最小值,则头是其中一个元素——选择方法是任意的。队列获取操作 poll
、remove
、peek
和 element
访问处于队列头的元素。
优先级队列是无界的,但是有一个内部容量,控制着用于存储队列元素的数组大小。它通常至少等于队列的大小。随着不断向优先级队列添加元素,其容量会自动增加。无需指定容量增加策略的细节。
此类及其迭代器实现了 Collection
和 Iterator
接口的所有可选 方法。方法 iterator()
中提供的迭代器不 保证以任何特定的顺序遍历优先级队列中的元素。如果需要按顺序遍历,请考虑使用 Arrays.sort(pq.toArray())
。
方法摘要 | ||
---|---|---|
boolean | add(E e) 将指定的元素插入此优先级队列。 | |
void | clear() 从此优先级队列中移除所有元素。 | |
Comparator<? super E> | comparator() 返回用来对此队列中的元素进行排序的比较器;如果此队列根据其元素的自然顺序进行排序,则返回 null 。 | |
boolean | contains(Object o) 如果此队列包含指定的元素,则返回 true 。 | |
Iterator<E> | iterator() 返回在此队列中的元素上进行迭代的迭代器。 | |
boolean | offer(E e) 将指定的元素插入此优先级队列。 | |
E | peek() 获取但不移除此队列的头;如果此队列为空,则返回 null。 | |
E | poll() 获取并移除此队列的头,如果此队列为空,则返回 null。 | |
boolean | remove(Object o) 从此队列中移除指定元素的单个实例(如果存在)。 | |
int | size() 返回此 collection 中的元素数。 | |
Object[] | toArray() 返回一个包含此队列所有元素的数组。 | |
| toArray(T[] a) 返回一个包含此队列所有元素的数组;返回数组的运行时类型是指定数组的类型。 |
三、总结:
1、堆在定义上是一个二叉树,但是实际实现的时候是一个数组。
二叉树的双亲结点与孩子结点 在数组中的下标关系:
left= parent * 2 +1;
right = parent * 2 + 2;
parent = (child-1)/2;
2、堆:在一些变动频繁的数据集中找出最值。
3、堆的核心操作:向下调整、建初堆。
4、能够实现 优先队列。
5、Top-k 问题。