【Java容器源码】PriorityQueue 源码分析

先来看看 PriorityQueue 继承关系,核心成员变量及主要构造函数:

// 可以看到 PriorityQueue 只是一个普通队列,并不是一个阻塞队列
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {
	
	// 通过数组保存队列数据,队列节点是Object
	// 这里采用的其实是堆这种数据结构,后面元素的排序也是采用的堆排序
	// 小顶堆,每次 poll、peek 都能得到最小的元素
	transient Object[] queue;
	
	transient int modCount = 0; 
	private int size = 0
	
	// 比较器,priortyqueue是优先队列,所以比较器是必须的
	private final Comparator<? super E> comparator;
	
	// 数组默认初始容量
	private static final int DEFAULT_INITIAL_CAPACITY = 11;
	// 数组最大容量
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	
	//----------------------------------构造函数------------------------------
	// 构造函数一:空参构造
	public PriorityQueue() {
		// 默认初始容量,无比较器
        this(DEFAULT_INITIAL_CAPACITY, null);
    }
    
    // 构造函数二:传入自定义比较器
    public PriorityQueue(Comparator<? super E> comparator) {
        // 默认初始容量,自定义比较器
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }
    
    // 构造函数三:传入指定容量和比较器
    public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        // 数组初始化
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }
}

1.结构分析

堆是什么?

堆在逻辑上一棵完全二叉树,所以可以通过数组进行数据存储,而其余的树大多采用链式结构进行数据存储

  • 堆分类:
    • 大顶堆:大顶堆就是无论在任何一棵(子)树中,父节点都是最大的
    • 小顶堆:小顶堆就是无论在任何一棵(子)树中,父节点都是最小的。PriortyQueue 采用的就是小顶堆
  • 堆的两种操作:
    • 上浮:一般用于向堆中添加新元素后的堆平衡
    • 下沉:一般用于取出堆顶并将堆尾换至堆顶后的堆平衡
  • 堆排序:利用大顶堆和小顶堆的特性,不断取出堆顶,取出的元素就是堆中元素的最值,然后再使堆平衡

PS:这里只做简介,想了解堆的代码实现及更多操作的同学可以参考 【数据结构】Java实现堆:堆的各种操作&堆排序

Comparable 对比 Comparator?

这里再说一下 Comparable 接口和 Comparator 接口,它俩的作用都是提供了同一类型下不同对象实体比较的方式,所以这里主要关注它们的区别:

1)Comparable 接口

  • 一般用作内部比较器,需要实体对象实现Comparable接口并重写 compareTo 方法
  • 比较时调用实体对象的 compareTo 方法
public interface Comparable<T> {
    public int compareTo(T o);
}

2)Comparator 接口

  • 一般用作外部比较器,提前写好实现类并重写compare方法后,然后作为参数传入需要比较器的方法。
  • 比较时直接向比较器中传入两个需要比较的对象。下面是一个示例:
// Comparator作为参数,匿名类,省略实现
// Dog:要比较的类型
Collections.sort(list, new Comparator<Dog>() {
        // 重写compare方法,参数是两个待比较的对象
        public int compare(Dog o1, Dog o2) {
        	// 比较策略
        	return o2.age - o1.age;
        }
});

注意:因为 Comparator 是接口所以不能 new,但可作为匿名类相当于省略了实现类。

2.入队

add()

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

offer()

offer 方法首先会判断是否需要扩容,然后再根据队列是否为决定空放入元素

public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
    	// 超出容量,扩容
        if (i >= queue.length)
            grow(i + 1);
        size = i + 1; // size++
    	
        if (i == 0)
        	// 如果队列为空直接将元素放到队首
            queue[0] = e;
        else
            // 上浮
            // 可以这么理解,队列(数组)中所有元素已经是排好序的,而新元素会追加到数组末尾,所以为了保证总体有序,可能需要将新元素上浮(前移)
            siftUp(i, e);
        return true;
}

注:因为 PriortyQueue 也是需要通过比较后得到元素放入的具体位置,所以不允许添加 null(跟 TreeMap 类似)。

siftUp()

判断比较时是用自定义比较器 Comparator,还是用对象实体的 compareTo 方法

private void siftUp(int k, E x) {
        if (comparator != null)
        	// 有自定义比较器,就用自定义比较器,调用 siftUpUsingComparator
            siftUpUsingComparator(k, x);
        else
        	// 无自定义比较器,就用元素的Comparable方法,调用 siftUpComparable
            siftUpComparable(k, x);
}

siftUpComparable()

通过实体对象的 compareTo 方法进行比较,完成新元素的上浮,使队列(数组)总体顺序是:小 => 大。具体过程请看注释:

// k:当前队列实际大小的位置(没有新元素),x:要插入的元素
private void siftUpComparable(int k, E x) {
    	// 将x强转为Comparable,可以调用CompareTo比较
        Comparable<? super E> key = (Comparable<? super E>) x;
    	    
    	// 通过不断与父节点进行比较,最后找到正确的位置
        while (k > 0) {
            // 得到k的父节点索引,即对 k 进行减倍
            int parent = (k - 1) >>> 1; 
            // 得到父节点的数据
            Object e = queue[parent]; 
            
            // 如果 x 比 parent 大,表示当前位置就是正确的位置
            if (key.compareTo((E) e) >= 0) 
                break;
                
            // x 比 parent 小,则将parent交换到x的位置
            queue[k] = e;  
            // 继续上浮,直到找到一个比 x 小的 parent
            k = parent; 
        }
        // 已将找到了合适的位置,将 x 放入
        queue[k] = key; 
}

3.出队:poll()

出队其实就是将队首,也即小顶堆的堆顶返回,然后通过下沉操作使堆恢复。

public E poll() {
        if (size == 0)
            return null;
        int s = --size; // size--
        modCount++;
    	// 取队首
        E result = (E) queue[0];
        
        // 保存数组最后一个元素,也即堆尾
        E x = (E) queue[s];
        // 将堆尾删除
        queue[s] = null;
        if (s != 0)
        	// 下沉(将堆尾x放到堆顶位置,然后下沉)
            siftDown(0, x);
        return result;
}

4.获取队首:peek()

public E peek() {
    	// 直接将arr[0]返回
        return (size == 0) ? null : (E) queue[0];
}

5.扩容:grow()

扩容的本质就是数组拷贝,所以问题就在于新数组到底要多大:

  • oldCap < 64 —> newCap = oldCap*2 + 2
  • oldCap >= 64 —> newCap = oldCap * 2
  • newCap > Max —> newCap = Integer.MAX_VALUE
private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // newCap
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));
        // 防止溢出,即超出最大容量
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
    	// 将拷贝后的大数组再赋给queue
        queue = Arrays.copyOf(queue, newCapacity);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A minor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值