堆、优先级队列以及TopK问题

堆的概念

如果有一个 关键码的集合 K = {k0 k1 k2 kn-1} ,把它的所有元素 按完全二叉树的顺序存储方式存储 在一 个一维数组中 ,并满足: Ki <= K2i+1 Ki<= K2i+2 (Ki >= K2i+1 Ki >= K2i+2) i = 0 1 2… ,则 称为 小堆 ( 或大 堆) 。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质

1、堆中某个节点的值总是不大于或不小于其父节点的值;

2、堆总是一棵完全二叉树。

最大堆的模拟实现

最大堆中的主要方法实现

1、最大堆的add方法

将元素加入到最后位置,然后通过上浮操作,根据堆的性质,将元素移动到合适位置

public void add(int val) {
        // 1.先尾插新元素到数组的尾部
        this.data.add(val);
        size ++;
        siftUp(size - 1);
    }

2、上浮操作

将元素通过上浮操作移动到合适的位置。在最大堆中,如果当前元素的值大于其父节点的值,则对其进行上浮操作,最小堆中正好相反。

private void siftUp(int k) {
        while (k > 0 && data.get(k) > data.get(parent(k))) {
            // 只有当还有父节点且当前节点值 > 父节点时才交换
            swap(k,parent(k));
            // 继续向上判断
            k = parent(k);
        }
    }

3、取出最大值

在堆中只有堆顶的元素是确定的最值,所以我们能将堆顶的元素取出,这样也衍生出了一种排序方法——堆排,它的时间复杂度为O(nlogn)。当取出堆顶元素后,为了使对堆的影响最小,会叫最后一个叶子结点换到堆顶,在对它进行下沉操作,这样操作之后还是一个最大堆。

public int extractMax() {
        if (data.isEmpty()) {
            throw new NoSuchElementException("heap is empty!cannot extract!");
        }
        int val = data.get(0);
        // 1.将当前堆的最后一个元素顶到根节点
        data.set(0,data.get(size - 1));
        data.remove(size - 1);
        size --;
        // 2.从根节点开始向下调整
        siftDown(0);
        return val;
    }

4.下沉操作

当堆的相对关系被打破后,我们需要对堆进行重建,下浮操作是为了将元素移动到合适的位置。在最大堆中当元素移动到叶子结点或者元素值大于左右孩子的值时,停止下沉操作。

 private void siftDown(int k) {
        // 首先保证有子树
        while (leftChild(k) < size) {
            // 判断是否存在右子树且右子树的值比左子树大
            int j = leftChild(k);
            if (j + 1 < size && data.get(j + 1) > data.get(j)) {
                j = j + 1;
            }
            // 此时j索引一定保存了左右子树的最大值~
            if (data.get(k) >= data.get(j)) {
                // 已经下沉到合适位置
                break;
            }else {
                swap(k,j);
                k = j;
            }
        }
    }

5、查看堆顶元素

public int peekMax() {
        if (data.isEmpty()) {
            throw new NoSuchElementException("heap is empty!cannot peek!");
        }
        return data.get(0);
    }

堆的实现

public class MaxHeap {
    // 具体保存元素的数组
    private List<Integer> data;
    // 堆中有效元素个数
    private int size;
    // heapify =》将任意的整型数组调整为最大堆
    public MaxHeap(int[] arr) {
        this.data = new ArrayList<>(arr.length);
        // 先依次将arr中的每个元素放入堆中
        for (int i : arr) {
            data.add(i);
            size ++;
        }
        // 从当前完全二叉树的最后一个非叶子结点开始向下调整,使得每个子树为堆
        for (int i = parent(size - 1);i >= 0;i --) {
            siftDown(i);
        }

    }


    // 在当前最大堆中取出最大值
    public int extractMax() {
        if (data.isEmpty()) {
            throw new NoSuchElementException("heap is empty!cannot extract!");
        }
        int val = data.get(0);
        // 1.将当前堆的最后一个元素顶到根节点
        data.set(0,data.get(size - 1));
        data.remove(size - 1);
        size --;
        // 2.从根节点开始向下调整
        siftDown(0);
        return val;
    }
    // 查看当前最大堆的最大值
    public int peekMax() {
        if (data.isEmpty()) {
            throw new NoSuchElementException("heap is empty!cannot peek!");
        }
        return data.get(0);
    }


