操作系统进行任务调度:
(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时间复杂度好,相应的代价是在每一个节点下沉的时候需要考虑的节点数变多了,它们之间存在一个制衡的关系
**
广义队列:只要支持队列的操作,就可以叫做一个队列