目录
堆简介:
存储方式
实现一个堆(代码):
思想延深:
堆化思想
堆的应用:优先级队列
JDK中优先级队列
堆的应用:Top K问题
做此类题的套路:
堆的应用:堆排序
4.堆有很多存储形式,二叉堆只是其中一种;二叉堆首先是一颗完全二叉树(结构上)
5. 堆的基本作用是,快速找集合中的最值;若是最大堆,堆顶元素就是最大值
完全二叉树/满二叉树可以使用也建议使用顺序表来存储;但是其他的二叉树不建议使用顺序表(数组);因为会浪费大量的空间来存储空节点(为了区分左右子树);完全二叉树不存在只有右子树没有左子树的情况,不用存储空节点
使用顺序表存储完全二叉树时,节点的索引和节点的关系如下:
1.根节点从0开始编号:
左子树的索引为(k << 1) + 1
右子树的索引为(k << 1) + 2
2.已知孩子(不区分左右)下标,则:
双亲下标 = (child - 1) / 2;
两个关注的小问题:
如何仅用索引就能判断一个节点是否有子树?
2k + 1 < 数组长度
给定一个节点索引为k,如何判断它的父节点是否存在?
在二叉树中只有一个节点没有父节点 - 根节点;只需要看父节点编号是否 > 0 即可;父节点的索引为(k - 1) >> 1;
/**
* 基于整型的最大堆实现
* 此时根节点从0开始编号,若此时节点编号为k
* 左子树的编号 = 2K + 1;
* 右子树的编号 = 2K + 2;
* 父节点的编号 = (K - 1) / 2;
* @author zx
*/
public class MaxHeap {
//使用JDK的动态数组(ArrayList)来存储一个最大堆
List data;
public MaxHeap(){
//构造方法的this调用
this(10);
}
/**
* @param size 初始化堆的大小
*/
public MaxHeap(int size){
data = new ArrayList<>(size);
}
/**
* @param arr 将任意数组堆化!!!!!!!思想很重要!!!!!!!!
*/
public MaxHeap(int[] arr){
data = new ArrayList<>(arr.length);
//1.先将arr的所有元素复制到data数组中
for(int i : arr){
data.add(i);
}
//2.从最后一个非叶子节点开始进行siftDown
for(int i = parent(data.size() - 1);i >= 0;i--){
siftDown(i);
}
}
public void add(int val){
//1.直接向数组末尾添加元素
data.add(val);
//2.进行元素的上浮操作
siftUp(data.size() - 1);
}
/**
* @param k 元素上浮操作
* 向上调整索引为k的节点,使其仍然满足堆的性质
*/
private void siftUp(int k) {
//上浮操作的终止条件:已经走到根节点 || 当前节点值 <= 父节点值(已经处在最终位置)
//循环的迭代条件:还存在父节点并且当前节点值 > 父节点值
while(k > 0 && data.get(k) > data.get(parent(k))){
//交换当前节点和父节点值
swap(k,parent(k));
k = parent(k);
}
}
private void swap(int i, int j) {
int temp = data.get(i);
data.set(i,data.get(j));
data.set(j,temp);
}
//根据索引得到父节点的索引
private int parent(int k) {
return (k - 1) >> 1;
}
/**
* @return 取出当前最大堆的最大值
*/
public int extractMax(){
//取值一定注意判空
if(data.isEmpty()){
throw new NoSuchElementException("heap is empty!cannot extract!");
}
int max = data.get(0);
//1.将数组末尾元素顶到堆顶
int lastVal = data.get(data.size() - 1);
data.set(0,lastVal);
//2.将数组末尾的元素删除
data.remove(data.size() - 1);
//3.进行元素的下沉操作
siftDown(0);
return max;
}
/**
* @param k 元素的下沉操作
*/
private void siftDown(int k) {
//还存在子树
while(leftChild(k) < data.size()){
int j = leftChild(k);
//判断一下是否有右子树
if(j + 1 < data.size() && data.get(j + 1) > data.get(j)){
//此时右树存在且大于左树的值
j = j + 1;
}
//此时j就对应左右子树的最大值
//和当前节点k去比较
if(data.get(k) >= data.get(j)){
//下沉结束
break;
}else{
swap(k,j);
k = j;
}
}
}
/**
* @return 根据索引得到父节点的索引
*/
private int leftChild(int k) {
return (k - 1) >> 1;
}
public boolean isEmpty(){
return data.size() == 0;
}
public int peekMax(){
if(isEmpty()){
throw new NoSuchElementException("heap is empty! cannot peek");
}
return data.get(0);
}
}
将任意的数组调整为堆的结构
1.任意数组都可以看成一个完全二叉树
2.将这N个元素逐步调用add方法插入到一个新堆中就得到一个最大堆
注:堆化方法的时间复杂度是Nlog(N)。
上面自己实现堆中的【MaxHeap(int[] arr)】
1.任意数组都可以看做一颗完全二叉树
2.从当前这个完全二叉树的最后一个非叶子节点开始,进行元素下沉操作即可调整为堆
如何找到最后一个非叶子节点?
//根据索引得到父节点的索引
private int parent(int k) {
return (k - 1) >> 1;
}
当K = arr.length - 1时,最后一个非叶子节点为:(arr.length - 1 - 1) / 2
最后一个叶子节点的父节点 => 最后一个叶子节点(数组的最后一个元素:arr.length - 1)
从小问题逐步往上走,不断去调整子树,将子树不断变为大树的过程中,就将整颗树调整为堆
时间复杂度:O(N)
优先级队列:看起来是个队列,底层是基于堆的实现
按照元素优先级的大小动态顺序出队
处理的元素个数是动态变化的,有进有出,不像排序处理的集合元素个数是固定的。
操作系统的进程调度来说,底层就维护了一个优先级队列。
public class PriorityQueue extends AbstractQueue
implements java.io.Serializable {
PriorityQueue类在Java1.5中引入并作为 Java Collections Framework 的一部分。PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供Comparator(比较器)在队列实例化时的排序。优先队列的头是基于自然排序或者Comparator排序的最小元素。如果有多个对象拥有同样的排序,那么就可能随机地取其中任意一个。当我们获取队列时,返回队列的头对象。优先队列不允许空值,而且不支持non-comparable(不可比较)的对象,比如用户自定义的类。优先队列要求使用Java Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。
优先队列的大小是不受限制的,但在创建时可以指定初始大小。当我们向优先队列增加元素的时候,队列大小会自动增加。
PriorityQueue是非线程安全的,所以Java提供了PriorityBlockingQueue(实现BlockingQueue接口)用于Java多线程环境。
PriorityQueue对元素采用的是堆排序,头是按指定排序方式的最小元素。堆排序只能保证根是最大(最小),整个堆并不是有序的。方法iterator()中提供的迭代器可能只是对整个数组的依次遍历。也就只能保证数组的第一个元素是最小的。
插入方法(offer()、poll()、remove() 、add() 方法)时间复杂度为O(log(n)) ;remove(Object) 和 contains(Object) 时间复杂度为O(n);检索方法(peek、element 和 size)时间复杂度为常量。
JDK中的优先级队列默认是最小堆的实现。队首元素就是当前队列中最小值。
使用Comparator将最小堆改造为最大堆使用
Queue queue = new PriorityQueue<>(new StudentComDesc());
//匿名内部类:创建一个Comparator接口的子类,这个子类只使用一次
Queue queue = new PriorityQueue<>(new Comparator() {
@Override
public int compare(Student o1, Student o2) {
return o2.getAge() - o1.getAge();
}
});
//lambda表达式写法
// Queue queue = new PriorityQueue<>((o1,o2) -> o2.getAge() - o1.getAge());
Student stu1 = new Student("铭哥",40);
Student stu2 = new Student("龙哥",20);
Student stu3 = new Student("蛋哥",18);
queue.offer(stu1);
queue.offer(stu2);
queue.offer(stu3);
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
Comparator接口的优点: 在有时需要升序,有时有需要降序的场景中,如果使用Comparable会不得不多次修改compareTo()方法,但使用Comparator就不需要,根据不同的需求配置不同的比较器即可。
class StudentCom implements Comparator {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();
}
}
class StudentComDesc implements Comparator {
@Override
public int compare(Student o1, Student o2) {
return o2.getAge() - o1.getAge();
}
}
class Student {
private String name;
private int age;
public int getAge() {
return age;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
最小或最大的K个*** => 都是优先级队列(堆的应用)
Top K问题的时间复杂度为:nlogK
另外的思路就是排序:nlogn
取大用小,取小用大 :
找最小的K个数,构造最大堆
找最大的K个数,构造最小堆
面试题 17.14. 最小K个数----力扣
思路分析:
找到最小的K个数,构造只有K个元素的最大堆,然后扫面这个数组,当把数组完全扫描完毕之后,最大堆中就存放了最小的K个元素
扫描到某个元素时,发现堆顶元素大于当前扫描元素
最大堆性质:堆顶元素是最大值,要找最小的,若扫描的元素 > 堆顶元素 => 大于堆中的所有值,这个值一定不是要找的。
当扫描的元素比堆顶元素小就堆顶出队,换入一个更小的值!!像是一个不断打擂的过程。
把大的PK掉,不断换上更小的值。
/**
* @return 最大堆
* 时间复杂度:O(NlogK)
* 因为 K << n ==>> logk < logn
* logK:堆中的元素个数
*/
public int[] smallestK(int[] arr, int k) {
//最小的k个数,取小用大
if(arr.length == 0 || k == 0){
return new int[0];
}
int[] res = new int[k];
//构造一个最大堆,JDK默认是最小堆,使用比较器改造为最大堆
//要把最小堆改为最大堆,就是告诉编译器值越大,让编译器看来它反而小
Queue queue = new PriorityQueue<>(new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
for(int i = 0;i < arr.length;i++){
if(queue.size() < k){
queue.offer(arr[i]);
}else{
//当前判断扫描元素和堆顶元素的关系
//若扫描元素 > 堆顶元素 > 堆中的所有值,这个值一定不是要找的数
int peek = queue.peek();
if(arr[i] > peek){
continue;
}else{
//此时当前元素 < 堆顶元素,将堆顶元素出队,将较小值i入队
queue.poll();
queue.offer(arr[i]);
}
}
}
//此时最大堆中就保存了最小的k个数
for(int j = 0;j < k;j++){
res[j] = queue.poll();
}
return res;
}
方法二:排序
/**
* @return 排序法
* 时间复杂度:O(nlogn)
*/
public int[] smallestK2(int[] arr, int k) {
int[] res = new int[k];
Arrays.sort(arr);
for(int i = 0;i < k;i++){
res[i] = arr[i];
}
return res;
}
373. 查找和最小的 K 对数字
692. 前K个高频单词(比347难)
347. 前 K 个高频元素
拜托,面试别再问我TopK了!!!_架构师之路-CSDN博客
面试时,就记住这个代码!!!!!!!!!!!!!!!!!!!!
/**
* @param arr 堆排序
* 时间复杂度 O(nlogn)
*/
public static void heapSort(int[] arr) {
// 1.将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
// 从最后一个非叶子节点开始进行siftDown操作
for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
siftDown(arr,i,arr.length);
}
// 此时arr就被我调整为大顶堆
// 2.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;**********
// 3.重新调整未调整节点,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤。
for (int i = arr.length - 1; i > 0; i--) {//O(N)
// arr[0] 堆顶元素,就是当前堆的最大值
swap(arr,0,i);
siftDown(arr,0,i);//O(logn)
}
}
/**
* 元素下沉操作
* 调整索引为i的节点不断下沉,直到到达最终位置:
* a.到达了叶子节点 2i + 1 >= size
* b.当前节点值 > 左右子树的最大值(下沉到最终位置)
** 时间复杂度分析:
* 时间复杂度为 O(logn)
* @param arr
* @param i 当前要下沉的索引
* @param length 数组长度
*/
private static void siftDown(int[] arr, int i, int length) {
//还存在子树(条件a)
while (2 * i + 1 < length) {
int j = (i * 2) + 1;
//判断一下是否有右子树
if (j + 1 < length && arr[j + 1] > arr[j]) {
//此时右树存在且大于左树的值
j = j + 1;
}
// j就是左右子树的最大值
if (arr[i] > arr[j]) {//条件(b)
// 下沉结束
break;
}else {
//循环结束后,就已经将树的最大值,放在了最顶
swap(arr,i,j);
i = j;
}
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}