堆
1 数据结构——堆 Heap
1.0 树
满二叉树:如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
完全二叉树:如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
1.1堆
参考文章:https://www.cnblogs.com/wangchaowei/p/8288216.html
1.1.1 什么是堆
堆是一棵完全二叉树,一般都用数组来表示堆
1)堆结构就是用数组实现的完全二叉树结构
2)大根堆:父节点的值大于或等于子节点的值;
3)小根堆: 父节点的值小于或等于子节点的值
1.1.2 堆的常用方法:
- 构建优先队列
- 堆排序
- 快速找出一个集合中的最小值(或者最大值)
1.2 数组构造大根堆
参考文章:
https://blog.csdn.net/zhizhengguan/article/details/106826270
https://www.cnblogs.com/CherishFX/p/4643940.html
1.2.1 节点与数组索引的对应关系
- 对于k节点,其父节点是 (k-1)/2 (注意: 0节点除外)
- 对于k节点,其两个儿子节点分布是: left = 2*k + 1 ; right = 2 *k + 2;
- 最后一个节点是
arr.length -1
那么最后一个节点的父节点就是最后一个非叶子节点: - 最后一个非叶子节点是
(arr.length - 2)/2;
(取整之后)
因为堆是对父节点-左/右孩子节点之间的约束,所以从最后一个非叶子节点开始调整。每次交换后,都要对下一层的子堆进行递归调整,因为交换后有可能破坏已调整子堆的结构
1.2.2 实际样例
从最后一个非叶子节点开始,下标为arr.length/2-1 = 5/2-1 = 1
,所以是6
从左至右,从下至上进行调整,for(int i=(array.length-2)/2;i>=0;i--)
找到第二个非叶节点4 【3/2 - 1 = 0】,找到【 4 9 8】中最大的元素进行交换。交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6
1.2.2 堆的相关操作
1、新增元素:新元素被加入到heap的末尾,自下往上,然后更新树以恢复堆的次序
public void push(int value) {
arr[heapSize++] = value;
adjustUp((heapSize-2)/2);
}
public void adjustUp(int k){
if(k < 0)//不是根节点
return;
int left = 2 * k + 1, right = 2 * k +2, largest = left;
if(right < heapSize && arr[right] > arr[left]){
largest = right;
}
if(arr[largest] > arr[k]){
swap(largest, k);
adjustUp((k-1)/2);//继续调节父节点
}
}
2、 删除最大值:将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。
public void poll(){
swap(0, heapSize-1);
heapSize --;
adjustDown(0);
}
public void adjustDown(int k){
// 非叶子节点,不用向下调整。
// 判断叶子节点:(堆大小是1 或 就一般的最后一个节点的父节点之后的节点都是叶子)
if(heapSize == 1 || k > (heapSize-2)/2 )
return;
int left = 2*k +1, right = 2 * k + 2, largest = left;
if(right < heapSize && arr[right] > arr[left]){
largest = right;
}
if(arr[largest] > arr[k]){
swap(largest, k);
adjustDown(largest);
}
}
1.3 堆排序
参考文章:思路非常清晰:【https://mp.weixin.qq.com/s/O6fXtHuuOUVlnECSwLLiPw】
堆排序的基本思想是:
1.将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点
2.将其与末尾元素进行交换,此时末尾就为最大值。
3.然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。
4.如此反复执行,便能得到一个有序序列了
public class HeapSort {
@Test
public void test() {
int[] arr = {3, 4, 7, 2, 9, 0, 3, 6};
heapSort(arr);
}
public void heapSort(int[] arr) {
// 从第一个非叶子节点进行调整
int startIndex = (arr.length - 1) / 2;
for (int i = startIndex; i >= 0; i--) {
toMaxHeap(arr, arr.length, i);
}
//已经变成大根堆,交换堆顶和最后一个元素,循环
for (int i = arr.length - 1; i > 0; i--) {
swap(arr, i, 0);
//size=i,一直在减小
toMaxHeap(arr, i, 0);
}
System.out.println(Arrays.toString(arr));
}
public void toMaxHeap(int[] arr, int size, int index) {
int leftChildIndex = 2 * index + 1;
int rightChildIndex = 2 * index + 2;
int maxIndex = index;
// 找到最大的元素
if (leftChildIndex < size && arr[leftChildIndex] > arr[maxIndex]) {
maxIndex = leftChildIndex;
}
if (rightChildIndex < size && arr[rightChildIndex] > arr[maxIndex]) {
maxIndex = rightChildIndex;
}
// 如果是子节点比父节点大,则进行交换
if (maxIndex != index) {
swap(arr, index, maxIndex);
// 重新调整大根堆顺序
toMaxHeap(arr, size, maxIndex);
}
}
public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
1.4 优先队列PriorityQueue
1.4.1 什么是优先级队列
- 优先级队列中,数据按关键词有序排列,插入新数据的时候,会自动插入到合适的位置保证队列有序。
- PriorityQueue是非线程安全的,所以Java提供了PriorityBlockingQueue(实现BlockingQueue接口)用于Java多线程环境
方法构成
方法 | 是否抛异常 | 是否有返回值 |
---|---|---|
add | 执行失败,抛异常 | |
offer | 执行失败,返回null | |
remove | 移除第一个元素,成功返回true,失败返回false,执行失败,抛异常 | |
poll | 获取并移除此队列的头,如果此队列为空,则返回 null 。 | |
element | 返回第一个元素,失败抛异常 | |
peek | 获取但不移除此队列的头;如果此队列为空,则返回 null 。 |
由于PriorityQueue默认是小顶堆实现(降序),这里想要改成大顶堆的话,只需要输入一个自定义比较器参数即可
PriorityQueue<Integer> pq = new PriorityQueue<Integer>(k, new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2){
return 02.compareTo(o1); //或者 o2 - o1
}
})
//更简单的写法
B=new PriorityQueue<>((x,y) -> (y-x));
1.4.2 自定义排序和Lambda回顾
//自然排序:类实现了java.lang.Comparable接口,重写compareTo()的规则
//这里固定指:o1表示位于前面的对象,o2表示后面的对象,并且表示o1比o2小
o1.compareTo(o2)
//升序
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
//o1比o2小,直接返回,就是不调整位置,所以是升序
return o1.getAge().compareTo(o2.getAge());
}
});
//定制排序:java.util.Comparator,重写compare方法
//这里o1表示位于前面的对象,o2表示后面的对象
compare(o1,o2)==o1.compareTo(o2)
返回-1(或负数),表示不需要交换01和02的位置,o1依旧排在o2前面,asc,升序
返回1(或正数),表示需要交换01和02的位置,o1排在o2后面,desc,降序
//Collections排序降序
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o2.getAge().compareTo(o1.getAge());//o2比o1小,所以是降序
}
});
//函数式接口,Lambda表达式
//例如创建一个线程,Runnable接口只包含一个方法,所以它被称为“函数接口”,所以它可以使用Lambad表达式来代替匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World!");
}
});
new Thread(() -> System.out.println("Hello World!"));
1.4.3 源码分析
参考文章
https://www.cnblogs.com/linghu-java/p/9467805.html
https://baijiahao.baidu.com/s?id=1665383380422326763&wfr=spider&for=pc
// Java 的 PriorityQueue 默认是小顶堆,添加 comparator 参数使其变成最大堆
Queue<Integer> heap = new PriorityQueue<>(k, (i1, i2) -> Integer.compare(i2, i1));//转换成降序
k表示我们维护的堆的大小
offer加入队列,之后调整优先级,如果是降序的,那么还是降序的
poll删除摸个元素,原来是降序,那么删除元素中最大的元素
2 面试经典TopK问题
2.1 题目列表:
百万数据处理,找到前K大的数字,从20亿个数字的文本中,找出最大的前100
2.2 TopK小
/**
* 1.基于快排的数组划分,此时只需要递归左半部分,直到k=j
* 快排变形,(平均)时间复杂度 O(n)
*
* 2.大根堆:每次从堆顶弹出的数都是堆中最大的,最小的 k 个元素一定会留在堆里
* 堆的实现:库函数中的优先队列数据结构,如 Java 中的 PriorityQueu
* 堆,时间复杂度 O(nlogk)
*
*/
2.2.1 基于快排找topk小
public class topK {
public int[] getLeastNumbers(int[] arr, int k) {
if (k >= arr.length) return arr;
return quickSort(arr, k, 0, arr.length - 1);
}
private int[] quickSort(int[] arr, int k, int left, int right) {
int i = left, j = right;
while (i < j) {
while (i < j && arr[j] >= arr[left]) j--;
while (i < j && arr[i] <= arr[left]) i++;
swap(arr, i, j);
}
swap(arr, j, left);
//i是基准值的索引,我们需要基准值刚好是k
if (i > k) return quickSort(arr, k, left, j - 1);
if (i < k) return quickSort(arr, k, j + 1, right);
//Arrays.copyOf(原始数组, 长度)
return Arrays.copyOf(arr, k);
}
private void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
2.2.2 大根堆法找topk小(优先队列法)
public class topk{
public int[] getLeastNumbers(int[] arr, int k) {
if (k == 0) {
return new int[0];
}
// 使用一个最大堆(大顶堆)
// Java 的 PriorityQueue 默认是小顶堆,添加 comparator 参数使其变成最大堆
Queue<Integer> heap = new PriorityQueue<>(k, (i1, i2) -> Integer.compare(i2, i1));
//要找topk小,小根堆是降序,所以只有当前遍历的数小于堆顶元素(最大元素)才入堆
for (int e : arr) {
// 当前数字小于堆顶元素才会入堆
if (heap.isEmpty() || heap.size() < k || e < heap.peek()) {
heap.offer(e);
}
//入堆导致堆大了,删除最大的元素
if (heap.size() > k) {
heap.poll(); // 删除堆顶最大元素
}
}
// 将堆中的元素存入数组
int[] res = new int[heap.size()];
int j = 0;
for (int e : heap) {
res[j++] = e;
}
return res;
}
}
2.3 TopK大
/**
百万数据处理,找到前K大的数字,从20亿个数字的文本中,找出最大的前100
- 最直观:小顶堆
- 较高效:Quick Select算法。
**我们可以建立一个容量为100堆,先将前100个数据存入。将堆变成小根堆,接下来每次从随机数字中取出一个数字,和堆顶的元素比较大小,如果大于堆顶的元素,将堆顶元素替换,再将堆变成小根堆。以此类推,直到所有元素遍历完**
优化的方法:可以把所有10亿个数据分组存放,比如分别放在1000个文件中。这样处理就可以分别在每个文件的10^6个数据中找出最大的10000个数,合并到一起在再找出最终的结果。
*/
求最大K个采用小根堆,而求最小K个采用大根堆。
1、 根据数据前K个建立K个节点的小根堆。
2、在后面的N-K的数据的扫描中,
- 如果数据大于小根堆的根节点,则根节点的值覆为该数据,并调节节点至小根堆。
- 如果数据小于或等于小根堆的根节点,小根堆无变化。
2.3.1 快排法实现
//方法一:快排
public int KthLargest(int[] arr,int k){
return quick(arr,0,arr.length-1,k);
}
public int quick(int[] arr, int left, int right, int k) {
//Math.random()表示[0,1]之间的数,随机化 pivot,防止极端情况快排失效
int pivot = left + (int)(Math.random()*(right - left + 1));
swap(arr, left, pivot);
int i = left, j = right;
while (i < j) {
while (i < j && arr[j] >= arr[left]) j--;
while (i < j && arr[i] <= arr[left]) i++;
swap(arr, i, j);
}
swap(arr, left, j);
if (j < k) quick(arr, j + 1, right, k);
if (j > k) quick(arr, left, j - 1, k);
return arr[k];
}
public void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
2.3.2 小根堆实现(自定义实现)
//可以直接用 PriorityQueue 优先队列实现
public class findKthLargest {
@Test
public void test(){
int[] arr = {56,275,12,6,45,478,41,1236,456,12,546,45};
int[] largest = KthLargest(arr, 5);
System.out.println(Arrays.toString(largest));
}
public int[] KthLargest(int[] arr,int k){
// 1.取前k个元素先组成topk数组
int[] topK = new int[k];
for (int i = 0; i < k; i++) {
topK[i] = arr[i];
}
// 2.构造小根堆,从第一个非叶子节点进行调整
int startIndex = (k - 1) / 2;
for (int i = startIndex; i >= 0; i--) {
toMinHeap(topK, k, i);
}
// 3.从k开始遍历数组,比较大小,是否替换元素
// 小根堆第一个元素最小,替换掉最小的
for (int i = k; i < arr.length; i++) {
if(arr[i]>topK[0]){
topK[0] = arr[i];
// 重新调整结构为小根堆
toMinHeap(topK, k, 0);
}
}
return topK;
}
public void toMinHeap(int[] arr,int size,int index){
int leftChild = index * 2 +1;
int rightChild = index * 2+2;
int minIndex = index;
if(leftChild<size && arr[leftChild]<arr[minIndex]){
minIndex = leftChild;
}
if(rightChild<size && arr[rightChild]<arr[minIndex]){
minIndex = rightChild;
}
if(minIndex!=index){
swap(arr,index,minIndex);
toMinHeap(arr,size,minIndex);
}
}
public void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}