PriorityQueue使用及源码探究

使用PriorityQueue,能够对大顶堆,小顶堆进行模拟,是java中很好用的数据结构,而现在我仅仅是会用的水平,打算看一下源码,深入了解一下底层实现(顺便复习一下堆排序doge)
少部分参考了博客https://blog.youkuaiyun.com/u010623927/article/details/87179364

1.PriorityQueue使用介绍,小顶堆或者是大顶堆

Java中PriorityQueue实现了Queue接口,不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。

注意:PriorityQueue中是允许有重复元素的,并不是像hashset/map一样

PriorityQueue默认是小顶堆,要使用大顶堆的话,要在构造时重写Comparator方法
PriorityQueue的peek() remove() poll() 都是对堆顶进行的操作
PriorityQueue的peek()和element操作是常数时间,add(), offer(), 无参数的remove()以及poll()方法的时间复杂度都是log(N)。

2.堆是由什么结构构成的?如何实现模拟堆?

实际上,这个堆是由数组构成的,在源码PriorityQueue中找到了下面的类中成员变量。

transient Object[] queue;

为什么数组可以模拟堆?
1.堆是一个完全二叉树,不需要去再考虑树的结构
2.因为是完全二叉树,所以可以直接通过下标去找父节点或者左右孩子节点 即:

leftNo = parentNo2+1
rightNo = parentNo
2+2
parentNo = (nodeNo-1)/2

在这里插入图片描述

3.为什么时间复杂度是log(n)?

照理来说,堆排序复杂度不是应该是nlog(n)吗?
什么是nlogn复杂度?是从0开始建立堆,是nlogn时间复杂度。对于在建立好的堆上进行增删,只需要logn复杂度,即去除堆顶后重新生成堆,或者新加入节点入堆,都是logn复杂度的

这个也是基于堆的结构,只要父节点大于/小于孩子节点即可,因此对于单个节点进出堆后的调整方法,也是比较简单的。

4.PriorityQueue常用函数介绍

就查查里面是public的函数呗
1.add(E e)/offer(E e) 这两个函数都是向堆中增加元素
2.peek() 获取堆顶元素(不出堆)
3.boolean remove(Object o) 尝试移除该对象,并返回一个布尔值,成功true,没找到false
4.contains(Object o) 是否包含该对象
5.clear() 清空队列
6.E poll() 取堆顶元素并返回
7.E removeAt(int i) 将索引为i的元素移除并返回。
8.E element() 这个函数是在父类AbstractQueue中被定义的,不过比较常用,拿出来写一下,也是和peek一样的操作,只不过不同的是,在没有元素时,peek返回null,element抛异常

所以对于这个的操作,也是可以看成简单的操作队列,只不过是里面的结构有点不同而已,函数基本是一致的

5.comparator内部是什么样的,重写的是哪个函数?

首先comparator是一个函数式接口@FunctionalInterface,支持lambda表达式,所以看到这种写法,也是在重写comparator

Queue<Integer> B = new PriorityQueue<>((x, y) -> (y - x));

教大家如何判断是小顶堆还是大顶堆(构造时重写comparator的就是大顶堆hhhh)

comparator常用的函数就是int compare(T o1, T o2),对这两个对象根据对象特征进行函数编写,根据调整正负关系,决定这个是大比较器还是小比较器。

6.ConcurrentModificationException,并发常见异常之一

fail-fast机制
在使用Iterator进行遍历的时候,调用next函数的时候,如果PriorityQueue有修改,会报这个错误,这个错误具体原因,是因为expectedModCount != modCount ,ArrayList和Hashmap也有这个机制,感兴趣可以查一下,我曾经写过,就不写了

在源码中搜索modCount++ 就可以准确的找到触发ConcurrentModificationException的函数啦
查找了一下,会触发的函数有offer() add()(add函数体中没有,但是add的操作就是直接调用offer,所以也是会触发的) clear() poll() removeAt(int i)

如果想要在遍历的同时也移除元素,可以使用Iterator的remove方法(应该不推荐吧)

7.add()和offer()源码分析

这两个函数都是向堆中添加元素,首先看add

public boolean add(E e) {
        return offer(e);
}

可以看到add就是调用了一下offer,那么接下来看offer

public boolean offer(E e) {
	if (e == null)
	    throw new NullPointerException();
	modCount++;//修改modCount,fail-fast机制
	int i = size;
	if (i >= queue.length)
	    grow(i + 1);//这一步是扩充堆容量
	size = i + 1;
	if (i == 0)
	    queue[0] = e;
	else
	    siftUp(i, e);//这个函数是负责调整堆到正确的构造上的
	return true;
}

那么,分析源码可以看到,**最关键的函数是siftUp,它是负责调整堆到正确的构造上的,**接下来看看这个函数。

private void siftUp(int k, E x) {
	if (comparator != null)
	    siftUpUsingComparator(k, x);
	else
	    siftUpComparable(k, x);
}

从这里,就可以看出重写comparator有什么用了,不同的comparator,它的构造堆的方法都是不一样的,所以会造成是大顶堆,还是小顶堆的区别,在这里,我去参考一下siftUpUsingComparator这个函数。

