• Java队列学习笔记(1)---AbstractQueue&PriorityQueue


    类层次图

    Queue

    AbstractQueue

    已实现方法 调用 Queue 接口方法 说明 异常类型
    add offer 元素入队; 但是队列有容量上限约束,队列满抛出异常 IllegalStateException
    remove poll 检索并移除队头元素; 队头为空/队列空抛出异常 NoSuchElementException
    element peek 检索队头元素; 队头为空/队列空抛出异常 NoSuchElementException
    clear poll 移除队列中的所有元素
    addAll add 把参数集合中的所有元素入队

    小节一下:

    1. 所有队列都有三种操作:入队操作,出队操作,检索队首元素(但不移除该元素)。
    2. 队列常见的两种限制:队列容量限制,队列为空限制。

    Delayed

    • getDelayed 方法返回对象相关的剩余延迟(本文的对象是队列)

    PriorityQueue

    1. 容量无上限:队列支持自动扩容。队列内部包含一个容量,这个容量会随着元素的增加而增大。
    2. 有优先级顺序:根据成员变量 comparator 决定优先级顺序。该成员变量是在调用构造函数时赋值的。如果该成员变量为 null,则入队的元素必须实现 Comparable 接口,并按顺序进行排序。
    3. 入队元素有限制:入队元素不允许为 null;如果没有指定非空的 comparator,则入队元素必须实现 Comparable 接口,否则抛出 ClassCastException
    4. 非线程安全的:不支持多线程并发修改队列
    5. 数据结构与算法:使用“小根堆”,效率更高。

    grow 扩容

    // minCapacity 表示本次扩容后,队列内部容量至少要达到 minCapacity 这个水平
    private void grow(int minCapacity) {
        // 保存队列当前的容量
        // 新的容量需要在老的容量 oldCapacity 基础上进行扩容
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        // 条件1:oldCapacity < 64
        // 条件成立,则 oldCapacity < 64,在此情况下,容量 newCapacity = 2 * n + 2
        // 条件不成立,则 oldCapacity >= 64,此时,newCapacity = 1.5 * n
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));
        // overflow-conscious code
        // 如果新容量 newCapacity 超过了数组的最大值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // “拷贝-转移”的方式实现数组的扩容
        queue = Arrays.copyOf(queue, newCapacity);
    }
    

    hugeCapacity

    private static int hugeCapacity(int minCapacity) {
        // minCapacity 转化为二进制数之后,最高位为 1,则表示负数
        // 抛出内存溢出异常
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        // 条件:minCapacity > MAX_ARRAY_SIZE
        // 条件成立:minCapacity > 0x7fff fff7,返回大容量 Integer.MAX_VALUE(0x7fff ffff)
        // 条件不成立:minCapacity <= 0x7fff ffff 返回大容量 MAX_ARRAY_SIZE(0x7fff fff7) 
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    

    offer 元素入队

    在 PriorityQueue 中,仅有 offer 方法调用 grow。

    public boolean offer(E e) {
        // PriorityQueue 不允许 null 入队
        if (e == null)
            throw new NullPointerException();
    
        // 每次有新的元素入队,修改版本号递增
        modCount++;
    
        // size 记录的是队列中元素的真实数量
        // queue.length 则是目前的内部最大容量,即数组容量
        int i = size;
        // 条件:i == queue.length
        // 条件成立:目前数组内已经无法容纳新加入的元素,因此必须先进行扩容
        // 条件不成立:此时数组中,还有尚未赋值元素引用的空余位置,因此不扩容    
        if (i >= queue.length)
            // 发生扩容时,希望新的容量最小为 i + 1,即 queue.length + 1,即最起码扩充一个空闲位置
            grow(i + 1);
        // 队列的元素真实数量新增1个
        size = i + 1;
        // 条件:i == 0
        // 条件成立:元素入队前,元素个数为 0。也就是说,当前队列是空的,此时新入队的元素成为队头
        // 条件不成立:将新入队的元素插入到数组中的空闲位置 i 处
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
    }
    

    siftUp

    // 在位置k处插入项x
    // 通过将x提升到树的上方
    // 直到它大于或等于其父级或者是根级
    // 从而保持堆不变。
    // k 表示插入的位置,x 表示欲插入的元素
    private void siftUp(int k, E x) {
        // 条件:comparator != null
        // 条件成立:如果构造函数中传入的是非空的 Comparator,此时根据比较器进行优先级排序
        // 条件不成立:此时要求对象 x 实现 Comparable 接口。根据该接口进行优先级排序
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }
    

    siftUpUsingComparator

    private void siftUpUsingComparator(int k, E x) {
        // 当想要插入的位置不是根结点位置,即 k == 0
        // 那么 k > 0 条件成立,进入循环体。
        while (k > 0) {
            // 首先,(k - 1) >>> 1 等同于 (k - 1) / 2,这样写的原因是计算机使用位移运算效率更高
            // 根据小根堆堆的存储规则,从逻辑结构上看,按层次遍历的第 k 个结点,他的父结点按层次遍历是第 (k - 1)/ 2 个结点。
            // 实际存储时,数组的下标也与之对应,存储在数组下标 k 位置的结点,他的逻辑父结点存储在数组下标为 (k - 1) / 2 的位置。
            int parent = (k - 1) >>> 1;
            // e 表示父结点元素
            Object e = queue[parent];
            // 条件 comparator.compare(x, (E) e) >= 0 表示拿父结点与待插入结点做比较
            // 条件成立:表示欲插入的元素>=当前父结点元素
            // 此时跳出循环,插入并作为当前父结点的子结点。
            // 子结点,即作为父结点的左子结点或者右子结点
            // 条件不成立:新插入的元素结点的值小于当前根结点
            if (comparator.compare(x, (E) e) >= 0)
                break;
            // 父结点“下移”
            queue[k] = e;
            // 现在欲插入新元素的位置 k 就等于本轮中的父结点位置了
            k = parent;
        }
        queue[k] = x;
    }
    

    siftUpComparable

    private void siftUpComparable(int k, E x) {
        // 将欲插入的对象 x 强转为 Comparable 接口
        // 如果 x 不是 Comparable 的实现,那么会发生 ClassCastException
        Comparable<? super E> key = (Comparable<? super E>) x;    
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            // 和上述 siftUpUsingComparator 最大的不同对比的调用方式不同 
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }
    

    补习小根堆

    首先,PriorityQueue 底层的数据结构是小根堆。
    小根堆(Min-heap): 父结点的值小于或等于子结点的值。

    注意,以下不是小根堆具有的性质:左子树上所有结点的值均小于它的根结点的值,右子树上所有结点的值均大于它的根结点的值,那是二叉排序树(Binary Search Tree)的性质,不要搞混了
    大根堆(Max-heap): 父结点的值大于或等于子结点的值。

    为什么 PriorityQueue 选择使用小根堆,而不是大根堆?
    因为在优先队列中,priority 值越低的对象,优先级越高,需要越早出队。因此这样的对象要存放在越接近二叉树根结点的地方。

    堆的存储

    一般都用数组来表示堆,i结点的父结点下标就为(i–1)/2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。

    由于堆存储在下标从0开始计数的数组中,因此,在堆中给定下标为i的结点时:

    (1)如果i=0,结点i是根结点,无父结点;否则结点i的父结点为结点(i-1)/2;

    (2)如果2i+1>n-1,则结点i无左子女;否则结点i的左子女为结点2i+1;

    (3)如果2i+2>n-1,则结点i无右子女;否则结点i的右子女为结点2i+2。

    数组中的数据存储顺序和逻辑完全二叉树的层次遍历的顺序一致

    完全二叉树&满二叉树

    小根堆和大根堆都属于完全二叉树。什么是完全二叉树呢?

    先来看一下满二叉树,又称完美二叉树,英文为 Perfect Binary Tree
    如果二叉树中除了叶子结点,每个结点的度都为 2。且所有叶子结点的深度相等
    A Perfect Binary Tree(PBT) is a tree with all leaf nodes at the same depth.
    All internal nodes have degree 2

    如果二叉树中除去最后一层结点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。满二叉树也属于完全二叉树。

    remove 移除元素

    remove(o) 从队列中移除指定元素。

    // 返回值:
    // false:表示没有找到要移除的元素
    // true:表示成功移除该元素
    public boolean remove(Object o) {
        // 查询元素o存储在数组中的下标
        int i = indexOf(o);
        if (i == -1)
            return false;
        else {
            // 移除位于坐标 i 的元素
            removeAt(i);
            return true;
        }
    }
    

    indexOf

    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

    // 从队列中删除第i个元素。
    
    private E removeAt(int i) {
        // assert i >= 0 && i < size;
        // 修改版本号+1,防止使用迭代器迭代时,此处的修改,导致与迭代器不一致
        modCount++;
        // 队列中的元素数量先减一
        int s = --size;
        // 移除小根堆层次遍历顺序下的最后一个元素,
        // 同时也是数组中的(size - 1)位置的元素
        // 此时不涉及小根堆的调整,直接移除即可
        if (s == i) // removed last element
            queue[i] = null;
        else {
            // 此时 i < s
            // s 表示 (size - 1)位置的元素,即小根堆上的最后一个元素
            // 用 moved 暂存小根堆上的最后一个元素
            E moved = (E) queue[s];
            // 移除队列中最后一个位置上的元素
            queue[s] = null;
            // 把暂存的最后一个元素,替代要移除的位于小根堆 i 位置上的元素
            // 为了保持小根堆,moved 代表的元素可能并不能刚好保存到位置 i 上
            // 有可能会被“下移”到 i 位置的子结点上面,而 i 位置的子结点中最小的元素会被“提升”到 i 位置
            siftDown(i, moved);
            // 条件:queue[i] == moved
            // 条件不成立:(size - 1)位置的元素不能直接覆盖i位置的值,需要向下调整,发生调整后,i 位置的子树作为“小根堆”也是符合条件的
            // 而 i 位置的子树原本作为整个树中的“小根堆”的一部分,也是稳定的。因此跳过 if 部分
            // 条件成立:(size-1)位置的元素可以直接覆盖i位置的值,此时,i 位置的子树,作为独立的“小根堆”是符合条件的
            // 但是因为 i 位置的值发生变化,整个“小根堆”有可能需要调整
            if (queue[i] == moved) {
                // 把 moved 元素放入到位置 i,如果出现无法保持“小根堆”的情况
                // 则把 moved 元素向上提升,实际插入到比 i 要小的位置。
                siftUp(i, moved);
                // 条件:queue[i] != moved
                // 条件成立: i-1(包括i-1)以内元素的位置没有发生改变,此时,该函数返回 null
                // 条件不成立:为了保持堆不变,队列末尾的元素在放入i之后,继续与 i 之前的元素发生交换。
                if (queue[i] != moved)
                    // 在这种情况下,此方法返回以前位于队列末尾的元素,现在位于i之前的某个位置。
                    return moved;
            }
        }
        return null;
    }
    

    siftDown

    // 这里采用了和 siftUp 类似的代码
    // 在位置k处插入元素x
    // 通过将x从树中反复降级
    // 直到它小于或等于其子项或是一片叶子,从而保持堆不变。
    private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }
    

    siftDownComparable

    private void siftDownComparable(int k, E x) {
        // 强制转换对象 x 为 Comparable 接口,如果 x 无法强转,抛出 ClassCastException 异常
        Comparable<? super E> key = (Comparable<? super E>)x;
        // 在调用 siftDown 之前,都会先调用 --size
        // 所以此时 size 表示的正是小根堆上删除的最后一个元素的位置
        // 这个最后一个元素的父结点下标为 size / 2。
        int half = size >>> 1;        // loop while a non-leaf
        while (k < half) {
            // k * 2 + 1 表示欲插入位置 k 的左结点
            int child = (k << 1) + 1; // assume left child is least
            // c 表示左孩子结点元素的值
            Object c = queue[child];
            // right 表示右孩子结点元素的值
            int right = child + 1;
            // 条件一:right < size
            // 条件不成立:表示右结点不存在,此时左结点就是比位置 k 中孩子结点中值较小的那个。
            // 条件成立:表示右结点存在,同时也说明位置 k 有左右两个孩子结点
            // 条件二:((Comparable<? super E>) c).compareTo((E) queue[right]) > 0
            // 条件不成立:之前的假设正确, 左结点的确是位置 k 的孩子结点中最小的一个
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                // 条件二成立:此时说明左结点的值比右结点的值大
                // 右结点才是位置 k 的两个孩子结点中值最小的一个
                c = queue[child = right];
            // 判断条件 key.compareTo((E) c) <= 0
            // key 表示要插入的元素关键字
            // 条件成立:表示该关键字插入 k 位置,符合小根堆规则!跳出循环
            if (key.compareTo((E) c) <= 0)
                break;
            // 条件不成立:关键字 key 比 k 位置的孩子结点中的最小数还要大,不符合小根堆规则,需要调整小根堆
            // 此时,插入的位置要把传入参数 k 要降低。
            // 传入参数 k 的较小的孩子结点被提升为父亲结点
            queue[k] = c;
            // 期望插入的位置 k 下降到被提升的孩子结点的位置
            k = child;
        }
        // 把欲插入的元素 x 插入到期望位置 k
        queue[k] = key;
    }
    

    poll 队首元素出队

    public E poll() {
        // 队列中没有元素,出队为 null
        if (size == 0)
            return null;
        // 队列中数量减一
        int s = --size;
        // 防止此处调用 poll 方法,导致 iterator.remove 丢失元素
        modCount++;
        // 返回结果为队首元素,即下标为0的元素
        E result = (E) queue[0];
        // x 表示的是队列中的最后一个元素,暂存到引用 x 中
        E x = (E) queue[s];
        // 把小根堆最后一个元素置为空
        queue[s] = null;
        if (s != 0)
            // 把队列中的最后一个元素,放到队首位置
            // 为了保持该树仍然符合小根堆的规则,x 可能需要从树根结点降级
            siftDown(0, x);
        return result;
    }
    

    heapify 小根堆化

    在用 Collection 作为 PriorityQueue 的构造器参数或者从反序列化方法readObject初始化 PriorityQueue 时,会调用“小根堆化”

    private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }
    

    总结

    优先级队列的底层数据结构为“小根堆”。为了保持小根堆,常见的调整操作包括 siftUpsiftDown

    • siftUp 保证的是将元素 x 存放到位置 k 时,位置 k 之前的所有元素符合小根堆的性质。
    • siftDown 是为了保证将元素 x 存放到位置 k 时,以 k 为“根节点”的子树符合小根堆的性质。

    参考文献

    小根堆(Heap)的详细实现

  • 相关阅读:
    DA_06_iptables 与 firewalld 防火墙
    DA_05_Linux(CentOS6.7) 安装MySql5.7数据库
    DA_04_解决Xshell中文乱码问题
    3.NumPy
    2.NumPy简介
    1.python环境安装
    4.5. scrapy两大爬虫类_Spider
    redis 锦集
    一位资深程序员大牛给予Java初学者的学习路线建议
    idea 使用过程中的一些设置记录
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/code_analysis_of_java_PriorityQueue.html
Copyright © 2020-2023  润新知