数据结构:优先队列和堆

在这里插入图片描述
操作系统进行任务调度:
(1)如果任务数量是固定的,不需要制作新的数据结构来处理,可以按照优先级进行排序然后执行,这个过程需要的是一个排序算法而不是优先队列
(2)实际中需要对源源不断的任务进行优先级排序并处理,不能在一开始就确定需要处理多少个任务,需要使用优先队列来解决
(3)在实现优先队列时不用管什么是优先级高的

对数据结构来说,如果有一项是O(n)复杂度的话,进行n个元素的操作,整个过程的时间复杂度是O(n2),相对耗时

在这里插入图片描述

设计一个抽象的数据结构,队列本身是一个抽象的数据结构,在这个基础上限制它的性质,创造出优先队列的概念,具体实现优先队列时可以使用不同的底层实现

堆:一棵完全二叉树的数组对象

(1)时间复杂度为O(logn),都与树结构有关;一个堆本身也是一棵树,最主流的方式是使用二叉树来表示堆,二叉堆,一颗满足特殊性质的二叉树

性质一 二叉堆是一棵完全二叉树,不一定是一棵满二叉树,不满的部分在二叉树的右下角,一层一层排放节点
在这里插入图片描述

性质二 堆中某个节点的值总是不大于或不小于其父节点的值,可定义出最大堆和最小堆,节点的大小和节点所处的层次没有必然的联系

由于最大堆是一棵完全二叉树,可以使用类似二分搜索树的方式来实现,完全二叉树相当于将节点按顺序一层一层排放,所以也可以使用数组的方式来表示一棵完全二叉树,优势是可以索引到每个节点的父亲节点

在这里插入图片描述
在这里插入图片描述
对于最大堆来说,跟二分搜索树一样,由于规定了每一个节点要大于等于它的孩子节点,所以这些节点之间必须具有可比较性

在这里插入图片描述

在最大堆中添加元素:过程满足两条性质(addLast siftUp)

(1)向堆中添加元素,对用户来说是添加元素,对堆来说,涉及到基础操作Sift Up(元素上浮)
(2)添加元素,对二叉树来说:在层序遍历完全二叉树的最后添加元素,对数组来说在数组的末尾添加元素
在这里插入图片描述
(3)此时的二叉树满足完全二叉树的性质,但不满足每个节点小于其父亲节点的性质,所以将节点依次与其父亲节点、父亲节点的父亲节点做比较 ,大于父亲节点时,交换两个节点
在这里插入图片描述

package heaptest;

public class Heap<E extends Comparable<E>>{
    private Array<E> array;

    public Heap(int capacity){
        array=new Array<>(capacity);
    }

    public Heap(){
        array=new Array<>();
    }

    //返回堆中的元素个数
    public int size(){
        return array.getSize();
    }

    //返回一个布尔值,表示堆中是否为空
    public boolean isEmpty(){
        return array.isEmpty();
    }

    //辅助函数,根据给定节点索引找到父亲节点、左右孩子节点的索引
    //返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
    private int parent(int Index){
        if(Index==0)
            throw new IllegalArgumentException("no parent");
        return (Index-1)/2;
    }

    private int left(int Index){
        return 2*Index+1;
    }

    private int right(int Index){
        return 2*Index+2;
    }




    public void add(E e){
        array.addLast(e);
        //传入要上浮的元素的所对应的索引
        siftUp(array.getSize()-1);

    }

    private void siftUp(int index) {
        //index不能抵达根节点,对当前节点的元素和父亲节点的元素进行比较
      while (index > 0&&array.get(parent(index)).compareTo(array.get(index))<0 ) {
            array.swap(index, parent(index));
            index = parent(index);
        }

    }

}

删除堆中元素:过程满足两条性质(swap removeLast siftDown)

