• 【数据结构】堆和优先级队列


    目录

    一、堆

    1.1堆的特点

    1.2如何构造一个最大堆

    (1)最大堆的构造以及常用方法的实现

    (2)添加操作

     (3)删除操作

    (3)将任意数组调整为堆

     二、TopK问题

    2.1使用优先级队列

    (1)找最大元素

     (2)找最小元素


    优先级队列:入队和队列是一样的默认都是队尾入队,出队的时候按照优先级出队,优先级相同的元素按照FIFO出队,优先级高的元素首先出队。

    优先级队列的两大特点:

    (1)按照优先级出队。

    (2)优先级高的元素首先出队。

    一、堆

    堆是优先级队列背后的数据结构。

    我们此处讲得堆其实是二叉堆,基于完全二叉树的二叉堆,广义的堆还有多叉堆(多叉树)。

    还有索引堆,普通的对只知道堆顶元素的大小,索引堆可以取得堆中任意位置元素=》在图的最小生成树prim算法和最短路径Diijkstra算法的优化中使用索引堆。

    1.1堆的特点

    (1)堆首先是完全二叉树,一般使用动态数组(不用存储null结点,元素都靠左排序)存储具体元素。

    (2)元素元素的值:

    堆中任意一个节点的值<=父节点值(最大堆/大根堆)数据结点>=子树节点。

    堆中任意一个节点的值>=父节点值(最小堆/小根堆)数据结点<=子树节点,JDK中的优先级队列默认是最小堆的实现。

    在堆中,结点值的大小和节点所处的层次没有明确关系。

    关于完全二叉树的节点编号:根据编号以及索引关系,在数据中寻找某一个结点的子节点与父节点编号和索引位置,都从0开始编号,若某个节点的编号(索引)为x。

    性质1:判断父节点:

    如何判定父结点存在,若存在,索引是多少?parent(x) = (x-1)/2。

    当x>0,则一定存在父节点(在二叉树中,只有根节点(根节点索引为0)没有父节点),且父节点的编号为(x-1)/2。

    性质2:判断左子树结点

    如何判断当前节点x还存在左子树结点?左子树结点编号是多少》

    当前完全二叉树的结点个数<=数组最大长度,左子树结点编号为2x+1

    性质3:判断右子树结点:

    右子树结点编号为2x+2

    1.2如何构造一个最大堆

    (1)最大堆的构造以及常用方法的实现

    创建一个线性表的数组,然后创建一个int类型的size来判断存储了多少个数据。

    默认状态数组长度为10,也可以通过有参构造自定义数组的长度。

    创建三个判断父节点左右节点坐标的方法以及判空方法和输出方法。

    1. public class MaxHeap {
    2. private List arr;
    3. private int size;
    4. public MaxHeap() {
    5. this(10);
    6. }
    7. public MaxHeap(int capacity) {
    8. this.arr = new ArrayList<>(capacity);
    9. }
    10. private int parent(int k){
    11. return (k-1)>>1;
    12. }
    13. private int leftChild(int k){
    14. return (k<<1)+1;
    15. }
    16. private int rightChild(int k) {
    17. return (k << 1) + 2;
    18. }
    19. public boolean isEmpty() {
    20. return size == 0;
    21. }
    22. public String toString() {
    23. return arr.toString();
    24. }
    25. }

    (2)添加操作

    尾插方法很简单直接调用线性表的add方法即可,然后size++,在调用我们的siftUp方法。

    任意数组都可以看做一颗完全二叉树,因此向堆中插入一个数据,就相当于在数组中插入一个数据(默认尾插)。

    siftUp(上浮)不断比较插入后的元素和其父节点的值大小关系,若当前节点值>父节点值,交换他俩元素顺序,直到这个元素落在了正确位置。结束条件:要么此时k==0,已经走到了树根节点,要么此刻arr[k]<=arr[parent[k]],当前上浮操作结束。

    在创建一个swap方法,用来调换两个元素。

    1. public void add(int val){
    2. this.arr.add(val);
    3. size++;
    4. siftUp(size - 1);
    5. }
    6. private void siftUp(int k) {
    7. while(k>0&&arr.get(k)>arr.get(parent(k))){
    8. swap(k,parent(k));
    9. k = parent(k);
    10. }
    11. }
    12. private void swap(int i, int j) {
    13. int temp = arr.get(i);
    14. arr.set(i,arr.get(j));
    15. arr.set(j,temp);
    16. }
    1. public static void main(String[] args) {
    2. MaxHeap heap = new MaxHeap();
    3. heap.add(17);
    4. heap.add(11);
    5. heap.add(3);
    6. heap.add(2);
    7. heap.add(5);
    8. System.out.println(heap);
    9. heap.add(20);
    10. System.out.println(heap);
    11. }

    一开始创建好的堆如下。

    ​ 然后要向尾部加入一个元素20。

    然后调用siftUp方法,先将20与它的父结点值进行比较发现20更大那么就调用swap进行交换。

    ​然后继续比较父节点的值,发现还是20更大继续交换。

    ​ 到这时20已经没有父节点了,所以插入操作就结束了。

     (3)删除操作

    extractMax():移除当前堆的最大值并返回。

    最大堆的最大值在堆顶,删除树根之后,为了对其他子树影响最小,将数组的最后一个元素顶到当前二叉树的堆顶=》这个二叉树仍然是一颗完全二叉树。

    需要进行运算的下沉操作siftDown,不断比较当前元素和子节点的最大值的关系,不断交换当前元素和子树的最大值,直到落到正确的位置。也有两个终止条件:1、此时元素已经落到了叶子结点)没有子树了。2、arr[k] >= arr[child[k]],大于左右子树的最大值,落在最终位置。

    实际删除的也是数组的最后一个元素。

    peekMax():查看当前最大堆的最大值元素。

    1. public int extractMax(){
    2. if (isEmpty()) {
    3. throw new NoSuchElementException("heap is empty!cannot extract!");
    4. }
    5. int k = arr.get(0);
    6. arr.set(0,arr.get(size-1));
    7. arr.remove(size-1);
    8. size--;
    9. siftDown(0);
    10. return k;
    11. }
    12. private void siftDown(int k) {
    13. while (leftChild(k)
    14. int j = leftChild(k);
    15. if(j+11)>arr.get(j)){
    16. j = j+1;
    17. }
    18. if(arr.get(k)>=arr.get(j)){
    19. break;
    20. }else{
    21. swap(k,j);
    22. k = j;
    23. }
    24. }
    25. }
    1. public static void main(String[] args) {
    2. MaxHeap heap = new MaxHeap();
    3. heap.add(3);
    4. heap.add(2);
    5. heap.add(5);
    6. heap.add(17);
    7. heap.add(11);
    8. System.out.println(heap);
    9. heap.add(20);
    10. System.out.println(heap);
    11. System.out.println(heap.extractMax());
    12. System.out.println(heap);
    13. }

    一开始插入操作得到的堆如下: 

     然后删除根节点,将最后一个元素补上。

     然后比较根节点3的左右子树获取最大值与根节点进行比较,右子树更大并且比根节点还打所以两个位置交换。

     这时3已经是叶子结点不能再继续比较,所以删除操作结束。

    (3)将任意数组调整为堆

    因为任意数组都可以看做是一棵完全二叉树,将完全二叉树进行适当的调整就可以得到一个堆。

    从最后一个非叶子节点 开始进行元素的siftDown操作,直到根节点。(最后一个叶子节点的父节点就是最后一个非叶子节点)。

    空间复杂度为O(n)。

    1. public MaxHeap(int[] num){
    2. this.arr = new ArrayList<>();
    3. for(int i:num){
    4. arr.add(i);
    5. size++;
    6. }
    7. for (int i = parent(size-1);i>=0;i--){
    8. siftDown(i);
    9. }
    10. }
    1. public static void main(String[] args) {
    2. int[] num = {3,2,17,11,20,5};
    3. System.out.println(new MaxHeap(num));
    4. }

     二、TopK问题

    在一组非常大的数组中,找出前k个最大/最小的元素。

    最能想到的方法就是先降序,然后依次取出前50个元素即可。时间复杂度是O(nlogn)。

    2.1使用优先级队列

    8字口诀:取大用小,取小用大。

    这类问题的难点在于如何自定义“大小关系”,巧妙自定义类来定义大小关系,所有的TopK问题都可以使用相同的思路解决。

    (1)找最大元素

    如果是在100w个数据中心,找出前50个最大的元素。

    就建立一个大小为50的最小堆,不断使用最小堆扫描整个数组,扫描完毕之后,最小堆中恰好能保存前50个最大元素。

    1、当堆中元素

    2、当堆中元素个数>=k时,不断比较当前扫描的元素和堆顶元素的情况。

    若当前扫描的元素<=堆顶元素,则这个元素小于当前堆中所有元素,直接跳过,一定不是要找的值。若扫描元素>堆顶元素,将堆顶元素移除,将当前元素添加到堆中。

    扫描过程中不断地把最小堆的堆顶元素替换为越来越大的值。

    3、当整个集合扫描完毕,堆中一定保存了最大的k各元素。

    首先用前k各元素创建一个最小堆。

     然后走到元素9,比对顶元素大,则出队,然后入队。

      然后走到元素7,比对顶元素大,则出队,然后入队。

       然后走到元素6,比对顶元素大,则出队,然后入队。

     再走到元素2,比堆顶元素小则直接不入队,下一个元素5同理。然后走到元素8,比对顶元素大,则出队,然后入队。

     然后最后一个元素10,比对顶元素大,则出队,然后入队。到此优先级队列中的三个元素就是这10个元素里的前3个最大元素。

    1. public static void main(String[] args) {
    2. int[] arr = {1,4,3,9,7,6,2,5,8,10};
    3. int k = 3;
    4. Queue queue = new PriorityQueue<>();
    5. for(int i : arr){
    6. if(queue.size()
    7. queue.offer(i);
    8. }else{
    9. int top = queue.peek();
    10. if(i>top){
    11. queue.poll();
    12. queue.offer(i);
    13. }
    14. }
    15. }
    16. System.out.println(queue);
    17. }

     第二种解法:直接让所有的元素逐渐入队,如果queue长度超过k,就直接poll,就是移除对顶元素,也是扔出最小的元素,仍然留下k个元素。

    1. public static void main(String[] args) {
    2. int[] arr = {1,4,3,9,7,6,2,5,8,10};
    3. int k = 3;
    4. Queue queue = new PriorityQueue<>();
    5. for(int i : arr){
    6. queue.offer(i);
    7. if(queue.size()>k){
    8. queue.poll();
    9. }
    10. }
    11. System.out.println(queue);
    12. }

     (2)找最小元素

    就需要用到之前讲过的比较器,将JDK中默认的最大堆改为最小堆。

    其他的都和最小堆的使用一样。

    1. class IntDesc implements Comparator{
    2. @Override
    3. public int compare(Integer o1, Integer o2) {
    4. return o2-o1;
    5. }
    6. }
    7. public class TopKTest {
    8. public static void main(String[] args) {
    9. int[] arr = {1,4,3,9,7,6,2,5,8,10};
    10. int k = 3;
    11. Queue queue = new PriorityQueue<>(new IntDesc());
    12. for(int i : arr){
    13. queue.offer(i);
    14. if(queue.size()>k){
    15. queue.poll();
    16. }
    17. }
    18. System.out.println(queue);
    19. }
    20. }

     更快的方法就是使用快速排序,可以在O(n)的时间复杂度找到k个元素。

  • 相关阅读:
    排序的学习(一)
    【java深入学习第2章】Spring Boot 结合 Screw:高效生成数据库设计文档之道
    Python如何使用Pyecharts+TextRank生成词云图?
    【MySQL入门】第一话 · 初入“数据库”大陆
    YOLO9000: Better, Faster, Stronger (Yolov2)论文详细解读
    TeamTalk中对一条连接收发消息的封装。
    2.2.3.1vim + ctags + cscope + taglist
    奇舞周刊第507期:通过 View Transition API 在状态之间添加丰富的过渡动画
    数学逻辑下的动态区间计算公式代码
    list.toArray
  • 原文地址:https://blog.csdn.net/m0_50987960/article/details/128124475