1.概念
(二叉)堆是一个数组,它可以看成一个近似的完全二叉树,堆有大根堆和小根堆两种,大根堆就是父节点大于其左右孩子节点,每个节点都满足如此规则构建成的一种数据结构(小根堆则相反),可以见下面图片:
2.相关操作
-
建堆:建堆的过程是一个自底向上的过程
-
调整堆:
3.Java代码实现
将传入的数组构建成一个大根堆(不使用PriorityQueue的实现)
// 原地堆化(最大堆)
// 堆化可以保证 h[0] 是堆顶元素,且 h[i] >= max(h[2*i+1], h[2*i+2])
private void heapify(int[] h) {
// 倒着遍历,从而保证 i 的左右子树一定是堆,那么 sink(h, i) 就可以把左右子树合并成一个堆
// 下标 >= h.length / 2 的元素是二叉树的叶子,无需下沉
for (int i = h.length / 2 - 1; i >= 0; i--) {
sink(h, i);
}
}
// 把 h[i] 不断下沉,每次找左右儿子中最大的交换,直到 i 的左右儿子都 <= h[i] 时停止
private void sink(int[] h, int i) {
int n = h.length;
while (2 * i + 1 < n) {
int j = 2 * i + 1; // i 的左儿子
if (j + 1 < n && h[j + 1] > h[j]) { // i 的右儿子比 i 的左儿子大
j++;
}
if (h[j] <= h[i]) { // 说明 i 的左右儿子都 <= h[i],停止下沉
break;
}
swap(h, i, j); // 下沉
i = j;
}
}
// 交换 h[i] 和 h[j]
private void swap(int[] h, int i, int j) {
int tmp = h[i];
h[i] = h[j];
h[j] = tmp;
}
4.堆的应用
4.1堆排序
堆排序的实现过程就是每一次将堆顶元素与堆最末尾的元素交换,然后堆的size-1(也就是最后一个元素排除出去了),然后调整堆,如此重复n-1次,最终得到一个有序数组
void HeapSort(ElemType A[], int len){
BuildMaxHeap(A, len);//建堆
for(i = len; i > 1; i--){
Swap(A[i], A[1]);//交换元素
HeadAdjust(A, 1, i-1);//调整堆
}
}
4.2优先队列
首先,我们要明白二叉堆是实现优先队列的一种底层结构。
PriorityQueue,即优先级队列。优先级队列可以保证每次取出来的元素都是队列中的最小或最大的元素(Java优先级队列默认每次取出来的为最小元素)。JDK1.8中的PriorityQueue底层使用了堆这种数据结构。
优先队列的相关操作详见下面文章(具体的操作可以查看Java API文档)
https://blog.csdn.net/LEE180501/article/details/128728900
4.3Java中优先队列的使用案例
我们以下面leetcode中的题目为使用场景,熟悉一下优先队列的使用。在本题的求解过程中,要取出队列的最大值进行操作之后,再将操作后的结果放回队列中,队列还需要保持有序性。
所以,我们在实现题解的过程了,用lambda表达式定义了一个比较器,这个比较器用于决定队列中元素的优先级。(不定义比较器的化,默认的就是小根堆,这里就是让元素值大的优先级高)
(a, b) -> b - a: 这是一个lambda表达式,它定义了一个比较器(comparator)。
a 和 b 是队列中待比较的两个元素。
-> 是lambda操作符,用于定义一个箭头函数。
b - a 是该函数的返回值,表示如果 b 的优先级高于 a,则返回一个正数;如果 a 的优先级高于 b,则返回一个负数;如果两者相等,则返回0。
class Solution {
public long pickGifts(int[] gifts, int k) {
//优先队列
PriorityQueue<Integer> pq = new PriorityQueue<>((a,b)->b -a);
for(int gift : gifts){
pq.offer(gift);
}
while(k > 0){
k--;
int x = pq.poll();
pq.offer((int) Math.sqrt(x));
}
long res = 0;
while(!pq.isEmpty()){
res += pq.poll();
}
return res;
}
}
5.C++、Python中堆的使用
以如下leetcode题目的应用场景为例,讲述C++、python中堆的使用。
5.1Python中的使用
默认是小根堆
class Solution:
def minStoneSum(self, piles: List[int], k: int) -> int:
for i in range(len(piles)):
piles[i] *= -1 # 变成相反数,这样堆化后就是最大堆了
heapify(piles) # 原地堆化
while k and piles[0] != -1:
heapreplace(piles, piles[0] // 2) # 负数下取整等于正数上取整
k -= 1
return -sum(piles)
5.2C++中的使用
默认初始化为大根堆
class Solution {
public:
int minStoneSum(vector<int> &piles, int k) {
make_heap(piles.begin(), piles.end()); // 原地堆化(最大堆)
while (k-- && piles[0] != 1) {
pop_heap(piles.begin(), piles.end()); // 弹出堆顶并移到末尾
piles.back() -= piles.back() / 2;
push_heap(piles.begin(), piles.end()); // 把末尾元素入堆
}
return accumulate(piles.begin(), piles.end(), 0);
}
};