优先队列
java中的优先队列使用堆实现的,所以优先队列我们通常是拿来当大顶堆小顶堆使用
大顶堆和小顶堆的作用就是当我们想要获取大量数据的前几个最大或者最小的数据时使用的,因为如果对所有数据排序然后取前几个,时间复杂度最小也是
O(nlogn)
,使用堆的话时间复杂度为O(nlongm)
,而m
就是我们想要进行获取的前几个数,一般为常量,比如想要获取学校n个同学里面前8名,使用大顶堆获取的时间复杂度就是O(nlog8)=O(n)
。
如图所示是一种自下向上的建堆过程:
在java中优先队列的创建方法:
//创建优先队列对象,默认建立小顶堆
PriorityQueue<Integer> heap = new PriorityQueue();
//将一个元素放入优先队列(堆)中
heap.offer(5);
//获取优先队列第一个元素(堆顶元素),但是不出队
int n = heap.peek();
//获取优先队列第一个元素(堆顶元素),出队
int n = heap.poll();
入队
接下来我们看一下入队的源码:
public boolean offer(E e) {
//如果入队的元素为空,则抛出异常
if (e == null)
throw new NullPointerException();
//modCount用来记录该队列修改的次数
modCount++;
//获取队列的大小,由于该队列是顺序存储也就是使用数组存储的二叉树,所以元素个数可以视作最后一个元素的下一个位置
//也就是要插入的位置
int i = size;
//判断队列是否需要扩容
if (i >= queue.length)
grow(i + 1);
//自下而上的调整堆的主方法
siftUp(i, e);
size = i + 1;
return true;
}
上面的如对方法中使用siftUp()
方法来进行调整堆:
//k为插入位置,x为插入元素
private void siftUp(int k, E x) {
// 如果传入了比较器,就使用传入的比较器进行比较来调整堆
if (comparator != null)
siftUpUsingComparator(k, x, queue, comparator);
else
// 如果没有传入比较器,就使用原始比较器进行比较来调整堆
siftUpComparable(k, x, queue);
}
看一下这两个方法:
private static <T> void siftUpComparable(int k, T x, Object[] es) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = es[parent];
//这就是原始比较器,如果返回的值大于等于0则结束不进行交换结点
if (key.compareTo((T) e) >= 0)
break;
//否则交换位置继续往上比较
es[k] = e;
k = parent;
}
es[k] = key;
}
private static <T> void siftUpUsingComparator(
int k, T x, Object[] es, Comparator<? super T> cmp) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = es[parent];
//使用自己的构造器也一样,当返回结果小于0时才进行交换
if (cmp.compare(x, (T) e) >= 0)
break;
es[k] = e;
k = parent;
}
es[k] = x;
}
出队
和如对相反,出队时要拿走对顶元素,所以采用自顶而下的调整方式,也就是比较删除结点的左右孩子来判断谁交换上去
public E poll() {
final Object[] es;
final E result;
//让result等于0号位值的元素,即堆顶
if ((result = (E) ((es = queue)[0])) != null) {
modCount++;
final int n;
//x = 尾部元素
final E x = (E) es[(n = --size)];
es[n] = null;
if (n > 0) {
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
//进行自顶向下的调整,仙子啊已经将0号位值逻辑上认为是x了
siftDownComparable(0, x, es, n);
else
siftDownUsingComparator(0, x, es, n, cmp);
}
}
return result;
}
private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
// assert n > 0;
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = es[child];
int right = child + 1;
if (right < n &&
((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
c = es[child = right];
if (key.compareTo((T) c) <= 0)
break;
es[k] = c;
k = child;
}
es[k] = key;
}
private static <T> void siftDownUsingComparator(
int k, T x, Object[] es, int n, Comparator<? super T> cmp) {
// assert n > 0;
int half = n >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = es[child];
int right = child + 1;
//先对左右根据比较器的逻辑进行判断谁与父节点比较
if (right < n && cmp.compare((T) c, (T) es[right]) > 0)
c = es[child = right];
if (cmp.compare(x, (T) c) <= 0)
break;
es[k] = c;
k = child;
}
es[k] = x;
}
总结
在出队和入队的时候的调整都是使用新的删除或者插入的元素和其他元素进行比较,也就是说比较器的两个传参,第一个肯定是将要插入或者删除的元素,如果在相等的情况下想按照入队先后次序进行比较的话,必然是第一个入队时间晚,因为他是新的。
比较的是非整型数该怎么办?
这就需要用到比较器
比较器
比较器是个很神奇的东西,它可以使得某些对象在排序的时候按照我们设计的比较规则进行排序,比如TreeMap
,TreeSet
等等,那现在来看看优先队列中的比较器。
在进行入队的时候需要进行堆的调整,堆调整需要进行比较来判断需不需要对结点进行调整
比较器的的简单使用:
//这种方式创建的优先队列和默认生成的优先队列是一样的,都是小顶堆
PriorityQueue<Integer> heap = new PriorityQueue<Integer>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
上面这种是简单的使用比较器生成的堆,记住下面的话:
小顶堆,
return
前减后(o1-o2);
大顶堆,return
后减前(o2-o1);
相等时特殊处理:
- 插入:
- 让
o1
上去,return
-1;- 不进行交换,
return
0或者1;- 删除:
- 不交换,
return
-1或者0;- 会和两个孩子中合适的相比,若两个孩子相同,
return 1
表示和右孩子比
在堆中,比较器的使用方式为:若返回值>=0,则表示不要交换,否则,需要交换
所以我们只需要认清传入的两个参数,o1
和o2
哪一个是谁我们就可以控制其是否要交换
只需要记住,o1
永远是新的,在插入时是child
,删除时是parent
在插入时,我们的调整是自底向上:
o1
为我们要插入的元素,也就是二叉树中两个结点中下面的一个,parent
o2
为原本就在二叉树中的结点,也就是两个结点中上面的一个,child
- 如果需要是小顶堆,我们需要小的调整上去,也就是说如果上面的元素
o1
大于o2
,那么我们就不需要交换- 而不需要交换我们需要返回值
>=0
- 如果
o1
小于o2
就需要交换,也就是需要返回值<0
- 那怎么能做到这种判断加返回值呢?我们可以使用
if-else
判断if(o1 > o2) return 1; //返回大于等于0的结果都可以 else if(o1 < o2) return -1; //返回小于0的结果都可以 //相同的情况下面再说
- 而这个
if-else
恰恰和o1-o2
的结果是一样的,所以一般我们为了方便直接写o1-o2
- 相同的,大顶堆相反
在删除时,我们的调整是自顶向下:
- 此时
o1
代表的是两个结点的上面一个,parent
o2
代表的是下面的一个,child
,这与擦汗如是相反- 那这种情况下交换规则的两者相减不就和插入时相反了吗,但是我们舰队的时候使用的是一个比较器啊
- 在源码中我们我们可以看到删除时,比较器的返回结果如果
<=0
则不进行交换,否则进行交换- 这个规则也和插入是相反的,所以反反得正
所以不看两元素相等返回值为0得情况:
- 若比较器中返回值为
o2-o1
:
- 插入时,表示
parent - child
,大于0
不交换,小于0
交换- 删除时,表示
child - parent
,大于0
交换,小于0
不交换- 所以总的来说就是
上大下小不交换,上小下大就交换
,正是大顶堆
- 若比较其中是
o1-o2
则情况相反:
在源码中我们可以发现放两个元素相等时无论是自顶向下还是自底向上都不进行交换
如果元素相同时我们想按照一定的规则让他们进行交换也是可以的:
以大顶堆为例,返回值我们应该写成return o2-o1;
,表示当parent>=child
的时候就不交换了
但是我们想在两者相等时再按照一定的规则进行排序,这个时候我们就要单独为相等时做一些操作:if(o1 != o2) return o2-o1; else{ //其他比较规则 //只需要记住返回规则即可 }
到这里我们应该清楚了在堆中比较器的用法
例题
如下一个公交车让座问题的算法,我们不去说他的具体算法,我们只看其优先队列的比较器怎么写
具体算法问题及代码见 算法:公交让座问题
这一题需要用到坐着的人的年龄组成的一个队列,其堆顶应该是最应该让座的人;还有一个站着的人的年龄组成的队列,其堆顶是最应该做下去的人,下面我们来设计座位上的人的优先队列:
PriorityQueue<String[]> seats = new PriorityQueue< String[]>(new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
// 不要在意这是什么,反正就是来比较年龄age1和age2
int age1 = Integer.parseInt(o1[1]);
int age2 = Integer.parseInt(o2[1]);
//当年龄相同时,我们按照上车的顺序
//那上车的顺序是什么呢?之前说过,如对的时候永远是那新的那个值和其他旧的值进行比较
//所以o1永远是新来的,所以如果相等,o1是最应该让座的人
//之前说过入队时想让o1上去,return -1;
if(age1 == age2){
return -1;
}
//不相等时就开始比较年龄
else{
//两者年龄都小于等于10,应该比较大的先让座
//类似于建立大顶堆,所以 return o2-o1;
if(age1<=10 && age2<=10){
return age2-age1;
}
//两者年龄都大于10,应该比较小的先让座
//类似于建立小顶堆,所以 return o1-o2;
else if(age1>10 && age2>10){
return age1 - age2;
}
//一个小于等于10,一个大于10,也是应该大的先让座
//类似于建立大顶堆,return o2-o1;
//应该和第一种合在一起,但是为了观看清晰,就不合了
else{
return age2-age1;
}
}
}
});
现在来分析一下站着的人最应该坐下的人该怎么做这个比较器:
//若年龄相同,则最先来的最应该坐下
//所以入队调整的时候遇到相同的不进行交换
if(age1 == age2){
return 1;
}
//若不相等
else{
//两者年龄都小于等于10,应该比较小的先坐
//类似于建立小顶堆,所以 return o1-o2;
if(age1<=10 && age2<=10){
return age1-age2;
}
//两者年龄都大于10,应该比较大的先坐
//类似于建立大顶堆,所以 return o2-o1;
else if(age1>10 && age2>10){
return age2 - age1;
}
//一个小于等于10,一个大于10,也是应该小的先坐
//类似于建立小顶堆,return o1-o2;
//应该和第一种合在一起,但是为了观看清晰,就不合了
else{
return age1-age2;
}
}
可以发现完全相反,所以我们可以将比较器抽取出来,两者使用的时候有一个取结果的负值即可
总结
优先队列中的比较器只需要记住一下几句话即可:
小顶堆,
return
前减后(o1-o2);
大顶堆,return
后减前(o2-o1);
相等时特殊处理:
- 插入:
- 让
o1
上去,return
-1;- 不进行交换,
return
0或者1;- 删除:
- 不交换,
return
-1或者0;- 和右比,``return 1;