(点击跳转即可哦)
文章目录
优先级队列(堆)
优先级队列(堆) :按照优先级的大小动态出队(动态指的是元素个数动态变化,而非固定)
普通队列:FIFO 按照元素的入队顺序出队,先入先出
时间复杂度 入队 出队
普通的链式队列: O(1) O(n)
优先级队列(堆): O(logn) O(logn)
在计算机领域,若见到 logn
时间复杂度,近乎一定和树
结构相关(并非一定要构造一棵树结构,算法过程逻辑上一定是一颗树)
二叉堆的特点
二叉堆:基于二叉树的堆(二叉堆,也是应用最广泛的堆),还有d叉堆,索引堆
二叉堆的特点:
1 完全二叉树
是一颗完全二叉树,基于数组 存储(元素都是靠左排列,数组中存储时不会浪费空间,不存储空节点)。只有完全二叉树适合使用数组这样的结构来存储,其他的二叉树都要链式结构。
2 关于节点值
堆中根节点值 >= 子树节点中的值(最大堆,大根堆)
堆中根节点的值 <= 子树节点的值(最小堆,小根堆)
JDK中的
Queue
接口的PriorityQueue
是基于最小堆的实现
拿最大堆举例来说,节点的层次 和节点的大小没有任何关系,只能保证当前树中,树根是最大值(比左子树树根,右子树树根大)。
其他节点层次不确定。
3 基于数组存储
因为堆是基于数组来存储的,节点之间的关系通过数组下标来表示。从0开始编号,数组下标也是从此开始。
假设此时节点编号为 i,且存在父子节点,自己实现时要注意边界条件。
父节点编号:parent = (i -1) / 2
左子树编号:left = 2 * i + 1
右子树编号:right = 2 * i + 2
节点之间通过数组的索引下标来找到父子节点
基于动态数组ArrayList实现的最大堆
1 向堆中添加元素
堆是数组实现的,添加元素就在末尾直接添加
此时这个堆仍然满足完全二叉树的性质,但是此时这个完全二叉树就不再是一个最大堆了,因此需要进行元素的上浮操作,让新元素上浮到合适位置。
siftUp 上浮操作:
上浮操作的终止条件:
1 当前已经上浮到了树根 -> 这个元素一定是最大值
2 当前元素 <= 父节点对应的元素值,此时元素落在正确位置
private void siftUp(int i){//要上浮元素的索引
while(i > 0 && data.get(i) > data.get((i-1)/2){//没有走到根节点,并且子节点大于根节点,进行交换
swap(i,(i-1)/2);//交换节点的值
i = (i-1)/2;//继续判断交换后的节点值是否小于根节点
}
}
//使用动态数组提供的方法进行交换操作
private void swap(int i,int j){
int iVal = data.get(i);
int jVal = data.get(j);
data.set(i,jVal);//将索引 i 对应的值 修改为 jVal
data.set(j,iVal);//将索引 j 对应的值 修改为 iVal
}
2 在堆中取出最大值(最大堆)
1 最大堆的最大值一定处在树根节点,直接取出树即可。
2 将堆中最后一个元素顶到堆顶,然后进行元素的下沉操作。
siftDown
(下沉操作) -> 使其仍然满足最大堆的性质
- 需要融合左右两个子树,使得取出树根后这颗树仍然是最大堆。此时融合操作还是比较复杂的,因为左右子树大小关系不定,且节点的大小和层次没有比必然联系。
- 使用最低的成本 来融合左右子树,使其仍然满足最大堆的性质。 移除数组末尾元素
//取出最大值,也就是堆顶元素
public int poll(){
if(size == 0){
throw new NoSuchElementException("heap is empty!");
}
//最大值就是堆顶元素
int max = root.get(0);
//将数组的末尾元素顶到堆顶
root.set(0,root.get(size-1));
//删除最后一个元素
root.remove(size-1);
size--;
//从头开始进行元素的下沉操作
siftDown(0);
return max;
}
siftDown
操作
private void siftDown(int i){
while(2*i-1 < size){
//右子树的索引
int j = 2*i-1;
//左子树的索引存在,并且左子树的值大于右子树时。进入循环
if(j+1 < size && root.get(j) < root.get(j+1)){
j++;
}
//此时j 对应的索引就是左右子树最大值的索引
if(root.get(i) > root.get(j)){
//父节点大于左右子树的节点,停止下沉操作
break;
}else{
//交换索引的值,继续下沉
swap(i,j);
i = j;
}
}
}
3 heapify
- 堆化
现在给你一个任意的整型数组 -> 都可以看作是一个完全二叉树,距离最大堆就差元素调整操作。
第一种方法:
将这数组中的n个元素 依次调用 add方法添加到一个新的最大堆中,遍历原数组,创建一个新的最大堆,
时间复杂度:nlogn
空间复杂度:O(n)
第二种方法:
原地
heapify
从最后一个非叶子节点 开始进行元素的siftDown操作。
不断的将子树调整为最大堆,最终走到树根节点时,左右子树已经是最大堆了,只需要最后下沉根节点就能得到最终的最大堆
public void MaxHeap(int[] arr){
root = new ArrayList<>(arr.length);
//1 将所有元素复制到root 数组中
for(int i : arr){
root.add(i);
size++;
}
//2 从最后一个非叶子节点开始进行siftDown操作
for (int i = parent(size-1); i >= 0; i--) {
siftDown(i);
}
}
在Java中比较两个元素的大小关系:
在Java中比较两个元素相等 equals
比较两个自定义对象的大小关系,需要类覆写Comparable 接口,实现compareTo方法。
若一个类Student implements Comparable,则这个类具备可比较的能力。
int compareTo(Object o){
//比较当前对象和传入对象的大小关系
> 0 : 当前对象 大于 传入对象o
< 0 : 当前对象 小于 传入对象o
= 0 : 当前对象 等于 传入对象o
}
java.util.Comparator;
接口
一个类若实现了这个接口,表示这个类天生就是为别的类的大小关系服务的。
class StudentSec implements Comparactor<Student>{
@Override
public int compare(Student o1,Student o2){
return 0;
> 0 : 当前对象 大于 传入对象o
< 0 : 当前对象 小于 传入对象o
= 0 : 当前对象 等于 传入对象o
}
}
策略模式
当把Student 类的大小关系从Student类中 “解耦”,此时的比较策略非常灵活,需要哪种方式,只需要创新一个新的类的实现Comparator 接口即可,根据此时大小关系的需要传入比较器对象。
基于堆的优先级队列到底如何实现, TopK问题
TopK问题 都可以使用优先级队列解决,取大用小,取小用大
若需要取出前K个最大元素 构造最小堆
若需要取出前K个最小元素 构造最大堆
面试题17.14 最小K个数
取出前4个最小元素 -> 构造一个大小为 4 的最大堆
思路:
1 若此时队列的元素个数 < k,直接添加到队列中。
2 若此时元素个数 == k
a 新扫描到的元素val >= 堆顶元素,一定大于此时堆中的所有元素,则val一定不是我要的结果,直接跳过
b 若此时val < 堆顶元素,堆顶元素出队,将新的元素val 入队
c 重复上述过程,直到整个集合被我们扫描完毕,队列中恰好就保存了前K个值
随着堆顶元素的不断交换,会把堆顶元素不断变小,最终队列扫描结束,就存放了最小 的k个数
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<>((o1, o2) -> o2-o1);
for(int i = 0; i < arr.length; i++){
if(i < k){
queue.offer(arr[i]);
}else{
if(arr[i] < queue.peek()){
queue.poll();
queue.offer(arr[i]);
}
}
}
int i = 0;
while(!queue.isEmpty()){
ret[i++] = queue.poll();
}
return ret;
}
o1,o2 -> o2-o1
所谓的内部类,就是一个类嵌套到另一个类的内部操作。
匿名内部类:
Queue<Integer> queue = new PriorityQueue<>(new Comparactor<Integer>()){
public int compare(Integer o1,Integer o2){
return o2-o1;
}
}
Queue<Integer> queue = new PriorityQueue<>((o1,o2) -> o2-o1);
Leetcode-347 前K个高频元素
/**
* 前 K 个高频元素
*/
public class Leetcode_347 {
public int[] topKFrequent(int[] nums, int k) {
//要返回的数组
int[] ret = new int[k];
//1 扫描原数组,把元素的值以及出现的频次存储到Map集合中
Map<Integer,Integer> map = new HashMap<>();
for(int i : nums){
//map集合中存在这个 i
if(map.containsKey(i)){
//找到key->i 所对应的 频次,进行+1
int num = map.get(i) + 1;
map.put(i,num);
}else {//此时map集合中没有这个元素i,
//添加元素
map.put(i,1);
}
}
//2 扫描Map集合,将出现频次最高的前K个元素放入优先级队列
//使用最小堆,这样根节点保存的永远都是 堆中频次最小的元素
//堆中传入自定义的类Arr,类中存储 元素以及元素出现的频次
Queue<Arr> queue = new PriorityQueue<>();
//遍历map集合
for(Map.Entry<Integer,Integer> entry : map.entrySet()){
//堆内元素小于k时,直接入队
if(queue.size() < k){
queue.offer(new Arr(entry.getKey(), entry.getValue()));
}else {//此时堆内已经存储了 k 个元素
//map集合中的频次 和堆内的根节点的频次进行比较
//大于堆内根节点,就进行替换,否则继续进行比较
if(entry.getValue() > queue.peek().val){
queue.poll();
queue.offer(new Arr(entry.getKey(), entry.getValue()));
}
}
}
//遍历优先级队列,输出
int i = 0;
while (!queue.isEmpty()){
ret[i++] = queue.poll().key;
}
return ret;
}
}
class Arr implements Comparable<Arr>{
//存储的元素
int key;
//元素出现的频次
int val;
public Arr() {
}
public Arr(int key, int val) {
this.key = key;
this.val = val;
}
@Override
public int compareTo(Arr o) {
return this.val - o.val;
}
}
Leetcode-373 查找最小的K对数字
import java.util.*;
/**
* 查找和最小的 K 对数字
*/
public class Leetcode_373 {
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
Queue<Pair> queue = new PriorityQueue<>((o1, o2) -> (o2.n1+ o2.n2) - (o1.n1+ o1.n2));
for (int i = 0; i < Math.min(nums1.length,k); i++) {
for (int j = 0; j < Math.min(nums2.length,k); j++) {
if(queue.size() < k){
queue.offer(new Pair(nums1[i],nums2[j]));
}else {
int sum = nums1[i] + nums2[j];
Pair test = queue.peek();
if(sum < test.n1+ test.n2){
queue.poll();
queue.offer(new Pair(nums1[i], nums2[j]));
}
}
}
}
//此时队列中已经存储了k对和最小的数字
List<List<Integer>> list = new LinkedList<>();
while (!queue.isEmpty()){
List<Integer> list1 = new LinkedList<>();
Pair pair = queue.poll();
list1.add(pair.n1);
list1.add(pair.n2);
list.add(list1);
}
return list;
}
}
class Pair{
//第一个数组的值
int n1;
//第二个数组的值
int n2;
public Pair() {
}
public Pair(int n1, int n2) {
this.n1 = n1;
this.n2 = n2;
}
}
Leetcode-692 前K个高频单词
核心:频次不同时,比较大小时比较次数使用最小堆的比较方法,频次相同时,按照字典码排序,使用最大堆
import java.util.*;
//前K个高频单词
public class Leetcode_692_MaxWord {
public List<String> topKFrequent(String[] words, int k) {
Map<String,Integer> map = new HashMap<>();
for(String x : words){
if(map.containsKey(x)){
map.put(x,map.get(x)+1);
}else {
map.put(x,1);
}
}
//按照频次比较,就是使用最小堆,当频次相同时,保存字节码小的,就采用最大堆排序
Queue<word> queue = new PriorityQueue<>();
for(Map.Entry<String,Integer> entry : map.entrySet()){
if(queue.size() < k){
queue.offer(new word(entry.getKey(),entry.getValue()));
}else {
if(queue.peek().num < entry.getValue()){
queue.poll();
queue.offer(new word(entry.getKey(),entry.getValue()));
}
else if(queue.peek().num == entry.getValue()){
if(queue.peek().words.compareTo(entry.getKey())>0){
queue.poll();
queue.offer(new word(entry.getKey(),entry.getValue()));
}
}
}
}
List<word> list = new ArrayList<>();
while (!queue.isEmpty()){
list.add(queue.poll());
}
List<String> list1 = new ArrayList<>();
for(int i = list.size()-1; i >= 0; i--){
list1.add(list.get(i).words);
}
return list1;
}
}
class word implements Comparable<word>{
String words;
int num;
public word(String words, int num) {
this.words = words;
this.num = num;
}
//比较
@Override
public int compareTo(word o) {
int i = this.num - o.num;
if(i == 0){
return o.words.compareTo(this.words);
}
return i;
}
}
要是对大家有所帮助的话,请帮我点个赞吧。