    // 从当前索引为k的位置开始向下调整
    private void siftDown(int k) {
        // 首先保证有子树
        while (leftChild(k) < size) {
            // 判断是否存在右子树且右子树的值比左子树大
            int j = leftChild(k);
            if (j + 1 < size && data.get(j + 1) > data.get(j)) {
                j = j + 1;
            }
            // 此时j索引一定保存了左右子树的最大值~
            if (data.get(k) >= data.get(j)) {
                // 已经下沉到合适位置
                break;
            }else {
                swap(k,j);
                k = j;
            }
        }
    }

    // 向最大堆中添加元素
    public void add(int val) {
        // 1.先尾插新元素到数组的尾部
        this.data.add(val);
        size ++;
        siftUp(size - 1);
    }

    // 元素上浮操作
    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);
    }

    public MaxHeap() {
        this(10);
    }

    public MaxHeap(int capacity) {
            this.data = new ArrayList<>(capacity);
    }
    // 获取父节点
    private int parent(int k) {
        return (k - 1) >> 1;
    }
    // 获取左子树的结点编号
    private int leftChild(int k) {
        return (k << 1) + 1;
    }
    // 获取右子树的结点编号
    private int rightChild(int k) {
        return (k << 1) + 2;
    }


    @Override
    public String toString() {
        return this.data.toString();
    }
}

优先队列

优先级队列的概念

优先级队列(priority queue) 是0个或多个元素的集合,每个元素都有一个优先权,对优先级队列执行的操作有查找、插入、删除一个新元素 一般情况下,查找操作用来搜索优先权最大的元素,删除操作用来删除该元素 。对于优先权相同的元素,可按先进先出次序处理或按任意优先权进行。

优先级队列的实现

主要方法实现

1、建堆

从最后一个非叶子结点开始,对元素位置进行调整,一直持续到到根结点做最后一次调整,讲一个大的问题分解为小问题。

public void createHeap(int[] array) {
        for(int i=0;i<array.length&&i<elem.length;i++){
            elem[i]=array[i];
            usedSize++;
        }
        for(int k=(usedSize-1)/2;k>=0;k--){
            siftDown(k);
        }
    }

2、下沉操作与上浮操作

具体原理与堆中上浮下沉操作相似

 //下沉
    public void siftDown(int k) {
        int j=2*k+1;
        while(j<usedSize){
            if(j+1<usedSize&&elem[j+1]>elem[j]){
                j=j+1;
            }
            if(elem[k]>=elem[j]){
                break;
            }else{
                swap(j,k);
                k=j;
                j=2*k+1;
            }
        }
    }

 //上浮
    public void siftUp(int k){
        while(k>0&&elem[(k-1)/2]<=elem[k]){
            swap((k-1)/2,k);
            k=(k-1)/2;
        }
    }

3、入队与出队

原理类似于队列,但是优先级队列元素入队后需要调整元素位置,出队也是元素出队后,将最后一个元素移动到队首,再进行调整位置。虽然优先级队列带着队列,但它的出队顺序并不是按照先入先出的顺序,而是按照优先级来出队。

public int pollHead(){
        if(isEmpty()){
            throw new NoSuchElementException("MyPriorityQueue is Empyt!cannot poolHead");
        }
        int oilval=elem[0];
        elem[0]=elem[usedSize-1];
        usedSize--;
        return oilval;
    }

 public void offer(int val){
        if(isFull()){
            int n=elem.length;
            elem=Arrays.copyOf(elem,2*n);
        }
        elem[usedSize++]=val;
        siftUp(usedSize-1);
    }

优先级队列的实现

