图文最详细的堆解析:从二叉树到堆到解析大根堆小根堆,分析堆排序,最后实现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.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(或负数),表示不需要交换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小(优先队列法)

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大

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

/**
百万数据处理,找到前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;
    }
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值