数据结构基础——优先级队列(堆)

本文介绍了优先级队列的概念及其在实际中的应用,如手术排期和任务调度。详细讲解了基于二叉堆的优先级队列,包括最大堆的实现,如何添加和移除元素,以及堆化过程。此外,讨论了优先级队列在解决TopK问题上的应用,并给出了使用堆排序的方法。
摘要由CSDN通过智能技术生成

一、什么是优先级队列(堆)

普通队列:按照元素的入队顺序出队,先入先出。

优先级队列:按照优先级的大小动态出队(动态指的是元素个数动态变化,而非固定)。

现实生活中的优先级队列:

  1. 医生根据病人病情的情况对手术排期

    病情相同的情况下按来的先后顺序,若病情较重优先安排手术

  2. 操作系统的任务调度

    优先级队列,系统的任务一般优先级都比普通应用高

优先级队列的数据是在动态变化的

时间复杂度对比:

入队出队
普通的链式队列O(1)O(n)
优先级队列O(log n)O(log n)

在计算机领域,若见到log n时间复杂度,近乎一定和“树”结构有关

二、基于二叉树的堆(二叉堆)

堆有很多种(d叉堆、索引堆…),二叉堆是应用最广泛的堆。

1.二叉堆的特点:

  1. 是一棵完全二叉树,基于数组存储(元素都是靠左排列,数组中存储不会浪费空间)

    只有完全二叉树适合使用数组这种结构来存储。其他二叉树都要用链式结构。

在这里插入图片描述

  1. 关于节点值:

    堆中根节点值 >= 子树节点中的值(最大堆、大根堆)

    堆中根节点值 <= 子树节点中的值(最小堆、小根堆)

    JDK中的PriorityQueue默认是给予最小堆的实现

    ①.完全二叉树

    ②.所有子树都满足

    ​ 根 >= 子树 最大堆

    ​ 根 <= 子树 最大堆

  2. 关于节点的编号:

因为堆是基于数组来存储的,节点之间的关系通过数组下标来表示,从0开始编号,数组下标也是从0开始

假设此时节点编号为i,且存在父节点

父节点编号 parent = (i - 1) / 2;

左子树编号 left = 2 * i + 1;

右子树编号 right = 2 * i + 2;

2. 最大堆的实现

向堆中添加元素

  1. 数组添加元素尾插,在数组末尾添加元素——>此时仍是一棵完全二叉树,节点紧密排列;

  2. 添加元素后,可能会破坏最大堆的定义,因此进行元素上浮操作siftUp,直到把当前元素上浮到合适位置。

    /**
     * 上浮操作,让新添加的元素去到它该去的位置
     * @param k
     */
    private void siftUp(int k) {
        while (k > 0 && elementData.get(k) > elementData.get(parent(k))) {
            swap(k , parent(k));
            k = parent(k);
        }
    }
    

在堆中取出最大值(最大堆)

  1. 当前最大堆的最大值就在树根节点,直接取出就可;

  2. 将堆中最后一个元素顶到堆顶(覆盖掉最大节点),然后进行元素下沉操作siftDown使其仍满足最大堆性质。

    /**
     * 从索引k开始进行下沉操作
     * @param k
     */
    private void siftDown(int k) {
        //还存在子树
        while (leftChid(k) < size){
            int j = leftChid(k);
            //此时还存在右子树
            if (j + 1 < size && elementData.get(j + 1) > elementData.get(j)) {
                //此时右子树的值大于左子树
                j ++;
            }
            //此时j一定对应了左右子树最大值索引
            if (elementData.get(k) >= elementData.get(j)) {
                //当前元素大于左右子树最大值,下沉结束,元素k落在了最终位置
                break;
            }else {
                swap(k,j);
                k = j;
            }
        }
    }
    

heapify——堆化

任意一个整形数组都可以看做一个完全二叉树,只需要进行元素调整操作。

方法:

  1. 将这n个元素依次调用add方法添加到一个新的最大堆中

    时间复杂度:O(n logn )

    空间复杂度:O(n)

  2. 原地堆化 ——> O(n)级别时间复杂度

    从最后一个非叶子节点开始进行元素siftDown操作。从当前二叉树中最后一个小子树开始调整,不断向前,直到调整到根节点

    public MaxHeap(int[] arr) {
        elementData = new ArrayList<>(arr.length);
        //1.先将所有元素复制到data数组中
        for (int i : arr) {
            elementData.add(i);
            size ++;
        }
        //2.从最后一个非叶子节点开始进行siftDown操作
        for (int i = parent(size - 1); i >= 0; i--) {
            siftDown(i);
        }
    }
    

三、基于堆的优先级队列

1.通过堆实现优先级队列

import seqlist.queue.Queue;
/**
 * 基于堆的最大优先级队列
 */
public class PriorityQueue implements Queue<Integer> {
    private MaxHeap heap;
    public PriorityQueue() {
        heap = new MaxHeap();
    }
    
    @Override
    public void offer(Integer val) {
        heap.add(val);
    }
    //所谓优先级就是最大值
    @Override
    public Integer poll() {
        return heap.extractMax();
    }

    @Override
    public Integer peek() {
        return heap.peekMax();
    }

    @Override
    public boolean isEmpty() {
        return heap.isEmpty();
    }
}

