111 阅读4分钟

堆通常是一个可以被看作一棵树的数组对象。堆具体的实现一般不通过指针域,而是通过构建一个堆树组与二叉树的父子节点进行对应,因此堆总是一颗完全二叉树。

对于任意一个父节点的序号N来说(这里N从0算),它的节点的序号一定是2N+1,2N+2...因此可以直接用数组来表示一个堆。

不仅如此,堆还有一个性质:堆中某个节点的值总是不大于或不小于其父节点的值。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

image.png

堆常用来实现优先队列,在面试中经常考的问题都是与排序有关,比如堆排序、topK问题等。由于堆的根节点是序列中最大或最小值,因而可以在建堆以及重建堆的过程中,筛选出数据序列中的极值,从而达到排序或者挑选topK值的目的。

案例PriorityBlockingQueue中二叉堆的使用

put过程:

public void put(E e) {
        offer(e); // never need to block
    }


 public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();//加锁
        int n, cap;
        Object[] array;
     	//如果元素个数大于等于数组长度,开始扩容
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);
        try {
            //一开始构建队列的时候传入comparator
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                //使用元素自带的comparator进行比较,并排序
                siftUpComparable(n, e, array);
            else
                //使用comparator进行比较,并排序
                siftUpUsingComparator(n, e, array, cmp);
            size = n + 1;//增加元素个数
            notEmpty.signal();//唤醒take线程
        } finally {
            lock.unlock();//解锁
        }
        return true;
    }

//新元素堆内上浮实现
    private static <T> void siftUpComparable(int k, T x, Object[] array) {
        //尝试转化插入对象为Comparable实例
        Comparable<? super T> key = (Comparable<? super T>) x; 
        while (k > 0) {
            // 新元素x的数组下标为k,对应的父节点的下标为(k-1)/2
            int parent = (k - 1) >>> 1; 
            Object e = array[parent];
            //如果子节点已经比父节点还要大,不需要再跟上层节点比较,新元素上浮结束
            if (key.compareTo((T) e) >= 0) 
                break;
            //如果子节点已经比父节点小,父节点下沉,新元素上浮一次
            array[k] = e; 
            k = parent; //新元素上浮后继续与新的父节点比较大小,直到k=0或者新的父节点小于新元素
        }
        array[k] = key;//新元素在堆中插入正确的位置。
    }

 private void tryGrow(Object[] array, int oldCap) {
     	//扩容期间需要解锁,能够让其他线程继续执行
        lock.unlock(); // must release and then re-acquire main lock
        Object[] newArray = null;//新数组
     	//cas标识,如果为0,表示当前没有线程对数组扩容
        if (allocationSpinLock == 0 &&
            //cas方式设置值0->1
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
                //开始扩容
                //计算新的数组长度
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
                //计算容量不能超过最大大小
                if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                    int minCap = oldCap + 1;
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
                    newCap = MAX_ARRAY_SIZE;
                }
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];//创建新的数组用来保存元素
            } finally {
                allocationSpinLock = 0;//重置标识,表示扩容完毕
            }
        }
        if (newArray == null) // back off if another thread is allocating
            Thread.yield();
        lock.lock();//重新加锁
     	//如果数组还是原来的数组
        if (newArray != null && queue == array) {
            queue = newArray;//设置为新数组
            System.arraycopy(array, 0, newArray, 0, oldCap);//转移元素到新数组
        }
    }

take过程

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();//加锁
        E result;
        try {
            while ( (result = dequeue()) == null)
                notEmpty.await();
        } finally {
            lock.unlock();
        }
        return result;
    }

private E dequeue() {
        int n = size - 1;
        if (n < 0)
            return null;//队列为空
        else {
            Object[] array = queue;
            E result = (E) array[0];//取出第一个,二叉堆中第一个最小
            E x = (E) array[n];
            array[n] = null;//设置为空
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                siftDownComparable(0, x, array, n);
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }

//空元素堆内下沉实现
    private static <T> void siftDownComparable(int k, T x, Object[] array, int n) {
        if (n > 0) {
            Comparable<? super T> key = (Comparable<? super T>) x;
            int half = n >>> 1; // loop while a non-leaf  half最后一个有子节点的父节点下标
            while (k < half) { 
                int child = (k << 1) + 1; // assume left child is least
                Object c = array[child];
                int right = child + 1;
                if (right < n
                        && ((Comparable<? super T>) c)
                                .compareTo((T) array[right]) > 0)
                    c = array[child = right];  //比较出左右子节点更小的那个子节点
                if (key.compareTo((T) c) <= 0) //如果左右子节点的最小值大于数组末尾的值,那么数组末尾的值直接放到父节点,空节点下沉结束
                    break;
                array[k] = c; // 如果子节点最小值小于数据末尾的值,子节点上浮到父空节点
                k = child; //空节点下滑到最小子节点的位置
            }
            array[k] = key; // 最后空节点填充数组最后的值
        }
    }