堆的介绍和优先级队列

1. 堆的基本介绍

1.1 定义

堆必须是一个完全二叉树。

根据堆序性,可以把堆分为两类,分别为大顶堆小顶堆。在大顶堆中,每个父节点元素要大于它的子节点元素。在小顶堆中,每个父节点元素都必须小于它的子节点元素。

Ex.大顶堆

小顶堆:

1.1.1 堆的存储:

首先我们按照层序遍历的顺序来给节点编号,从上到下,从左到右把这些元素对应到数组的下标,然后把树的元素存入到相应的下标里。

若节点的下标为i,那么左子节点的下标为2i+1,右子节点的下标为2i+2。

1.2 基本操作

运用以下两个操作基本能实现堆的所有功能。

1.2.1 下滤

我们把根节点向下调整的操作称为下滤。

例如图中这棵树,可以看到只有他的根元素不满足堆序性。因此我们通过操作可以把他调整为堆。

我们将这个节点与他的最大子节点进行比较(因为是大根堆,如果是小根堆则相反),如果小于则与最大子节点进行交换,直到大于它的最大子节点或者移动的底部为止。

下滤的时间复杂度为:

O(nlgn)

1.2.2 上滤

这个操作主要用于插入新元素到堆中

例如图中这棵树,只有树的最后一个元素破坏了堆序性。

我们将这个节点与他的父节点进行比较,如果大于则与父节点进行交换,直到小于它的父节点或者移动的顶部为止。

上滤的时间复杂度为:

O(nlgn)

1.2.3 建堆

如果有一个乱序的数组,要怎么操作才能把他转化为堆呢?

1.2.3.1 自顶而下

将新元素放到堆的最后一位,然后对其进行上滤操作。直到所有元素插入后,建堆完成。

1.2.3.2 自下而上

对每个父节点进行下滤,复杂度是O(n)

2. 堆的具体应用:优先级队列

优先级队列有两个操作,一个是插入队列,另一个为弹出最小元素。这种队列可以用小根堆来实现。因为小根堆的根节点是最小元素,所以直接弹出根节点即可完成操作。

弹出后要继续调整成堆,调整方法也很简单,将最后一个元素放到根节点的位置,然后进行下滤操作。

堆排序

将优先队列的所有元素依次弹出。

我们可以发现,用大根堆的堆排序结果是正序的,用小根堆是倒序的。

LeeCode 347: 前K个高频元素

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

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

提示:

  • 1 <= nums.length <= 105

  • k 的取值范围是 [1, 数组中不相同的元素的个数]

  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

1. 思路分析

这道题可以用大顶堆做也可以用小顶堆做,具体在Java中表现为对优先级队列的构造器里的比较器进行设置。但是确实这道题在做的时候还是有些问题的,一是对于优先级队列不了解,里面的元素可以是数组元素,并且要返回的是map里的key而不是value,也就是说不是次数而是数本身。话不多说,直接上代码进行分析。

2.代码

方法一:大顶堆实现

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        if(nums.length == 1) {
            return new int[]{nums[0]};
        }
        int[] res = new int[k];
        HashMap<Integer,Integer> map = new HashMap<>()
        
        for(int i : nums) {
            map.put(i, map.getOrDefault(i, 0) + 1);
        }
​
        PriorityQueue<int[]> que = new PriorityQueue<>(((pair1,pair2)->pair2[1]-pair1[1]));
        for(Map.Entry<Integer,Integer> entry : map.entrySet()) {
            que.add(new int[]{entry.getKey(),entry.getValue()});
        }
​
        for(int i = 0; i < k; i++) {
            res[i] = que.poll()[0];
        }
​
        return res;
    }
}

需要记忆的方法:

HashMap中的map.entrySet()是从来没有用过的方法。在这个地方需要重点注意一下啊。

方法二:用小顶堆实现

用小顶堆实现其实更符合这道题的要求,但是我一开始没有想到这个东西。

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        if(nums.length == 1) {
            return new int[]{nums[0]};
        }
        int[] res = new int[k];
        HashMap<Integer,Integer> map = new HashMap<>();
        
        for(int i : nums) {
            map.put(i, map.getOrDefault(i, 0) + 1);
        }
​
        PriorityQueue<int[]> que = new PriorityQueue<>(k,((pair1,pair2)->pair1[1]-pair2[1]));
        for(Map.Entry<Integer,Integer> entry : map.entrySet()) {
            if(que.size() < k) {
                que.add(new int[]{entry.getKey(), entry.getValue()});
            } else {
                if(entry.getValue() > que.peek()[1]) {
                    que.poll();
                    que.add(new int[]{entry.getKey(), entry.getValue()});
                }
            }
        }
​
        for(int i = k - 1; i >= 0; i--) {
            res[i] = que.poll()[0];
        }
        return res;
    }
}

事实上使用小顶堆后会发现算法的时间复杂度下降了很多。以此做个记录,到时候反复复习。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值