(1)从最大堆取出元素只取出堆顶的最大元素,而不能取出其他元素;
(2)取出堆顶后剩余两棵子树融合困难,取完全二叉树的最后一个节点与堆顶元素交换位置
在这里插入图片描述
在这里插入图片描述
(3)删除数组的最后一个元素,从个数上减少了一个元素,且删除的是原来堆顶的元素,仍然满足完全二叉树的性质
在这里插入图片描述
(4)此时不满足每一个节点大于等于孩子节点对应的值,调整堆顶根节点的元素,需要进行数据的下沉:要下沉的元素和左右孩子比较,选择两个孩子中最大的元素,如果比下沉元素大的话,进行交换,此时的堆顶节点一定比左右孩子大,下沉元素继续向下比较
在这里插入图片描述
在这里插入图片描述

//看堆中的最大元素
public E findMax(){
    if(isEmpty())
        throw new IllegalArgumentException("empty");
    return array.getFirst();
}
//取出堆中最大元素
public E extractMax(){
    E ret=findMax();
    array.swap(0, array.getSize()-1);
    array.removeLast();
    siftDown(0);
    return ret;
}

private void siftDown(int index) {
    //注意条件判断方法,多学习
    //index不能是叶子节点(left(index)>=array.getSize()索引越界
    while(left(index)<array.getSize()){
       //找到左右孩子节点中较大的节点
        int j=left(index);
        //右节点不存在,左节点较大
        //右节点存在,左节点较大
        //右节点存在,右节点较大
        //避免左节点较大的两种情况较复杂,只列出右节点较大的情况,改变j
        if(j+1<array.getSize()&&array.get(j+1).compareTo(array.get(j))>0)
           j=right(index);
        //array[j]是左右孩子中的最大值
        if(array.get(index).compareTo(array.get(j))<0) {
            array.swap(index, j);
            index=j;
        }

    }
}


//测试
public class HeapMain {
    public static void main(String[] args) {
        int n=100000;
        Heap<Integer> heap=new Heap<>();
        Random random=new Random();
        for(int i=0;i<n;i++){
            //从0到Integer的最大值
            heap.add(random.nextInt(Integer.MAX_VALUE));
        }
        int[] arr=new int[n];
        for(int i=0;i<n;i++)
           arr[i]=heap.extractMax();

        for(int i=1;i<n;i++){
            if(arr[i-1]<arr[i])
                throw new IllegalArgumentException("error");}

        System.out.println("FINISHED");
    }
}

在这里插入图片描述
二叉树的高度级别,由于是一棵完全二叉树,不会退化成一个链表,在堆中这两个操作非常高效

replace:取出最大元素后,放入一个新的元素

在这里插入图片描述
heapify和replace都可以用之前的extractMax和add组合实现,也可以单独进行优化实现

//取出堆中的最大元素,并替换成元素e
public E replace(E e){
    E ret=findMax();
    array.set(0,e);
    siftDown(0);
    return ret;
}

Heapify:将任意数组整理成堆的形状,由于堆是一棵完全二叉树,可以用数组表示,现在给定一个数组,只要合理的交换数组中元素的位置,可以将数组整理成堆的形状

流程 首先将给定数组当作一棵完全二叉树,此时不满足最大堆的性质,但仍然可以将最大堆看成一棵完全二叉树,最后一个非叶子节点开始,不断向前进行siftDown操作

问题 最后一个非叶子节点的索引是多少:拿到最后一个叶子节点的索引,得到其父亲节点的索引就是最后一个非叶子节点的索引