2.优先级队列的应用——TopK问题

所有TopK问题都可以用优先级队列解决,取大用小,取小用大。

若需要取出前k个最大元素,构造最小堆

若需要取出前k个最小元素,构造最大堆

设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可

示例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tePseYbu-1654996272537)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220609185249128.png)]

取出前4个最小元素——>构造一个大小为4的最大堆

  1. 若此时队列的元素个数<k,直接添加到队列中

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UcRo0PcM-1654996272538)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220609185605035.png)]

  2. 若此时元素个数 == k

    1. 若扫描到的元素val > 堆顶元素,一定大于此时堆中的所有元素,则val一定不是需要的结果!!直接跳过
    2. 若此时val<堆顶元素,将新元素val添加到队列中
    3. 重复以上过程,直到整个集合被我们扫描完毕,队列中恰好就保存了前k个最小值

这种方法时间复杂度为O(n logk),当k<<<n时,log k 和 log n 差距明显

需要注意的是,JDK默认是一个最小堆,需要改造成一个最大堆。

class IntegerReverse implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
}
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

/**
 * 最小的K个数
 */

public class Num17_14_SmallestK {
    public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k];
        if (arr.length == 0 || k == 0) {
            return ret;
        }
        //JDK默认是一个最小堆,需要改造成一个最大堆
        Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        //遍历原集合,队列只保存k个元素
        for (int i = 0; i < arr.length; i++) {
            if (queue.size() < k) {
                queue.offer(arr[i]);
            } else {
                int max = queue.peek();
                if (arr[i] < max) {
                    queue.poll();
                    queue.offer(arr[i]);
                }
            }
        }
        int i = 0;
        while (!queue.isEmpty()) {
            ret[i] = queue.poll();
            i ++;
        }
        return ret;
    }
}

前k个高频元素

给定一个整数数组nums和一个整数 k ,返回其中出现频率前 k 高的元素。可以按任意顺序返回答案。

import java.util.*;

/**
 * 前K个高频元素×
 */
public class Num347_TopKFrequent {
    public int[] topKFrequent(int[] nums, int k) {
        int[] ret = new int[k];
        // 1.先扫描原数组,将出现的元素以及其频次保存到Map集合中。
        Map<Integer,Integer> map = new HashMap<>();
        for (int i : nums) {
            if (map.containsKey(i)) {
                // 此时元素i已经出现过,只要将频次 ++ 即可
                int times = map.get(i);
                map.put(i, times + 1);
            }else {
                // 此时i第一次出现,就将该元素保存到map中
                map.put(i,1);
            }
        }
        // 2.扫描Map集合,将出现频次最高的前k个元素添加到优先级队列中
        Queue<Freq> queue = new PriorityQueue<>();
        for (Map.Entry<Integer,Integer> entry : map.entrySet()) {
            if (queue.size() < k) {
                queue.offer(new Freq(entry.getKey(),entry.getValue()));
            }else {
                // 判断堆顶元素和当前元素的出现频次
                // 只有当前元素的出现频次 > 堆顶元素,入队,打擂思想,不断将出现频次大的元素对换入到队列中
                Freq freq = queue.peek();
                if (entry.getValue() > freq.value) {
                    queue.poll();
                    queue.offer(new Freq(entry.getKey(),entry.getValue()));
                }
            }
        }
        // 3.此时队列中就保存了出现频次最大的前k个数对 - k个Freq对象
        // 遍历队列,将Freq对象中的key取出放入结果集中即可
        int i = 0;
        while (!queue.isEmpty()) {
            ret[i++] = queue.poll().key;
        }
        return ret;
    }
}
class Freq implements Comparable<Freq>{
    //数组中出现的元素
    int key;
    //出现了几次
    int value;
    public Freq(int key, int value) {
        this.key = key;
        this.value = value;
    }
    @Override
    public int compareTo(Freq o) {
        return this.value - o.value;
    }
}

四、原地堆排序

给定一个任意数组,在这个数组的基础上进行堆排序,不创建任何额外空间

  1. 任意数组其实就可以看做一个完全二叉树,就是将这个数组调整为最大堆。

    heapify——>从最后一个非叶子节点开始进行siftDown,将数组调整为最大堆

  2. 不断交换堆顶元素和最后一个元素的位置(最大值放在最终位置),将堆顶元素继续进行siftDown

    直到数组剩下最后一个未排序的元素为止

 /**
 * 堆排序
 * @param arr
 */
public static void heapSort(int[] arr) {
    //1.将数组调整为最大堆
    for (int i = (arr.length - 2) / 2; i >= 0; i--) {
        siftDown(arr , i , arr.length);
    }
    //2.不断交换堆顶元素到末尾,每交换一个元素就有一个元素落在了最终位置
    for (int i = arr.length - 1; i > 0; i--) {
        swap(arr,0,i);
        siftDown(arr,0,i);
    }
}

/**
 * 元素下沉操作
 * @param arr
 * @param i 下沉的索引
 * @param length 数组的长度
 */
private static void siftDown(int[] arr, int i, int length) {
    while (2 * i + 1 < length){
        int j = 2 * i + 1;
        if (j + 1 < length && arr[j + 1] > arr[j]) {
            j = j + 1;
        }
        if (arr[i] > arr[j]) {
            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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值