图文最详细的堆解析:从二叉树到堆到解析大根堆小根堆,分析堆排序,最后实现topK经典面试问题

Posted 尚墨1111

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图文最详细的堆解析:从二叉树到堆到解析大根堆小根堆,分析堆排序,最后实现topK经典面试问题相关的知识,希望对你有一定的参考价值。

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.3 Java实现

大根堆两个主要算法:

  • 自下往上调整算法: 主要用于插入新元数的时候;自下往上的意思是说,子节点和父节点交换
  • 自上往下调整算法: 用于从数组创建一个大根堆,或者删除元素的时候;

1.3.1 自下往上法

//构建大根堆:将array看成完全二叉树的顺序存储结构
    private int[] buildMaxHeap(int[] array){
        //从最后一个节点array.length-1的父节点(array.length-1-1)/2开始,直到根节点0,反复调整堆
        for(int i=(array.length-2)/2;i>=0;i--){
            adjustDownToUp(array, i,array.length);
        }
        return array;
    }

    //将元素array[k]自下往上逐步调整树形结构,
    private void adjustDownToUp(int[] array,int k,int length){
        int temp = array[k];
        for(int i=2*k+1; i<length-1; i=2*i+1){    //i为初始化为节点k的左孩子,沿节点较大的子节点向下调整
            if(i<length && array[i]<array[i+1]){  //取节点较大的子节点的下标
                i++;   //如果节点的右孩子>左孩子,则取右孩子节点的下标
            }
            if(temp>=array[i]){  //根节点 >=左右子女中关键字较大者,调整结束
                break;
            }else{   //根节点 <左右子女中关键字较大者
                array[k] = array[i];  //将左右子结点中较大值array[i]调整到双亲节点上
                k = i; //【关键】修改k值,以便继续向下调整
            }
        }
        array[k] = temp;  //被调整的结点的值放入最终位置
    }

1.3.2 堆的相关操作

1、新增元素:新元素被加入到heap的末尾,自下往上,然后更新树以恢复堆的次序

在这里插入图片描述

//插入操作:向大根堆array中插入数据data
    public int[] insertData(int[] array, int data){
        array[array.length-1] = data; //将新节点放在堆的末端
        int k = array.length-1;  //需要调整的节点
        int parent = (k-1)/2;    //双亲节点
        while(parent >=0 && data>array[parent]){
            array[k] = array[parent];  //双亲节点下调
            k = parent;
            if(parent != 0){
                parent = (parent-1)/2;  //继续向上比较
            }else{  //根节点已调整完毕,跳出循环
                break;
            }
        }
        array[k] = data;  //将插入的结点放到正确的位置
        return array;
    }

//方法二:
    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 int[] deleteMax(int[] array){
        //将堆的最后一个元素与堆顶元素交换,堆底元素值设为-99999
        array[0] = array[array.length-1];
        array[array.length-1] = -99999;
        //对此时的根节点进行向下调整
        adjustDownToUp(array, 0, array.length);
        return array;
    }

//方法二:
    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);
        }

    }

3、具体实现总览

参考:https://www.cnblogs.com/sidewinder/p/13733329.html

public class Heap {
        int[] arr = new int[10];
        int heapSize = 0;
        // 构建一个最大堆的过程:就是从后往前
        // 针对每个非叶子节点,做向下调整算
        // 参数是:将传如数组,构建大根堆。
        public void buildMaxHeap(int[] _arr){
            arr = _arr;
            heapSize = _arr.length;
            // 找到非叶子节点,然后向下调整;
            for(int i = (arr.length -2)/2; i >= 0; i --){
                adjustDown(i);
            }
        }
    
       // 新增元素
        public void push(int value) {
                arr[heapSize++] = value;
                adjustUp((heapSize-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);
            }

        }
        
        // 向上调整算法
        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);
            }

        }
        void swap(int i, int j){
            int tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }

        // 排序只需要一直删除,堆顶元素, 放到堆末尾;
        // 大根堆,就能进行从小到大排序。
        void sort(){
            for(int i = 0; i < arr.length; i++){
                poll();
            }
        }

}

1.4 堆排序

先让整个数组都变成大根堆结构,建立堆的过程:

  • 从上到下的方法,时间复杂度为O(NlogN)
  • 从下到上的方法,时间复杂度为O(N)

思想:堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,一直周而复始,时间复杂度为O(N*logN)
堆的大小减小成0之后,排序完成!

//堆排序
public int[] heapSort(int[] array){
    array = buildMaxHeap(array); //初始建堆,array[0]为第一趟值最大的元素
    for(int i=array.length-1;i>1;i--){
        int temp = array[0];  //将堆顶元素和堆低元素交换,即得到当前最大元素正确的排序位置
        array[0] = array[i];
        array[i] = temp;
        adjustDownToUp(array, 0,i);  //整理,将剩余的元素整理成堆
    }
    return array;
}

1.4 优先队列PriorityQueue

1.4.1 什么是优先级队列

  • 队列的核心思想就是先进先出,这个优先级队列有点不太一样。

  • 优先级队列中,数据按关键词有序排列,插入新数据的时候,会自动插入到合适的位置保证队列有序。

  • 序有两种形式:升序或者是降序

PriorityQueue类在Java1.5中引入并作为java集合API 的一部分。PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。

PriorityQueue是非线程安全的,所以Java提供了PriorityBlockingQueue(实现BlockingQueue接口)用于Java多线程环境

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(或负数),表示不需要交换0102的位置,o1依旧排在o2前面,asc,升序
返回1(或正数),表示需要交换0102的位置,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 题目列表:

剑指 Offer 40. 最小的k个数

215. 数组中的第K个最大元素

百万数据处理,找到前K大的数字,从20亿个数字的文本中,找出最大的前100


2.2 TopK小

剑指 Offer 40. 最小的k个数

/**
* 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小

堆排序Java实现

数据结构_二叉树Ⅲ——堆与优先队列

数据结构总结

数据结构中的堆

堆堆排序

[数据结构]优先级队列(最大堆)详解