Java PriorityQueue 中二叉堆原理

问:谈谈你对二叉堆数据结构的理解及在 PriorityQueue 中的实现?

答:这算是一道比较有深度的问题了,要回答好首先得解释什么是二叉堆数据结构,接着解释其优点,然后解释在 JDK 1.5 的 PriorityQueue 中是怎么使用的,只有这几个方面都点到才算比较满意的答案。

首先堆的特点总是一棵完全二叉树且某个节点值总是不大于或不小于其父节点值,PriorityQueue 使用的是堆中比较特殊的二叉堆,二叉堆是完全二叉树或者是近似完全二叉树,二叉堆分为最大堆和最小堆,最大堆的父结点值总是大于或等于任何一个子节点值,最小堆的父结点值总是小于或等于任何一个子节点值。如下图就是一个最小二叉堆结构图:

可以看见二叉堆(完全二叉树)在第 N 层深度被填满之前是不会开始填第 N+1 层的,且元素插入也是从左往右顺序。此外我们通过上面的树状图和数组连续内存分布可以看到父子节点的索引顺序存在如下关系:

parentNodeIndex = (currentNodeIndex-1)/2;

leftNodeIndex = parentNoIndex*2+1;

rightNodeIndex = parentNodeIndex*2+2;

可以看见,通过公式能直接计算出某个节点的父节点以及子节点的下标,所以这也就是为什么可以直接用数组来存储二叉堆而不用链表的原因之一,故 PriorityQueue 的 peek()/element() 操作时间复杂度是 O(1),而 add()/offer()/poll()/remove() 操作的时间复杂度是 O(log(N))。

了解了二叉堆的原理和特点之后我们就来看看 PriorityQueue 中是怎么使用二叉堆实现操作的,我们主要要看的方法为add()/offer()/peek()/element()/poll()/remove(),下面会对这些方法进行分组实现解说。

1. add()/offer()

PriorityQueue 的 add()/offer() 操作都是向优先队列中插入元素,add() 的实现就是直接调用 offer() 方法返回,所以我们直接看下 offer() 方法的实现:

   public boolean offer(E e) {
        //PriorityQueue元素不允许为null
        if (e == null) throw new NullPointerException();
        modCount++;
        int i = size;
        //数组需要扩容,arraycopy操作 
        if (i >= queue.length) grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
            //队列为空时第一个元素插在数组开头 
        else
            siftUp(i, e);
        //队列不为空时堆结构调整数组元素位置 
        return true;
    }

    // 使用不同的比较器进行比较
    private void siftUp(int k, E x) {
        if (comparator != null) siftUpUsingComparator(k, x);
        else siftUpComparable(k, x);
    }

    //k为currentNodeIndex,x为要插入的元素 
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            //等价于parentNodeIndex=(currentNodeIndex-1)/2; 
            Object e = queue[parent];
            //将x逐层与parent元素比较交换,只到x>=queue[parent]结束 
            if (key.compareTo((E) e) >= 0) break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

上面的代码用图示流程演示如下(9个元素的优先级列表插入一个调整后变为10个元素):

2. peek()/element()

PriorityQueue 的 peek()/element() 操作,都是取出最小堆顶元素但不删除队列堆顶元素,区别就是 element() 的实现是 peek() 且 element() 取出元素为 null 会抛出异常而 peek() 不会,所以我们直接看下 peek() 方法的实现:

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

演示流程图如下:


3. poll()

PriorityQueue 的 poll() 操作,其目的就是取出最小堆顶部元素并从队列删除,当失败时返回 null,所以该方法的实现如下:

    public E poll() {
        if (size == 0) return null;
        int s = --size;
        modCount++;
        //最小二叉堆的最小元素自然在数组的index为0处
        E result = (E) queue[0];
        // 取出数组最后一个元素,即二叉堆树最深层最右侧的元素
        E x = (E) queue[s];
        // 最后一个元素位置置空
        queue[s] = null;
        if (s != 0) siftDown(0, x);
        // 调整二叉堆数组元素位置
        return result;
    }

    // 直接看siftDown中的Comparable情况,k索引0开始,x为二叉堆最后一个元素
    private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        int half = size >>> 1; // loop while a non-leaf
        while (k < half) {
            // 找到当前元素的左子节点索引
            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];
            if (key.compareTo((E) c) <= 0) break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

上面的代码用图示流程演示如下(10个元素的优先级列表 poll 删除一个调整后变为9个元素):

4. remove()

PriorityQueue 的 remove(E e) 操作,其目的就是将指定的元素从队列删除,当失败时返回 false,该方法的实质是先遍历获取 e 在数组的 index 索引,然后调用 removeAt(index) 方法,所以我们看下 removeAt(index) 方法源码如下:

        //i为要删除的元素在数组的索引
        E removeAt ( int i )
        {
            // assert i >= 0 && i < size;
            modCount++;
            int s = --size;
            if (s == i) // removed last element
                queue[i] = null;
                //如果要删除的元素恰巧在最后一个则直接删除不用调整
            else {
                //取出二叉堆树的最后一个节点元素
                E moved = (E) queue[s];
                //最后一个节点置为空
                queue[s] = null;
                //然后类似poll进行siftDown向下子节点比较交换(从i位置当做顶层父节点)
                siftDown(i, moved);
                // 向下沉淀完发现没变化则需要向上浮动,说明最后一个元素换到 i 位置后是最小元素
                if (queue[i] == moved) {
                    siftUp(i, moved);
                    if (queue[i] != moved) return moved;
                }
            }
            return null;
        }

上面的代码用图示流程演示如下(10个元素的优先级列表 remove 删除一个调整后变为9个元素):


在作答这个题时你可以选择画图也可以选择直接写父子节点关系公式和 siftUp、siftDown 的机制即可,核心答出来就行,当然不要忘记最小二叉堆是数组实现且 PriorityQueue 元素不允许为空的特性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容