深入解析PriorityQueue工作原理,详解优先队列,Java源码

PriorityQueue

在算法中我们经常会使用到最大优先队列和最小优先队列(默认是最小优先队列,可以自定义排序规则)

在正式了解源码前我们先看一下他的一些类变量(类中带static的变量为类变量)

// 表示初始的容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 表示队列的大小
private int size = 0;
// 此变量仅可在构造函数的时候初始化 一旦初始化就不可以在改变因为加了final关键字
private final Comparator<? super E> comparator;
// 队列改变了多少次
transient int modCount = 0; // non-private to simplify nested class access
// 用数组来维护树结构
transient Object[] queue;

在PriorityQueue的构造函数中可以有两个变量

  1. Capacity 用来初始化queue数组 因此我们可以清晰的知道 他的代表的含义
    1. 此值并不是永远不变的,当大小不够的时候他会grow
    2. new Object[initialCapacity];
  2. Comparator:用来自定义排序顺序

添加元素

		public boolean add(E e) {
				// add等于offer
		    return offer(e);
		}
		public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
				// 修改次数+1
        modCount++;
				// 代表当前队列大小
        int i = size;
				// 当树大小超过 数组的初始化大小也就是initCapcity 则会增加Capacity
        if (i >= queue.length)
            grow(i + 1);
				// 大小加1
        size = i + 1;
        if (i == 0)
						// 若队列大小为0,则将其放在第一个位置
            queue[0] = e;
        else
						// 反之需要复杂操作
            siftUp(i, e);
        return true;
    }

siftUp

// 根据是否自定义Comparator 来选择不同的siftUp函数
// 这两个函数内部唯一的差别就是一个调用的是自定义排序
// 一个是在函数内部临时定义Comparator (默认递增)
private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}
// k 为此时的size x为插入的元素
private void siftUpComparable(int k, E x) {
		// 局部变量 默认递增也就是最小优先队列
    Comparable<? super E> key = (Comparable<? super E>) x;
		// 通过while循环 得到需要在数组中插入的位置k
		// 首先假设插入在数组的最末尾,也就是最后一个叶子节点的右边
    while (k > 0) {
				// 用位运算快速计算除以二得到父节点的位置
				// 完全二叉树的性质就是 子节点的position/2 得到父节点的位置
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
				// 将插入元素与父节点对比
				// 如果这个节点的大于父节点则找到位置 默认是最小优先队列
        if (key.compareTo((E) e) >= 0)
            break;
				//父节点和子节点互换位置
				// 也就是叶子节点promo 
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}
// 同上
private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

小结

  1. 从上面源码可以看出,其实priorityQueue的插入实现比较简单就是通过子节点和父节点的不断比较找到第一个比父节点小的位置(符合Comprator的要求)。在向上寻找的过程中就是子节点和父节点不断交换的过程。
  2. 同时我们也可以看出PriorityQueue也仅仅只维护了父节点比子节点大。但是这样就已经足够了因为我们只需要每一次都取根节点就可以保证取出来的元素是符合我们要求的。
  3. 每一次的插入都只网上了一层最坏情况就是从叶子节点交换到了根节点 时间复杂度是O(logn)

出队(取出符合条件的第一个元素)

public E poll() {
		// 若本来队列为空,则返回null
    if (size == 0)
        return null;
		// 队列元素自减
		// s为size-1 同时size也减小了 非常的巧妙
    int s = --size;
		// 修改队列次数+1
    modCount++;
		// 队头就是根节点就是符合条件的第一个元素
    E result = (E) queue[0];
		// 队尾的元素,最后一个元素,最右边的叶子节点
    E x = (E) [s];
		// 让队尾的元素为null
    queue[s] = null;
		
    if (s != 0)
        siftDown(0, x);
    return result;
}

siftDown

// 与上面同理这里就不赘述了
private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
// 从根节点往下寻找
private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
		// size/2
    int half = size >>> 1;        // loop while a non-leaf
    while (k < half) {
				// 得到了左孩子的位置
				// 注意这里是从0开始的 刚开始我也疑惑为什么要+1
				// 因为我们学习的完全二叉树都是k/2为左孩子
        int child = (k << 1) + 1; // assume left child is least
        // 拷贝一份左孩子
				Object c = queue[child];
        int right = child + 1;
				// 找到根节点的左右孩子中小的那一个
        if (right < size &&
            ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            c = queue[child = right];
				// key为队尾元素
        if (key.compareTo((E) c) <= 0)
            break;
				// 为队尾元素找到合适的位置
				// 将最小的左右孩子放入root
				// k表示的就是root的位置(也可以是子树的root)
        queue[k] = c;
				// 子树的root
        k = child;
    }
		// 第一个比左右子树小的的位置并放入
    queue[k] = key;
}

小结

  1. 从源码可知其实队列的取出也是需要时间复杂度O(logn),因为他会从root遍历到叶子节点。
  2. 总体流程:
    1. 将root移出队列,此时树中(数组中出现了空缺需要填补),否则就浪费了空间。
    2. 将队尾元素作为候选元素与每一个root的左右子树进行对比如果选择三者中最小的一个放入root中
    3. 若没有在第一次就为队尾元素找到合适的位置,那么promo到root节点的位置就有空了出来不断的while循环直到找到了第一个小于root的左右孩子位置,并将队尾元素放入root

Grow 扩大自己的容量

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
		// 原来的容量
    int oldCapacity = queue.length;
		//如果如果本来的容量比较小(小于64)则直接double
		// 反之如果大于等于64 则增加一半(50%)
		// >> 位运算快速/2
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                     (oldCapacity + 2) :
                                     (oldCapacity >> 1));
    // overflow-conscious code
		
    if (newCapacity -MAX_ARRAY_SIZE> 0)
        newCapacity =hugeCapacity(minCapacity);
		// 重新获取一个数组,同时拷贝元素到数组,并使用新的扩大的容量
    queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
		// 如果大于了数组的最大元素个数 则返回Integer的最大值
		// 反之则返回最大的数组元素个数
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值