public class MyPriorityQueue {
    public int[] elem;
    public int usedSize;
    public MyPriorityQueue(){
        this(5);
    }
    public MyPriorityQueue(int capacity){
        this.elem=new int[capacity];
    }
    //建堆
    public void createHeap(int[] array) {
        for(int i=0;i<array.length&&i<elem.length;i++){
            elem[i]=array[i];
            usedSize++;
        }
        for(int k=(usedSize-1)/2;k>=0;k--){
            siftDown(k);
        }
    }

    //下沉
    public void siftDown(int k) {
        int j=2*k+1;
        while(j<usedSize){
            if(j+1<usedSize&&elem[j+1]>elem[j]){
                j=j+1;
            }
            if(elem[k]>=elem[j]){
                break;
            }else{
                swap(j,k);
                k=j;
                j=2*k+1;
            }
        }
    }
    public void swap(int a,int b){
        int tmp=elem[a];
        elem[a]=elem[b];
        elem[b]=tmp;
    }


    public void offer(int val){
        if(isFull()){
            int n=elem.length;
            elem=Arrays.copyOf(elem,2*n);
        }
        elem[usedSize++]=val;
        siftUp(usedSize-1);
    }


    //上浮
    public void siftUp(int k){
        while(k>0&&elem[(k-1)/2]<=elem[k]){
            swap((k-1)/2,k);
            k=(k-1)/2;
        }
    }
    public boolean isFull(){
        return usedSize==elem.length;
    }

    public int pollHead(){
        if(isEmpty()){
            throw new NoSuchElementException("MyPriorityQueue is Empyt!cannot poolHead");
        }
        int oilval=elem[0];
        elem[0]=elem[usedSize-1];
        usedSize--;
        return oilval;
    }

    public int peekHead(){
        return elem[0];
    }

    public boolean isEmpty(){
        return usedSize==0;
    }

    public String toString(){
        StringBuilder sb=new StringBuilder();
        sb.append("[");
        for(int i=0;i<usedSize;i++){
            sb.append(elem[i]);
            if(i!=usedSize-1){
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }



}

TopK问题

在日常生生活中我们会遇到排行榜的问题,那么这些问题是如何解决呢?

在这之前我们介绍了优先级队列,而我们的优先级队列,正好能解决这个问题。topK问题一般是去第K个元素或者前K个元素,并且元素不是排序过的。

我们的一般想法是将元素排序,再找出排序后位置的元素,内部排序的时间复杂度最快是O(nlogn),那么还有没有更快的方法呢?我们以LeetCode中的面试题17.14为例

 它需要最小的K个数,那么我们就需要建一个大小为K的优先级队列,这个优先级队列是用最大堆实现,当队列内元素小于K时直接入队,而当元素等于K时,就需要判断队首(也就是堆顶)元素与当前元素的关系,若大于队首元素,则直接跳过,小于才入队,并将队首元素出队,重复上述操作,直到元素遍历完,此时内有K个值,最大值在堆顶。

此时时间复杂度变为了O(nlogk)

最小K个数代码实现

public int[] smallestK(int[] arr, int k) {
        PriorityQueue<Integer> queue= new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;
            }
        });
        for (int i:arr) {
            queue.offer(i);
            if(queue.size()>k){
                queue.poll();
            }
        }
        int[] res=new int[k];
        for(int i=0;i<k;i++){
            res[i]=queue.poll();
        }
        return res;
    }

比较器

jdk中提供的优先级队列是最小堆实现的,而在日常的一些问题中,我们需要将最小堆变为最大堆,这就需要用到比较器。

在java中一旦实现了Comparable接口那么就有了客比较的能力,在jdk中如整形的默认比较为,大于返回大于0的数,等于返回0,小于返回小于0的数。我们想要将最小堆改为最大堆就需要实现Comparable接口并将其返回值修改为大于返回负数,小于返回正数。

如上述使用到jdk的优先队列,需要用到最大堆实现优先队列,那么我使用匿名内部类修改了比较的返回值。Comparable与Comparator接口都是实现元素比较的,只不过Comparable接口将比较代码嵌入自身类中而Comparator既可以嵌入到自身类中,也可以在一个独立的类中实现比较。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值