1. 堆(heap)
- 堆逻辑上是一棵完全二叉树
- 堆物理上是保存在数组中 比特科技
- 满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者大堆
- 反之,则是小堆,或者小根堆,或者小堆
- 堆的基本作用是,快速找集合中的最值
小根堆
大根堆
2.操作-向下调整
前提:左右子树必须已经是一个堆,才能调整。
说明:
- array 代表存储堆的数组
- size 代表数组中被视为堆数据的个数
- index 代表要调整位置的下标
- left 代表 index 左孩子下标
- right 代表 index 右孩子下标
- min 代表 index 的小值孩子的下标
过程(以小堆为例):
- index 如果已经是叶子结点,则整个调整过程结束 1. 判断 index 位置有没有孩子 2. 因为堆是完全二叉树,没有左孩子就一定没有右孩子,所以判断是否有左孩子 3. 因为堆的存储结构是数组,所以判断是否有左孩子即判断左孩子下标是否越界,即 left >= size 越界
- 确定 left 或 right,谁是 index 的小孩子 min 1. 如果右孩子不存在,则 min = left 2. 否则,比较 array[left] 和 array[right] 值得大小,选择小的为 min
- 比较 array[index] 的值 和 array[min] 的值,如果 array[index] <= array[min],则满足堆的性质,调整结束
- 否则,交换 array[index] 和 array[min] 的值
- 然后因为 min 位置的堆的性质可能被破坏,所以把 min 视作 index,向下重复以上过程
时间复杂度分析:
坏的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为 O(log(n))
// 以大堆为例
// 借助向下调整的操作来建堆
public static void shiftDown(int[] array, int size, int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < size) {
// child 本来是左子树的下标, 再 + 1 就是右子树下标
// 在找左子树和右子树谁大
if (child + 1 < size
&& array[child + 1] > array[child]) {
child = child + 1;
}
// if 之后 child 不知道它是左还是右了, 而一定是左右中的最大值
if (array[child] > array[parent]) {
// 不符合大堆的特性, 交换 child 和 parent 的位置
swap(array, child, parent);
} else {
// 如果发现满足堆的特性, 调整就结束了
break;
}
parent = child;
child = 2 * parent + 1;
}
}
public static void swap(int[] array, int x, int y) {
int tmp = array[x];
array[x] = array[y];
array[y] = tmp;
}
3.操作-建堆
下面一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
图示(以大堆为例):
// 建堆前
int[] array = { 1,5,3,8,7,6 };
// 建堆后
int[] array = { 8,7,6,5,1,3 };
时间复杂度分析:
粗略估算,可以认为是在循环中执行向下调整,为 O(n * log(n)) ,实际上是 O(n)
// 要把 [0, size) 范围中的元素建成堆
public static void createHeap(int[] array, int size) {
// 从最后一个非叶子节点出发, 从后往前走, 针对每个节点, 进行向下调整
// 第一个 size - 1 是为了找到最后一个元素的下标
// 再在最后一个元素下标的基础上再 - 1 再除以 2
for (int i = (size - 1 - 1) / 2; i >= 0; i--) {
shiftDown(array, size, i);
}
}
4. 堆的应用-优先级队列
数据结构提供两个基本的操作,一个是返回高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)
优先级队列的实现方式有很多,但常见的是使用堆来构建。
操作-入队列
过程(以大堆为例):
- 首先按尾插方式放入数组
- 比较其和其双亲的值的大小,如果双亲的值大,则满足堆的性质,插入结束
- 否则,交换其和双亲位置的值,重新进行 2、3 步骤
- 直到根结点
操作-出队列(优先级最高)
为了防止破坏堆的结构,删除时并不是直接将堆顶元素删除,而是用数组的后一个元素替换堆顶元素,然后通过向 下调整方式重新调整成堆
返回队首元素(优先级最高)
返回堆顶元素即可
public class MyPriorityQueue {
// 这个数组就是队列本体. 基于这个数组建立堆
private int[] array = new int[100];
// 队列中元素的个数, 堆的大小
private int size = 0;
public void offer(int x) {
if (size >= array.length) {
return;
}
array[size] = x;
size++;
shiftUp(array, size - 1);
}
// 复杂度是 O(logN)
private void shiftUp(int[] array, int index) {
int child = index;
int parent = (index - 1) / 2;
while (child > 0) {
if (array[parent] < array[child]) {
// 交换两个元素
swap(array, parent, child);
} else {
// 调整完了
break;
}
child = parent;
parent = (child - 1) / 2;
}
}
private void shiftDown(int[] array, int size, int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < size) {
if (child + 1 < size
&& array[child + 1] > array[child]) {
child = child + 1;
}
// 条件结束, 预期 child 指向左右子树的最大值
// 再拿 child 和 parent 进行对比
if (array[parent] < array[child]) {
// 不满足大堆要求, 交换
swap(array, parent, child);
} else {
break;
}
parent = child;
child = parent * 2 + 1;
}
}
private void swap(int[] array, int x, int y) {
int tmp = array[x];
array[x] = array[y];
array[y] = tmp;
}
// 堆顶元素就是返回值
// 删除堆顶元素并不是直接删除.
// 用最后一个元素来覆盖堆顶元素
// 再从堆顶位置向下调整即可
public Integer poll() {
if (size == 0) {
return null;
}
int ret = array[0];
array[0] = array[size - 1];
size--;
shiftDown(array, size, 0);
return ret;
}
public Integer peek() {
if (size == 0) {
return null;
}
return array[0];
}
}
TopK 问题
找前 K 个大的,要建 K 个大小的小堆
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
public class TeatTopK {
// Pair 表示数对, 方便使用 优先队列 来进行管理
// 一个普通的类, 无法直接放到优先队列中的.
// 此时优先级如何定义的, 还不明确.
// 可以把当前的类实现 Comparable 接口, 并实现 compareTo 方法
// 此时优先队列就可以借助 compareTo 决定谁是优先级高, 谁是优先级低
static class Pair implements Comparable<Pair> {
public int n1;
public int n2;
public int sum;
public Pair(int n1, int n2) {
this.n1 = n1;
this.n2 = n2;
this.sum = n1 + n2;
}
@Override
public int compareTo(Pair o) {
// this, other
// 如果希望 this 在前 other 在后, 返回 < 0
// 如果希望 this 在后 other 在前, 返回 > 0
// 如果希望相等, 返回 0
if (this.sum < o.sum) {
return 1;
}
if (this.sum > o.sum) {
return -1;
}
return 0;
}
}
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
List<List<Integer>> result = new ArrayList<>();
if (k < 1) {
return result;
}
// 创建一个优先队列, 通过这个优先队列来作为堆, 完成最终的 topk 求解
PriorityQueue<Pair> queue = new PriorityQueue<>();
for (int i = 0; i < nums1.length && i < k; i++) {
for (int j = 0; j < nums2.length && i < k; j++) {
queue.offer(new Pair(nums1[i], nums2[j]));
if (queue.size() > k) {
// 始终保证 队列中 不超过 k 个元素
// 超过的话就需要淘汰掉弱者
queue.poll();
}
}
}
// 这两重循环结束之后, 此时 queue 就保存了需要的 k 对数字
while (!queue.isEmpty()) {
Pair pair = queue.poll();
List<Integer> tmp = new ArrayList<>();
tmp.add(pair.n1);
tmp.add(pair.n2);
// 每次出队列的值插入到 result 最前面
result.add(0, tmp);
}
return result;
}
public static void main(String[] args) {
}
}