优势 一开始就不对叶子节点进行操作,对完全二叉树来说近乎抛弃一般的元素,对剩余元素进行siftDown的操作,比从一个空的堆开始添加元素要快(对每一个元素都执行一遍logn级别的操作
在这里插入图片描述
在这里插入图片描述

public Array(E[] arr){
    data=(E[]) new Object[arr.length];
     for(int i=0;i<arr.length;i++)
         data[i]=arr[i];
     size=arr.length;
}

public Heap(E[] arr){
    array=new Array<>(arr);
    int lastIndex=parent(arr.length-1);
    for(int i=lastIndex;i>=0;i--){
        siftDown(i);
    }
}

基于堆实现优先队列:底层实现是最大堆或最小堆

由于优先队列需要排优先级,队列的元素必须具有可比较性


public class PriorityQueue<E extends Comparable<E>> implements Queue<E>{
     private MaxHeap<E> maxHeap;
    public PriorityQueue(){
        maxHeap=new MaxHeap<>();
    }

    @Override
    public void enqueue(E n) {
        maxHeap.add(n);
    }

    @Override
    public E dequeue() {
        maxHeap.extractMax();
    }

    @Override
    public E getFront() {
        return maxHeap.findMax();
    }

    @Override
    public int getSize() {
        return maxHeap.size();
    }

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

对于getFront不需要判断maxHeap为空的操作,因为在findMax中已经对堆中元素为空的情况进行了错误处理;对于enqueue操作不需要考虑堆为空的情况,因为在extractMax方法中调用了findMax,findMax对堆为空的异常进行错误处理

在N个元素中选出前M个元素

在这里插入图片描述

流程:使用优先队列,维护当前看到的前M个元素:

(1)对N个元素扫描一遍
(2)将前M个元素放进优先队列中
(3)之后每看到一个新的元素,如果新的元素比当前的优先队列中最小的元素还要大,丢弃优先队列中最小的元素,换上新的元素
(4)一直维护优先队列中的前M个元素,直到将N个元素全部扫描完,优先队列中最终留下的M个元素就是要求的前M个元素
(5)需要使用最小堆选出当前能看到的前M个元素中的最小元素,不停地将最小元素进行替换
注意:实际上解决这个问题不需要真的使用最小堆,依然使用最大堆,关键是如何定义优先队列地优先级,由于每次需要先取出优先队列中最小的元素,实质上完全可以定义元素的值越小,优先级越高,那么依然可以使用底层实现是最大堆的优先队列来实现功能

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class Solution5{
        //创建一个私有的内部类,既包含元素又包含元素对应的频次,可将map中的键值数据对都做成freq类的对象存进优先队列
        private class Freq implements Comparable<Freq>{
           public int e,freq;
            //在compareTo中定义什么是优先级高
            //由于需要在优先队列中取出频次最低的元素
            //定义频次越低,优先级越高
            public Freq(int e,int freq) {
                this.e = e;
                this.freq=freq;
            }
            @Override
            public int compareTo(Freq o) {
                if(this.freq<o.freq)
                   return 1;
                else if(this.freq>o.freq)
                    return -1;
                else
                    return 0;
            }
        }

        public List<Integer> topKFrequent(int[] nums, int k) {
            //先统计频次
            TreeMap<Integer,Integer> map=new TreeMap<>();
            for(int n:nums){
                if(!map.containsKey(n))
                    map.put(n,1);
                else
                    map.put(n,map.get(n)+1);
            }
           //需要根据元素的频次来决定优先级,与此同时最后返回的结果是频次对应的具体元素,我们需要关心两者
            //优先队列承载的元素类型应该是key为元素值,value为频次的键值对
            //放入优先队列的Freq对象必须是可比较的

            //利用优先队列求出前k个元素
           PriorityQueue<Freq> pq=new PriorityQueue<Freq>();
            for(int key:map.keySet())
                //没有存够k个元素
                if(pq.getSize()<k)
                pq.enqueue(new Freq(key,map.get(key)));
                //已经有k个元素
                else if(map.get(key)>pq.getFront().freq){
                    //检查新遍历的key是不是比已有的k个元素的最小元素大
                        pq.dequeue();
                        pq.enqueue(new Freq(key,map.get(key)));}

            ArrayList<Integer> res=new ArrayList(k);
           while(!pq.isEmpty())
               res.add(pq.dequeue().e);

            return res;

        }


    }

改进一:使用Java标准库的优先队列

Java标准库中的PriorityQueue使用的是最小堆,那么Freq的类内部,直接按照默认的freq的大小来进行比较

import java.util.*;
import java.util.PriorityQueue;

public class Solution6 {
    //创建一个私有的内部类,既包含元素又包含元素对应的频次,可将map中的键值数据对都做成freq类的对象存进优先队列
   private class Freq implements Comparable<Freq>{

      public int e,freq;
//        //在compareTo中定义什么是优先级高
//        //由于需要在优先队列中取出频次最低的元素
//        //定义频次越低,优先级越高
        public Freq(int e,int freq) {
           this.e = e;
           this.freq=freq;
       }
//        @Override
      public int compareTo(Freq o) {
          if(this.freq>o.freq)
               return 1;
            else if(this.freq<o.freq)
                return -1;
           else
                return 0;
        }
    }

ublic List<Integer> topKFrequent(int[] nums, int k) {
            //先统计频次
            TreeMap<Integer,Integer> map=new TreeMap<>();
            for(int n:nums){
                if(!map.containsKey(n))
                    map.put(n,1);
                else
                    map.put(n,map.get(n)+1);
            }
            //需要根据元素的频次来决定优先级,与此同时最后返回的结果是频次对应的具体元素,我们需要关心两者
            //优先队列承载的元素类型应该是key为元素值,value为频次的键值对
            //放入优先队列的Freq对象必须是可比较的

            //利用优先队列求出前k个元素
           PriorityQueue<Freq> pq=new PriorityQueue<Freq>();
          for(int key:map.keySet())
                //没有存够k个元素
              if(pq.size()<k)
                   pq.add(new Freq(key,map.get(key)));
                    //已经有k个元素
                   else if(map.get(key)>pq.peek().freq){
                    //检查新遍历的key是不是比已有的k个元素的最小元素大
                   pq.remove();
                   pq.add(new Freq(key,map.get(key)));}
                 ArrayList<Integer> res=new ArrayList(k);
        while(!pq.isEmpty())
                res.add(pq.remove().e);

            return res;

        }

    }

改进二: 由于设定了属于自己的结构,implements Comparable,设定相应的可比较的优先级;多数情况下需要改变java标准库中的类相应的比较方式,设定一个比较器;如果优先队列中传入的是标准库中的类,如字符串,当需要修改字符串的比较方式时,如按照字符串的长度来比较字符串的大小,不能修改java内置的字符串中相应的compareTo方法,那么就可以在外面设置一个属于自己的字符串比较器,然后传给优先队列

import java.util.*;
import java.util.PriorityQueue;

public class Solution6 {
    //创建一个私有的内部类,既包含元素又包含元素对应的频次,可将map中的键值数据对都做成freq类的对象存进优先队列

    private class Freq{
        public int e,freq;
    
//在compareTo中定义什么是优先级高
//由于需要在优先队列中取出频次最低的元素
//定义频次越低,优先级越高
public Freq(int e,int freq) {
    this.e = e;
    this.freq=freq;
}
 private class FreqComparator implements Comparator<Freq> {
    传入两个要比较的类型相应的对象
    @Override
    public int compare(Freq o1, Freq o2) {
        return o1.freq-o2.freq;
        逻辑和Freq类中的compareTo方法的逻辑是一样的,只不过返回的值不一定是1-1
    }
}
    public List<Integer> topKFrequent(int[] nums, int k) {
            //先统计频次
            TreeMap<Integer,Integer> map=new TreeMap<>();
            for(int n:nums){
                if(!map.containsKey(n))
                    map.put(n,1);
                else
                    map.put(n,map.get(n)+1);
            }
            //需要根据元素的频次来决定优先级,与此同时最后返回的结果是频次对应的具体元素,我们需要关心两者
            //优先队列承载的元素类型应该是key为元素值,value为频次的键值对
            //放入优先队列的Freq对象必须是可比较的

            //利用优先队列求出前k个元素
           PriorityQueue<Freq> pq=new PriorityQueue<Freq>(new FreqComparator());
            for(int key:map.keySet())
            if(pq.size()<k)
              pq.add(new Freq(key,map.get(key)));
    //已经有k个元素
           else if(map.get(key)>pq.peek().freq){
    //检查新遍历的key是不是比已有的k个元素的最小元素大
             pq.remove();
             pq.add(new Freq(key,map.get(key)));}
    
            ArrayList<Integer> res=new ArrayList(k);
             while(!pq.isEmpty())
                res.add(pq.remove().e);

            return res;

        }

    }

改进三:将只使用一次的类写成匿名内部类

public class Solution6 {
    //创建一个私有的内部类,既包含元素又包含元素对应的频次,可将map中的键值数据对都做成freq类的对象存进优先队列

    private class Freq{
        public int e,freq;
        //在compareTo中定义什么是优先级高
        //由于需要在优先队列中取出频次最低的元素
        //定义频次越低,优先级越高
        public Freq(int e,int freq) {
            this.e = e;
            this.freq=freq;
        }
        }
        
public List<Integer> topKFrequent(int[] nums, int k) {
            //先统计频次
            TreeMap<Integer,Integer> map=new TreeMap<>();
            for(int n:nums){
                if(!map.containsKey(n))
                    map.put(n,1);
                else
                    map.put(n,map.get(n)+1);
            }
            //需要根据元素的频次来决定优先级,与此同时最后返回的结果是频次对应的具体元素,我们需要关心两者
            //优先队列承载的元素类型应该是key为元素值,value为频次的键值对
            //放入优先队列的Freq对象必须是可比较的

            //利用优先队列求出前k个元素



        PriorityQueue<Freq> pq=new PriorityQueue<>(new Comparator<Freq>() {
     @Override
       public int compare(Freq o1, Freq o2) {
                return o1.freq-o2.freq;
            }
           });
            for(int key:map.keySet())
                //没有存够k个元素
                if(pq.size()<k)
                    pq.add(new Freq(key,map.get(key)));
                    //已经有k个元素
                else if(map.get(key)>pq.peek().freq){
                    //检查新遍历的key是不是比已有的k个元素的最小元素大
                    pq.remove();
                    pq.add(new Freq(key,map.get(key)));}
           ArrayList<Integer> res=new ArrayList(k);
          while(!pq.isEmpty())
                res.add(pq.remove().e);

            return res;

        }

    }

改进四:进一步,匿名内部类具有变量捕获的能力,在匿名内部类中能拿到函数作用域中的声明的所有变量,Priority中可以只存Integer元素,相当于只存nums列表中对应的元素,但比较的方式是按频率进行比较的(此时不需要Freq内部类)灵活的利用匿名内部类改变java内置类型,如Integer两个整型之间比较的逻辑

public class Solution6 {
  
  public List<Integer> topKFrequent(int[] nums, int k) {
            //先统计频次
            TreeMap<Integer,Integer> map=new TreeMap<>();
            for(int n:nums){
                if(!map.containsKey(n))
                    map.put(n,1);
                else
                    map.put(n,map.get(n)+1);
            }
            //需要根据元素的频次来决定优先级,与此同时最后返回的结果是频次对应的具体元素,我们需要关心两者
            //优先队列承载的元素类型应该是key为元素值,value为频次的键值对
            //放入优先队列的Freq对象必须是可比较的

            //利用优先队列求出前k个元素
         PriorityQueue<Integer> pq=new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            
            灵活的利用匿名内部类改变java内置类型,如Integer两个整型之间比较的逻辑
             public int compare(Integer a, Integer b) {
                 return map.get(a) - map.get(b);
             }
         });
            for(int key:map.keySet())
                //没有存够k个元素
                       if(pq.size()<k)
                    pq.add(key);
                    //已经有k个元素
                else if(map.get(key)>map.get(pq.peek())){
                    //检查新遍历的key是不是比已有的k个元素的最小元素大
                    pq.remove();
                    pq.add(key);}

            ArrayList<Integer> res=new ArrayList(k);
        while(!pq.isEmpty())
                res.add(pq.remove());

            return res;

        }

    }

其他的堆

在这里插入图片描述
**d叉堆层数更低,给d叉堆添加或删除一个元素,相应的时间复杂度都变成了logdN,比log2N时间复杂度好,相应的代价是在每一个节点下沉的时候需要考虑的节点数变多了,它们之间存在一个制衡的关系
**
在这里插入图片描述
在这里插入图片描述
广义队列:只要支持队列的操作,就可以叫做一个队列

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值