private void siftUpUsingComparator(int k, E x) {
	while (k > 0) {//这里面的k最开始是数组的size,也就是堆的最下方
	    int parent = (k - 1) >>> 1;//找到父节点所在位置
	    Object e = queue[parent];
	    if (comparator.compare(x, (E) e) >= 0)//如果根据比较器的比较结果大于0,结束
	        break;
	    queue[k] = e;//否则父节点的值下移,
	    k = parent;
	}
	queue[k] = x;
}

这个函数里面的k最开始是数组的size,也就是堆的最下方,然后不断地通过与父节点比较,修改k的值,找到这个新插入的值x应该在的位置。最后完成赋值,以及调整堆的工作。这个函数从下到上调整堆,当然k的值是可能改变的,不一定总是数组的size(见removeAt)
所以这个函数具体就是,将元素x放到指定的索引k之后,然后从下到上调整堆

8.element()和peek()

element()和peek()区别:在没有元素时,peek返回null,element抛异常

element函数是在父类AbstractQueue中被定义的,实际上就是调用了peek,以及检查是否为null,代码如下:

public E element() {
	E x = peek();
	 if (x != null)
	     return x;
	 else
	     throw new NoSuchElementException();
}

再看peek():

public E peek() {
	return (size == 0) ? null : (E) queue[0];
}

更加的简洁,就是获取数组第0位的元素

9.poll()

poll中,关键的函数siftDown
每一次需要移除堆中元素的时候,如poll()和removeAt,为了保证堆为完全二叉树结构,先将最后一个元素保存下来,然后删除最后一个元素,然后将最后一个元素放到被删除元素的位置,然后调整堆

public E poll() {
	if (size == 0)//
	    return null;
	int s = --size;
	modCount++;//修改标记
	E result = (E) queue[0];
	E x = (E) queue[s];//这边出堆的时候,会将最后一个元素删去,保证完全二叉
	queue[s] = null;
	if (s != 0)
	    siftDown(0, x);//给最后一个元素找位置
	return result;
}

在offer()中出现的函数siftUp,是从下向上调整堆,这个siftDown,是从上到下调整堆,这两个函数是优先队列调整堆的主要方法

private void siftDown(int k, E x) {
	if (comparator != null)
	    siftDownUsingComparator(k, x);
	else
	    siftDownComparable(k, x);
}

依然继续查看siftDownUsingComparator函数

private void siftDownUsingComparator(int k, E x) {
	int half = size >>> 1;
	while (k < half) {
	   int child = (k << 1) + 1;//左孩子
	   Object c = queue[child];//要和父节点判断的对象
	   int right = child + 1;//右孩子
	   if (right < size &&
	       comparator.compare((E) c, (E) queue[right]) > 0)//如果右孩子比左孩子经过比较后大,交换c为右孩子
	       c = queue[child = right];
	   if (comparator.compare(x, (E) c) <= 0)//如果父节点比较未通过,结束循环
	       break;
	   queue[k] = c;//父节点被孩子所替代,向下继续调整堆
	   k = child;
	}
	queue[k] = x;
}

所以这个函数具体就是,将元素x放到指定的索引k之后,然后上到下调整堆。至此,堆调整的两个函数就看完了,从上调整和从下调整

10.remove(Object o)和removeAt(int i)

remove(Object o)这个函数会尝试移除值为o的元素

public boolean remove(Object o) {
	int i = indexOf(o);//查找元素所在位置
	if (i == -1)
	    return false;
	else {
	    removeAt(i);//移除位于该处的元素
	    return true;
	}
}

index函数如下,这里又一次提醒了我重写equals的重要性,对象间比较大多用equals

private int indexOf(Object o) {
	if (o != null) {
	    for (int i = 0; i < size; i++)//直接遍历寻找
	        if (o.equals(queue[i]))
	            return i;
	}
	return -1;
}

removeAt这个函数是最复杂的,具体我写在注释中了,可能会遭遇两次调整
还有同学可能会疑问为什么这个函数需要返回值,在其他函数中都不需要这个返回值,但是在PriorityQueue的iterator的remove()函数中,用到了这个返回值,所以,如果不使用这个的迭代器,不需要关心返回值

if (queue[i] == moved)这里代表最后那个元素被放到该节点之后,调整了一次,但是没有被移动过,那么原因?是因为被删除的那个元素在叶子上,或者下面的树结构是满足堆要求,无需调整,所以才继续要去从下向上调整,保证堆的正确性吧。

private E removeAt(int i) {
	// assert i >= 0 && i < size;
	modCount++;//修改标记
	int s = --size;//s是数组最后一个元素
	if (s == i) // removed last element
	    queue[i] = null;
	else {
	    E moved = (E) queue[s];//保存元素s
	    queue[s] = null;//将最后一个位置置空,保证完全二叉树结构
	    siftDown(i, moved);//从上向下调整,找到最后一个元素该放的位置
	    if (queue[i] == moved) { //这次调整将该节点未移动
	        siftUp(i, moved);//从下到上调整,调整堆到正确位置
	        if (queue[i] != moved)
	            return moved;
	    }
	}
	return null;
}

11.总结

1)如果要使用PriorityQueue存储Object类的东西的时候,记得重写equals和hashcode方法
2)不要向里面存null值
3)每一次需要移除堆中元素的时候,如poll()和removeAt,为了保证堆为完全二叉树结构,先将最后一个元素保存下来,然后删除最后一个元素,然后将最后一个元素放到被删除元素的位置,然后调整堆

因为是个人瞎写没参考资料,如果有错误欢迎大家指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值